diff --git a/.babelrc b/.babelrc
index 2f4ad04d0b2205ef645da290fea0d4c1ed0a7a89..f70183c4586f87b1be522ea5790997acb31a2c04 100644
--- a/.babelrc
+++ b/.babelrc
@@ -5,7 +5,8 @@
     "transform-decorators-legacy",
     ["transform-builtin-extend", {
         "globals": ["Error", "Array"]
-    }]
+    }],
+    "syntax-trailing-function-commas"
   ],
   "presets": ["es2015", "stage-0", "react"],
   "env": {
diff --git a/.editorconfig b/.editorconfig
index 1dfee5e6ced81dc0c96eab93013f5632ab73ae5b..7ce521f7d487c646a954be44a853b6dd10c1a35b 100644
--- a/.editorconfig
+++ b/.editorconfig
@@ -21,10 +21,10 @@ indent_size = 2
 indent_size = 2
 
 [*.html]
-indent_size = 4
+indent_size = 2
 
 [*.js]
-indent_size = 4
+indent_size = 2
 
 [*.jsx]
-indent_size = 4
+indent_size = 2
diff --git a/.eslintrc b/.eslintrc
index 483af44e716721de3bf3108885c53cf68755d000..aa4c8e9f63613906e5ab6efead0a560c877894db 100644
--- a/.eslintrc
+++ b/.eslintrc
@@ -42,9 +42,7 @@
         "flowtype/use-flow-type": 1
     },
     "globals": {
-        "pending": false,
-        "t": false,
-        "jt": false
+        "pending": false
     },
     "env": {
         "browser": true,
diff --git a/.prettierrc b/.prettierrc
new file mode 100644
index 0000000000000000000000000000000000000000..f1ce769cc165c9951869a20bd25308957eec8cb0
--- /dev/null
+++ b/.prettierrc
@@ -0,0 +1,3 @@
+{
+  "trailing-comma": "all"
+}
diff --git a/circle.yml b/circle.yml
index 42dfbb60e542d70d1f534ac92abe1c4ebbafde7f..45657b85b5a6884de2ac03675ac35cf1246eb64d 100644
--- a/circle.yml
+++ b/circle.yml
@@ -5,7 +5,7 @@ machine:
     version:
       openjdk7
   node:
-    version: 6.7.0
+    version: 8.9.0
   services:
     - docker
 dependencies:
diff --git a/docs/api-documentation.md b/docs/api-documentation.md
index d8b3c001d721cbd0b7f54f227771308836850390..b1a42b61809a4d63560777ab9dfe3fc788ca5a77 100644
--- a/docs/api-documentation.md
+++ b/docs/api-documentation.md
@@ -1991,7 +1991,7 @@ Fetch the current `User`.
 
 ## `POST /api/user/`
 
-Create a new `User`, or or reäctivate an existing one.
+Create a new `User`, or reactivate an existing one.
 
 You must be a superuser to do this.
 
diff --git a/docs/developers-guide.md b/docs/developers-guide.md
index 526e3a3215e0751668252282fb2e638971fcc0b7..7c925ca75e35195832f291221786e30639079c3a 100644
--- a/docs/developers-guide.md
+++ b/docs/developers-guide.md
@@ -4,6 +4,7 @@
 *  [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)
+*  [How to add support in Metabase for other languages](#internationalization)
 
 
 # Contributing
@@ -263,6 +264,31 @@ Start up an instant cheatsheet for the project + dependencies by running
 
     lein instant-cheatsheet
 
+## Internationalization
+We are an application with lots of users all over the world. To help them use Metabase in their own language, we mark all of our strings as i18n. The general workflow is:
+
+1. Tag strings in the frontend using `t` and `jt` ES6 template literals (see more details in https://c-3po.js.org/):
+
+```javascript
+const someString = t`Hello ${name}!`;
+const someJSX = <div>{ jt`Hello ${name}` }</div>
+```
+
+and in the backend using `trs` and related macros (see more details in https://github.com/puppetlabs/clj-i18n):
+
+```clojure
+(trs "Hello {0}!" name)
+```
+
+2. When you have added/edited tagged strings in the code, run `./bin/i18n/update-translations` to update the base `locales/metabase.pot` template and each existing `locales/LOCALE.po`
+3. To add a new translaction run `./bin/i18n/update-translation LOCALE`
+4. Edit translation in `locales/LOCALE.po`
+5. Run `./bin/i18n/build-translation-resources` to compile translations for frontend and backend
+6. Restart or rebuild Metabase
+
+To try it out, change your browser's language (e.x. chrome://settings/?search=language) to one of the locales to see it working. Run metabase with the `JAVA_TOOL_OPTIONS=-Duser.language=LOCALE` environment variable set to set the locale on the backend, e.x. for pulses and emails (eventually we'll also add a setting in the app)
+
+
 ## License
 
 Copyright © 2017 Metabase, Inc
diff --git a/docs/operations-guide/running-metabase-on-heroku.md b/docs/operations-guide/running-metabase-on-heroku.md
index 64da2722111c700db0f202ac7b4a409712fd9e9d..4ef364c065b57c15af571904e7feae51328a7030 100644
--- a/docs/operations-guide/running-metabase-on-heroku.md
+++ b/docs/operations-guide/running-metabase-on-heroku.md
@@ -68,3 +68,10 @@ git push -f heroku master
 ```
 
 * Wait for the deploy to finish
+
+* If there have been no new changes to the metabase-deploy repository, you will need to add an empty commit. This triggers Heroku to re-deploy the code, fetching the newest version of Metabase in the process.
+
+```bash
+git commit --allow-empty -m "empty commit"
+git push -f heroku master
+```
diff --git a/docs/users-guide/10-pulses.md b/docs/users-guide/10-pulses.md
index f8e0bd600bd08092fb5d8d1fe4ed685f027f5108..dfd7c5fba0b1dab35ef2276d168e14a16e8b6ed2 100644
--- a/docs/users-guide/10-pulses.md
+++ b/docs/users-guide/10-pulses.md
@@ -23,15 +23,15 @@ When you select a saved question, Metabase will show you a preview of how it’l
 #### Attaching a .csv or .xls with results
 You can also optionally include the results of a saved question in an emailed pulse as a .csv or .xls file attachment. Just click the paperclip icon on an included saved question to add the attachment. Click the paperclip again to remove the attachment.
 
-![Attach button](images/pulses/attach-button.png)
+![Attach button](images/pulses/attachments/attach-button.png)
 
 Choose between a .csv or .xls file by clicking on the text buttons:
 
-![Attached](images/pulses/attached.png)
+![Attached](images/pulses/attachments/attached.png)
 
 Your attachments will be included in your emailed pulse just like a regular email attachment:
 
-![Email attachment](images/pulses/email.png)
+![Email attachment](images/pulses/attachments/email.png)
 
 #### Limitations
 Currently, there are a few restrictions on what kinds of saved questions you can put into a pulse:
diff --git a/docs/users-guide/13-sql-parameters.md b/docs/users-guide/13-sql-parameters.md
index bc36b96f8a3541764434a35e0fc3ddd193f19cb1..953543fb20b38b03359fd9a5aa7cab9ad115192c 100644
--- a/docs/users-guide/13-sql-parameters.md
+++ b/docs/users-guide/13-sql-parameters.md
@@ -34,7 +34,7 @@ WHERE {% raw %}{{created_at}}{% endraw %}
 ```
 
 ##### Creating SQL question filters using field filter variables
-First, insert a variable tag in your SQL, like `{{my_var}}`. Then, in the side panel, select the `Field Filter` variable type, and choose which field to map your variable to. In order to display a filter widget, you'll have to choose a field whose Type in the Data Model section of the Admin Panel is one of the following:
+First, insert a variable tag in your SQL, like `{% raw %}{{my_var}}{% endraw %}`. Then, in the side panel, select the `Field Filter` variable type, and choose which field to map your variable to. In order to display a filter widget, you'll have to choose a field whose Type in the Data Model section of the Admin Panel is one of the following:
 - Category
 - City
 - Entity Key
@@ -44,6 +44,7 @@ First, insert a variable tag in your SQL, like `{{my_var}}`. Then, in the side p
 - UNIX Timestamp (Seconds)
 - UNIX Timestamp (Milliseconds)
 - ZIP or Postal Code
+
 The field can also be a datetime one (which can be left as `No special type` in the Data Model).
 
 You'll then see a dropdown labeled `Widget`, which will let you choose the kind of filter widget you want on your question, which is especially useful for datetime fields (you can select `None` if you don't want a widget at all). **Note:** If you're not seeing the option to display a filter widget, make sure the mapped field is set to one of the above types, and then try manually syncing your database from the Databases section of the Admin Panel to force Metabase to scan and cache the field's values.
@@ -64,7 +65,7 @@ Filter widgets **can't** be displayed if the variable is mapped to a field marke
 If you input a default value for your field filter, this value will be selected in the filter whenever you come back to this question. If you clear out the filter, though, no value will be passed (i.e., not even the default value). The default value has no effect on the behavior of your SQL question when viewed in a dashboard.
 
 ##### Connecting a SQL question to a dashboard filter
-In order for a saved SQL question to be usable with a dashboard filter, it must contain at least one field filter. The kind of dashboard filter that can be used with the SQL question depends on the field that you map to the question's field filter(s). For example, if you have a field filter called `{{var}}` and you map it to a State field, you can map a Location dashboard filter to your SQL question. In this example, you'd create a new dashboard or go to an existing one, click the Edit button, and the SQL question that contains your State field filter, add a new dashboard filter or edit an existing Location filter, then click the dropdown on the SQL question card to see the State field filter. [Learn more about dashboard filters here](08-dashboard-filters.md).
+In order for a saved SQL question to be usable with a dashboard filter, it must contain at least one field filter. The kind of dashboard filter that can be used with the SQL question depends on the field that you map to the question's field filter(s). For example, if you have a field filter called `{% raw %}{{var}}{% endraw %}` and you map it to a State field, you can map a Location dashboard filter to your SQL question. In this example, you'd create a new dashboard or go to an existing one, click the Edit button, and the SQL question that contains your State field filter, add a new dashboard filter or edit an existing Location filter, then click the dropdown on the SQL question card to see the State field filter. [Learn more about dashboard filters here](08-dashboard-filters.md).
 
 ![Field filter](images/sql-parameters/state-field-filter.png)
 
diff --git a/frontend/interfaces/icepick.js b/frontend/interfaces/icepick.js
index 89eec22f31d3e40362d3e45e6332abf08fcdc98d..450651d22728ad8b2856263b722971480c22b284 100644
--- a/frontend/interfaces/icepick.js
+++ b/frontend/interfaces/icepick.js
@@ -2,16 +2,38 @@ type Key = string | number;
 type Value = any;
 
 declare module icepick {
-    declare function assoc<O:Object|Array<any>, K:Key, V:Value>(object: O, key: K, value: V): O;
-    declare function dissoc<O:Object|Array<any>, K:Key, V:Value>(object: O, key: K): O;
+  declare function assoc<O: Object | Array<any>, K: Key, V: Value>(
+    object: O,
+    key: K,
+    value: V,
+  ): O;
+  declare function dissoc<O: Object | Array<any>, K: Key, V: Value>(
+    object: O,
+    key: K,
+  ): O;
 
-    declare function getIn<O:Object|Array<any>, K:Key, V:Value>(object: ?O, path: Array<K>): ?V;
-    declare function setIn<O:Object|Array<any>, K:Key, V:Value>(object: O, path: Array<K>, value: V): O;
-    declare function assocIn<O:Object|Array<any>, K:Key, V:Value>(object: O, path: Array<K>, value: V): O;
-    declare function updateIn<O:Object|Array<any>, K:Key, V:Value>(object: O, path: Array<K>, callback: ((value: V) => V)): O;
+  declare function getIn<O: Object | Array<any>, K: Key, V: Value>(
+    object: ?O,
+    path: Array<K>,
+  ): ?V;
+  declare function setIn<O: Object | Array<any>, K: Key, V: Value>(
+    object: O,
+    path: Array<K>,
+    value: V,
+  ): O;
+  declare function assocIn<O: Object | Array<any>, K: Key, V: Value>(
+    object: O,
+    path: Array<K>,
+    value: V,
+  ): O;
+  declare function updateIn<O: Object | Array<any>, K: Key, V: Value>(
+    object: O,
+    path: Array<K>,
+    callback: (value: V) => V,
+  ): O;
 
-    declare function merge<O:Object|Array<any>>(object: O, other: O): O;
+  declare function merge<O: Object | Array<any>>(object: O, other: O): O;
 
-    // TODO: improve this
-    declare function chain<O:Object|Array<any>>(object: O): any;
+  // TODO: improve this
+  declare function chain<O: Object | Array<any>>(object: O): any;
 }
diff --git a/frontend/interfaces/redux-actions_v2.x.x.js b/frontend/interfaces/redux-actions_v2.x.x.js
index 728652bd8c67d850da489c38fa9c80f3f1008fd8..22056747b8deec34adeec1e6b0e3e910278fb571 100644
--- a/frontend/interfaces/redux-actions_v2.x.x.js
+++ b/frontend/interfaces/redux-actions_v2.x.x.js
@@ -1,7 +1,6 @@
 // Origin: https://github.com/flowtype/flow-typed/blob/master/definitions/npm/redux-actions_v2.x.x/flow_v0.34.x-/redux-actions_v2.x.x.js
 
-declare module 'redux-actions' {
-
+declare module "redux-actions" {
   /*
    * Use `ActionType` to get the type of the action created by a given action
    * creator. For example:
@@ -17,7 +16,6 @@ declare module 'redux-actions' {
   declare type ActionType<ActionCreator> = _ActionType<*, ActionCreator>;
   declare type _ActionType<R, Fn: (payload: *, ...rest: any[]) => R> = R;
 
-
   /*
    * To get the most from Flow type checking use a `payloadCreator` argument
    * with `createAction`. Make sure that Flow can infer the argument type of the
@@ -29,30 +27,39 @@ declare module 'redux-actions' {
    */
   declare function createAction<T, P>(
     type: T,
-    $?: empty  // hack to force Flow to not use this signature when more than one argument is given
+    $?: empty, // hack to force Flow to not use this signature when more than one argument is given
   ): (payload: P, ...rest: any[]) => { type: T, payload: P, error?: boolean };
 
   declare function createAction<T, P, P2>(
     type: T,
     payloadCreator: (_: P) => P2,
-    $?: empty
+    $?: empty,
   ): (payload: P, ...rest: any[]) => { type: T, payload: P2, error?: boolean };
 
   declare function createAction<T, P, P2, M>(
     type: T,
     payloadCreator: (_: P) => P2,
-    metaCreator: (_: P) => M
-  ): (payload: P, ...rest: any[]) => { type: T, payload: P2, error?: boolean, meta: M };
+    metaCreator: (_: P) => M,
+  ): (
+    payload: P,
+    ...rest: any[]
+  ) => { type: T, payload: P2, error?: boolean, meta: M };
 
   declare function createAction<T, P, M>(
     type: T,
     payloadCreator: null | void,
-    metaCreator: (_: P) => M
-  ): (payload: P, ...rest: any[]) => { type: T, payload: P, error?: boolean, meta: M };
+    metaCreator: (_: P) => M,
+  ): (
+    payload: P,
+    ...rest: any[]
+  ) => { type: T, payload: P, error?: boolean, meta: M };
 
   // `createActions` is quite difficult to write a type for. Maybe try not to
   // use this one?
-  declare function createActions(actionMap: Object, ...identityActions: string[]): Object;
+  declare function createActions(
+    actionMap: Object,
+    ...identityActions: string[]
+  ): Object;
   declare function createActions(...identityActions: string[]): Object;
 
   declare type Reducer<S, A> = (state: S, action: A) => S;
@@ -60,7 +67,7 @@ declare module 'redux-actions' {
   declare type ReducerMap<S, A> =
     | { next: Reducer<S, A> }
     | { throw: Reducer<S, A> }
-    | { next: Reducer<S, A>, throw: Reducer<S, A> }
+    | { next: Reducer<S, A>, throw: Reducer<S, A> };
 
   /*
    * To get full advantage from Flow, use a type annotation on the action
@@ -79,14 +86,17 @@ declare module 'redux-actions' {
   declare function handleAction<Type, State, Action: { type: Type }>(
     type: Type,
     reducer: Reducer<State, Action> | ReducerMap<State, Action>,
-    defaultState: State
+    defaultState: State,
   ): Reducer<State, Action>;
 
   declare function handleActions<State, Action>(
-    reducers: { [key: string]: Reducer<State, Action> | ReducerMap<State, Action> },
-    defaultState?: State
+    reducers: {
+      [key: string]: Reducer<State, Action> | ReducerMap<State, Action>,
+    },
+    defaultState?: State,
   ): Reducer<State, Action>;
 
-  declare function combineActions(...types: (string | Symbol | Function)[]) : string;
-
+  declare function combineActions(
+    ...types: (string | Symbol | Function)[]
+  ): string;
 }
diff --git a/frontend/interfaces/underscore.js b/frontend/interfaces/underscore.js
index 4c9edd5d120f3bcb168833766c6b521c0803d665..43d9d747f6583b79885d2742a9ac2e640cec3cae 100644
--- a/frontend/interfaces/underscore.js
+++ b/frontend/interfaces/underscore.js
@@ -1,9 +1,15 @@
 // type definitions for (some of) underscore
 
 declare module "underscore" {
-  declare function find<T>(list: ?T[], predicate: (val: T)=>boolean): ?T;
-  declare function findWhere<T>(list: ?T[], properties: {[key:string]: any}): ?T;
-  declare function findIndex<T>(list: ?T[], predicate: (val: T)=>boolean): number;
+  declare function find<T>(list: ?(T[]), predicate: (val: T) => boolean): ?T;
+  declare function findWhere<T>(
+    list: ?(T[]),
+    properties: { [key: string]: any },
+  ): ?T;
+  declare function findIndex<T>(
+    list: ?(T[]),
+    predicate: (val: T) => boolean,
+  ): number;
 
   declare function clone<T>(obj: T): T;
 
@@ -15,59 +21,95 @@ declare module "underscore" {
 
   declare function flatten<S>(a: Array<Array<S>>): S[];
 
-
-  declare function each<T>(o: {[key:string]: T}, iteratee: (val: T, key: string)=>void): void;
-  declare function each<T>(a: T[], iteratee: (val: T, key: string)=>void): void;
-
-  declare function map<T, U>(a: T[], iteratee: (val: T, n?: number)=>U): U[];
-  declare function map<K, T, U>(a: {[key:K]: T}, iteratee: (val: T, k?: K)=>U): U[];
+  declare function each<T>(
+    o: { [key: string]: T },
+    iteratee: (val: T, key: string) => void,
+  ): void;
+  declare function each<T>(
+    a: T[],
+    iteratee: (val: T, key: string) => void,
+  ): void;
+
+  declare function map<T, U>(a: T[], iteratee: (val: T, n?: number) => U): U[];
+  declare function map<K, T, U>(
+    a: { [key: K]: T },
+    iteratee: (val: T, k?: K) => U,
+  ): U[];
   declare function mapObject(
-        object: Object,
-        iteratee: (val: any, key: string) => Object,
-        context?: mixed
+    object: Object,
+    iteratee: (val: any, key: string) => Object,
+    context?: mixed,
   ): Object;
 
-  declare function object<T>(a: Array<[string, T]>): {[key:string]: T};
+  declare function object<T>(a: Array<[string, T]>): { [key: string]: T };
 
-  declare function every<T>(a: Array<T>, pred: (val: T)=>boolean): boolean;
-  declare function some<T>(a: Array<T>, pred: (val: T)=>boolean): boolean;
-  declare function all<T>(a: Array<T>, pred: (val: T)=>boolean): boolean;
-  declare function any<T>(a: Array<T>, pred: (val: T)=>boolean): boolean;
+  declare function every<T>(a: Array<T>, pred: (val: T) => boolean): boolean;
+  declare function some<T>(a: Array<T>, pred: (val: T) => boolean): boolean;
+  declare function all<T>(a: Array<T>, pred: (val: T) => boolean): boolean;
+  declare function any<T>(a: Array<T>, pred: (val: T) => boolean): boolean;
   declare function contains<T>(a: Array<T>, val: T): boolean;
 
   declare function initial<T>(a: Array<T>, n?: number): Array<T>;
   declare function rest<T>(a: Array<T>, index?: number): Array<T>;
 
-  declare function sortBy<T>(a: T[], iteratee: string|(val: T)=>any): T[];
+  declare function sortBy<T>(a: T[], iteratee: string | ((val: T) => any)): T[];
 
-  declare function filter<T>(o: {[key:string]: T}, pred: (val: T, k: string)=>boolean): T[];
+  declare function filter<T>(
+    o: { [key: string]: T },
+    pred: (val: T, k: string) => boolean,
+  ): T[];
 
   declare function isEmpty(o: any): boolean;
   declare function isString(o: any): boolean;
   declare function isObject(o: any): boolean;
   declare function isArray(o: any): boolean;
 
-  declare function groupBy<T>(a: Array<T>, iteratee: string|(val: T, index: number)=>any): {[key:string]: T[]};
+  declare function groupBy<T>(
+    a: Array<T>,
+    iteratee: string | ((val: T, index: number) => any),
+  ): { [key: string]: T[] };
 
-  declare function min<T>(a: Array<T>|{[key:any]: T}): T;
-  declare function max<T>(a: Array<T>|{[key:any]: T}): T;
+  declare function min<T>(a: Array<T> | { [key: any]: T }): T;
+  declare function max<T>(a: Array<T> | { [key: any]: T }): T;
 
   declare function uniq<T>(a: T[], iteratee?: (val: T) => boolean): T[];
-  declare function uniq<T>(a: T[], isSorted: boolean, iteratee?: (val: T) => boolean): T[];
-
-  declare function values<T>(o: {[key: any]: T}): T[];
-  declare function omit(o: {[key: any]: any}, ...properties: string[]): {[key: any]: any};
-  declare function omit(o: {[key: any]: any}, predicate: (val: any, key: any, object: {[key: any]: any})=>boolean): {[key: any]: any};
-  declare function pick(o: {[key: any]: any}, ...properties: string[]): {[key: any]: any};
-  declare function pick(o: {[key: any]: any}, predicate: (val: any, key: any, object: {[key: any]: any})=>boolean): {[key: any]: any};
-  declare function pluck(o: Array<{[key: any]: any}>, propertyNames: string): Array<any>;
-  declare function has(o: {[key: any]: any}, ...properties: string[]): boolean;
+  declare function uniq<T>(
+    a: T[],
+    isSorted: boolean,
+    iteratee?: (val: T) => boolean,
+  ): T[];
+
+  declare function values<T>(o: { [key: any]: T }): T[];
+  declare function omit(
+    o: { [key: any]: any },
+    ...properties: string[]
+  ): { [key: any]: any };
+  declare function omit(
+    o: { [key: any]: any },
+    predicate: (val: any, key: any, object: { [key: any]: any }) => boolean,
+  ): { [key: any]: any };
+  declare function pick(
+    o: { [key: any]: any },
+    ...properties: string[]
+  ): { [key: any]: any };
+  declare function pick(
+    o: { [key: any]: any },
+    predicate: (val: any, key: any, object: { [key: any]: any }) => boolean,
+  ): { [key: any]: any };
+  declare function pluck(
+    o: Array<{ [key: any]: any }>,
+    propertyNames: string,
+  ): Array<any>;
+  declare function has(
+    o: { [key: any]: any },
+    ...properties: string[]
+  ): boolean;
 
   declare function difference<T>(array: T[], ...others: T[][]): T[];
 
   declare function flatten(a: Array<any>): Array<any>;
 
-  declare function debounce<T: (any) => any>(func: T): T;
+  declare function debounce<T: any => any>(func: T): T;
 
   // TODO: improve this
   declare function chain<S>(obj: S): any;
diff --git a/frontend/src/metabase-lib/lib/Action.js b/frontend/src/metabase-lib/lib/Action.js
index c42268a3e5822c358ee9eb0e990b86af18574070..c05d4fe336a9345a33f69fea2024962a40ef27e9 100644
--- a/frontend/src/metabase-lib/lib/Action.js
+++ b/frontend/src/metabase-lib/lib/Action.js
@@ -1,7 +1,7 @@
 /* @flow weak */
 
 export default class Action {
-    perform() {}
+  perform() {}
 }
 
 export class ActionClick {}
diff --git a/frontend/src/metabase-lib/lib/Alert.js b/frontend/src/metabase-lib/lib/Alert.js
index 67f14fd16b0cd85e5f8f3ea70c26d886c08c1428..9560959a3dff5c6484cfeaef5f291f406d93a208 100644
--- a/frontend/src/metabase-lib/lib/Alert.js
+++ b/frontend/src/metabase-lib/lib/Alert.js
@@ -3,34 +3,35 @@ export const ALERT_TYPE_TIMESERIES_GOAL = "alert-type-timeseries-goal";
 export const ALERT_TYPE_PROGRESS_BAR_GOAL = "alert-type-progress-bar-goal";
 
 export type AlertType =
-    | ALERT_TYPE_ROWS
-    | ALERT_TYPE_TIMESERIES_GOAL
-    | ALERT_TYPE_PROGRESS_BAR_GOAL;
+  | ALERT_TYPE_ROWS
+  | ALERT_TYPE_TIMESERIES_GOAL
+  | ALERT_TYPE_PROGRESS_BAR_GOAL;
 
 export const getDefaultAlert = (question, user, visualizationSettings) => {
-    const alertType = question.alertType(visualizationSettings);
+  const alertType = question.alertType(visualizationSettings);
 
-    const typeDependentAlertFields = alertType === ALERT_TYPE_ROWS
-        ? { alert_condition: "rows", alert_first_only: false }
-        : {
-              alert_condition: "goal",
-              alert_first_only: true,
-              alert_above_goal: true
-          };
+  const typeDependentAlertFields =
+    alertType === ALERT_TYPE_ROWS
+      ? { alert_condition: "rows", alert_first_only: false }
+      : {
+          alert_condition: "goal",
+          alert_first_only: true,
+          alert_above_goal: true,
+        };
 
-    const defaultEmailChannel = {
-        enabled: true,
-        channel_type: "email",
-        recipients: [user],
-        schedule_day: "mon",
-        schedule_frame: null,
-        schedule_hour: 0,
-        schedule_type: "daily"
-    };
+  const defaultEmailChannel = {
+    enabled: true,
+    channel_type: "email",
+    recipients: [user],
+    schedule_day: "mon",
+    schedule_frame: null,
+    schedule_hour: 0,
+    schedule_type: "daily",
+  };
 
-    return {
-        card: { id: question.id() },
-        channels: [defaultEmailChannel],
-        ...typeDependentAlertFields
-    };
+  return {
+    card: { id: question.id() },
+    channels: [defaultEmailChannel],
+    ...typeDependentAlertFields,
+  };
 };
diff --git a/frontend/src/metabase-lib/lib/Dashboard.js b/frontend/src/metabase-lib/lib/Dashboard.js
index bc10bd91865d1c40e775691235cd942e0ad28597..d48812c102511d5e8e4b62b824967fe4659e8313 100644
--- a/frontend/src/metabase-lib/lib/Dashboard.js
+++ b/frontend/src/metabase-lib/lib/Dashboard.js
@@ -1,3 +1,3 @@
 export default class Dashboard {
-    getParameters() {}
+  getParameters() {}
 }
diff --git a/frontend/src/metabase-lib/lib/Dimension.js b/frontend/src/metabase-lib/lib/Dimension.js
index 39ec3621b811dcb5d0f8805949f99352bbd51dc2..1b2adcfff12a268a5126cc4e80744168005d3140 100644
--- a/frontend/src/metabase-lib/lib/Dimension.js
+++ b/frontend/src/metabase-lib/lib/Dimension.js
@@ -11,12 +11,12 @@ import Field from "./metadata/Field";
 import Metadata from "./metadata/Metadata";
 
 import type {
-    ConcreteField,
-    LocalFieldReference,
-    ForeignFieldReference,
-    DatetimeField,
-    ExpressionReference,
-    DatetimeUnit
+  ConcreteField,
+  LocalFieldReference,
+  ForeignFieldReference,
+  DatetimeField,
+  ExpressionReference,
+  DatetimeUnit,
 } from "metabase/meta/types/Query";
 
 import type { IconName } from "metabase/meta/types";
@@ -25,8 +25,8 @@ import type { IconName } from "metabase/meta/types";
  * A dimension option returned by the query_metadata API
  */
 type DimensionOption = {
-    mbql: any,
-    name?: string
+  mbql: any,
+  name?: string,
 };
 
 /**
@@ -38,250 +38,252 @@ type DimensionOption = {
  * @abstract
  */
 export default class Dimension {
-    _parent: ?Dimension;
-    _args: any;
-    _metadata: ?Metadata;
-
-    // Display names provided by the backend
-    _subDisplayName: ?String;
-    _subTriggerDisplayName: ?String;
-
-    /**
-     * Dimension constructor
-     */
-    constructor(
-        parent: ?Dimension,
-        args: any[],
-        metadata?: Metadata
-    ): Dimension {
-        this._parent = parent;
-        this._args = args;
-        this._metadata = metadata || (parent && parent._metadata);
-    }
-
-    /**
-     * Parses an MBQL expression into an appropriate Dimension subclass, if possible.
-     * Metadata should be provided if you intend to use the display name or render methods.
-     */
-    static parseMBQL(mbql: ConcreteField, metadata?: Metadata): ?Dimension {
-        for (const D of DIMENSION_TYPES) {
-            const dimension = D.parseMBQL(mbql, metadata);
-            if (dimension != null) {
-                return dimension;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * Returns true if these two dimensions are identical to one another.
-     */
-    static isEqual(a: ?Dimension | ConcreteField, b: ?Dimension): boolean {
-        let dimensionA: ?Dimension = a instanceof Dimension
-            ? a
-            : // $FlowFixMe
-              Dimension.parseMBQL(a, this._metadata);
-        let dimensionB: ?Dimension = b instanceof Dimension
-            ? b
-            : // $FlowFixMe
-              Dimension.parseMBQL(b, this._metadata);
-        return !!dimensionA && !!dimensionB && dimensionA.isEqual(dimensionB);
-    }
-
-    /**
-     * Sub-dimensions for the provided dimension of this type.
-     * @abstract
-     */
-    // TODO Atte Keinänen 5/21/17: Rename either this or the instance method with the same name
-    // Also making it clear in the method name that we're working with sub-dimensions would be good
-    static dimensions(parent: Dimension): Dimension[] {
-        return [];
-    }
-
-    /**
-     * The default sub-dimension for the provided dimension of this type, if any.
-     * @abstract
-     */
-    static defaultDimension(parent: Dimension): ?Dimension {
-        return null;
-    }
-
-    /**
-     * Returns "sub-dimensions" of this dimension.
-     * @abstract
-     */
-    // TODO Atte Keinänen 5/21/17: Rename either this or the static method with the same name
-    // Also making it clear in the method name that we're working with sub-dimensions would be good
-    dimensions(DimensionTypes?: typeof Dimension[]): Dimension[] {
-        const dimensionOptions = this.field().dimension_options;
-        if (!DimensionTypes && dimensionOptions) {
-            return dimensionOptions.map(option =>
-                this._dimensionForOption(option));
-        } else {
-            return [].concat(
-                ...(DimensionTypes || [])
-                    .map(DimensionType => DimensionType.dimensions(this))
-            );
-        }
-    }
-
-    /**
-     * Returns the default sub-dimension of this dimension, if any.
-     * @abstract
-     */
-    defaultDimension(DimensionTypes: any[] = DIMENSION_TYPES): ?Dimension {
-        const defaultDimensionOption = this.field().default_dimension_option;
-        if (defaultDimensionOption) {
-            return this._dimensionForOption(defaultDimensionOption);
-        } else {
-            for (const DimensionType of DimensionTypes) {
-                const defaultDimension = DimensionType.defaultDimension(this);
-                if (defaultDimension) {
-                    return defaultDimension;
-                }
-            }
-        }
-
-        return null;
-    }
-
-    // Internal method gets a Dimension from a DimensionOption
-    _dimensionForOption(option: DimensionOption) {
-        // fill in the parent field ref
-        const fieldRef = this.baseDimension().mbql();
-        let mbql = option.mbql;
-        if (mbql) {
-            mbql = [mbql[0], fieldRef, ...mbql.slice(2)];
-        } else {
-            mbql = fieldRef;
-        }
-        let dimension = Dimension.parseMBQL(mbql, this._metadata);
-        if (option.name) {
-            dimension._subDisplayName = option.name;
-            dimension._subTriggerDisplayName = option.name;
-        }
+  _parent: ?Dimension;
+  _args: any;
+  _metadata: ?Metadata;
+
+  // Display names provided by the backend
+  _subDisplayName: ?String;
+  _subTriggerDisplayName: ?String;
+
+  /**
+   * Dimension constructor
+   */
+  constructor(parent: ?Dimension, args: any[], metadata?: Metadata): Dimension {
+    this._parent = parent;
+    this._args = args;
+    this._metadata = metadata || (parent && parent._metadata);
+  }
+
+  /**
+   * Parses an MBQL expression into an appropriate Dimension subclass, if possible.
+   * Metadata should be provided if you intend to use the display name or render methods.
+   */
+  static parseMBQL(mbql: ConcreteField, metadata?: Metadata): ?Dimension {
+    for (const D of DIMENSION_TYPES) {
+      const dimension = D.parseMBQL(mbql, metadata);
+      if (dimension != null) {
         return dimension;
-    }
-
-    /**
-     * Is this dimension idential to another dimension or MBQL clause
-     */
-    isEqual(other: ?Dimension | ConcreteField): boolean {
-        if (other == null) {
-            return false;
-        }
-
-        let otherDimension: ?Dimension = other instanceof Dimension
-            ? other
-            : Dimension.parseMBQL(other, this._metadata);
-        if (!otherDimension) {
-            return false;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns true if these two dimensions are identical to one another.
+   */
+  static isEqual(a: ?Dimension | ConcreteField, b: ?Dimension): boolean {
+    let dimensionA: ?Dimension =
+      a instanceof Dimension
+        ? a
+        : // $FlowFixMe
+          Dimension.parseMBQL(a, this._metadata);
+    let dimensionB: ?Dimension =
+      b instanceof Dimension
+        ? b
+        : // $FlowFixMe
+          Dimension.parseMBQL(b, this._metadata);
+    return !!dimensionA && !!dimensionB && dimensionA.isEqual(dimensionB);
+  }
+
+  /**
+   * Sub-dimensions for the provided dimension of this type.
+   * @abstract
+   */
+  // TODO Atte Keinänen 5/21/17: Rename either this or the instance method with the same name
+  // Also making it clear in the method name that we're working with sub-dimensions would be good
+  static dimensions(parent: Dimension): Dimension[] {
+    return [];
+  }
+
+  /**
+   * The default sub-dimension for the provided dimension of this type, if any.
+   * @abstract
+   */
+  static defaultDimension(parent: Dimension): ?Dimension {
+    return null;
+  }
+
+  /**
+   * Returns "sub-dimensions" of this dimension.
+   * @abstract
+   */
+  // TODO Atte Keinänen 5/21/17: Rename either this or the static method with the same name
+  // Also making it clear in the method name that we're working with sub-dimensions would be good
+  dimensions(DimensionTypes?: typeof Dimension[]): Dimension[] {
+    const dimensionOptions = this.field().dimension_options;
+    if (!DimensionTypes && dimensionOptions) {
+      return dimensionOptions.map(option => this._dimensionForOption(option));
+    } else {
+      return [].concat(
+        ...(DimensionTypes || []).map(DimensionType =>
+          DimensionType.dimensions(this),
+        ),
+      );
+    }
+  }
+
+  /**
+   * Returns the default sub-dimension of this dimension, if any.
+   * @abstract
+   */
+  defaultDimension(DimensionTypes: any[] = DIMENSION_TYPES): ?Dimension {
+    const defaultDimensionOption = this.field().default_dimension_option;
+    if (defaultDimensionOption) {
+      return this._dimensionForOption(defaultDimensionOption);
+    } else {
+      for (const DimensionType of DimensionTypes) {
+        const defaultDimension = DimensionType.defaultDimension(this);
+        if (defaultDimension) {
+          return defaultDimension;
         }
-        // must be instace of the same class
-        if (this.constructor !== otherDimension.constructor) {
-            return false;
-        }
-        // must both or neither have a parent
-        if (!this._parent !== !otherDimension._parent) {
-            return false;
-        }
-        // parents must be equal
-        if (this._parent && !this._parent.isEqual(otherDimension._parent)) {
-            return false;
-        }
-        // args must be equal
-        if (!_.isEqual(this._args, otherDimension._args)) {
-            return false;
-        }
-        return true;
-    }
-
-    /**
-     * Does this dimension have the same underlying base dimension, typically a field
-     */
-    isSameBaseDimension(other: ?Dimension | ConcreteField): boolean {
-        if (other == null) {
-            return false;
-        }
-
-        let otherDimension: ?Dimension = other instanceof Dimension
-            ? other
-            : Dimension.parseMBQL(other, this._metadata);
-
-        const baseDimensionA = this.baseDimension();
-        const baseDimensionB = otherDimension && otherDimension.baseDimension();
-
-        return !!baseDimensionA &&
-            !!baseDimensionB &&
-            baseDimensionA.isEqual(baseDimensionB);
-    }
-
-    /**
-     * The base dimension of this dimension, typically a field. May return itself.
-     */
-    baseDimension(): Dimension {
-        return this;
-    }
-
-    /**
-     * The underlying field for this dimension
-     */
-    field(): Field {
-        return new Field();
-    }
-
-    /**
-     * Valid operators on this dimension
-     */
-    operators() {
-        return this.field().operators || [];
-    }
-
-    /**
-     * The operator with the provided operator name (e.x. `=`, `<`, etc)
-     */
-    operator(op) {
-        return this.field().operator(op);
-    }
-
-    /**
-     * The display name of this dimension, e.x. the field's display_name
-     * @abstract
-     */
-    displayName(): string {
-        return "";
-    }
-
-    /**
-     * The name to be shown when this dimension is being displayed as a sub-dimension of another
-     * @abstract
-     */
-    subDisplayName(): string {
-        return this._subDisplayName || "";
-    }
-
-    /**
-     * A shorter version of subDisplayName, e.x. to be shown in the dimension picker trigger
-     * @abstract
-     */
-    subTriggerDisplayName(): string {
-        return this._subTriggerDisplayName || "";
-    }
-
-    /**
-     * An icon name representing this dimension's type, to be used in the <Icon> component.
-     * @abstract
-     */
-    icon(): ?IconName {
-        return null;
-    }
-
-    /**
-     * Renders a dimension to React
-     */
-    render(): ?React$Element<any> {
-        return [this.displayName()];
-    }
+      }
+    }
+
+    return null;
+  }
+
+  // Internal method gets a Dimension from a DimensionOption
+  _dimensionForOption(option: DimensionOption) {
+    // fill in the parent field ref
+    const fieldRef = this.baseDimension().mbql();
+    let mbql = option.mbql;
+    if (mbql) {
+      mbql = [mbql[0], fieldRef, ...mbql.slice(2)];
+    } else {
+      mbql = fieldRef;
+    }
+    let dimension = Dimension.parseMBQL(mbql, this._metadata);
+    if (option.name) {
+      dimension._subDisplayName = option.name;
+      dimension._subTriggerDisplayName = option.name;
+    }
+    return dimension;
+  }
+
+  /**
+   * Is this dimension idential to another dimension or MBQL clause
+   */
+  isEqual(other: ?Dimension | ConcreteField): boolean {
+    if (other == null) {
+      return false;
+    }
+
+    let otherDimension: ?Dimension =
+      other instanceof Dimension
+        ? other
+        : Dimension.parseMBQL(other, this._metadata);
+    if (!otherDimension) {
+      return false;
+    }
+    // must be instace of the same class
+    if (this.constructor !== otherDimension.constructor) {
+      return false;
+    }
+    // must both or neither have a parent
+    if (!this._parent !== !otherDimension._parent) {
+      return false;
+    }
+    // parents must be equal
+    if (this._parent && !this._parent.isEqual(otherDimension._parent)) {
+      return false;
+    }
+    // args must be equal
+    if (!_.isEqual(this._args, otherDimension._args)) {
+      return false;
+    }
+    return true;
+  }
+
+  /**
+   * Does this dimension have the same underlying base dimension, typically a field
+   */
+  isSameBaseDimension(other: ?Dimension | ConcreteField): boolean {
+    if (other == null) {
+      return false;
+    }
+
+    let otherDimension: ?Dimension =
+      other instanceof Dimension
+        ? other
+        : Dimension.parseMBQL(other, this._metadata);
+
+    const baseDimensionA = this.baseDimension();
+    const baseDimensionB = otherDimension && otherDimension.baseDimension();
+
+    return (
+      !!baseDimensionA &&
+      !!baseDimensionB &&
+      baseDimensionA.isEqual(baseDimensionB)
+    );
+  }
+
+  /**
+   * The base dimension of this dimension, typically a field. May return itself.
+   */
+  baseDimension(): Dimension {
+    return this;
+  }
+
+  /**
+   * The underlying field for this dimension
+   */
+  field(): Field {
+    return new Field();
+  }
+
+  /**
+   * Valid operators on this dimension
+   */
+  operators() {
+    return this.field().operators || [];
+  }
+
+  /**
+   * The operator with the provided operator name (e.x. `=`, `<`, etc)
+   */
+  operator(op) {
+    return this.field().operator(op);
+  }
+
+  /**
+   * The display name of this dimension, e.x. the field's display_name
+   * @abstract
+   */
+  displayName(): string {
+    return "";
+  }
+
+  /**
+   * The name to be shown when this dimension is being displayed as a sub-dimension of another
+   * @abstract
+   */
+  subDisplayName(): string {
+    return this._subDisplayName || "";
+  }
+
+  /**
+   * A shorter version of subDisplayName, e.x. to be shown in the dimension picker trigger
+   * @abstract
+   */
+  subTriggerDisplayName(): string {
+    return this._subTriggerDisplayName || "";
+  }
+
+  /**
+   * An icon name representing this dimension's type, to be used in the <Icon> component.
+   * @abstract
+   */
+  icon(): ?IconName {
+    return null;
+  }
+
+  /**
+   * Renders a dimension to React
+   */
+  render(): ?React$Element<any> {
+    return [this.displayName()];
+  }
 }
 
 /**
@@ -289,286 +291,285 @@ export default class Dimension {
  * @abstract
  */
 export class FieldDimension extends Dimension {
-    field(): Field {
-        if (this._parent instanceof FieldDimension) {
-            return this._parent.field();
-        }
-        return new Field();
-    }
-
-    displayName(): string {
-        return stripId(
-            Query_DEPRECATED.getFieldPathName(
-                this.field().id,
-                this.field().table
-            )
-        );
-    }
-
-    subDisplayName(): string {
-        if (this._subDisplayName) {
-            return this._subTriggerDisplayName;
-        } else if (this._parent) {
-            // TODO Atte Keinänen 8/1/17: Is this used at all?
-            // foreign key, show the field name
-            return this.field().display_name;
-        } else {
-            // TODO Atte Keinänen 8/1/17: Is this used at all?
-            return "Default";
-        }
-    }
-
-    subTriggerDisplayName(): string {
-        if (this.defaultDimension() instanceof BinnedDimension) {
-            return "Unbinned";
-        } else {
-            return "";
-        }
-    }
-
-    icon() {
-        return this.field().icon();
-    }
+  field(): Field {
+    if (this._parent instanceof FieldDimension) {
+      return this._parent.field();
+    }
+    return new Field();
+  }
+
+  displayName(): string {
+    return stripId(
+      Query_DEPRECATED.getFieldPathName(this.field().id, this.field().table),
+    );
+  }
+
+  subDisplayName(): string {
+    if (this._subDisplayName) {
+      return this._subTriggerDisplayName;
+    } else if (this._parent) {
+      // TODO Atte Keinänen 8/1/17: Is this used at all?
+      // foreign key, show the field name
+      return this.field().display_name;
+    } else {
+      // TODO Atte Keinänen 8/1/17: Is this used at all?
+      return "Default";
+    }
+  }
+
+  subTriggerDisplayName(): string {
+    if (this.defaultDimension() instanceof BinnedDimension) {
+      return "Unbinned";
+    } else {
+      return "";
+    }
+  }
+
+  icon() {
+    return this.field().icon();
+  }
 }
 
 /**
  * Field ID-based dimension, `["field-id", field-id]`
  */
 export class FieldIDDimension extends FieldDimension {
-    static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata) {
-        if (typeof mbql === "number") {
-            // DEPRECATED: bare field id
-            return new FieldIDDimension(null, [mbql], metadata);
-        } else if (Array.isArray(mbql) && mbqlEq(mbql[0], "field-id")) {
-            return new FieldIDDimension(null, mbql.slice(1), metadata);
-        }
-        return null;
-    }
-
-    mbql(): LocalFieldReference {
-        return ["field-id", this._args[0]];
-    }
-
-    field() {
-        return (this._metadata && this._metadata.fields[this._args[0]]) ||
-            new Field();
-    }
+  static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata) {
+    if (typeof mbql === "number") {
+      // DEPRECATED: bare field id
+      return new FieldIDDimension(null, [mbql], metadata);
+    } else if (Array.isArray(mbql) && mbqlEq(mbql[0], "field-id")) {
+      return new FieldIDDimension(null, mbql.slice(1), metadata);
+    }
+    return null;
+  }
+
+  mbql(): LocalFieldReference {
+    return ["field-id", this._args[0]];
+  }
+
+  field() {
+    return (
+      (this._metadata && this._metadata.fields[this._args[0]]) || new Field()
+    );
+  }
 }
 
 /**
  * Foreign key-based dimension, `["fk->", fk-field-id, dest-field-id]`
  */
 export class FKDimension extends FieldDimension {
-    static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata): ?Dimension {
-        if (Array.isArray(mbql) && mbqlEq(mbql[0], "fk->")) {
-            // $FlowFixMe
-            const fkRef: ForeignFieldReference = mbql;
-            const parent = Dimension.parseMBQL(fkRef[1], metadata);
-            return new FKDimension(parent, fkRef.slice(2));
-        }
-        return null;
-    }
-
-    static dimensions(parent: Dimension): Dimension[] {
-        if (parent instanceof FieldDimension) {
-            const field = parent.field();
-            if (field.target && field.target.table) {
-                return field.target.table.fields.map(
-                    field => new FKDimension(parent, [field.id])
-                );
-            }
-        }
-        return [];
-    }
-
-    mbql(): ForeignFieldReference {
-        // TODO: not sure `this._parent._args[0]` is the best way to handle this?
-        // we don't want the `["field-id", ...]` wrapper from the `this._parent.mbql()`
-        return ["fk->", this._parent._args[0], this._args[0]];
-    }
-
-    field() {
-        return (this._metadata && this._metadata.fields[this._args[0]]) ||
-            new Field();
-    }
-
-    render() {
-        return [
-            stripId(this._parent.field().display_name),
-            <Icon name="connections" className="px1" size={10} />,
-            this.field().display_name
-        ];
-    }
+  static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata): ?Dimension {
+    if (Array.isArray(mbql) && mbqlEq(mbql[0], "fk->")) {
+      // $FlowFixMe
+      const fkRef: ForeignFieldReference = mbql;
+      const parent = Dimension.parseMBQL(fkRef[1], metadata);
+      return new FKDimension(parent, fkRef.slice(2));
+    }
+    return null;
+  }
+
+  static dimensions(parent: Dimension): Dimension[] {
+    if (parent instanceof FieldDimension) {
+      const field = parent.field();
+      if (field.target && field.target.table) {
+        return field.target.table.fields.map(
+          field => new FKDimension(parent, [field.id]),
+        );
+      }
+    }
+    return [];
+  }
+
+  mbql(): ForeignFieldReference {
+    // TODO: not sure `this._parent._args[0]` is the best way to handle this?
+    // we don't want the `["field-id", ...]` wrapper from the `this._parent.mbql()`
+    return ["fk->", this._parent._args[0], this._args[0]];
+  }
+
+  field() {
+    return (
+      (this._metadata && this._metadata.fields[this._args[0]]) || new Field()
+    );
+  }
+
+  render() {
+    return [
+      stripId(this._parent.field().display_name),
+      <Icon name="connections" className="px1" size={10} />,
+      this.field().display_name,
+    ];
+  }
 }
 
 import { DATETIME_UNITS, formatBucketing } from "metabase/lib/query_time";
 
 const isFieldDimension = dimension =>
-    dimension instanceof FieldIDDimension || dimension instanceof FKDimension;
+  dimension instanceof FieldIDDimension || dimension instanceof FKDimension;
 
 /**
  * DatetimeField dimension, `["datetime-field", field-reference, datetime-unit]`
  */
 export class DatetimeFieldDimension extends FieldDimension {
-    static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata): ?Dimension {
-        if (Array.isArray(mbql) && mbqlEq(mbql[0], "datetime-field")) {
-            const parent = Dimension.parseMBQL(mbql[1], metadata);
-            // DEPRECATED: ["datetime-field", id, "of", unit]
-            if (mbql.length === 4) {
-                return new DatetimeFieldDimension(parent, mbql.slice(3));
-            } else {
-                return new DatetimeFieldDimension(parent, mbql.slice(2));
-            }
-        }
-        return null;
-    }
-
-    static dimensions(parent: Dimension): Dimension[] {
-        if (isFieldDimension(parent) && parent.field().isDate()) {
-            return DATETIME_UNITS.map(
-                unit => new DatetimeFieldDimension(parent, [unit])
-            );
-        }
-        return [];
-    }
-
-    static defaultDimension(parent: Dimension): ?Dimension {
-        if (isFieldDimension(parent) && parent.field().isDate()) {
-            return new DatetimeFieldDimension(parent, ["day"]);
-        }
-        return null;
-    }
-
-    mbql(): DatetimeField {
-        return ["datetime-field", this._parent.mbql(), this._args[0]];
-    }
-
-    baseDimension(): Dimension {
-        return this._parent.baseDimension();
-    }
-
-    bucketing(): DatetimeUnit {
-        return this._args[0];
-    }
-
-    subDisplayName(): string {
-        return formatBucketing(this._args[0]);
-    }
-
-    subTriggerDisplayName(): string {
-        return "by " + formatBucketing(this._args[0]).toLowerCase();
-    }
-
-    render() {
-        return [...super.render(), ": ", this.subDisplayName()];
-    }
+  static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata): ?Dimension {
+    if (Array.isArray(mbql) && mbqlEq(mbql[0], "datetime-field")) {
+      const parent = Dimension.parseMBQL(mbql[1], metadata);
+      // DEPRECATED: ["datetime-field", id, "of", unit]
+      if (mbql.length === 4) {
+        return new DatetimeFieldDimension(parent, mbql.slice(3));
+      } else {
+        return new DatetimeFieldDimension(parent, mbql.slice(2));
+      }
+    }
+    return null;
+  }
+
+  static dimensions(parent: Dimension): Dimension[] {
+    if (isFieldDimension(parent) && parent.field().isDate()) {
+      return DATETIME_UNITS.map(
+        unit => new DatetimeFieldDimension(parent, [unit]),
+      );
+    }
+    return [];
+  }
+
+  static defaultDimension(parent: Dimension): ?Dimension {
+    if (isFieldDimension(parent) && parent.field().isDate()) {
+      return new DatetimeFieldDimension(parent, ["day"]);
+    }
+    return null;
+  }
+
+  mbql(): DatetimeField {
+    return ["datetime-field", this._parent.mbql(), this._args[0]];
+  }
+
+  baseDimension(): Dimension {
+    return this._parent.baseDimension();
+  }
+
+  bucketing(): DatetimeUnit {
+    return this._args[0];
+  }
+
+  subDisplayName(): string {
+    return formatBucketing(this._args[0]);
+  }
+
+  subTriggerDisplayName(): string {
+    return "by " + formatBucketing(this._args[0]).toLowerCase();
+  }
+
+  render() {
+    return [...super.render(), ": ", this.subDisplayName()];
+  }
 }
 
 /**
  * Binned dimension, `["binning-strategy", field-reference, strategy, ...args]`
  */
 export class BinnedDimension extends FieldDimension {
-    static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata) {
-        if (Array.isArray(mbql) && mbqlEq(mbql[0], "binning-strategy")) {
-            const parent = Dimension.parseMBQL(mbql[1], metadata);
-            return new BinnedDimension(parent, mbql.slice(2));
-        }
-        return null;
-    }
-
-    static dimensions(parent: Dimension): Dimension[] {
-        // Subdimensions are are provided by the backend through the dimension_options field property
-        return [];
-    }
-
-    mbql() {
-        return ["binning-strategy", this._parent.mbql(), ...this._args];
-    }
-
-    baseDimension(): Dimension {
-        return this._parent.baseDimension();
-    }
-
-    subTriggerDisplayName(): string {
-        if (this._args[0] === "num-bins") {
-            return `${this._args[1]} ${inflect("bins", this._args[1])}`;
-        } else if (this._args[0] === "bin-width") {
-            const binWidth = this._args[1];
-            const units = this.field().isCoordinate() ? "°" : "";
-            return `${binWidth}${units}`;
-        } else {
-            return "Auto binned";
-        }
-    }
-
-    render() {
-        return [...super.render(), ": ", this.subTriggerDisplayName()];
-    }
+  static parseMBQL(mbql: ConcreteField, metadata?: ?Metadata) {
+    if (Array.isArray(mbql) && mbqlEq(mbql[0], "binning-strategy")) {
+      const parent = Dimension.parseMBQL(mbql[1], metadata);
+      return new BinnedDimension(parent, mbql.slice(2));
+    }
+    return null;
+  }
+
+  static dimensions(parent: Dimension): Dimension[] {
+    // Subdimensions are are provided by the backend through the dimension_options field property
+    return [];
+  }
+
+  mbql() {
+    return ["binning-strategy", this._parent.mbql(), ...this._args];
+  }
+
+  baseDimension(): Dimension {
+    return this._parent.baseDimension();
+  }
+
+  subTriggerDisplayName(): string {
+    if (this._args[0] === "num-bins") {
+      return `${this._args[1]} ${inflect("bins", this._args[1])}`;
+    } else if (this._args[0] === "bin-width") {
+      const binWidth = this._args[1];
+      const units = this.field().isCoordinate() ? "°" : "";
+      return `${binWidth}${units}`;
+    } else {
+      return "Auto binned";
+    }
+  }
+
+  render() {
+    return [...super.render(), ": ", this.subTriggerDisplayName()];
+  }
 }
 
 /**
  * Expression reference, `["expression", expression-name]`
  */
 export class ExpressionDimension extends Dimension {
-    tag = "Custom";
+  tag = "Custom";
 
-    static parseMBQL(mbql: any, metadata?: ?Metadata): ?Dimension {
-        if (Array.isArray(mbql) && mbqlEq(mbql[0], "expression")) {
-            return new ExpressionDimension(null, mbql.slice(1));
-        }
+  static parseMBQL(mbql: any, metadata?: ?Metadata): ?Dimension {
+    if (Array.isArray(mbql) && mbqlEq(mbql[0], "expression")) {
+      return new ExpressionDimension(null, mbql.slice(1));
     }
+  }
 
-    mbql(): ExpressionReference {
-        return ["expression", this._args[0]];
-    }
+  mbql(): ExpressionReference {
+    return ["expression", this._args[0]];
+  }
 
-    displayName(): string {
-        return this._args[0];
-    }
+  displayName(): string {
+    return this._args[0];
+  }
 
-    icon(): IconName {
-        // TODO: eventually will need to get the type from the return type of the expression
-        return "int";
-    }
+  icon(): IconName {
+    // TODO: eventually will need to get the type from the return type of the expression
+    return "int";
+  }
 }
 
 /**
  * Aggregation reference, `["aggregation", aggregation-index]`
  */
 export class AggregationDimension extends Dimension {
-    static parseMBQL(mbql: any, metadata?: ?Metadata): ?Dimension {
-        if (Array.isArray(mbql) && mbqlEq(mbql[0], "aggregation")) {
-            return new AggregationDimension(null, mbql.slice(1));
-        }
+  static parseMBQL(mbql: any, metadata?: ?Metadata): ?Dimension {
+    if (Array.isArray(mbql) && mbqlEq(mbql[0], "aggregation")) {
+      return new AggregationDimension(null, mbql.slice(1));
     }
+  }
 
-    constructor(parent, args, metadata, displayName) {
-        super(parent, args, metadata);
-        this._displayName = displayName;
-    }
+  constructor(parent, args, metadata, displayName) {
+    super(parent, args, metadata);
+    this._displayName = displayName;
+  }
 
-    displayName(): string {
-        return this._displayName;
-    }
+  displayName(): string {
+    return this._displayName;
+  }
 
-    aggregationIndex(): number {
-        return this._args[0];
-    }
+  aggregationIndex(): number {
+    return this._args[0];
+  }
 
-    mbql() {
-        return ["aggregation", this._args[0]];
-    }
+  mbql() {
+    return ["aggregation", this._args[0]];
+  }
 
-    icon() {
-        return "int";
-    }
+  icon() {
+    return "int";
+  }
 }
 
 const DIMENSION_TYPES: typeof Dimension[] = [
-    FieldIDDimension,
-    FKDimension,
-    DatetimeFieldDimension,
-    ExpressionDimension,
-    BinnedDimension,
-    AggregationDimension
+  FieldIDDimension,
+  FKDimension,
+  DatetimeFieldDimension,
+  ExpressionDimension,
+  BinnedDimension,
+  AggregationDimension,
 ];
diff --git a/frontend/src/metabase-lib/lib/Mode.js b/frontend/src/metabase-lib/lib/Mode.js
index 385a5ffba4befdf3fb0a280b6fef48c0b13680db..badcebc266dd59029bed44591b3b243d8c93a7d4 100644
--- a/frontend/src/metabase-lib/lib/Mode.js
+++ b/frontend/src/metabase-lib/lib/Mode.js
@@ -4,52 +4,54 @@ import Question from "metabase-lib/lib/Question";
 import { getMode } from "metabase/qb/lib/modes";
 
 import type {
-    ClickAction,
-    ClickObject,
-    QueryMode
+  ClickAction,
+  ClickObject,
+  QueryMode,
 } from "metabase/meta/types/Visualization";
 
 export default class Mode {
-    _question: Question;
-    _queryMode: QueryMode;
-
-    constructor(question: Question, queryMode: QueryMode) {
-        this._question = question;
-        this._queryMode = queryMode;
-    }
-
-    static forQuestion(question: Question): ?Mode {
-        // TODO Atte Keinänen 6/22/17: Move getMode here and refactor it after writing tests
-        const card = question.card();
-        const tableMetadata = question.tableMetadata();
-        const queryMode = getMode(card, tableMetadata);
-
-        if (queryMode) {
-            return new Mode(question, queryMode);
-        } else {
-            return null;
-        }
-    }
-
-    queryMode() {
-        return this._queryMode;
-    }
-
-    name() {
-        return this._queryMode.name;
-    }
-
-    actions(settings): ClickAction[] {
-        return _.flatten(
-            this._queryMode.actions.map(actionCreator =>
-                actionCreator({ question: this._question, settings }))
-        );
-    }
-
-    actionsForClick(clicked: ?ClickObject, settings): ClickAction[] {
-        return _.flatten(
-            this._queryMode.drills.map(actionCreator =>
-                actionCreator({ question: this._question, settings, clicked }))
-        );
+  _question: Question;
+  _queryMode: QueryMode;
+
+  constructor(question: Question, queryMode: QueryMode) {
+    this._question = question;
+    this._queryMode = queryMode;
+  }
+
+  static forQuestion(question: Question): ?Mode {
+    // TODO Atte Keinänen 6/22/17: Move getMode here and refactor it after writing tests
+    const card = question.card();
+    const tableMetadata = question.tableMetadata();
+    const queryMode = getMode(card, tableMetadata);
+
+    if (queryMode) {
+      return new Mode(question, queryMode);
+    } else {
+      return null;
     }
+  }
+
+  queryMode() {
+    return this._queryMode;
+  }
+
+  name() {
+    return this._queryMode.name;
+  }
+
+  actions(settings): ClickAction[] {
+    return _.flatten(
+      this._queryMode.actions.map(actionCreator =>
+        actionCreator({ question: this._question, settings }),
+      ),
+    );
+  }
+
+  actionsForClick(clicked: ?ClickObject, settings): ClickAction[] {
+    return _.flatten(
+      this._queryMode.drills.map(actionCreator =>
+        actionCreator({ question: this._question, settings, clicked }),
+      ),
+    );
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/Question.js b/frontend/src/metabase-lib/lib/Question.js
index a494bb0080a617d51f512330239fd9c406172a08..65ca7614916ab9107b954ca2923ebbf1ea8f7dbe 100644
--- a/frontend/src/metabase-lib/lib/Question.js
+++ b/frontend/src/metabase-lib/lib/Question.js
@@ -7,7 +7,7 @@ import Table from "./metadata/Table";
 import Field from "./metadata/Field";
 
 import StructuredQuery, {
-    STRUCTURED_QUERY_TEMPLATE
+  STRUCTURED_QUERY_TEMPLATE,
 } from "./queries/StructuredQuery";
 import NativeQuery from "./queries/NativeQuery";
 
@@ -17,25 +17,25 @@ import * as Card_DEPRECATED from "metabase/lib/card";
 import { getParametersWithExtras } from "metabase/meta/Card";
 
 import {
-    summarize,
-    pivot,
-    filter,
-    breakout,
-    toUnderlyingRecords,
-    drillUnderlyingRecords
+  summarize,
+  pivot,
+  filter,
+  breakout,
+  toUnderlyingRecords,
+  drillUnderlyingRecords,
 } from "metabase/qb/lib/actions";
 
 import _ from "underscore";
 import { chain, assoc } from "icepick";
 
 import type {
-    Parameter as ParameterObject,
-    ParameterValues
+  Parameter as ParameterObject,
+  ParameterValues,
 } from "metabase/meta/types/Parameter";
 import type {
-    DatasetQuery,
-    Card as CardObject,
-    VisualizationSettings
+  DatasetQuery,
+  Card as CardObject,
+  VisualizationSettings,
 } from "metabase/meta/types/Card";
 
 import { MetabaseApi, CardApi } from "metabase/services";
@@ -47,482 +47,480 @@ import type { DatabaseId } from "metabase/meta/types/Database";
 import * as Urls from "metabase/lib/urls";
 import Mode from "metabase-lib/lib/Mode";
 import {
-    ALERT_TYPE_PROGRESS_BAR_GOAL,
-    ALERT_TYPE_ROWS,
-    ALERT_TYPE_TIMESERIES_GOAL
+  ALERT_TYPE_PROGRESS_BAR_GOAL,
+  ALERT_TYPE_ROWS,
+  ALERT_TYPE_TIMESERIES_GOAL,
 } from "metabase-lib/lib/Alert";
 
 /**
  * This is a wrapper around a question/card object, which may contain one or more Query objects
  */
 export default class Question {
-    /**
-     * The Question wrapper requires a metadata object because the queries it contains (like {@link StructuredQuery))
-     * need metadata for accessing databases, tables and metrics.
-     */
-    _metadata: Metadata;
-
-    /**
-     * The plain object presentation of this question, equal to the format that Metabase REST API understands.
-     * It is called `card` for both historical reasons and to make a clear distinction to this class.
-     */
-    _card: CardObject;
-
-    /**
-     * Parameter values mean either the current values of dashboard filters or SQL editor template parameters.
-     * They are in the grey area between UI state and question state, but having them in Question wrapper is convenient.
-     */
-    _parameterValues: ParameterValues;
-
-    /**
-     * Question constructor
-     */
-    constructor(
-        metadata: Metadata,
-        card: CardObject,
-        parameterValues?: ParameterValues
-    ) {
-        this._metadata = metadata;
-        this._card = card;
-        this._parameterValues = parameterValues || {};
-    }
-
-    /**
-     * TODO Atte Keinänen 6/13/17: Discussed with Tom that we could use the default Question constructor instead,
-     * but it would require changing the constructor signature so that `card` is an optional parameter and has a default value
-     */
-    static create(
-        {
-            databaseId,
-            tableId,
-            metadata,
-            parameterValues,
-            ...cardProps
-        }: {
-            databaseId?: DatabaseId,
-            tableId?: TableId,
-            metadata: Metadata,
-            parameterValues?: ParameterValues
-        } = {}
-    ) {
-        // $FlowFixMe
-        const card: Card = {
-            name: cardProps.name || null,
-            display: cardProps.display || "table",
-            visualization_settings: cardProps.visualization_settings || {},
-            dataset_query: STRUCTURED_QUERY_TEMPLATE // temporary placeholder
+  /**
+   * The Question wrapper requires a metadata object because the queries it contains (like {@link StructuredQuery))
+   * need metadata for accessing databases, tables and metrics.
+   */
+  _metadata: Metadata;
+
+  /**
+   * The plain object presentation of this question, equal to the format that Metabase REST API understands.
+   * It is called `card` for both historical reasons and to make a clear distinction to this class.
+   */
+  _card: CardObject;
+
+  /**
+   * Parameter values mean either the current values of dashboard filters or SQL editor template parameters.
+   * They are in the grey area between UI state and question state, but having them in Question wrapper is convenient.
+   */
+  _parameterValues: ParameterValues;
+
+  /**
+   * Question constructor
+   */
+  constructor(
+    metadata: Metadata,
+    card: CardObject,
+    parameterValues?: ParameterValues,
+  ) {
+    this._metadata = metadata;
+    this._card = card;
+    this._parameterValues = parameterValues || {};
+  }
+
+  /**
+   * TODO Atte Keinänen 6/13/17: Discussed with Tom that we could use the default Question constructor instead,
+   * but it would require changing the constructor signature so that `card` is an optional parameter and has a default value
+   */
+  static create({
+    databaseId,
+    tableId,
+    metadata,
+    parameterValues,
+    ...cardProps
+  }: {
+    databaseId?: DatabaseId,
+    tableId?: TableId,
+    metadata: Metadata,
+    parameterValues?: ParameterValues,
+  } = {}) {
+    // $FlowFixMe
+    const card: Card = {
+      name: cardProps.name || null,
+      display: cardProps.display || "table",
+      visualization_settings: cardProps.visualization_settings || {},
+      dataset_query: STRUCTURED_QUERY_TEMPLATE, // temporary placeholder
+    };
+
+    const initialQuestion = new Question(metadata, card, parameterValues);
+    const query = StructuredQuery.newStucturedQuery({
+      question: initialQuestion,
+      databaseId,
+      tableId,
+    });
+
+    return initialQuestion.setQuery(query);
+  }
+
+  metadata(): Metadata {
+    return this._metadata;
+  }
+
+  card() {
+    return this._card;
+  }
+  setCard(card: CardObject): Question {
+    return new Question(this._metadata, card, this._parameterValues);
+  }
+
+  withoutNameAndId() {
+    return this.setCard(
+      chain(this.card())
+        .dissoc("id")
+        .dissoc("name")
+        .dissoc("description")
+        .value(),
+    );
+  }
+
+  /**
+   * A question contains either a:
+   * - StructuredQuery for queries written in MBQL
+   * - NativeQuery for queries written in data source's native query language
+   *
+   * This is just a wrapper object, the data is stored in `this._card.dataset_query` in a format specific to the query type.
+   */
+  @memoize
+  query(): Query {
+    const datasetQuery = this._card.dataset_query;
+
+    for (const QueryClass of [StructuredQuery, NativeQuery]) {
+      if (QueryClass.isDatasetQueryType(datasetQuery)) {
+        return new QueryClass(this, datasetQuery);
+      }
+    }
+
+    throw new Error("Unknown query type: " + datasetQuery.type);
+  }
+
+  isNative(): boolean {
+    return this.query() instanceof NativeQuery;
+  }
+
+  /**
+   * Returns a new Question object with an updated query.
+   * The query is saved to the `dataset_query` field of the Card object.
+   */
+  setQuery(newQuery: Query): Question {
+    if (this._card.dataset_query !== newQuery.datasetQuery()) {
+      return this.setCard(
+        assoc(this.card(), "dataset_query", newQuery.datasetQuery()),
+      );
+    }
+    return this;
+  }
+
+  setDatasetQuery(newDatasetQuery: DatasetQuery): Question {
+    return this.setCard(assoc(this.card(), "dataset_query", newDatasetQuery));
+  }
+
+  /**
+   * Returns a list of atomic queries (NativeQuery or StructuredQuery) contained in this question
+   */
+  atomicQueries(): AtomicQuery[] {
+    const query = this.query();
+    if (query instanceof AtomicQuery) return [query];
+    return [];
+  }
+
+  /**
+   * The visualization type of the question
+   */
+  display(): string {
+    return this._card && this._card.display;
+  }
+  setDisplay(display) {
+    return this.setCard(assoc(this.card(), "display", display));
+  }
+
+  visualizationSettings(): VisualizationSettings {
+    return this._card && this._card.visualization_settings;
+  }
+  setVisualizationSettings(settings: VisualizationSettings) {
+    return this.setCard(assoc(this.card(), "visualization_settings", settings));
+  }
+
+  isEmpty(): boolean {
+    return this.query().isEmpty();
+  }
+  /**
+   * Question is valid (as far as we know) and can be executed
+   */
+  canRun(): boolean {
+    return this.query().canRun();
+  }
+
+  canWrite(): boolean {
+    return this._card && this._card.can_write;
+  }
+
+  /**
+   * Returns the type of alert that current question supports
+   *
+   * The `visualization_settings` in card object doesn't contain default settings,
+   * so you can provide the complete visualization settings object to `alertType`
+   * for taking those into account
+   */
+  alertType(visualizationSettings) {
+    const display = this.display();
+
+    if (!this.canRun()) {
+      return null;
+    }
+
+    const isLineAreaBar =
+      display === "line" || display === "area" || display === "bar";
+
+    if (display === "progress") {
+      return ALERT_TYPE_PROGRESS_BAR_GOAL;
+    } else if (isLineAreaBar) {
+      const vizSettings = visualizationSettings
+        ? visualizationSettings
+        : this.card().visualization_settings;
+
+      const goalEnabled = vizSettings["graph.show_goal"];
+      const hasSingleYAxisColumn =
+        vizSettings["graph.metrics"] &&
+        vizSettings["graph.metrics"].length === 1;
+
+      // We don't currently support goal alerts for multiseries question
+      if (goalEnabled && hasSingleYAxisColumn) {
+        return ALERT_TYPE_TIMESERIES_GOAL;
+      } else {
+        return ALERT_TYPE_ROWS;
+      }
+    } else {
+      return ALERT_TYPE_ROWS;
+    }
+  }
+
+  /**
+   * Visualization drill-through and action widget actions
+   *
+   * Although most of these are essentially a way to modify the current query, having them as a part
+   * of Question interface instead of Query interface makes it more convenient to also change the current visualization
+   */
+  summarize(aggregation) {
+    const tableMetadata = this.tableMetadata();
+    return this.setCard(summarize(this.card(), aggregation, tableMetadata));
+  }
+  breakout(b) {
+    return this.setCard(breakout(this.card(), b));
+  }
+  pivot(breakouts = [], dimensions = []) {
+    const tableMetadata = this.tableMetadata();
+    return this.setCard(
+      // $FlowFixMe: tableMetadata could be null
+      pivot(this.card(), tableMetadata, breakouts, dimensions),
+    );
+  }
+  filter(operator, column, value) {
+    return this.setCard(filter(this.card(), operator, column, value));
+  }
+  drillUnderlyingRecords(dimensions) {
+    return this.setCard(drillUnderlyingRecords(this.card(), dimensions));
+  }
+  toUnderlyingRecords(): ?Question {
+    const newCard = toUnderlyingRecords(this.card());
+    if (newCard) {
+      return this.setCard(newCard);
+    }
+  }
+  toUnderlyingData(): Question {
+    return this.setDisplay("table");
+  }
+
+  composeThisQuery(): ?Question {
+    const SAVED_QUESTIONS_FAUX_DATABASE = -1337;
+
+    if (this.id()) {
+      const card = {
+        display: "table",
+        dataset_query: {
+          type: "query",
+          database: SAVED_QUESTIONS_FAUX_DATABASE,
+          query: {
+            source_table: "card__" + this.id(),
+          },
+        },
+      };
+      return this.setCard(card);
+    }
+  }
+
+  drillPK(field: Field, value: Value): ?Question {
+    const query = this.query();
+    if (query instanceof StructuredQuery) {
+      return query
+        .reset()
+        .setTable(field.table)
+        .addFilter(["=", ["field-id", field.id], value])
+        .question();
+    }
+  }
+
+  // deprecated
+  tableMetadata(): ?Table {
+    const query = this.query();
+    if (query instanceof StructuredQuery) {
+      return query.table();
+    } else {
+      return null;
+    }
+  }
+
+  mode(): ?Mode {
+    return Mode.forQuestion(this);
+  }
+
+  /**
+   * A user-defined name for the question
+   */
+  displayName(): ?string {
+    return this._card && this._card.name;
+  }
+
+  setDisplayName(name: String) {
+    return this.setCard(assoc(this.card(), "name", name));
+  }
+
+  collectionId(): ?number {
+    return this._card && this._card.collection_id;
+  }
+
+  setCollectionId(collectionId: number) {
+    return this.setCard(assoc(this.card(), "collection_id", collectionId));
+  }
+
+  id(): number {
+    return this._card && this._card.id;
+  }
+
+  isSaved(): boolean {
+    return !!this.id();
+  }
+
+  publicUUID(): string {
+    return this._card && this._card.public_uuid;
+  }
+
+  getUrl(originalQuestion?: Question): string {
+    const isDirty =
+      !originalQuestion || this.isDirtyComparedTo(originalQuestion);
+
+    return isDirty
+      ? Urls.question(null, this._serializeForUrl())
+      : Urls.question(this.id(), "");
+  }
+
+  setResultsMetadata(resultsMetadata) {
+    let metadataColumns = resultsMetadata && resultsMetadata.columns;
+    let metadataChecksum = resultsMetadata && resultsMetadata.checksum;
+
+    return this.setCard({
+      ...this.card(),
+      result_metadata: metadataColumns,
+      metadata_checksum: metadataChecksum,
+    });
+  }
+
+  /**
+   * Runs the query and returns an array containing results for each single query.
+   *
+   * If we have a saved and clean single-query question, we use `CardApi.query` instead of a ad-hoc dataset query.
+   * This way we benefit from caching and query optimizations done by Metabase backend.
+   */
+  async apiGetResults({
+    cancelDeferred,
+    isDirty = false,
+    ignoreCache = false,
+  } = {}): Promise<[Dataset]> {
+    // TODO Atte Keinänen 7/5/17: Should we clean this query with Query.cleanQuery(query) before executing it?
+
+    const canUseCardApiEndpoint = !isDirty && this.isSaved();
+
+    const parameters = this.parametersList()
+      // include only parameters that have a value applied
+      .filter(param => _.has(param, "value"))
+      // only the superset of parameters object that API expects
+      .map(param => _.pick(param, "type", "target", "value"));
+
+    if (canUseCardApiEndpoint) {
+      const queryParams = {
+        cardId: this.id(),
+        ignore_cache: ignoreCache,
+        parameters,
+      };
+
+      return [
+        await CardApi.query(queryParams, {
+          cancelled: cancelDeferred.promise,
+        }),
+      ];
+    } else {
+      const getDatasetQueryResult = datasetQuery => {
+        const datasetQueryWithParameters = {
+          ...datasetQuery,
+          parameters,
         };
 
-        const initialQuestion = new Question(metadata, card, parameterValues);
-        const query = StructuredQuery.newStucturedQuery({
-            question: initialQuestion,
-            databaseId,
-            tableId
-        });
-
-        return initialQuestion.setQuery(query);
-    }
-
-    metadata(): Metadata {
-        return this._metadata;
-    }
-
-    card() {
-        return this._card;
-    }
-    setCard(card: CardObject): Question {
-        return new Question(this._metadata, card, this._parameterValues);
-    }
-
-    withoutNameAndId() {
-        return this.setCard(
-            chain(this.card())
-                .dissoc("id")
-                .dissoc("name")
-                .dissoc("description")
-                .value()
+        return MetabaseApi.dataset(
+          datasetQueryWithParameters,
+          cancelDeferred ? { cancelled: cancelDeferred.promise } : {},
         );
-    }
-
-    /**
-     * A question contains either a:
-     * - StructuredQuery for queries written in MBQL
-     * - NativeQuery for queries written in data source's native query language
-     *
-     * This is just a wrapper object, the data is stored in `this._card.dataset_query` in a format specific to the query type.
-     */
-    @memoize query(): Query {
-        const datasetQuery = this._card.dataset_query;
-
-        for (const QueryClass of [StructuredQuery, NativeQuery]) {
-            if (QueryClass.isDatasetQueryType(datasetQuery)) {
-                return new QueryClass(this, datasetQuery);
-            }
-        }
-
-        throw new Error("Unknown query type: " + datasetQuery.type);
-    }
-
-    isNative(): boolean {
-        return this.query() instanceof NativeQuery;
-    }
-
-    /**
-     * Returns a new Question object with an updated query.
-     * The query is saved to the `dataset_query` field of the Card object.
-     */
-    setQuery(newQuery: Query): Question {
-        if (this._card.dataset_query !== newQuery.datasetQuery()) {
-            return this.setCard(
-                assoc(this.card(), "dataset_query", newQuery.datasetQuery())
-            );
-        }
-        return this;
-    }
-
-    setDatasetQuery(newDatasetQuery: DatasetQuery): Question {
-        return this.setCard(
-            assoc(this.card(), "dataset_query", newDatasetQuery)
-        );
-    }
-
-    /**
-     * Returns a list of atomic queries (NativeQuery or StructuredQuery) contained in this question
-     */
-    atomicQueries(): AtomicQuery[] {
-        const query = this.query();
-        if (query instanceof AtomicQuery) return [query];
-        return [];
-    }
-
-    /**
-     * The visualization type of the question
-     */
-    display(): string {
-        return this._card && this._card.display;
-    }
-    setDisplay(display) {
-        return this.setCard(assoc(this.card(), "display", display));
-    }
-
-    visualizationSettings(): VisualizationSettings {
-        return this._card && this._card.visualization_settings;
-    }
-    setVisualizationSettings(settings: VisualizationSettings) {
-        return this.setCard(
-            assoc(this.card(), "visualization_settings", settings)
-        );
-    }
-
-    isEmpty(): boolean {
-        return this.query().isEmpty();
-    }
-    /**
-     * Question is valid (as far as we know) and can be executed
-     */
-    canRun(): boolean {
-        return this.query().canRun();
-    }
-
-    canWrite(): boolean {
-        return this._card && this._card.can_write;
-    }
-
-    /**
-     * Returns the type of alert that current question supports
-     *
-     * The `visualization_settings` in card object doesn't contain default settings,
-     * so you can provide the complete visualization settings object to `alertType`
-     * for taking those into account
-     */
-    alertType(visualizationSettings) {
-        const display = this.display();
-
-        if (!this.canRun()) {
-            return null;
-        }
-
-        const isLineAreaBar = display === "line" ||
-            display === "area" ||
-            display === "bar";
-
-        if (display === "progress") {
-            return ALERT_TYPE_PROGRESS_BAR_GOAL;
-        } else if (isLineAreaBar) {
-            const vizSettings = visualizationSettings
-                ? visualizationSettings
-                : this.card().visualization_settings;
-
-            const goalEnabled = vizSettings["graph.show_goal"];
-            const hasSingleYAxisColumn = vizSettings["graph.metrics"] &&
-                vizSettings["graph.metrics"].length === 1;
-
-            // We don't currently support goal alerts for multiseries question
-            if (goalEnabled && hasSingleYAxisColumn) {
-                return ALERT_TYPE_TIMESERIES_GOAL;
-            } else {
-                return ALERT_TYPE_ROWS;
-            }
-        } else {
-            return ALERT_TYPE_ROWS;
-        }
-    }
-
-    /**
-     * Visualization drill-through and action widget actions
-     *
-     * Although most of these are essentially a way to modify the current query, having them as a part
-     * of Question interface instead of Query interface makes it more convenient to also change the current visualization
-     */
-    summarize(aggregation) {
-        const tableMetadata = this.tableMetadata();
-        return this.setCard(summarize(this.card(), aggregation, tableMetadata));
-    }
-    breakout(b) {
-        return this.setCard(breakout(this.card(), b));
-    }
-    pivot(breakouts = [], dimensions = []) {
-        const tableMetadata = this.tableMetadata();
-        return this.setCard(
-            // $FlowFixMe: tableMetadata could be null
-            pivot(this.card(), tableMetadata, breakouts, dimensions)
-        );
-    }
-    filter(operator, column, value) {
-        return this.setCard(filter(this.card(), operator, column, value));
-    }
-    drillUnderlyingRecords(dimensions) {
-        return this.setCard(drillUnderlyingRecords(this.card(), dimensions));
-    }
-    toUnderlyingRecords(): ?Question {
-        const newCard = toUnderlyingRecords(this.card());
-        if (newCard) {
-            return this.setCard(newCard);
-        }
-    }
-    toUnderlyingData(): Question {
-        return this.setDisplay("table");
-    }
-
-    composeThisQuery(): ?Question {
-        const SAVED_QUESTIONS_FAUX_DATABASE = -1337;
-
-        if (this.id()) {
-            const card = {
-                display: "table",
-                dataset_query: {
-                    type: "query",
-                    database: SAVED_QUESTIONS_FAUX_DATABASE,
-                    query: {
-                        source_table: "card__" + this.id()
-                    }
-                }
-            };
-            return this.setCard(card);
-        }
-    }
-
-    drillPK(field: Field, value: Value): ?Question {
-        const query = this.query();
-        if (query instanceof StructuredQuery) {
-            return query
-                .reset()
-                .setTable(field.table)
-                .addFilter(["=", ["field-id", field.id], value])
-                .question();
-        }
-    }
-
-    // deprecated
-    tableMetadata(): ?Table {
-        const query = this.query();
-        if (query instanceof StructuredQuery) {
-            return query.table();
-        } else {
-            return null;
-        }
-    }
-
-    mode(): ?Mode {
-        return Mode.forQuestion(this);
-    }
-
-    /**
-     * A user-defined name for the question
-     */
-    displayName(): ?string {
-        return this._card && this._card.name;
-    }
-
-    setDisplayName(name: String) {
-        return this.setCard(assoc(this.card(), "name", name));
-    }
-
-    collectionId(): ?number {
-        return this._card && this._card.collection_id;
-    }
-
-    setCollectionId(collectionId: number) {
-        return this.setCard(assoc(this.card(), "collection_id", collectionId));
-    }
-
-    id(): number {
-        return this._card && this._card.id;
-    }
-
-    isSaved(): boolean {
-        return !!this.id();
-    }
-
-    publicUUID(): string {
-        return this._card && this._card.public_uuid;
-    }
-
-    getUrl(originalQuestion?: Question): string {
-        const isDirty = !originalQuestion ||
-            this.isDirtyComparedTo(originalQuestion);
-
-        return isDirty
-            ? Urls.question(null, this._serializeForUrl())
-            : Urls.question(this.id(), "");
-    }
-
-    setResultsMetadata(resultsMetadata) {
-        let metadataColumns = resultsMetadata && resultsMetadata.columns;
-        let metadataChecksum = resultsMetadata && resultsMetadata.checksum;
-
-        return this.setCard({
-            ...this.card(),
-            result_metadata: metadataColumns,
-            metadata_checksum: metadataChecksum
-        });
-    }
-
-    /**
-     * Runs the query and returns an array containing results for each single query.
-     *
-     * If we have a saved and clean single-query question, we use `CardApi.query` instead of a ad-hoc dataset query.
-     * This way we benefit from caching and query optimizations done by Metabase backend.
-     */
-    async apiGetResults(
-        { cancelDeferred, isDirty = false, ignoreCache = false } = {}
-    ): Promise<[Dataset]> {
-        // TODO Atte Keinänen 7/5/17: Should we clean this query with Query.cleanQuery(query) before executing it?
-
-        const canUseCardApiEndpoint = !isDirty && this.isSaved();
-
-        const parameters = this.parametersList()
-            // include only parameters that have a value applied
-            .filter(param => _.has(param, "value"))
-            // only the superset of parameters object that API expects
-            .map(param => _.pick(param, "type", "target", "value"));
-
-        if (canUseCardApiEndpoint) {
-            const queryParams = {
-                cardId: this.id(),
-                ignore_cache: ignoreCache,
-                parameters
-            };
-
-            return [
-                await CardApi.query(queryParams, {
-                    cancelled: cancelDeferred.promise
-                })
-            ];
-        } else {
-            const getDatasetQueryResult = datasetQuery => {
-                const datasetQueryWithParameters = {
-                    ...datasetQuery,
-                    parameters
-                };
-
-                return MetabaseApi.dataset(
-                    datasetQueryWithParameters,
-                    cancelDeferred ? { cancelled: cancelDeferred.promise } : {}
-                );
-            };
-
-            const datasetQueries = this.atomicQueries().map(query =>
-                query.datasetQuery());
-            return Promise.all(datasetQueries.map(getDatasetQueryResult));
-        }
-    }
-
-    async apiCreate() {
-        const createdCard = await CardApi.create(this.card());
-        return this.setCard(createdCard);
-    }
-
-    async apiUpdate() {
-        const updatedCard = await CardApi.update(this.card());
-        return this.setCard(updatedCard);
-    }
-
-    // TODO: Fix incorrect Flow signature
-    parameters(): ParameterObject[] {
-        return getParametersWithExtras(this.card(), this._parameterValues);
-    }
-
-    parametersList(): ParameterObject[] {
-        // $FlowFixMe
-        return (Object.values(this.parameters()): ParameterObject[]);
-    }
-
-    // predicate function that dermines if the question is "dirty" compared to the given question
-    isDirtyComparedTo(originalQuestion: Question) {
-        // TODO Atte Keinänen 6/8/17: Reconsider these rules because they don't completely match
-        // the current implementation which uses original_card_id for indicating that question has a lineage
-
-        // The rules:
-        //   - if it's new, then it's dirty when
-        //       1) there is a database/table chosen or
-        //       2) when there is any content on the native query
-        //   - if it's saved, then it's dirty when
-        //       1) the current card doesn't match the last saved version
-
-        if (!this._card) {
-            return false;
-        } else if (!this._card.id) {
-            if (
-                this._card.dataset_query.query &&
-                this._card.dataset_query.query.source_table
-            ) {
-                return true;
-            } else if (
-                this._card.dataset_query.type === "native" &&
-                !_.isEmpty(this._card.dataset_query.native.query)
-            ) {
-                return true;
-            } else {
-                return false;
-            }
-        } else {
-            const origCardSerialized = originalQuestion._serializeForUrl({
-                includeOriginalCardId: false
-            });
-            const currentCardSerialized = this._serializeForUrl({
-                includeOriginalCardId: false
-            });
-            return currentCardSerialized !== origCardSerialized;
-        }
-    }
-
-    // Internal methods
-    _serializeForUrl({ includeOriginalCardId = true } = {}) {
-        const cleanedQuery = this.query().clean();
-
-        const cardCopy = {
-            name: this._card.name,
-            description: this._card.description,
-            dataset_query: cleanedQuery.datasetQuery(),
-            display: this._card.display,
-            parameters: this._card.parameters,
-            visualization_settings: this._card.visualization_settings,
-            ...(includeOriginalCardId
-                ? { original_card_id: this._card.original_card_id }
-                : {})
-        };
-
-        return Card_DEPRECATED.utf8_to_b64url(JSON.stringify(cardCopy));
-    }
+      };
+
+      const datasetQueries = this.atomicQueries().map(query =>
+        query.datasetQuery(),
+      );
+      return Promise.all(datasetQueries.map(getDatasetQueryResult));
+    }
+  }
+
+  async apiCreate() {
+    const createdCard = await CardApi.create(this.card());
+    return this.setCard(createdCard);
+  }
+
+  async apiUpdate() {
+    const updatedCard = await CardApi.update(this.card());
+    return this.setCard(updatedCard);
+  }
+
+  // TODO: Fix incorrect Flow signature
+  parameters(): ParameterObject[] {
+    return getParametersWithExtras(this.card(), this._parameterValues);
+  }
+
+  parametersList(): ParameterObject[] {
+    // $FlowFixMe
+    return (Object.values(this.parameters()): ParameterObject[]);
+  }
+
+  // predicate function that dermines if the question is "dirty" compared to the given question
+  isDirtyComparedTo(originalQuestion: Question) {
+    // TODO Atte Keinänen 6/8/17: Reconsider these rules because they don't completely match
+    // the current implementation which uses original_card_id for indicating that question has a lineage
+
+    // The rules:
+    //   - if it's new, then it's dirty when
+    //       1) there is a database/table chosen or
+    //       2) when there is any content on the native query
+    //   - if it's saved, then it's dirty when
+    //       1) the current card doesn't match the last saved version
+
+    if (!this._card) {
+      return false;
+    } else if (!this._card.id) {
+      if (
+        this._card.dataset_query.query &&
+        this._card.dataset_query.query.source_table
+      ) {
+        return true;
+      } else if (
+        this._card.dataset_query.type === "native" &&
+        !_.isEmpty(this._card.dataset_query.native.query)
+      ) {
+        return true;
+      } else {
+        return false;
+      }
+    } else {
+      const origCardSerialized = originalQuestion._serializeForUrl({
+        includeOriginalCardId: false,
+      });
+      const currentCardSerialized = this._serializeForUrl({
+        includeOriginalCardId: false,
+      });
+      return currentCardSerialized !== origCardSerialized;
+    }
+  }
+
+  // Internal methods
+  _serializeForUrl({ includeOriginalCardId = true } = {}) {
+    const cleanedQuery = this.query().clean();
+
+    const cardCopy = {
+      name: this._card.name,
+      description: this._card.description,
+      dataset_query: cleanedQuery.datasetQuery(),
+      display: this._card.display,
+      parameters: this._card.parameters,
+      visualization_settings: this._card.visualization_settings,
+      ...(includeOriginalCardId
+        ? { original_card_id: this._card.original_card_id }
+        : {}),
+    };
+
+    return Card_DEPRECATED.utf8_to_b64url(JSON.stringify(cardCopy));
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/AggregationOption.js b/frontend/src/metabase-lib/lib/metadata/AggregationOption.js
index 81c32bf9963518fda389eb2c2e5f6eaf77cd971a..e775b3b5aaf8884b09da9a6c8cfbf111c1781cd1 100644
--- a/frontend/src/metabase-lib/lib/metadata/AggregationOption.js
+++ b/frontend/src/metabase-lib/lib/metadata/AggregationOption.js
@@ -6,23 +6,23 @@ import type { Field } from "metabase/meta/types/Field";
  * Wrapper class for an aggregation object
  */
 export default class AggregationOption extends Base {
-    name: string;
-    short: string;
-    // TODO: Now just a plain object; wrap to a Field wrapper class
-    fields: Field[];
-    validFieldsFilters: [(fields: Field[]) => Field[]];
+  name: string;
+  short: string;
+  // TODO: Now just a plain object; wrap to a Field wrapper class
+  fields: Field[];
+  validFieldsFilters: [(fields: Field[]) => Field[]];
 
-    /**
-     * Aggregation has one or more required fields
-     */
-    hasFields(): boolean {
-        return this.validFieldsFilters.length > 0;
-    }
+  /**
+   * Aggregation has one or more required fields
+   */
+  hasFields(): boolean {
+    return this.validFieldsFilters.length > 0;
+  }
 
-    toAggregation(): AggregationWrapper {
-        return new AggregationWrapper(
-            null,
-            [this.short].concat(this.fields.map(field => null))
-        );
-    }
+  toAggregation(): AggregationWrapper {
+    return new AggregationWrapper(
+      null,
+      [this.short].concat(this.fields.map(field => null)),
+    );
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Base.js b/frontend/src/metabase-lib/lib/metadata/Base.js
index a4d7e9fda8146f3d694b1a512c623c99aab10753..5222560f1f63b97d84cbf0c80476e129ea737123 100644
--- a/frontend/src/metabase-lib/lib/metadata/Base.js
+++ b/frontend/src/metabase-lib/lib/metadata/Base.js
@@ -1,17 +1,17 @@
 export default class Base {
-    _plainObject = null;
-    constructor(object = {}) {
-        this._plainObject = object;
-        for (const property in object) {
-            this[property] = object[property];
-        }
+  _plainObject = null;
+  constructor(object = {}) {
+    this._plainObject = object;
+    for (const property in object) {
+      this[property] = object[property];
     }
+  }
 
-    /**
-     * Get the plain metadata object without hydrated fields.
-     * Useful for situations where you want serialize the metadata object.
-     */
-    getPlainObject() {
-        return this._plainObject;
-    }
+  /**
+   * Get the plain metadata object without hydrated fields.
+   * Useful for situations where you want serialize the metadata object.
+   */
+  getPlainObject() {
+    return this._plainObject;
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Database.js b/frontend/src/metabase-lib/lib/metadata/Database.js
index 874751022b05c9fd18a4911dbb9be2db26af4019..ed54b686d701878f40fb5c7585316d14d323220d 100644
--- a/frontend/src/metabase-lib/lib/metadata/Database.js
+++ b/frontend/src/metabase-lib/lib/metadata/Database.js
@@ -16,28 +16,28 @@ import type { SchemaName } from "metabase/meta/types/Table";
  * Backed by types/Database data structure which matches the backend API contract
  */
 export default class Database extends Base {
-    // TODO Atte Keinänen 6/11/17: List all fields here (currently only in types/Database)
-
-    displayName: string;
-    description: ?string;
-
-    tables: Table[];
-    schemas: Schema[];
-
-    tablesInSchema(schemaName: ?SchemaName) {
-        return this.tables.filter(table => table.schema === schemaName);
-    }
-
-    schemaNames(): Array<SchemaName> {
-        return _.uniq(
-            this.tables
-                .map(table => table.schema)
-                .filter(schemaName => schemaName != null)
-        );
-    }
-
-    newQuestion(): Question {
-        // $FlowFixMe
-        return new Question();
-    }
+  // TODO Atte Keinänen 6/11/17: List all fields here (currently only in types/Database)
+
+  displayName: string;
+  description: ?string;
+
+  tables: Table[];
+  schemas: Schema[];
+
+  tablesInSchema(schemaName: ?SchemaName) {
+    return this.tables.filter(table => table.schema === schemaName);
+  }
+
+  schemaNames(): Array<SchemaName> {
+    return _.uniq(
+      this.tables
+        .map(table => table.schema)
+        .filter(schemaName => schemaName != null),
+    );
+  }
+
+  newQuestion(): Question {
+    // $FlowFixMe
+    return new Question();
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Field.js b/frontend/src/metabase-lib/lib/metadata/Field.js
index 4bc93e7f9a2c95c340f00b41f0fc537bdc82af5c..752fe9905dbd94a296a61d20cfb5cead0089fb47 100644
--- a/frontend/src/metabase-lib/lib/metadata/Field.js
+++ b/frontend/src/metabase-lib/lib/metadata/Field.js
@@ -7,124 +7,206 @@ import { FieldIDDimension } from "../Dimension";
 
 import { getFieldValues } from "metabase/lib/query/field";
 import {
-    isDate,
-    isTime,
-    isNumber,
-    isNumeric,
-    isBoolean,
-    isString,
-    isSummable,
-    isCategory,
-    isDimension,
-    isMetric,
-    isPK,
-    isFK,
-    isCoordinate,
-    getIconForField,
-    getFieldType
+  isDate,
+  isTime,
+  isNumber,
+  isNumeric,
+  isBoolean,
+  isString,
+  isSummable,
+  isCategory,
+  isDimension,
+  isMetric,
+  isPK,
+  isFK,
+  isEntityName,
+  isCoordinate,
+  getIconForField,
+  getFieldType,
 } from "metabase/lib/schema_metadata";
 
 import type { FieldValues } from "metabase/meta/types/Field";
 
+import _ from "underscore";
+
 /**
  * Wrapper class for field metadata objects. Belongs to a Table.
  */
 export default class Field extends Base {
-    displayName: string;
-    description: string;
-
-    table: Table;
-
-    fieldType() {
-        return getFieldType(this);
-    }
-
-    isDate() {
-        return isDate(this);
-    }
-    isTime() {
-        return isTime(this);
-    }
-    isNumber() {
-        return isNumber(this);
-    }
-    isNumeric() {
-        return isNumeric(this);
-    }
-    isBoolean() {
-        return isBoolean(this);
-    }
-    isString() {
-        return isString(this);
-    }
-    isSummable() {
-        return isSummable(this);
-    }
-    isCategory() {
-        return isCategory(this);
-    }
-    isMetric() {
-        return isMetric(this);
-    }
-
-    isCompatibleWith(field: Field) {
-        return this.isDate() === field.isDate() ||
-            this.isNumeric() === field.isNumeric() ||
-            this.id === field.id;
-    }
-
-    /**
-     * Tells if this column can be used in a breakout
-     * Currently returns `true` for everything expect for aggregation columns
-     */
-    isDimension() {
-        return isDimension(this);
-    }
-    isID() {
-        return isPK(this) || isFK(this);
-    }
-    isPK() {
-        return isPK(this);
-    }
-    isFK() {
-        return isFK(this);
-    }
-
-    isCoordinate() {
-        return isCoordinate(this);
-    }
-
-    fieldValues(): FieldValues {
-        return getFieldValues(this._object);
-    }
-
-    icon() {
-        return getIconForField(this);
-    }
-
-    dimension() {
-        return new FieldIDDimension(null, [this.id], this.metadata);
-    }
-
-    operator(op) {
-        if (this.operators_lookup) {
-            return this.operators_lookup[op];
-        }
-    }
-
-    /**
-     * Returns a default breakout MBQL clause for this field
-     *
-     * Tries to look up a default subdimension (like "Created At: Day" for "Created At" field)
-     * and if it isn't found, uses the plain field id dimension (like "Product ID") as a fallback.
-     */
-    getDefaultBreakout = () => {
-        const fieldIdDimension = this.dimension();
-        const defaultSubDimension = fieldIdDimension.defaultDimension();
-        if (defaultSubDimension) {
-            return defaultSubDimension.mbql();
-        } else {
-            return fieldIdDimension.mbql();
-        }
-    };
+  displayName: string;
+  description: string;
+
+  table: Table;
+
+  fieldType() {
+    return getFieldType(this);
+  }
+
+  isDate() {
+    return isDate(this);
+  }
+  isTime() {
+    return isTime(this);
+  }
+  isNumber() {
+    return isNumber(this);
+  }
+  isNumeric() {
+    return isNumeric(this);
+  }
+  isBoolean() {
+    return isBoolean(this);
+  }
+  isString() {
+    return isString(this);
+  }
+  isSummable() {
+    return isSummable(this);
+  }
+  isCategory() {
+    return isCategory(this);
+  }
+  isMetric() {
+    return isMetric(this);
+  }
+
+  isCompatibleWith(field: Field) {
+    return (
+      this.isDate() === field.isDate() ||
+      this.isNumeric() === field.isNumeric() ||
+      this.id === field.id
+    );
+  }
+
+  /**
+   * Tells if this column can be used in a breakout
+   * Currently returns `true` for everything expect for aggregation columns
+   */
+  isDimension() {
+    return isDimension(this);
+  }
+  isID() {
+    return isPK(this) || isFK(this);
+  }
+  isPK() {
+    return isPK(this);
+  }
+  isFK() {
+    return isFK(this);
+  }
+  isEntityName() {
+    return isEntityName(this);
+  }
+
+  isCoordinate() {
+    return isCoordinate(this);
+  }
+
+  fieldValues(): FieldValues {
+    return getFieldValues(this._object);
+  }
+
+  icon() {
+    return getIconForField(this);
+  }
+
+  dimension() {
+    return new FieldIDDimension(null, [this.id], this.metadata);
+  }
+
+  operator(op) {
+    if (this.operators_lookup) {
+      return this.operators_lookup[op];
+    }
+  }
+
+  /**
+   * Returns a default breakout MBQL clause for this field
+   *
+   * Tries to look up a default subdimension (like "Created At: Day" for "Created At" field)
+   * and if it isn't found, uses the plain field id dimension (like "Product ID") as a fallback.
+   */
+  getDefaultBreakout = () => {
+    const fieldIdDimension = this.dimension();
+    const defaultSubDimension = fieldIdDimension.defaultDimension();
+    if (defaultSubDimension) {
+      return defaultSubDimension.mbql();
+    } else {
+      return fieldIdDimension.mbql();
+    }
+  };
+
+  /**
+   * Returns the remapped field, if any
+   */
+  remappedField(): ?Field {
+    const displayFieldId =
+      this.dimensions && this.dimensions.human_readable_field_id;
+    if (displayFieldId != null) {
+      return this.metadata.fields[displayFieldId];
+    }
+    // this enables "implicit" remappings from type/PK to type/Name on the same table,
+    // used in FieldValuesWidget, but not table/object detail listings
+    if (this.isPK()) {
+      const nameField = _.find(this.table.fields, f => f.isEntityName());
+      if (nameField) {
+        return nameField;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * Returns the human readable remapped value, if any
+   */
+  remappedValue(value): ?string {
+    // TODO: Ugh. Should this be handled further up by the parameter widget?
+    if (this.isNumeric() && typeof value !== "number") {
+      value = parseFloat(value);
+    }
+    return this.remapping && this.remapping.get(value);
+  }
+
+  /**
+   * Returns whether the field has a human readable remapped value for this value
+   */
+  hasRemappedValue(value): ?string {
+    // TODO: Ugh. Should this be handled further up by the parameter widget?
+    if (this.isNumeric() && typeof value !== "number") {
+      value = parseFloat(value);
+    }
+    return this.remapping && this.remapping.has(value);
+  }
+
+  /**
+   * Returns true if this field can be searched, e.x. in filter or parameter widgets
+   */
+  isSearchable(): boolean {
+    // TODO: ...?
+    return this.isString();
+  }
+
+  /**
+   * Returns the field to be searched for this field, either the remapped field or itself
+   */
+  parameterSearchField(): ?Field {
+    let remappedField = this.remappedField();
+    if (remappedField && remappedField.isSearchable()) {
+      return remappedField;
+    }
+    if (this.isSearchable()) {
+      return this;
+    }
+    return null;
+  }
+
+  filterSearchField(): ?Field {
+    if (this.isPK()) {
+      if (this.isSearchable()) {
+        return this;
+      }
+    } else {
+      return this.parameterSearchField();
+    }
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Metadata.js b/frontend/src/metabase-lib/lib/metadata/Metadata.js
index c4ccb35410635c8f582584c180d8cb1d9d52d7c7..2a2eb8a208ac33c6dbae7782678ac4055234cae5 100644
--- a/frontend/src/metabase-lib/lib/metadata/Metadata.js
+++ b/frontend/src/metabase-lib/lib/metadata/Metadata.js
@@ -18,29 +18,41 @@ import type { SegmentId } from "metabase/meta/types/Segment";
  * Wrapper class for the entire metadata store
  */
 export default class Metadata extends Base {
-    databases: { [id: DatabaseId]: Database };
-    tables: { [id: TableId]: Table };
-    fields: { [id: FieldId]: Field };
-    metrics: { [id: MetricId]: Metric };
-    segments: { [id: SegmentId]: Segment };
-
-    databasesList(): Database[] {
-        // $FlowFixMe
-        return (Object.values(this.databases): Database[]);
-    }
-
-    tablesList(): Database[] {
-        // $FlowFixMe
-        return (Object.values(this.tables): Database[]);
-    }
-
-    metricsList(): Metric[] {
-        // $FlowFixMe
-        return (Object.values(this.metrics): Metric[]);
-    }
-
-    segmentsList(): Metric[] {
-        // $FlowFixMe
-        return (Object.values(this.segments): Segment[]);
-    }
+  databases: { [id: DatabaseId]: Database };
+  tables: { [id: TableId]: Table };
+  fields: { [id: FieldId]: Field };
+  metrics: { [id: MetricId]: Metric };
+  segments: { [id: SegmentId]: Segment };
+
+  databasesList(): Database[] {
+    // $FlowFixMe
+    return (Object.values(this.databases): Database[]);
+  }
+
+  tablesList(): Database[] {
+    // $FlowFixMe
+    return (Object.values(this.tables): Database[]);
+  }
+
+  metricsList(): Metric[] {
+    // $FlowFixMe
+    return (Object.values(this.metrics): Metric[]);
+  }
+
+  segmentsList(): Metric[] {
+    // $FlowFixMe
+    return (Object.values(this.segments): Segment[]);
+  }
+
+  database(databaseId): ?Database {
+    return (databaseId != null && this.databases[databaseId]) || null;
+  }
+
+  table(tableId): ?Table {
+    return (tableId != null && this.tables[tableId]) || null;
+  }
+
+  field(fieldId): ?Field {
+    return (fieldId != null && this.fields[fieldId]) || null;
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Metric.js b/frontend/src/metabase-lib/lib/metadata/Metric.js
index 915b6c27dc656ca01e680b3193da32e60460f345..caa84bf43c9fd8391f448f39e672c4a28fcc9559 100644
--- a/frontend/src/metabase-lib/lib/metadata/Metric.js
+++ b/frontend/src/metabase-lib/lib/metadata/Metric.js
@@ -9,17 +9,17 @@ import type { Aggregation } from "metabase/meta/types/Query";
  * Wrapper class for a metric. Belongs to a {@link Database} and possibly a {@link Table}
  */
 export default class Metric extends Base {
-    displayName: string;
-    description: string;
+  displayName: string;
+  description: string;
 
-    database: Database;
-    table: Table;
+  database: Database;
+  table: Table;
 
-    aggregationClause(): Aggregation {
-        return ["METRIC", this.id];
-    }
+  aggregationClause(): Aggregation {
+    return ["METRIC", this.id];
+  }
 
-    isActive(): boolean {
-        return !!this.is_active;
-    }
+  isActive(): boolean {
+    return !!this.is_active;
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Schema.js b/frontend/src/metabase-lib/lib/metadata/Schema.js
index 02f33ed0107050a18306c5b87d8d6ec238a5a294..1b0af9a76d56599427ec0b6ffb554615e1a70ed1 100644
--- a/frontend/src/metabase-lib/lib/metadata/Schema.js
+++ b/frontend/src/metabase-lib/lib/metadata/Schema.js
@@ -8,8 +8,8 @@ import Table from "./Table";
  * Wrapper class for a {@link Database} schema. Contains {@link Table}s.
  */
 export default class Schema extends Base {
-    displayName: string;
+  displayName: string;
 
-    database: Database;
-    tables: Table[];
+  database: Database;
+  tables: Table[];
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Segment.js b/frontend/src/metabase-lib/lib/metadata/Segment.js
index 9c1bfad6c6b2697b41f8728584ca7d850f28e9de..36e9d21158264d8b5829ec3fd7da951c73ec7690 100644
--- a/frontend/src/metabase-lib/lib/metadata/Segment.js
+++ b/frontend/src/metabase-lib/lib/metadata/Segment.js
@@ -9,17 +9,17 @@ import type { FilterClause } from "metabase/meta/types/Query";
  * Wrapper class for a segment. Belongs to a {@link Database} and possibly a {@link Table}
  */
 export default class Segment extends Base {
-    displayName: string;
-    description: string;
+  displayName: string;
+  description: string;
 
-    database: Database;
-    table: Table;
+  database: Database;
+  table: Table;
 
-    filterClause(): FilterClause {
-        return ["SEGMENT", this.id];
-    }
+  filterClause(): FilterClause {
+    return ["SEGMENT", this.id];
+  }
 
-    isActive(): boolean {
-        return !!this.is_active;
-    }
+  isActive(): boolean {
+    return !!this.is_active;
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/metadata/Table.js b/frontend/src/metabase-lib/lib/metadata/Table.js
index e1421774b660404b494ca70dabef4436935068f7..5d626e1cc3fd070be0db0de8007ad33bc6512089 100644
--- a/frontend/src/metabase-lib/lib/metadata/Table.js
+++ b/frontend/src/metabase-lib/lib/metadata/Table.js
@@ -1,5 +1,8 @@
 /* @flow weak */
 
+// NOTE: this needs to be imported first due to some cyclical dependency nonsense
+import Q_DEPRECATED from "metabase/lib/query";
+
 import Question from "../Question";
 
 import Base from "./Base";
@@ -7,45 +10,52 @@ import Database from "./Database";
 import Field from "./Field";
 
 import type { SchemaName } from "metabase/meta/types/Table";
+import type { FieldMetadata } from "metabase/meta/types/Metadata";
+import type { ConcreteField, DatetimeUnit } from "metabase/meta/types/Query";
 
 import Dimension from "../Dimension";
 
 import _ from "underscore";
-import type { FieldMetadata } from "metabase/meta/types/Metadata";
 
 /** This is the primary way people interact with tables */
 export default class Table extends Base {
-    displayName: string;
-    description: string;
+  displayName: string;
+  description: string;
+
+  schema: ?SchemaName;
+  db: Database;
 
-    schema: ?SchemaName;
-    db: Database;
+  fields: FieldMetadata[];
 
-    fields: FieldMetadata[];
+  // $FlowFixMe Could be replaced with hydrated database property in selectors/metadata.js (instead / in addition to `table.db`)
+  get database() {
+    return this.db;
+  }
 
-    // $FlowFixMe Could be replaced with hydrated database property in selectors/metadata.js (instead / in addition to `table.db`)
-    get database() {
-        return this.db;
-    }
+  newQuestion(): Question {
+    // $FlowFixMe
+    return new Question();
+  }
 
-    newQuestion(): Question {
-        // $FlowFixMe
-        return new Question();
-    }
+  dimensions(): Dimension[] {
+    return this.fields.map(field => field.dimension());
+  }
 
-    dimensions(): Dimension[] {
-        return this.fields.map(field => field.dimension());
-    }
+  dateFields(): Field[] {
+    return this.fields.filter(field => field.isDate());
+  }
 
-    dateFields(): Field[] {
-        return this.fields.filter(field => field.isDate());
-    }
+  aggregations() {
+    return this.aggregation_options || [];
+  }
 
-    aggregations() {
-        return this.aggregation_options || [];
-    }
+  aggregation(agg) {
+    return _.findWhere(this.aggregations(), { short: agg });
+  }
 
-    aggregation(agg) {
-        return _.findWhere(this.aggregations(), { short: agg });
-    }
+  fieldTarget(
+    fieldRef: ConcreteField,
+  ): { field: Field, table: Table, unit?: DatetimeUnit, path: Field[] } {
+    return Q_DEPRECATED.getFieldTarget(fieldRef, this);
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/queries/Aggregation.js b/frontend/src/metabase-lib/lib/queries/Aggregation.js
index 8e07b96159f8d9d9df03ed29a1ea5101fedc4006..43539b8fb7ec53c55b5f4d2e7eb254b1a0621110 100644
--- a/frontend/src/metabase-lib/lib/queries/Aggregation.js
+++ b/frontend/src/metabase-lib/lib/queries/Aggregation.js
@@ -1,9 +1,5 @@
-import type {
-    Aggregation as AggregationObject
-} from "metabase/meta/types/Query";
-import {
-    AggregationClause as AggregationClause_DEPRECATED
-} from "metabase/lib/query";
+import type { Aggregation as AggregationObject } from "metabase/meta/types/Query";
+import { AggregationClause as AggregationClause_DEPRECATED } from "metabase/lib/query";
 import { MetricId } from "metabase/meta/types/Metric";
 import { AggregationOption, Operator } from "metabase/meta/types/Metadata";
 import { FieldId } from "metabase/meta/types/Field";
@@ -13,104 +9,101 @@ import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
  * Wrapper for an aggregation contained by a {@link StructuredQuery}
  */
 export default class Aggregation {
-    _query: ?StructuredQuery;
+  _query: ?StructuredQuery;
 
-    clause: AggregationObject;
+  clause: AggregationObject;
 
-    constructor(
-        query?: StructuredQuery,
-        clause: AggregationObject
-    ): Aggregation {
-        this._query = query;
-        this.clause = clause;
-    }
+  constructor(query?: StructuredQuery, clause: AggregationObject): Aggregation {
+    this._query = query;
+    this.clause = clause;
+  }
 
-    /**
-     * Gets the aggregation option matching this aggregation
-     * Returns `null` if the clause isn't in a standard format
-     */
-    getOption(): ?AggregationOption {
-        if (this._query == null) return null;
+  /**
+   * Gets the aggregation option matching this aggregation
+   * Returns `null` if the clause isn't in a standard format
+   */
+  getOption(): ?AggregationOption {
+    if (this._query == null) return null;
 
-        const operator = this.getOperator();
-        return operator
-            ? this._query
-                  .aggregationOptions()
-                  .find(option => option.short === operator)
-            : null;
-    }
+    const operator = this.getOperator();
+    return operator
+      ? this._query
+          .aggregationOptions()
+          .find(option => option.short === operator)
+      : null;
+  }
 
-    /**
-     * Predicate function to test if a given aggregation clause is fully formed
-     */
-    isValid(): boolean {
-        return AggregationClause_DEPRECATED.isValid(this.clause);
-    }
+  /**
+   * Predicate function to test if a given aggregation clause is fully formed
+   */
+  isValid(): boolean {
+    return AggregationClause_DEPRECATED.isValid(this.clause);
+  }
 
-    /**
-     * Predicate function to test if the given aggregation clause represents a Bare Rows aggregation
-     */
-    isBareRows(): boolean {
-        return AggregationClause_DEPRECATED.isBareRows(this.clause);
-    }
+  /**
+   * Predicate function to test if the given aggregation clause represents a Bare Rows aggregation
+   */
+  isBareRows(): boolean {
+    return AggregationClause_DEPRECATED.isBareRows(this.clause);
+  }
 
-    /**
-     * Predicate function to test if a given aggregation clause represents a standard aggregation
-     */
-    isStandard(): boolean {
-        return AggregationClause_DEPRECATED.isStandard(this.clause);
-    }
+  /**
+   * Predicate function to test if a given aggregation clause represents a standard aggregation
+   */
+  isStandard(): boolean {
+    return AggregationClause_DEPRECATED.isStandard(this.clause);
+  }
 
-    getAggregation() {
-        return AggregationClause_DEPRECATED.getAggregation(this.clause);
-    }
+  getAggregation() {
+    return AggregationClause_DEPRECATED.getAggregation(this.clause);
+  }
 
-    /**
-     * Predicate function to test if a given aggregation clause represents a metric
-     */
-    isMetric(): boolean {
-        return AggregationClause_DEPRECATED.isMetric(this.clause);
-    }
+  /**
+   * Predicate function to test if a given aggregation clause represents a metric
+   */
+  isMetric(): boolean {
+    return AggregationClause_DEPRECATED.isMetric(this.clause);
+  }
 
-    /**
-     * Get metricId from a metric aggregation clause
-     * Returns `null` if the clause doesn't represent a metric
-     */
-    getMetric(): ?MetricId {
-        return AggregationClause_DEPRECATED.getMetric(this.clause);
-    }
+  /**
+   * Get metricId from a metric aggregation clause
+   * Returns `null` if the clause doesn't represent a metric
+   */
+  getMetric(): ?MetricId {
+    return AggregationClause_DEPRECATED.getMetric(this.clause);
+  }
 
-    /**
-     * Is a custom expression created with the expression editor
-     */
-    isCustom(): boolean {
-        return AggregationClause_DEPRECATED.isCustom(this.clause);
-    }
+  /**
+   * Is a custom expression created with the expression editor
+   */
+  isCustom(): boolean {
+    return AggregationClause_DEPRECATED.isCustom(this.clause);
+  }
 
-    /**
-     * Get the operator from a standard aggregation clause
-     * Returns `null` if the clause isn't in a standard format
-     */
-    getOperator(): ?Operator {
-        return AggregationClause_DEPRECATED.getOperator(this.clause);
-    }
+  /**
+   * Get the operator from a standard aggregation clause
+   * Returns `null` if the clause isn't in a standard format
+   */
+  getOperator(): ?Operator {
+    return AggregationClause_DEPRECATED.getOperator(this.clause);
+  }
 
-    /**
-     * Get the fieldId from a standard aggregation clause
-     * Returns `null` if the clause isn't in a standard format
-     */
-    getField(): ?FieldId {
-        return AggregationClause_DEPRECATED.getField(this.clause);
-    }
+  /**
+   * Get the fieldId from a standard aggregation clause
+   * Returns `null` if the clause isn't in a standard format
+   */
+  getField(): ?FieldId {
+    return AggregationClause_DEPRECATED.getField(this.clause);
+  }
 
-    /**
-     * Set the fieldId on a standard aggregation clause.
-     * If the clause isn't in a standard format, no modifications are done.
-     */
-    setField(fieldId: FieldId): Aggregation {
-        return new Aggregation(
-            this._query,
-            AggregationClause_DEPRECATED.setField(this.clause, fieldId)
-        );
-    }
+  /**
+   * Set the fieldId on a standard aggregation clause.
+   * If the clause isn't in a standard format, no modifications are done.
+   */
+  setField(fieldId: FieldId): Aggregation {
+    return new Aggregation(
+      this._query,
+      AggregationClause_DEPRECATED.setField(this.clause, fieldId),
+    );
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/queries/AtomicQuery.js b/frontend/src/metabase-lib/lib/queries/AtomicQuery.js
index ad923902825d31fb9176b01feff29c4f2f506f2e..d40ecb9e5b17a666d791cbb787e83883e25726dd 100644
--- a/frontend/src/metabase-lib/lib/queries/AtomicQuery.js
+++ b/frontend/src/metabase-lib/lib/queries/AtomicQuery.js
@@ -8,22 +8,22 @@ import type Database from "metabase-lib/lib/metadata/Database";
  * and form a single MBQL / native query clause
  */
 export default class AtomicQuery extends Query {
-    /**
-     * Tables this query could use, if the database is set
-     */
-    tables(): ?(Table[]) {
-        return null;
-    }
+  /**
+   * Tables this query could use, if the database is set
+   */
+  tables(): ?(Table[]) {
+    return null;
+  }
 
-    databaseId(): ?DatabaseId {
-        return null;
-    }
+  databaseId(): ?DatabaseId {
+    return null;
+  }
 
-    database(): ?Database {
-        return null;
-    }
+  database(): ?Database {
+    return null;
+  }
 
-    engine(): ?DatabaseEngine {
-        return null;
-    }
+  engine(): ?DatabaseEngine {
+    return null;
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/queries/NativeQuery.js b/frontend/src/metabase-lib/lib/queries/NativeQuery.js
index 4bdaeaf1c3105b0f09481b4910b125603dabcc7b..542a5d19b2ff6787e90f43b08f5e47a611b57067 100644
--- a/frontend/src/metabase-lib/lib/queries/NativeQuery.js
+++ b/frontend/src/metabase-lib/lib/queries/NativeQuery.js
@@ -11,260 +11,259 @@ import { humanize } from "metabase/lib/formatting";
 import Utils from "metabase/lib/utils";
 
 import {
-    getEngineNativeAceMode,
-    getEngineNativeType,
-    getEngineNativeRequiresTable
+  getEngineNativeAceMode,
+  getEngineNativeType,
+  getEngineNativeRequiresTable,
 } from "metabase/lib/engine";
 
 import { chain, assoc, getIn, assocIn } from "icepick";
 import _ from "underscore";
 
 import type {
-    DatasetQuery,
-    NativeDatasetQuery
+  DatasetQuery,
+  NativeDatasetQuery,
 } from "metabase/meta/types/Card";
 import type { TemplateTags, TemplateTag } from "metabase/meta/types/Query";
 import type { DatabaseEngine, DatabaseId } from "metabase/meta/types/Database";
 import AtomicQuery from "metabase-lib/lib/queries/AtomicQuery";
 
 export const NATIVE_QUERY_TEMPLATE: NativeDatasetQuery = {
-    database: null,
-    type: "native",
-    native: {
-        query: "",
-        template_tags: {}
-    }
+  database: null,
+  type: "native",
+  native: {
+    query: "",
+    template_tags: {},
+  },
 };
 
 export default class NativeQuery extends AtomicQuery {
-    // For Flow type completion
-    _nativeDatasetQuery: NativeDatasetQuery;
-
-    constructor(
-        question: Question,
-        datasetQuery: DatasetQuery = NATIVE_QUERY_TEMPLATE
-    ) {
-        super(question, datasetQuery);
-
-        this._nativeDatasetQuery = (datasetQuery: NativeDatasetQuery);
-    }
-
-    static isDatasetQueryType(datasetQuery: DatasetQuery): boolean {
-        return datasetQuery.type === NATIVE_QUERY_TEMPLATE.type;
-    }
-
-    /* Query superclass methods */
-
-    canRun() {
-        return this.databaseId() != null &&
-            this.queryText().length > 0 &&
-            (!this.requiresTable() || this.collection());
-    }
-
-    isEmpty() {
-        return this.databaseId() == null || this.queryText().length == 0;
-    }
-
-    databases(): Database[] {
-        return super
-            .databases()
-            .filter(database => database.native_permissions === "write");
+  // For Flow type completion
+  _nativeDatasetQuery: NativeDatasetQuery;
+
+  constructor(
+    question: Question,
+    datasetQuery: DatasetQuery = NATIVE_QUERY_TEMPLATE,
+  ) {
+    super(question, datasetQuery);
+
+    this._nativeDatasetQuery = (datasetQuery: NativeDatasetQuery);
+  }
+
+  static isDatasetQueryType(datasetQuery: DatasetQuery): boolean {
+    return datasetQuery.type === NATIVE_QUERY_TEMPLATE.type;
+  }
+
+  /* Query superclass methods */
+
+  canRun() {
+    return (
+      this.databaseId() != null &&
+      this.queryText().length > 0 &&
+      (!this.requiresTable() || this.collection())
+    );
+  }
+
+  isEmpty() {
+    return this.databaseId() == null || this.queryText().length == 0;
+  }
+
+  databases(): Database[] {
+    return super
+      .databases()
+      .filter(database => database.native_permissions === "write");
+  }
+
+  /* AtomicQuery superclass methods */
+
+  tables(): ?(Table[]) {
+    const database = this.database();
+    return (database && database.tables) || null;
+  }
+
+  databaseId(): ?DatabaseId {
+    // same for both structured and native
+    return this._nativeDatasetQuery.database;
+  }
+  database(): ?Database {
+    const databaseId = this.databaseId();
+    return databaseId != null ? this._metadata.databases[databaseId] : null;
+  }
+  engine(): ?DatabaseEngine {
+    const database = this.database();
+    return database && database.engine;
+  }
+
+  /* Methods unique to this query type */
+
+  /**
+   * @returns a new query with the provided Database set.
+   */
+  setDatabase(database: Database): NativeQuery {
+    if (database.id !== this.databaseId()) {
+      // TODO: this should reset the rest of the query?
+      return new NativeQuery(
+        this._originalQuestion,
+        assoc(this.datasetQuery(), "database", database.id),
+      );
+    } else {
+      return this;
     }
-
-    /* AtomicQuery superclass methods */
-
-    tables(): ?(Table[]) {
-        const database = this.database();
-        return (database && database.tables) || null;
-    }
-
-    databaseId(): ?DatabaseId {
-        // same for both structured and native
-        return this._nativeDatasetQuery.database;
-    }
-    database(): ?Database {
-        const databaseId = this.databaseId();
-        return databaseId != null ? this._metadata.databases[databaseId] : null;
-    }
-    engine(): ?DatabaseEngine {
-        const database = this.database();
-        return database && database.engine;
+  }
+
+  hasWritePermission(): boolean {
+    const database = this.database();
+    return database != null && database.native_permissions === "write";
+  }
+
+  supportsNativeParameters(): boolean {
+    const database = this.database();
+    return (
+      database != null && _.contains(database.features, "native-parameters")
+    );
+  }
+
+  table(): ?Table {
+    const database = this.database();
+    const collection = this.collection();
+    if (!database || !collection) {
+      return null;
     }
-
-    /* Methods unique to this query type */
-
-    /**
-     * @returns a new query with the provided Database set.
-     */
-    setDatabase(database: Database): NativeQuery {
-        if (database.id !== this.databaseId()) {
-            // TODO: this should reset the rest of the query?
-            return new NativeQuery(
-                this._originalQuestion,
-                assoc(this.datasetQuery(), "database", database.id)
-            );
+    return _.findWhere(database.tables, { name: collection }) || null;
+  }
+
+  queryText(): string {
+    return getIn(this.datasetQuery(), ["native", "query"]) || "";
+  }
+
+  updateQueryText(newQueryText: string): Query {
+    return new NativeQuery(
+      this._originalQuestion,
+      chain(this._datasetQuery)
+        .assocIn(["native", "query"], newQueryText)
+        .assocIn(
+          ["native", "template_tags"],
+          this._getUpdatedTemplateTags(newQueryText),
+        )
+        .value(),
+    );
+  }
+
+  collection(): ?string {
+    return getIn(this.datasetQuery(), ["native", "collection"]);
+  }
+
+  updateCollection(newCollection: string) {
+    return new NativeQuery(
+      this._originalQuestion,
+      assocIn(this._datasetQuery, ["native", "collection"], newCollection),
+    );
+  }
+
+  lineCount(): number {
+    const queryText = this.queryText();
+    return queryText ? countLines(queryText) : 0;
+  }
+
+  /**
+   * The ACE Editor mode name, e.g. 'ace/mode/json'
+   */
+  aceMode(): string {
+    return getEngineNativeAceMode(this.engine());
+  }
+
+  /**
+   * Name used to describe the text written in that mode, e.g. 'JSON'. Used to fill in the blank in 'This question is written in _______'.
+   */
+  nativeQueryLanguage() {
+    return getEngineNativeType(this.engine()).toUpperCase();
+  }
+
+  /**
+   * Whether the DB selector should be a DB + Table selector. Mongo needs both DB + Table.
+   */
+  requiresTable() {
+    return getEngineNativeRequiresTable(this.engine());
+  }
+
+  // $FlowFixMe
+  templateTags(): TemplateTag[] {
+    return Object.values(this.templateTagsMap());
+  }
+  templateTagsMap(): TemplateTags {
+    return getIn(this.datasetQuery(), ["native", "template_tags"]) || {};
+  }
+
+  setDatasetQuery(datasetQuery: DatasetQuery): NativeQuery {
+    return new NativeQuery(this._originalQuestion, datasetQuery);
+  }
+
+  /**
+   * special handling for NATIVE cards to automatically detect parameters ... {{varname}}
+   */
+  _getUpdatedTemplateTags(queryText: string): TemplateTags {
+    if (queryText && this.supportsNativeParameters()) {
+      let tags = [];
+
+      // look for variable usage in the query (like '{{varname}}').  we only allow alphanumeric characters for the variable name
+      // a variable name can optionally end with :start or :end which is not considered part of the actual variable name
+      // expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}}
+      // anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored
+      let match,
+        re = /\{\{\s*([A-Za-z0-9_]+?)\s*\}\}/g;
+      while ((match = re.exec(queryText)) != null) {
+        tags.push(match[1]);
+      }
+
+      // eliminate any duplicates since it's allowed for a user to reference the same variable multiple times
+      const existingTemplateTags = this.templateTagsMap();
+
+      tags = _.uniq(tags);
+      let existingTags = Object.keys(existingTemplateTags);
+
+      // if we ended up with any variables in the query then update the card parameters list accordingly
+      if (tags.length > 0 || existingTags.length > 0) {
+        let newTags = _.difference(tags, existingTags);
+        let oldTags = _.difference(existingTags, tags);
+
+        let templateTags = { ...existingTemplateTags };
+        if (oldTags.length === 1 && newTags.length === 1) {
+          // renaming
+          templateTags[newTags[0]] = { ...templateTags[oldTags[0]] };
+
+          if (templateTags[newTags[0]].display_name === humanize(oldTags[0])) {
+            templateTags[newTags[0]].display_name = humanize(newTags[0]);
+          }
+
+          templateTags[newTags[0]].name = newTags[0];
+          delete templateTags[oldTags[0]];
         } else {
-            return this;
+          // remove old vars
+          for (const name of oldTags) {
+            delete templateTags[name];
+          }
+
+          // create new vars
+          for (let tagName of newTags) {
+            templateTags[tagName] = {
+              id: Utils.uuid(),
+              name: tagName,
+              display_name: humanize(tagName),
+              type: null,
+            };
+          }
         }
-    }
-
-    hasWritePermission(): boolean {
-        const database = this.database();
-        return database != null && database.native_permissions === "write";
-    }
-
-    supportsNativeParameters(): boolean {
-        const database = this.database();
-        return database != null &&
-            _.contains(database.features, "native-parameters");
-    }
 
-    table(): ?Table {
-        const database = this.database();
-        const collection = this.collection();
-        if (!database || !collection) {
-            return null;
+        // ensure all tags have an id since we need it for parameter values to work
+        // $FlowFixMe
+        for (const tag: TemplateTag of Object.values(templateTags)) {
+          if (tag.id == undefined) {
+            tag.id = Utils.uuid();
+          }
         }
-        return _.findWhere(database.tables, { name: collection }) || null;
-    }
-
-    queryText(): string {
-        return getIn(this.datasetQuery(), ["native", "query"]) || "";
-    }
-
-    updateQueryText(newQueryText: string): Query {
-        return new NativeQuery(
-            this._originalQuestion,
-            chain(this._datasetQuery)
-                .assocIn(["native", "query"], newQueryText)
-                .assocIn(
-                    ["native", "template_tags"],
-                    this._getUpdatedTemplateTags(newQueryText)
-                )
-                .value()
-        );
-    }
-
-    collection(): ?string {
-        return getIn(this.datasetQuery(), ["native", "collection"]);
-    }
-
-    updateCollection(newCollection: string) {
-        return new NativeQuery(
-            this._originalQuestion,
-            assocIn(this._datasetQuery, ["native", "collection"], newCollection)
-        );
-    }
 
-    lineCount(): number {
-        const queryText = this.queryText();
-        return queryText ? countLines(queryText) : 0;
-    }
-
-    /**
-     * The ACE Editor mode name, e.g. 'ace/mode/json'
-     */
-    aceMode(): string {
-        return getEngineNativeAceMode(this.engine());
-    }
-
-    /**
-     * Name used to describe the text written in that mode, e.g. 'JSON'. Used to fill in the blank in 'This question is written in _______'.
-     */
-    nativeQueryLanguage() {
-        return getEngineNativeType(this.engine()).toUpperCase();
-    }
-
-    /**
-     * Whether the DB selector should be a DB + Table selector. Mongo needs both DB + Table.
-     */
-    requiresTable() {
-        return getEngineNativeRequiresTable(this.engine());
-    }
-
-    // $FlowFixMe
-    templateTags(): TemplateTag[] {
-        return Object.values(this.templateTagsMap());
-    }
-    templateTagsMap(): TemplateTags {
-        return getIn(this.datasetQuery(), ["native", "template_tags"]) || {};
-    }
-
-    setDatasetQuery(datasetQuery: DatasetQuery): NativeQuery {
-        return new NativeQuery(this._originalQuestion, datasetQuery);
-    }
-
-    /**
-     * special handling for NATIVE cards to automatically detect parameters ... {{varname}}
-     */
-    _getUpdatedTemplateTags(queryText: string): TemplateTags {
-        if (queryText && this.supportsNativeParameters()) {
-            let tags = [];
-
-            // look for variable usage in the query (like '{{varname}}').  we only allow alphanumeric characters for the variable name
-            // a variable name can optionally end with :start or :end which is not considered part of the actual variable name
-            // expected pattern is like mustache templates, so we are looking for something like {{category}} or {{date:start}}
-            // anything that doesn't match our rule is ignored, so {{&foo!}} would simply be ignored
-            let match, re = /\{\{\s*([A-Za-z0-9_]+?)\s*\}\}/g;
-            while ((match = re.exec(queryText)) != null) {
-                tags.push(match[1]);
-            }
-
-            // eliminate any duplicates since it's allowed for a user to reference the same variable multiple times
-            const existingTemplateTags = this.templateTagsMap();
-
-            tags = _.uniq(tags);
-            let existingTags = Object.keys(existingTemplateTags);
-
-            // if we ended up with any variables in the query then update the card parameters list accordingly
-            if (tags.length > 0 || existingTags.length > 0) {
-                let newTags = _.difference(tags, existingTags);
-                let oldTags = _.difference(existingTags, tags);
-
-                let templateTags = { ...existingTemplateTags };
-                if (oldTags.length === 1 && newTags.length === 1) {
-                    // renaming
-                    templateTags[newTags[0]] = { ...templateTags[oldTags[0]] };
-
-                    if (
-                        templateTags[newTags[0]].display_name ===
-                        humanize(oldTags[0])
-                    ) {
-                        templateTags[newTags[0]].display_name = humanize(
-                            newTags[0]
-                        );
-                    }
-
-                    templateTags[newTags[0]].name = newTags[0];
-                    delete templateTags[oldTags[0]];
-                } else {
-                    // remove old vars
-                    for (const name of oldTags) {
-                        delete templateTags[name];
-                    }
-
-                    // create new vars
-                    for (let tagName of newTags) {
-                        templateTags[tagName] = {
-                            id: Utils.uuid(),
-                            name: tagName,
-                            display_name: humanize(tagName),
-                            type: null
-                        };
-                    }
-                }
-
-                // ensure all tags have an id since we need it for parameter values to work
-                // $FlowFixMe
-                for (const tag: TemplateTag of Object.values(templateTags)) {
-                    if (tag.id == undefined) {
-                        tag.id = Utils.uuid();
-                    }
-                }
-
-                return templateTags;
-            }
-        }
-        return {};
+        return templateTags;
+      }
     }
+    return {};
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/queries/Query.js b/frontend/src/metabase-lib/lib/queries/Query.js
index 0babf8175b80bc0ce4823e34196c7a01ef77a273..6599c22c7422dd5cd44b1724a013d2f224c7ae5e 100644
--- a/frontend/src/metabase-lib/lib/queries/Query.js
+++ b/frontend/src/metabase-lib/lib/queries/Query.js
@@ -11,93 +11,94 @@ import { memoize } from "metabase-lib/lib/utils";
  * An abstract class for all query types (StructuredQuery & NativeQuery)
  */
 export default class Query {
-    _metadata: Metadata;
-
-    /**
-     * Note that Question is not always in sync with _datasetQuery,
-     * calling question() will always merge the latest _datasetQuery to the question object
-     */
-    _originalQuestion: Question;
-    _datasetQuery: DatasetQuery;
-
-    constructor(question: Question, datasetQuery: DatasetQuery) {
-        this._metadata = question._metadata;
-        this._datasetQuery = datasetQuery;
-        this._originalQuestion = question;
-    }
-
-    /**
-     * Returns a question updated with the current dataset query.
-     * Can only be applied to query that is a direct child of the question.
-     */
-    @memoize question(): Question {
-        const isDirectChildOfQuestion = typeof this._originalQuestion.query() ===
-            typeof this;
-
-        if (isDirectChildOfQuestion) {
-            return this._originalQuestion.setQuery(this);
-        } else {
-            throw new Error(
-                "Can't derive a question from a query that is a child of other query"
-            );
-        }
-    }
-
-    clean(): Query {
-        return this;
-    }
-
-    /**
-     * Convenience method for accessing the global metadata
-     */
-    metadata() {
-        return this._metadata;
-    }
-
-    /**
-     * Does this query have the sufficient metadata for editing it?
-     */
-    isEditable(): boolean {
-        return true;
-    }
-
-    /**
-     * Returns the dataset_query object underlying this Query
-     */
-    datasetQuery(): DatasetQuery {
-        return this._datasetQuery;
-    }
-
-    setDatasetQuery(datasetQuery: DatasetQuery): Query {
-        return this;
-    }
-
-    /**
-     *
-     * Query is considered empty, i.e. it is in a plain state with no properties / query clauses set
-     */
-    isEmpty(): boolean {
-        return false;
-    }
-
-    /**
-     * Query is valid (as far as we know) and can be executed
-     */
-    canRun(): boolean {
-        return false;
-    }
-
-    /**
-     * Databases this query could use
-     */
-    databases(): Database[] {
-        return this._metadata.databasesList();
-    }
-
-    /**
-     * Helper for updating with functions that expect a DatasetQuery object
-     */
-    update(fn: (datasetQuery: DatasetQuery) => void) {
-        return fn(this.datasetQuery());
+  _metadata: Metadata;
+
+  /**
+   * Note that Question is not always in sync with _datasetQuery,
+   * calling question() will always merge the latest _datasetQuery to the question object
+   */
+  _originalQuestion: Question;
+  _datasetQuery: DatasetQuery;
+
+  constructor(question: Question, datasetQuery: DatasetQuery) {
+    this._metadata = question._metadata;
+    this._datasetQuery = datasetQuery;
+    this._originalQuestion = question;
+  }
+
+  /**
+   * Returns a question updated with the current dataset query.
+   * Can only be applied to query that is a direct child of the question.
+   */
+  @memoize
+  question(): Question {
+    const isDirectChildOfQuestion =
+      typeof this._originalQuestion.query() === typeof this;
+
+    if (isDirectChildOfQuestion) {
+      return this._originalQuestion.setQuery(this);
+    } else {
+      throw new Error(
+        "Can't derive a question from a query that is a child of other query",
+      );
     }
+  }
+
+  clean(): Query {
+    return this;
+  }
+
+  /**
+   * Convenience method for accessing the global metadata
+   */
+  metadata() {
+    return this._metadata;
+  }
+
+  /**
+   * Does this query have the sufficient metadata for editing it?
+   */
+  isEditable(): boolean {
+    return true;
+  }
+
+  /**
+   * Returns the dataset_query object underlying this Query
+   */
+  datasetQuery(): DatasetQuery {
+    return this._datasetQuery;
+  }
+
+  setDatasetQuery(datasetQuery: DatasetQuery): Query {
+    return this;
+  }
+
+  /**
+   *
+   * Query is considered empty, i.e. it is in a plain state with no properties / query clauses set
+   */
+  isEmpty(): boolean {
+    return false;
+  }
+
+  /**
+   * Query is valid (as far as we know) and can be executed
+   */
+  canRun(): boolean {
+    return false;
+  }
+
+  /**
+   * Databases this query could use
+   */
+  databases(): Database[] {
+    return this._metadata.databasesList();
+  }
+
+  /**
+   * Helper for updating with functions that expect a DatasetQuery object
+   */
+  update(fn: (datasetQuery: DatasetQuery) => void) {
+    return fn(this.datasetQuery());
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
index 9960bcf6100512e9396db3e7b0c0289d4ec6d92f..0ab5d5a636cdd8c3afdc4180f68b489316f659f3 100644
--- a/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
+++ b/frontend/src/metabase-lib/lib/queries/StructuredQuery.js
@@ -6,8 +6,8 @@
 
 import * as Q from "metabase/lib/query/query";
 import Q_deprecated, {
-    AggregationClause,
-    NamedClause
+  AggregationClause,
+  NamedClause,
 } from "metabase/lib/query";
 import { format as formatExpression } from "metabase/lib/expressions/formatter";
 import { getAggregator } from "metabase/lib/schema_metadata";
@@ -16,26 +16,26 @@ import _ from "underscore";
 import { chain, assoc, updateIn } from "icepick";
 
 import type {
-    StructuredQuery as StructuredQueryObject,
-    Aggregation,
-    Breakout,
-    Filter,
-    LimitClause,
-    OrderBy
+  StructuredQuery as StructuredQueryObject,
+  Aggregation,
+  Breakout,
+  Filter,
+  LimitClause,
+  OrderBy,
 } from "metabase/meta/types/Query";
 import type {
-    DatasetQuery,
-    StructuredDatasetQuery
+  DatasetQuery,
+  StructuredDatasetQuery,
 } from "metabase/meta/types/Card";
 import type {
-    TableMetadata,
-    DimensionOptions
+  TableMetadata,
+  DimensionOptions,
 } from "metabase/meta/types/Metadata";
 
 import Dimension, {
-    FKDimension,
-    ExpressionDimension,
-    AggregationDimension
+  FKDimension,
+  ExpressionDimension,
+  AggregationDimension,
 } from "metabase-lib/lib/Dimension";
 
 import type Table from "../metadata/Table";
@@ -50,701 +50,686 @@ import AggregationOption from "metabase-lib/lib/metadata/AggregationOption";
 import Utils from "metabase/lib/utils";
 
 export const STRUCTURED_QUERY_TEMPLATE = {
-    database: null,
-    type: "query",
-    query: {
-        source_table: null
-    }
+  database: null,
+  type: "query",
+  query: {
+    source_table: null,
+  },
 };
 
 /**
  * A wrapper around an MBQL (`query` type @type {DatasetQuery}) object
  */
 export default class StructuredQuery extends AtomicQuery {
-    static isDatasetQueryType(datasetQuery: DatasetQuery): boolean {
-        return datasetQuery.type === STRUCTURED_QUERY_TEMPLATE.type;
-    }
-
-    // For Flow type completion
-    _structuredDatasetQuery: StructuredDatasetQuery;
-
-    /**
-     * Creates a new StructuredQuery based on the provided DatasetQuery object
-     */
-    constructor(
-        question: Question,
-        datasetQuery: DatasetQuery = STRUCTURED_QUERY_TEMPLATE
-    ) {
-        super(question, datasetQuery);
-
-        this._structuredDatasetQuery = (datasetQuery: StructuredDatasetQuery);
-    }
-
-    static newStucturedQuery(
-        {
-            question,
-            databaseId,
-            tableId
-        }: { question: Question, databaseId?: DatabaseId, tableId?: TableId }
-    ) {
-        const datasetQuery = {
-            ...STRUCTURED_QUERY_TEMPLATE,
-            database: databaseId || null,
-            query: {
-                source_table: tableId || null
-            }
-        };
-
-        return new StructuredQuery(question, datasetQuery);
-    }
-
-    /* Query superclass methods */
-
-    /**
-     * @returns true if this is new query that hasn't been modified yet.
-     */
-    isEmpty() {
-        return !this.databaseId();
-    }
-
-    /**
-     * @returns true if this query is in a state where it can be run.
-     */
-    canRun() {
-        return Q_deprecated.canRun(this.query());
-    }
-
-    /**
-     * @returns true if this query is in a state where it can be edited. Must have database and table set, and metadata for the table loaded.
-     */
-    isEditable(): boolean {
-        return !!this.tableMetadata();
-    }
-
-    /* AtomicQuery superclass methods */
-
-    /**
-     * @returns all tables in the currently selected database that can be used.
-     */
-    tables(): ?(Table[]) {
-        const database = this.database();
-        return (database && database.tables) || null;
-    }
-
-    /**
-     * @returns the currently selected database ID, if any is selected.
-     */
-    databaseId(): ?DatabaseId {
-        // same for both structured and native
-        return this._structuredDatasetQuery.database;
-    }
-
-    /**
-     * @returns the currently selected database metadata, if a database is selected and loaded.
-     */
-    database(): ?Database {
-        const databaseId = this.databaseId();
-        return databaseId != null ? this._metadata.databases[databaseId] : null;
-    }
-
-    /**
-     * @returns the database engine object, if a database is selected and loaded.
-     */
-    engine(): ?DatabaseEngine {
-        const database = this.database();
-        return database && database.engine;
-    }
-
-    /* Methods unique to this query type */
-
-    /**
-     * @returns a new reset @type {StructuredQuery} with the same parent @type {Question}
-     */
-    reset(): StructuredQuery {
-        return new StructuredQuery(this._originalQuestion);
-    }
-
-    /**
-     * @returns the underlying MBQL query object
-     */
-    query(): StructuredQueryObject {
-        return this._structuredDatasetQuery.query;
-    }
-
-    /**
-     * @returns a new query with the provided Database set.
-     */
-    setDatabase(database: Database): StructuredQuery {
-        if (database.id !== this.databaseId()) {
-            // TODO: this should reset the rest of the query?
-            return new StructuredQuery(
-                this._originalQuestion,
-                assoc(this.datasetQuery(), "database", database.id)
-            );
-        } else {
-            return this;
-        }
-    }
-
-    /**
-     * @returns a new query with the provided Table set.
-     */
-    setTable(table: Table): StructuredQuery {
-        if (table.id !== this.tableId()) {
-            return new StructuredQuery(
-                this._originalQuestion,
-                chain(this.datasetQuery())
-                    .assoc("database", table.database.id)
-                    .assocIn(["query", "source_table"], table.id)
-                    .value()
-            );
-        } else {
-            return this;
-        }
-    }
-
-    /**
-     * @returns the table ID, if a table is selected.
-     */
-    tableId(): ?TableId {
-        return this.query().source_table;
-    }
-
-    /**
-     * @returns the table object, if a table is selected and loaded.
-     * FIXME: actual return type should be `?Table`
-     */
-    table(): Table {
-        return this._metadata.tables[this.tableId()];
-    }
-
-    /**
-     * @deprecated Alias of `table()`. Use only when partially porting old code that uses @type {TableMetadata} object.
-     */
-    tableMetadata(): ?TableMetadata {
-        return this.table();
-    }
-
-    clean() {
-        const datasetQuery = this.datasetQuery();
-        if (datasetQuery.query) {
-            const query = Utils.copy(datasetQuery.query);
-
-            return this.setDatasetQuery({
-                ...datasetQuery,
-                query: Q_deprecated.cleanQuery(query)
-            });
-        } else {
-            return this;
-        }
-    }
-
-    // AGGREGATIONS
-
-    /**
-     * @returns an array of MBQL @type {Aggregation}s.
-     */
-    aggregations(): Aggregation[] {
-        return Q.getAggregations(this.query());
-    }
-
-    /**
-     * @returns an array of aggregation wrapper objects
-     * TODO Atte Keinänen 6/11/17: Make the wrapper objects the standard format for aggregations
-     */
-    aggregationsWrapped(): AggregationWrapper[] {
-        return this.aggregations().map(
-            agg => new AggregationWrapper(this, agg)
-        );
-    }
-
-    /**
-     * @returns an array of aggregation options for the currently selected table
-     */
-    aggregationOptions(): AggregationOption[] {
-        // TODO Should `aggregation_options` be wrapped already in selectors/metadata.js?
-        const optionObjects = this.table() && this.table().aggregations();
-        return optionObjects
-            ? optionObjects.map(agg => new AggregationOption(agg))
-            : [];
-    }
-
-    /**
-     * @returns an array of aggregation options for the currently selected table, excluding the "rows" pseudo-aggregation
-     */
-    aggregationOptionsWithoutRows(): AggregationOption[] {
-        return this.aggregationOptions().filter(
-            option => option.short !== "rows"
-        );
-    }
-
-    /**
-     * @returns the field options for the provided aggregation
-     */
-    aggregationFieldOptions(agg): DimensionOptions {
-        const aggregation = this.table().aggregation(agg);
-        if (aggregation) {
-            const fieldOptions = this.fieldOptions(field => {
-                return aggregation.validFieldsFilters[0]([field]).length === 1;
-            });
-
-            // HACK Atte Keinänen 6/18/17: Using `fieldOptions` with a field filter function
-            // ends up often omitting all expressions because the field object of ExpressionDimension is empty.
-            // Expressions can be applied to all aggregations so we can simply add all expressions to the
-            // dimensions list in this hack.
-            //
-            // A real solution would have a `dimensionOptions` method instead of `fieldOptions` which would
-            // enable filtering based on dimension properties.
-            return {
-                ...fieldOptions,
-                dimensions: _.uniq([
-                    ...this.expressionDimensions(),
-                    ...fieldOptions.dimensions.filter(
-                        d => !(d instanceof ExpressionDimension)
-                    )
-                ])
-            };
-        } else {
-            return { count: 0, fks: [], dimensions: [] };
-        }
-    }
-
-    /**
-     * @returns true if the aggregation can be removed
-     */
-    canRemoveAggregation(): boolean {
-        return this.aggregations().length > 1;
-    }
-
-    /**
-     * @returns true if the query has no aggregation
-     */
-    isBareRows(): boolean {
-        return Q.isBareRows(this.query());
-    }
-
-    /**
-     * @returns the formatted named of the aggregation at the provided index.
-     */
-    aggregationName(index: number = 0): ?string {
-        const aggregation = this.aggregations()[index];
-        if (NamedClause.isNamed(aggregation)) {
-            return NamedClause.getName(aggregation);
-        } else if (AggregationClause.isCustom(aggregation)) {
-            return formatExpression(aggregation, {
-                tableMetadata: this.tableMetadata(),
-                customFields: this.expressions()
-            });
-        } else if (AggregationClause.isMetric(aggregation)) {
-            const metricId = AggregationClause.getMetric(aggregation);
-            const metric = this._metadata.metrics[metricId];
-            if (metric) {
-                return metric.name;
-            }
-        } else {
-            const selectedAggregation = getAggregator(
-                AggregationClause.getOperator(aggregation)
-            );
-            if (selectedAggregation) {
-                let aggregationName = selectedAggregation.name.replace(
-                    " of ...",
-                    ""
-                );
-                const fieldId = Q_deprecated.getFieldTargetId(
-                    AggregationClause.getField(aggregation)
-                );
-                const field = fieldId && this._metadata.fields[fieldId];
-                if (field) {
-                    aggregationName += " of " + field.display_name;
-                }
-                return aggregationName;
-            }
-        }
-        return null;
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the provided MBQL @type {Aggregation} added.
-     */
-    addAggregation(aggregation: Aggregation): StructuredQuery {
-        return this._updateQuery(Q.addAggregation, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the MBQL @type {Aggregation} updated at the provided index.
-     */
-    updateAggregation(
-        index: number,
-        aggregation: Aggregation
-    ): StructuredQuery {
-        return this._updateQuery(Q.updateAggregation, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the aggregation at the provided index removed.
-     */
-    removeAggregation(index: number): StructuredQuery {
-        return this._updateQuery(Q.removeAggregation, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with all aggregations removed.
-     */
-    clearAggregations(): StructuredQuery {
-        return this._updateQuery(Q.clearAggregations, arguments);
-    }
-
-    // BREAKOUTS
-
-    /**
-     * @returns An array of MBQL @type {Breakout}s.
-     */
-    breakouts(): Breakout[] {
-        return Q.getBreakouts(this.query());
-    }
-
-    /**
-     * @param includedBreakout The breakout to include even if it's already used
-     * @param fieldFilter An option @type {Field} predicate to filter out options
-     * @returns @type {DimensionOptions} that can be used as breakouts, excluding used breakouts, unless @param {breakout} is provided.
-     */
-    breakoutOptions(includedBreakout?: any, fieldFilter = () => true) {
-        // the set of field ids being used by other breakouts
-        const usedFields = new Set(
-            this.breakouts()
-                .filter(b => !_.isEqual(b, includedBreakout))
-                .map(b => Q_deprecated.getFieldTargetId(b))
-        );
-
-        return this.fieldOptions(
-            field => fieldFilter(field) && !usedFields.has(field.id)
+  static isDatasetQueryType(datasetQuery: DatasetQuery): boolean {
+    return datasetQuery.type === STRUCTURED_QUERY_TEMPLATE.type;
+  }
+
+  // For Flow type completion
+  _structuredDatasetQuery: StructuredDatasetQuery;
+
+  /**
+   * Creates a new StructuredQuery based on the provided DatasetQuery object
+   */
+  constructor(
+    question: Question,
+    datasetQuery: DatasetQuery = STRUCTURED_QUERY_TEMPLATE,
+  ) {
+    super(question, datasetQuery);
+
+    this._structuredDatasetQuery = (datasetQuery: StructuredDatasetQuery);
+  }
+
+  static newStucturedQuery({
+    question,
+    databaseId,
+    tableId,
+  }: {
+    question: Question,
+    databaseId?: DatabaseId,
+    tableId?: TableId,
+  }) {
+    const datasetQuery = {
+      ...STRUCTURED_QUERY_TEMPLATE,
+      database: databaseId || null,
+      query: {
+        source_table: tableId || null,
+      },
+    };
+
+    return new StructuredQuery(question, datasetQuery);
+  }
+
+  /* Query superclass methods */
+
+  /**
+   * @returns true if this is new query that hasn't been modified yet.
+   */
+  isEmpty() {
+    return !this.databaseId();
+  }
+
+  /**
+   * @returns true if this query is in a state where it can be run.
+   */
+  canRun() {
+    return Q_deprecated.canRun(this.query());
+  }
+
+  /**
+   * @returns true if this query is in a state where it can be edited. Must have database and table set, and metadata for the table loaded.
+   */
+  isEditable(): boolean {
+    return !!this.tableMetadata();
+  }
+
+  /* AtomicQuery superclass methods */
+
+  /**
+   * @returns all tables in the currently selected database that can be used.
+   */
+  tables(): ?(Table[]) {
+    const database = this.database();
+    return (database && database.tables) || null;
+  }
+
+  /**
+   * @returns the currently selected database ID, if any is selected.
+   */
+  databaseId(): ?DatabaseId {
+    // same for both structured and native
+    return this._structuredDatasetQuery.database;
+  }
+
+  /**
+   * @returns the currently selected database metadata, if a database is selected and loaded.
+   */
+  database(): ?Database {
+    const databaseId = this.databaseId();
+    return databaseId != null ? this._metadata.databases[databaseId] : null;
+  }
+
+  /**
+   * @returns the database engine object, if a database is selected and loaded.
+   */
+  engine(): ?DatabaseEngine {
+    const database = this.database();
+    return database && database.engine;
+  }
+
+  /* Methods unique to this query type */
+
+  /**
+   * @returns a new reset @type {StructuredQuery} with the same parent @type {Question}
+   */
+  reset(): StructuredQuery {
+    return new StructuredQuery(this._originalQuestion);
+  }
+
+  /**
+   * @returns the underlying MBQL query object
+   */
+  query(): StructuredQueryObject {
+    return this._structuredDatasetQuery.query;
+  }
+
+  /**
+   * @returns a new query with the provided Database set.
+   */
+  setDatabase(database: Database): StructuredQuery {
+    if (database.id !== this.databaseId()) {
+      // TODO: this should reset the rest of the query?
+      return new StructuredQuery(
+        this._originalQuestion,
+        assoc(this.datasetQuery(), "database", database.id),
+      );
+    } else {
+      return this;
+    }
+  }
+
+  /**
+   * @returns a new query with the provided Table set.
+   */
+  setTable(table: Table): StructuredQuery {
+    if (table.id !== this.tableId()) {
+      return new StructuredQuery(
+        this._originalQuestion,
+        chain(this.datasetQuery())
+          .assoc("database", table.database.id)
+          .assocIn(["query", "source_table"], table.id)
+          .value(),
+      );
+    } else {
+      return this;
+    }
+  }
+
+  /**
+   * @returns the table ID, if a table is selected.
+   */
+  tableId(): ?TableId {
+    return this.query().source_table;
+  }
+
+  /**
+   * @returns the table object, if a table is selected and loaded.
+   * FIXME: actual return type should be `?Table`
+   */
+  table(): Table {
+    return this._metadata.tables[this.tableId()];
+  }
+
+  /**
+   * @deprecated Alias of `table()`. Use only when partially porting old code that uses @type {TableMetadata} object.
+   */
+  tableMetadata(): ?TableMetadata {
+    return this.table();
+  }
+
+  clean() {
+    const datasetQuery = this.datasetQuery();
+    if (datasetQuery.query) {
+      const query = Utils.copy(datasetQuery.query);
+
+      return this.setDatasetQuery({
+        ...datasetQuery,
+        query: Q_deprecated.cleanQuery(query),
+      });
+    } else {
+      return this;
+    }
+  }
+
+  // AGGREGATIONS
+
+  /**
+   * @returns an array of MBQL @type {Aggregation}s.
+   */
+  aggregations(): Aggregation[] {
+    return Q.getAggregations(this.query());
+  }
+
+  /**
+   * @returns an array of aggregation wrapper objects
+   * TODO Atte Keinänen 6/11/17: Make the wrapper objects the standard format for aggregations
+   */
+  aggregationsWrapped(): AggregationWrapper[] {
+    return this.aggregations().map(agg => new AggregationWrapper(this, agg));
+  }
+
+  /**
+   * @returns an array of aggregation options for the currently selected table
+   */
+  aggregationOptions(): AggregationOption[] {
+    // TODO Should `aggregation_options` be wrapped already in selectors/metadata.js?
+    const optionObjects = this.table() && this.table().aggregations();
+    return optionObjects
+      ? optionObjects.map(agg => new AggregationOption(agg))
+      : [];
+  }
+
+  /**
+   * @returns an array of aggregation options for the currently selected table, excluding the "rows" pseudo-aggregation
+   */
+  aggregationOptionsWithoutRows(): AggregationOption[] {
+    return this.aggregationOptions().filter(option => option.short !== "rows");
+  }
+
+  /**
+   * @returns the field options for the provided aggregation
+   */
+  aggregationFieldOptions(agg): DimensionOptions {
+    const aggregation = this.table().aggregation(agg);
+    if (aggregation) {
+      const fieldOptions = this.fieldOptions(field => {
+        return aggregation.validFieldsFilters[0]([field]).length === 1;
+      });
+
+      // HACK Atte Keinänen 6/18/17: Using `fieldOptions` with a field filter function
+      // ends up often omitting all expressions because the field object of ExpressionDimension is empty.
+      // Expressions can be applied to all aggregations so we can simply add all expressions to the
+      // dimensions list in this hack.
+      //
+      // A real solution would have a `dimensionOptions` method instead of `fieldOptions` which would
+      // enable filtering based on dimension properties.
+      return {
+        ...fieldOptions,
+        dimensions: _.uniq([
+          ...this.expressionDimensions(),
+          ...fieldOptions.dimensions.filter(
+            d => !(d instanceof ExpressionDimension),
+          ),
+        ]),
+      };
+    } else {
+      return { count: 0, fks: [], dimensions: [] };
+    }
+  }
+
+  /**
+   * @returns true if the aggregation can be removed
+   */
+  canRemoveAggregation(): boolean {
+    return this.aggregations().length > 1;
+  }
+
+  /**
+   * @returns true if the query has no aggregation
+   */
+  isBareRows(): boolean {
+    return Q.isBareRows(this.query());
+  }
+
+  /**
+   * @returns the formatted named of the aggregation at the provided index.
+   */
+  aggregationName(index: number = 0): ?string {
+    const aggregation = this.aggregations()[index];
+    if (NamedClause.isNamed(aggregation)) {
+      return NamedClause.getName(aggregation);
+    } else if (AggregationClause.isCustom(aggregation)) {
+      return formatExpression(aggregation, {
+        tableMetadata: this.tableMetadata(),
+        customFields: this.expressions(),
+      });
+    } else if (AggregationClause.isMetric(aggregation)) {
+      const metricId = AggregationClause.getMetric(aggregation);
+      const metric = this._metadata.metrics[metricId];
+      if (metric) {
+        return metric.name;
+      }
+    } else {
+      const selectedAggregation = getAggregator(
+        AggregationClause.getOperator(aggregation),
+      );
+      if (selectedAggregation) {
+        let aggregationName = selectedAggregation.name.replace(" of ...", "");
+        const fieldId = Q_deprecated.getFieldTargetId(
+          AggregationClause.getField(aggregation),
         );
-    }
-
-    /**
-     * @returns whether a new breakout can be added or not
-     */
-    canAddBreakout(): boolean {
-        return this.breakoutOptions().count > 0;
-    }
-
-    /**
-     * @returns whether the current query has a valid breakout
-     */
-    hasValidBreakout(): boolean {
-        return Q_deprecated.hasValidBreakout(this.query());
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the provided MBQL @type {Breakout} added.
-     */
-    addBreakout(breakout: Breakout) {
-        return this._updateQuery(Q.addBreakout, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the MBQL @type {Breakout} updated at the provided index.
-     */
-    updateBreakout(index: number, breakout: Breakout) {
-        return this._updateQuery(Q.updateBreakout, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the breakout at the provided index removed.
-     */
-    removeBreakout(index: number) {
-        return this._updateQuery(Q.removeBreakout, arguments);
-    }
-    /**
-     * @returns {StructuredQuery} new query with all breakouts removed.
-     */
-    clearBreakouts() {
-        return this._updateQuery(Q.clearBreakouts, arguments);
-    }
-
-    // FILTERS
-
-    /**
-     * @returns An array of MBQL @type {Filter}s.
-     */
-    filters(): Filter[] {
-        return Q.getFilters(this.query());
-    }
-
-    /**
-     * @returns @type {DimensionOptions} that can be used in filters.
-     */
-    filterFieldOptions(): DimensionOptions {
-        return this.fieldOptions();
-    }
-
-    /**
-     * @returns @type {Segment}s that can be used as filters.
-     * TODO: exclude used segments
-     */
-    filterSegmentOptions(): Segment[] {
-        return this.table().segments.filter(sgmt => sgmt.is_active === true);
-    }
-
-    /**
-     * @returns whether a new filter can be added or not
-     */
-    canAddFilter(): boolean {
-        return Q.canAddFilter(this.query()) &&
-            (this.filterFieldOptions().count > 0 ||
-                this.filterSegmentOptions().length > 0);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the provided MBQL @type {Filter} added.
-     */
-    addFilter(filter: Filter) {
-        return this._updateQuery(Q.addFilter, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the MBQL @type {Filter} updated at the provided index.
-     */
-    updateFilter(index: number, filter: Filter) {
-        return this._updateQuery(Q.updateFilter, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with the filter at the provided index removed.
-     */
-    removeFilter(index: number) {
-        return this._updateQuery(Q.removeFilter, arguments);
-    }
-
-    /**
-     * @returns {StructuredQuery} new query with all filters removed.
-     */
-    clearFilters() {
-        return this._updateQuery(Q.clearFilters, arguments);
-    }
-
-    // SORTS
-
-    // TODO: standardize SORT vs ORDER_BY terminology
-
-    sorts(): OrderBy[] {
-        return Q.getOrderBys(this.query());
-    }
-    sortOptions(sort): DimensionOptions {
-        let sortOptions = { count: 0, dimensions: [], fks: [] };
-        // in bare rows all fields are sortable, otherwise we only sort by our breakout columns
-        if (this.isBareRows()) {
-            const usedFields = new Set(
-                this.sorts()
-                    .filter(b => !_.isEqual(b, sort))
-                    .map(b => Q_deprecated.getFieldTargetId(b[0]))
-            );
-
-            return this.fieldOptions(field => !usedFields.has(field.id));
-        } else if (this.hasValidBreakout()) {
-            for (const breakout of this.breakouts()) {
-                sortOptions.dimensions.push(
-                    Dimension.parseMBQL(breakout, this._metadata)
-                );
-                sortOptions.count++;
-            }
-            for (const [index, aggregation] of this.aggregations().entries()) {
-                if (Q_deprecated.canSortByAggregateField(this.query(), index)) {
-                    sortOptions.dimensions.push(
-                        new AggregationDimension(
-                            null,
-                            [index],
-                            this._metadata,
-                            aggregation[0]
-                        )
-                    );
-                    sortOptions.count++;
-                }
-            }
-        }
-        return sortOptions;
-    }
-    canAddSort(): boolean {
-        const sorts = this.sorts();
-        return this.sortOptions().count > 0 &&
-            (sorts.length === 0 || sorts[sorts.length - 1][0] != null);
-    }
-
-    addSort(order_by: OrderBy) {
-        return this._updateQuery(Q.addOrderBy, arguments);
-    }
-    updateSort(index: number, order_by: OrderBy) {
-        return this._updateQuery(Q.updateOrderBy, arguments);
-    }
-    removeSort(index: number) {
-        return this._updateQuery(Q.removeOrderBy, arguments);
-    }
-    clearSort() {
-        return this._updateQuery(Q.clearOrderBy, arguments);
-    }
-    replaceSort(order_by: OrderBy) {
-        return this.clearSort().addSort(order_by);
-    }
-
-    // LIMIT
-
-    limit(): ?number {
-        return Q.getLimit(this.query());
-    }
-    updateLimit(limit: LimitClause) {
-        return this._updateQuery(Q.updateLimit, arguments);
-    }
-    clearLimit() {
-        return this._updateQuery(Q.clearLimit, arguments);
-    }
-
-    // EXPRESSIONS
-
-    expressions(): { [key: string]: any } {
-        return Q.getExpressions(this.query());
-    }
-
-    updateExpression(name, expression, oldName) {
-        return this._updateQuery(Q.updateExpression, arguments);
-    }
-
-    removeExpression(name) {
-        return this._updateQuery(Q.removeExpression, arguments);
-    }
-
-    // FIELD OPTIONS
-
-    // TODO Atte Keinänen 6/18/17: Refactor to dimensionOptions which takes a dimensionFilter
-    // See aggregationFieldOptions for an explanation why that covers more use cases
-    fieldOptions(fieldFilter = () => true): DimensionOptions {
-        const fieldOptions = {
-            count: 0,
-            fks: [],
-            dimensions: []
-        };
-
-        const table = this.tableMetadata();
-        if (table) {
-            const dimensionFilter = dimension => {
-                const field = dimension.field && dimension.field();
-                return !field || (field.isDimension() && fieldFilter(field));
-            };
-
-            const dimensionIsFKReference = dimension =>
-                dimension.field &&
-                dimension.field() &&
-                dimension.field().isFK();
-
-            const filteredNonFKDimensions = this.dimensions()
-                .filter(dimensionFilter)
-                .filter(d => !dimensionIsFKReference(d));
-
-            for (const dimension of filteredNonFKDimensions) {
-                fieldOptions.count++;
-                fieldOptions.dimensions.push(dimension);
-            }
-
-            const fkDimensions = this.dimensions().filter(
-                dimensionIsFKReference
-            );
-            for (const dimension of fkDimensions) {
-                const fkDimensions = dimension
-                    .dimensions([FKDimension])
-                    .filter(dimensionFilter);
-
-                if (fkDimensions.length > 0) {
-                    fieldOptions.count += fkDimensions.length;
-                    fieldOptions.fks.push({
-                        field: dimension.field(),
-                        dimension: dimension,
-                        dimensions: fkDimensions
-                    });
-                }
-            }
+        const field = fieldId && this._metadata.fields[fieldId];
+        if (field) {
+          aggregationName += " of " + field.display_name;
         }
-
-        return fieldOptions;
-    }
-
-    // DIMENSIONS
-
-    dimensions(): Dimension[] {
-        return [...this.expressionDimensions(), ...this.tableDimensions()];
-    }
-
-    tableDimensions(): Dimension[] {
-        const table: Table = this.table();
-        return table ? table.dimensions() : [];
-    }
-
-    expressionDimensions(): Dimension[] {
-        return Object.entries(this.expressions()).map(([
-            expressionName,
-            expression
-        ]) => {
-            return new ExpressionDimension(null, [expressionName]);
-        });
-    }
-
-    aggregationDimensions() {
-        return this.breakouts().map(breakout =>
-            Dimension.parseMBQL(breakout, this._metadata));
-    }
-
-    metricDimensions() {
-        return this.aggregations().map(
-            (aggregation, index) =>
-                new AggregationDimension(
-                    null,
-                    [index],
-                    this._metadata,
-                    aggregation[0]
-                )
+        return aggregationName;
+      }
+    }
+    return null;
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the provided MBQL @type {Aggregation} added.
+   */
+  addAggregation(aggregation: Aggregation): StructuredQuery {
+    return this._updateQuery(Q.addAggregation, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the MBQL @type {Aggregation} updated at the provided index.
+   */
+  updateAggregation(index: number, aggregation: Aggregation): StructuredQuery {
+    return this._updateQuery(Q.updateAggregation, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the aggregation at the provided index removed.
+   */
+  removeAggregation(index: number): StructuredQuery {
+    return this._updateQuery(Q.removeAggregation, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with all aggregations removed.
+   */
+  clearAggregations(): StructuredQuery {
+    return this._updateQuery(Q.clearAggregations, arguments);
+  }
+
+  // BREAKOUTS
+
+  /**
+   * @returns An array of MBQL @type {Breakout}s.
+   */
+  breakouts(): Breakout[] {
+    return Q.getBreakouts(this.query());
+  }
+
+  /**
+   * @param includedBreakout The breakout to include even if it's already used
+   * @param fieldFilter An option @type {Field} predicate to filter out options
+   * @returns @type {DimensionOptions} that can be used as breakouts, excluding used breakouts, unless @param {breakout} is provided.
+   */
+  breakoutOptions(includedBreakout?: any, fieldFilter = () => true) {
+    // the set of field ids being used by other breakouts
+    const usedFields = new Set(
+      this.breakouts()
+        .filter(b => !_.isEqual(b, includedBreakout))
+        .map(b => Q_deprecated.getFieldTargetId(b)),
+    );
+
+    return this.fieldOptions(
+      field => fieldFilter(field) && !usedFields.has(field.id),
+    );
+  }
+
+  /**
+   * @returns whether a new breakout can be added or not
+   */
+  canAddBreakout(): boolean {
+    return this.breakoutOptions().count > 0;
+  }
+
+  /**
+   * @returns whether the current query has a valid breakout
+   */
+  hasValidBreakout(): boolean {
+    return Q_deprecated.hasValidBreakout(this.query());
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the provided MBQL @type {Breakout} added.
+   */
+  addBreakout(breakout: Breakout) {
+    return this._updateQuery(Q.addBreakout, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the MBQL @type {Breakout} updated at the provided index.
+   */
+  updateBreakout(index: number, breakout: Breakout) {
+    return this._updateQuery(Q.updateBreakout, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the breakout at the provided index removed.
+   */
+  removeBreakout(index: number) {
+    return this._updateQuery(Q.removeBreakout, arguments);
+  }
+  /**
+   * @returns {StructuredQuery} new query with all breakouts removed.
+   */
+  clearBreakouts() {
+    return this._updateQuery(Q.clearBreakouts, arguments);
+  }
+
+  // FILTERS
+
+  /**
+   * @returns An array of MBQL @type {Filter}s.
+   */
+  filters(): Filter[] {
+    return Q.getFilters(this.query());
+  }
+
+  /**
+   * @returns @type {DimensionOptions} that can be used in filters.
+   */
+  filterFieldOptions(): DimensionOptions {
+    return this.fieldOptions();
+  }
+
+  /**
+   * @returns @type {Segment}s that can be used as filters.
+   * TODO: exclude used segments
+   */
+  filterSegmentOptions(): Segment[] {
+    return this.table().segments.filter(sgmt => sgmt.is_active === true);
+  }
+
+  /**
+   * @returns whether a new filter can be added or not
+   */
+  canAddFilter(): boolean {
+    return (
+      Q.canAddFilter(this.query()) &&
+      (this.filterFieldOptions().count > 0 ||
+        this.filterSegmentOptions().length > 0)
+    );
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the provided MBQL @type {Filter} added.
+   */
+  addFilter(filter: Filter) {
+    return this._updateQuery(Q.addFilter, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the MBQL @type {Filter} updated at the provided index.
+   */
+  updateFilter(index: number, filter: Filter) {
+    return this._updateQuery(Q.updateFilter, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with the filter at the provided index removed.
+   */
+  removeFilter(index: number) {
+    return this._updateQuery(Q.removeFilter, arguments);
+  }
+
+  /**
+   * @returns {StructuredQuery} new query with all filters removed.
+   */
+  clearFilters() {
+    return this._updateQuery(Q.clearFilters, arguments);
+  }
+
+  // SORTS
+
+  // TODO: standardize SORT vs ORDER_BY terminology
+
+  sorts(): OrderBy[] {
+    return Q.getOrderBys(this.query());
+  }
+  sortOptions(sort): DimensionOptions {
+    let sortOptions = { count: 0, dimensions: [], fks: [] };
+    // in bare rows all fields are sortable, otherwise we only sort by our breakout columns
+    if (this.isBareRows()) {
+      const usedFields = new Set(
+        this.sorts()
+          .filter(b => !_.isEqual(b, sort))
+          .map(b => Q_deprecated.getFieldTargetId(b[0])),
+      );
+
+      return this.fieldOptions(field => !usedFields.has(field.id));
+    } else if (this.hasValidBreakout()) {
+      for (const breakout of this.breakouts()) {
+        sortOptions.dimensions.push(
+          Dimension.parseMBQL(breakout, this._metadata),
         );
-    }
-
-    fieldReferenceForColumn(column) {
-        if (column.fk_field_id != null) {
-            return ["fk->", column.fk_field_id, column.id];
-        } else if (column.id != null) {
-            return ["field-id", column.id];
-        } else if (column["expression-name"] != null) {
-            return ["expression", column["expression-name"]];
-        } else if (column.source === "aggregation") {
-            // FIXME: aggregations > 0?
-            return ["aggregation", 0];
+        sortOptions.count++;
+      }
+      for (const [index, aggregation] of this.aggregations().entries()) {
+        if (Q_deprecated.canSortByAggregateField(this.query(), index)) {
+          sortOptions.dimensions.push(
+            new AggregationDimension(
+              null,
+              [index],
+              this._metadata,
+              aggregation[0],
+            ),
+          );
+          sortOptions.count++;
         }
-    }
-
-    parseFieldReference(fieldRef): ?Dimension {
-        const dimension = Dimension.parseMBQL(fieldRef, this._metadata);
-        if (dimension) {
-            // HACK
-            if (dimension instanceof AggregationDimension) {
-                dimension._displayName = this.aggregations()[
-                    dimension._args[0]
-                ][0];
-            }
-            return dimension;
+      }
+    }
+    return sortOptions;
+  }
+  canAddSort(): boolean {
+    const sorts = this.sorts();
+    return (
+      this.sortOptions().count > 0 &&
+      (sorts.length === 0 || sorts[sorts.length - 1][0] != null)
+    );
+  }
+
+  addSort(order_by: OrderBy) {
+    return this._updateQuery(Q.addOrderBy, arguments);
+  }
+  updateSort(index: number, order_by: OrderBy) {
+    return this._updateQuery(Q.updateOrderBy, arguments);
+  }
+  removeSort(index: number) {
+    return this._updateQuery(Q.removeOrderBy, arguments);
+  }
+  clearSort() {
+    return this._updateQuery(Q.clearOrderBy, arguments);
+  }
+  replaceSort(order_by: OrderBy) {
+    return this.clearSort().addSort(order_by);
+  }
+
+  // LIMIT
+
+  limit(): ?number {
+    return Q.getLimit(this.query());
+  }
+  updateLimit(limit: LimitClause) {
+    return this._updateQuery(Q.updateLimit, arguments);
+  }
+  clearLimit() {
+    return this._updateQuery(Q.clearLimit, arguments);
+  }
+
+  // EXPRESSIONS
+
+  expressions(): { [key: string]: any } {
+    return Q.getExpressions(this.query());
+  }
+
+  updateExpression(name, expression, oldName) {
+    return this._updateQuery(Q.updateExpression, arguments);
+  }
+
+  removeExpression(name) {
+    return this._updateQuery(Q.removeExpression, arguments);
+  }
+
+  // FIELD OPTIONS
+
+  // TODO Atte Keinänen 6/18/17: Refactor to dimensionOptions which takes a dimensionFilter
+  // See aggregationFieldOptions for an explanation why that covers more use cases
+  fieldOptions(fieldFilter = () => true): DimensionOptions {
+    const fieldOptions = {
+      count: 0,
+      fks: [],
+      dimensions: [],
+    };
+
+    const table = this.tableMetadata();
+    if (table) {
+      const dimensionFilter = dimension => {
+        const field = dimension.field && dimension.field();
+        return !field || (field.isDimension() && fieldFilter(field));
+      };
+
+      const dimensionIsFKReference = dimension =>
+        dimension.field && dimension.field() && dimension.field().isFK();
+
+      const filteredNonFKDimensions = this.dimensions().filter(dimensionFilter);
+      // .filter(d => !dimensionIsFKReference(d));
+
+      for (const dimension of filteredNonFKDimensions) {
+        fieldOptions.count++;
+        fieldOptions.dimensions.push(dimension);
+      }
+
+      const fkDimensions = this.dimensions().filter(dimensionIsFKReference);
+      for (const dimension of fkDimensions) {
+        const fkDimensions = dimension
+          .dimensions([FKDimension])
+          .filter(dimensionFilter);
+
+        if (fkDimensions.length > 0) {
+          fieldOptions.count += fkDimensions.length;
+          fieldOptions.fks.push({
+            field: dimension.field(),
+            dimension: dimension,
+            dimensions: fkDimensions,
+          });
         }
-    }
-
-    setDatasetQuery(datasetQuery: DatasetQuery): StructuredQuery {
-        return new StructuredQuery(this._originalQuestion, datasetQuery);
-    }
-
-    // INTERNAL
-
-    _updateQuery(
-        updateFunction: (
-            query: StructuredQueryObject,
-            ...args: any[]
-        ) => StructuredQueryObject,
-        args: any[]
-    ): StructuredQuery {
-        return this.setDatasetQuery(
-            updateIn(this._datasetQuery, ["query"], query =>
-                updateFunction(query, ...args))
-        );
-    }
+      }
+    }
+
+    return fieldOptions;
+  }
+
+  // DIMENSIONS
+
+  dimensions(): Dimension[] {
+    return [...this.expressionDimensions(), ...this.tableDimensions()];
+  }
+
+  tableDimensions(): Dimension[] {
+    const table: Table = this.table();
+    return table ? table.dimensions() : [];
+  }
+
+  expressionDimensions(): Dimension[] {
+    return Object.entries(this.expressions()).map(
+      ([expressionName, expression]) => {
+        return new ExpressionDimension(null, [expressionName]);
+      },
+    );
+  }
+
+  aggregationDimensions() {
+    return this.breakouts().map(breakout =>
+      Dimension.parseMBQL(breakout, this._metadata),
+    );
+  }
+
+  metricDimensions() {
+    return this.aggregations().map(
+      (aggregation, index) =>
+        new AggregationDimension(null, [index], this._metadata, aggregation[0]),
+    );
+  }
+
+  fieldReferenceForColumn(column) {
+    if (column.fk_field_id != null) {
+      return ["fk->", column.fk_field_id, column.id];
+    } else if (column.id != null) {
+      return ["field-id", column.id];
+    } else if (column["expression-name"] != null) {
+      return ["expression", column["expression-name"]];
+    } else if (column.source === "aggregation") {
+      // FIXME: aggregations > 0?
+      return ["aggregation", 0];
+    }
+  }
+
+  parseFieldReference(fieldRef): ?Dimension {
+    const dimension = Dimension.parseMBQL(fieldRef, this._metadata);
+    if (dimension) {
+      // HACK
+      if (dimension instanceof AggregationDimension) {
+        dimension._displayName = this.aggregations()[dimension._args[0]][0];
+      }
+      return dimension;
+    }
+  }
+
+  setDatasetQuery(datasetQuery: DatasetQuery): StructuredQuery {
+    return new StructuredQuery(this._originalQuestion, datasetQuery);
+  }
+
+  // INTERNAL
+
+  _updateQuery(
+    updateFunction: (
+      query: StructuredQueryObject,
+      ...args: any[]
+    ) => StructuredQueryObject,
+    args: any[],
+  ): StructuredQuery {
+    return this.setDatasetQuery(
+      updateIn(this._datasetQuery, ["query"], query =>
+        updateFunction(query, ...args),
+      ),
+    );
+  }
 }
diff --git a/frontend/src/metabase-lib/lib/utils.js b/frontend/src/metabase-lib/lib/utils.js
index d8fceab79d85134dedc29ac0e45143ab6fc3e795..0faf4f700324676658992337314aa9005aca49d2 100644
--- a/frontend/src/metabase-lib/lib/utils.js
+++ b/frontend/src/metabase-lib/lib/utils.js
@@ -1,35 +1,32 @@
 export function nyi(target, key, descriptor) {
-    let method = descriptor.value;
-    descriptor.value = function() {
-        console.warn(
-            "Method not yet implemented: " +
-                target.constructor.name +
-                "::" +
-                key
-        );
-        return method.apply(this, arguments);
-    };
-    return descriptor;
+  let method = descriptor.value;
+  descriptor.value = function() {
+    console.warn(
+      "Method not yet implemented: " + target.constructor.name + "::" + key,
+    );
+    return method.apply(this, arguments);
+  };
+  return descriptor;
 }
 
 let memoized = new WeakMap();
 
 function getWithFallback(map, key, fallback) {
-    if (!map.has(key)) {
-        map.set(key, fallback());
-    }
-    return map.get(key);
+  if (!map.has(key)) {
+    map.set(key, fallback());
+  }
+  return map.get(key);
 }
 
 export function memoize(target, name, descriptor) {
-    let method = target[name];
-    descriptor.value = function(...args) {
-        const path = [this, method, ...args];
-        const last = path.pop();
-        const map = path.reduce(
-            (map, key) => getWithFallback(map, key, () => new Map()),
-            memoized
-        );
-        return getWithFallback(map, last, () => method.apply(this, args));
-    };
+  let method = target[name];
+  descriptor.value = function(...args) {
+    const path = [this, method, ...args];
+    const last = path.pop();
+    const map = path.reduce(
+      (map, key) => getWithFallback(map, key, () => new Map()),
+      memoized,
+    );
+    return getWithFallback(map, last, () => method.apply(this, args));
+  };
 }
diff --git a/frontend/src/metabase/App.jsx b/frontend/src/metabase/App.jsx
index 389ac21c4902d205209073709fe9d4ed561e6a13..0fcd8adccb0bdac56e42de1582dd15357dd7680a 100644
--- a/frontend/src/metabase/App.jsx
+++ b/frontend/src/metabase/App.jsx
@@ -1,7 +1,7 @@
 /* @flow weak */
 
-import React, {Component} from "react";
-import {connect} from "react-redux";
+import React, { Component } from "react";
+import { connect } from "react-redux";
 
 import Navbar from "metabase/nav/containers/Navbar.jsx";
 
@@ -12,32 +12,40 @@ import Unauthorized from "metabase/components/Unauthorized.jsx";
 import Archived from "metabase/components/Archived.jsx";
 
 const mapStateToProps = (state, props) => ({
-    errorPage: state.app.errorPage
+  errorPage: state.app.errorPage,
 });
 
-const getErrorComponent = ({status, data, context}) => {
-    if (status === 403) {
-        return <Unauthorized />
-    } else if (data && data.error_code === "archived" && context === "dashboard") {
-        return <Archived entityName="dashboard" linkTo="/dashboards/archive" />
-    } else if (data && data.error_code === "archived" && context === "query-builder") {
-        return <Archived entityName="question" linkTo="/questions/archive" />
-    } else {
-        return <NotFound />
-    }
-}
+const getErrorComponent = ({ status, data, context }) => {
+  if (status === 403) {
+    return <Unauthorized />;
+  } else if (
+    data &&
+    data.error_code === "archived" &&
+    context === "dashboard"
+  ) {
+    return <Archived entityName="dashboard" linkTo="/dashboards/archive" />;
+  } else if (
+    data &&
+    data.error_code === "archived" &&
+    context === "query-builder"
+  ) {
+    return <Archived entityName="question" linkTo="/questions/archive" />;
+  } else {
+    return <NotFound />;
+  }
+};
 
 @connect(mapStateToProps)
 export default class App extends Component {
-    render() {
-        const { children, location, errorPage } = this.props;
-
-        return (
-            <div className="spread flex flex-column">
-                <Navbar location={location} className="flex-no-shrink"/>
-                { errorPage ? getErrorComponent(errorPage) : children }
-                <UndoListing />
-            </div>
-        )
-    }
+  render() {
+    const { children, location, errorPage } = this.props;
+
+    return (
+      <div className="spread flex flex-column">
+        <Navbar location={location} className="flex-no-shrink" />
+        {errorPage ? getErrorComponent(errorPage) : children}
+        <UndoListing />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/admin.js b/frontend/src/metabase/admin/admin.js
index 3fafa57f7edcf894693e18488ea11c142ccd72e1..58e29ac79938814988e1f287aea5ed8172f933b1 100644
--- a/frontend/src/metabase/admin/admin.js
+++ b/frontend/src/metabase/admin/admin.js
@@ -11,9 +11,9 @@ import settings from "metabase/admin/settings/settings";
 import { combineReducers } from "metabase/lib/redux";
 
 export default combineReducers({
-    databases,
-    datamodel,
-    people,
-    permissions,
-    settings
-})
+  databases,
+  datamodel,
+  people,
+  permissions,
+  settings,
+});
diff --git a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
index d69deb027ae81fa4d703e8aa1cebeeaebb3b3075..f6eecbacf615870c7721b6f5427aa24a4d0c4700 100644
--- a/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
+++ b/frontend/src/metabase/admin/databases/components/CreatedDatabaseModal.jsx
@@ -1,39 +1,46 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import ModalContent from "metabase/components/ModalContent.jsx";
 
 import * as Urls from "metabase/lib/urls";
 
 export default class CreatedDatabaseModal extends Component {
-    static propTypes = {
-        databaseId: PropTypes.number.isRequired,
-        onClose: PropTypes.func.isRequired,
-        onDone: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    databaseId: PropTypes.number.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onDone: PropTypes.func.isRequired,
+  };
 
-    render() {
-        const { onClose, onDone, databaseId } = this.props;
-        return (
-            <ModalContent
-                title={t`Your database has been added!`}
-                onClose={onClose}
-            >
-                <div className="Form-inputs mb4">
-                    <p>
-                        {jt`We're analyzing its schema now to make some educated guesses about its
-                        metadata. ${<Link to={`/admin/datamodel/database/${databaseId}`}>View this
-                        database</Link>} in the Data Model section to see what we've found and to
-                        make edits, or ${<Link to={Urls.question(null, `?db=${databaseId}`)}>ask a question</Link>} about
+  render() {
+    const { onClose, onDone, databaseId } = this.props;
+    return (
+      <ModalContent title={t`Your database has been added!`} onClose={onClose}>
+        <div className="Form-inputs mb4">
+          <p>
+            {jt`We're analyzing its schema now to make some educated guesses about its
+                        metadata. ${(
+                          <Link to={`/admin/datamodel/database/${databaseId}`}>
+                            View this database
+                          </Link>
+                        )} in the Data Model section to see what we've found and to
+                        make edits, or ${(
+                          <Link to={Urls.question(null, `?db=${databaseId}`)}>
+                            ask a question
+                          </Link>
+                        )} about
                         this database.`}
-                    </p>
-                </div>
+          </p>
+        </div>
 
-                <div className="Form-actions flex layout-centered">
-                    <button className="Button Button--primary px3" onClick={onDone}>{t`Done`}</button>
-                </div>
-            </ModalContent>
-        );
-    }
+        <div className="Form-actions flex layout-centered">
+          <button
+            className="Button Button--primary px3"
+            onClick={onDone}
+          >{t`Done`}</button>
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx b/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx
index 387a0da7b070d69cb50ad4c4807ef93c7ec21272..c05c94b2f865bd011daa9c9f529c6773a44e331c 100644
--- a/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx
+++ b/frontend/src/metabase/admin/databases/components/DatabaseEditForms.jsx
@@ -2,56 +2,81 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import DatabaseDetailsForm from "metabase/components/DatabaseDetailsForm.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class DatabaseEditForms extends Component {
-    static propTypes = {
-        database: PropTypes.object,
-        details: PropTypes.object,
-        engines: PropTypes.object.isRequired,
-        hiddenFields: PropTypes.object,
-        selectEngine: PropTypes.func.isRequired,
-        save: PropTypes.func.isRequired,
-        formState: PropTypes.object
-    };
+  static propTypes = {
+    database: PropTypes.object,
+    details: PropTypes.object,
+    engines: PropTypes.object.isRequired,
+    hiddenFields: PropTypes.object,
+    selectEngine: PropTypes.func.isRequired,
+    save: PropTypes.func.isRequired,
+    formState: PropTypes.object,
+  };
 
-    render() {
-        let { database, details, hiddenFields, engines, formState: { formError, formSuccess, isSubmitting } } = this.props;
+  render() {
+    let {
+      database,
+      details,
+      hiddenFields,
+      engines,
+      formState: { formError, formSuccess, isSubmitting },
+    } = this.props;
 
-        let errors = {};
-        return (
-            <div className="mt4">
-                <div className={cx("Form-field", {"Form--fieldError": errors["engine"]})}>
-                    <label className="Form-label Form-offset">Database type: <span>{errors["engine"]}</span></label>
-                    <label className="Select Form-offset mt1">
-                        <select className="Select" defaultValue={database.engine}
-                                onChange={(e) => this.props.selectEngine(e.target.value)}>
-                            <option value="" disabled>{t`Select a database type`}</option>
-                            {Object.keys(engines).sort().map(opt =>
-                                <option key={opt} value={opt}>{engines[opt]['driver-name']}</option>
-                            )}
-                        </select>
-                    </label>
-                </div>
-                { database.engine ?
-                    <DatabaseDetailsForm
-                        details={{...details, name: database.name, is_full_sync: database.is_full_sync}}
-                        engine={database.engine}
-                        engines={engines}
-                        formError={formError}
-                        formSuccess={formSuccess}
-                        hiddenFields={hiddenFields}
-                        submitFn={(database) => this.props.save({
-                            ...database,
-                            id: this.props.database.id
-                        }, database.details)}
-                        isNewDatabase={!database.id}
-                        submitButtonText={t`Save`}
-                        submitting={isSubmitting}>
-                    </DatabaseDetailsForm>
-                    : null
-                }
-            </div>
-        );
-    }
+    let errors = {};
+    return (
+      <div className="mt4">
+        <div
+          className={cx("Form-field", { "Form--fieldError": errors["engine"] })}
+        >
+          <label className="Form-label Form-offset">
+            Database type: <span>{errors["engine"]}</span>
+          </label>
+          <label className="Select Form-offset mt1">
+            <select
+              className="Select"
+              defaultValue={database.engine}
+              onChange={e => this.props.selectEngine(e.target.value)}
+            >
+              <option value="" disabled>{t`Select a database type`}</option>
+              {Object.keys(engines)
+                .sort()
+                .map(opt => (
+                  <option key={opt} value={opt}>
+                    {engines[opt]["driver-name"]}
+                  </option>
+                ))}
+            </select>
+          </label>
+        </div>
+        {database.engine ? (
+          <DatabaseDetailsForm
+            details={{
+              ...details,
+              name: database.name,
+              is_full_sync: database.is_full_sync,
+            }}
+            engine={database.engine}
+            engines={engines}
+            formError={formError}
+            formSuccess={formSuccess}
+            hiddenFields={hiddenFields}
+            submitFn={database =>
+              this.props.save(
+                {
+                  ...database,
+                  id: this.props.database.id,
+                },
+                database.details,
+              )
+            }
+            isNewDatabase={!database.id}
+            submitButtonText={t`Save`}
+            submitting={isSubmitting}
+          />
+        ) : null}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx b/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx
index 15a807c1f6425aa4ae7ff44d1c612fa58fbe2f36..289fa6ce53330a46a9a552403e99fccc2ddc9477 100644
--- a/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx
+++ b/frontend/src/metabase/admin/databases/components/DatabaseSchedulingForm.jsx
@@ -2,187 +2,228 @@ import React, { Component } from "react";
 import cx from "classnames";
 import _ from "underscore";
 import { assocIn } from "icepick";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import FormMessage from "metabase/components/form/FormMessage";
 
 import SchedulePicker from "metabase/components/SchedulePicker";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
-export const SyncOption = ({ selected, name, children, select }) =>
-    <div className={cx("py3 relative", {"cursor-pointer": !selected})} onClick={() => select(name.toLowerCase()) }>
+export const SyncOption = ({ selected, name, children, select }) => (
+  <div
+    className={cx("py3 relative", { "cursor-pointer": !selected })}
+    onClick={() => select(name.toLowerCase())}
+  >
+    <div
+      className={cx("circle ml2 flex align-center justify-center absolute")}
+      style={{
+        width: 18,
+        height: 18,
+        borderWidth: 2,
+        borderColor: selected ? "#509ee3" : "#ddd",
+        borderStyle: "solid",
+      }}
+    >
+      {selected && (
         <div
-            className={cx('circle ml2 flex align-center justify-center absolute')}
-            style={{
-                width: 18,
-                height: 18,
-                borderWidth: 2,
-                borderColor: selected ? '#509ee3': '#ddd',
-                borderStyle: 'solid'
-            }}
-        >
-            { selected &&
-                <div
-                    className="circle"
-                    style={{ width: 8, height: 8, backgroundColor: selected ? '#509ee3' : '#ddd' }}
-                />
-            }
-        </div>
-        <div className="Form-offset ml1">
-            <div className={cx({ 'text-brand': selected })}>
-                <h3>{name}</h3>
-            </div>
-            { selected && children && <div className="mt2">{children}</div> }
-        </div>
+          className="circle"
+          style={{
+            width: 8,
+            height: 8,
+            backgroundColor: selected ? "#509ee3" : "#ddd",
+          }}
+        />
+      )}
     </div>
-
+    <div className="Form-offset ml1">
+      <div className={cx({ "text-brand": selected })}>
+        <h3>{name}</h3>
+      </div>
+      {selected && children && <div className="mt2">{children}</div>}
+    </div>
+  </div>
+);
 
 export default class DatabaseSchedulingForm extends Component {
-    constructor(props) {
-        super();
-
-        this.state = {
-            unsavedDatabase: props.database
-        }
-    }
-
-    updateSchemaSyncSchedule = (newSchedule, changedProp) => {
-        MetabaseAnalytics.trackEvent(
-            "DatabaseSyncEdit",
-            "SchemaSyncSchedule:" + changedProp.name,
-            changedProp.value
-        );
-
-        this.setState(assocIn(this.state, ["unsavedDatabase", "schedules", "metadata_sync"], newSchedule));
-    }
-
-    updateFieldScanSchedule = (newSchedule, changedProp) => {
-        MetabaseAnalytics.trackEvent(
-            "DatabaseSyncEdit",
-            "FieldScanSchedule:" + changedProp.name,
-            changedProp.value
-        );
-
-        this.setState(assocIn(this.state, ["unsavedDatabase", "schedules", "cache_field_values"], newSchedule));
-    }
-
-    setIsFullSyncIsOnDemand = (isFullSync, isOnDemand) => {
-        // TODO: Add event tracking
-        let state = assocIn(this.state, ["unsavedDatabase", "is_full_sync"], isFullSync);
-        state = assocIn(state, ["unsavedDatabase", "is_on_demand"], isOnDemand);
-        this.setState(state);
-    }
-
-    onSubmitForm = (event) => {
-        event.preventDefault();
-
-        const { unsavedDatabase } = this.state
-        this.props.save(unsavedDatabase, unsavedDatabase.details);
-    }
-
-    render() {
-        const { submitButtonText, formState: { formError, formSuccess, isSubmitting } } = this.props
-        const { unsavedDatabase } = this.state
-
-        return (
-            <LoadingAndErrorWrapper loading={!this.props.database} error={null}>
-                { () =>
-                    <form onSubmit={this.onSubmitForm} noValidate>
-
-                        <div className="Form-offset mr4 mt4">
-                            <div style={{maxWidth: 600}} className="border-bottom pb2">
-                                <p className="text-paragraph text-measure">
-                                  {t`To do some of its magic, Metabase needs to scan your database. We will also rescan it periodically to keep the metadata up-to-date. You can control when the periodic rescans happen below.`}
-                                </p>
-                            </div>
-
-                            <div className="border-bottom pb4">
-                                <h4 className="mt4 text-bold text-uppercase">{t`Database syncing`}</h4>
-                                <p className="text-paragraph text-measure">{t`This is a lightweight process that checks for
+  constructor(props) {
+    super();
+
+    this.state = {
+      unsavedDatabase: props.database,
+    };
+  }
+
+  updateSchemaSyncSchedule = (newSchedule, changedProp) => {
+    MetabaseAnalytics.trackEvent(
+      "DatabaseSyncEdit",
+      "SchemaSyncSchedule:" + changedProp.name,
+      changedProp.value,
+    );
+
+    this.setState(
+      assocIn(
+        this.state,
+        ["unsavedDatabase", "schedules", "metadata_sync"],
+        newSchedule,
+      ),
+    );
+  };
+
+  updateFieldScanSchedule = (newSchedule, changedProp) => {
+    MetabaseAnalytics.trackEvent(
+      "DatabaseSyncEdit",
+      "FieldScanSchedule:" + changedProp.name,
+      changedProp.value,
+    );
+
+    this.setState(
+      assocIn(
+        this.state,
+        ["unsavedDatabase", "schedules", "cache_field_values"],
+        newSchedule,
+      ),
+    );
+  };
+
+  setIsFullSyncIsOnDemand = (isFullSync, isOnDemand) => {
+    // TODO: Add event tracking
+    let state = assocIn(
+      this.state,
+      ["unsavedDatabase", "is_full_sync"],
+      isFullSync,
+    );
+    state = assocIn(state, ["unsavedDatabase", "is_on_demand"], isOnDemand);
+    this.setState(state);
+  };
+
+  onSubmitForm = event => {
+    event.preventDefault();
+
+    const { unsavedDatabase } = this.state;
+    this.props.save(unsavedDatabase, unsavedDatabase.details);
+  };
+
+  render() {
+    const {
+      submitButtonText,
+      formState: { formError, formSuccess, isSubmitting },
+    } = this.props;
+    const { unsavedDatabase } = this.state;
+
+    return (
+      <LoadingAndErrorWrapper loading={!this.props.database} error={null}>
+        {() => (
+          <form onSubmit={this.onSubmitForm} noValidate>
+            <div className="Form-offset mr4 mt4">
+              <div style={{ maxWidth: 600 }} className="border-bottom pb2">
+                <p className="text-paragraph text-measure">
+                  {t`To do some of its magic, Metabase needs to scan your database. We will also rescan it periodically to keep the metadata up-to-date. You can control when the periodic rescans happen below.`}
+                </p>
+              </div>
+
+              <div className="border-bottom pb4">
+                <h4 className="mt4 text-bold text-uppercase">{t`Database syncing`}</h4>
+                <p className="text-paragraph text-measure">{t`This is a lightweight process that checks for
                                     updates to this database’s schema. In most cases, you should be fine leaving this
                                     set to sync hourly.`}</p>
-                                <SchedulePicker
-                                    schedule={!_.isString(unsavedDatabase.schedules && unsavedDatabase.schedules.metadata_sync)
-                                            ? unsavedDatabase.schedules.metadata_sync
-                                            : {
-                                                schedule_day: "mon",
-                                                schedule_frame: null,
-                                                schedule_hour: 0,
-                                                schedule_type: "daily"
-                                            }
-                                    }
-                                    scheduleOptions={["hourly", "daily"]}
-                                    onScheduleChange={this.updateSchemaSyncSchedule}
-                                    textBeforeInterval={t`Scan`}
-                                />
-                            </div>
-
-                            <div className="mt4">
-                                <h4 className="text-bold text-default text-uppercase">{t`Scanning for Filter Values`}</h4>
-                                <p className="text-paragraph text-measure">{t`Metabase can scan the values present in each
+                <SchedulePicker
+                  schedule={
+                    !_.isString(
+                      unsavedDatabase.schedules &&
+                        unsavedDatabase.schedules.metadata_sync,
+                    )
+                      ? unsavedDatabase.schedules.metadata_sync
+                      : {
+                          schedule_day: "mon",
+                          schedule_frame: null,
+                          schedule_hour: 0,
+                          schedule_type: "daily",
+                        }
+                  }
+                  scheduleOptions={["hourly", "daily"]}
+                  onScheduleChange={this.updateSchemaSyncSchedule}
+                  textBeforeInterval={t`Scan`}
+                />
+              </div>
+
+              <div className="mt4">
+                <h4 className="text-bold text-default text-uppercase">{t`Scanning for Filter Values`}</h4>
+                <p className="text-paragraph text-measure">{t`Metabase can scan the values present in each
                                     field in this database to enable checkbox filters in dashboards and questions. This
                                     can be a somewhat resource-intensive process, particularly if you have a very large
                                     database.`}</p>
 
-                                <h3>{t`When should Metabase automatically scan and cache field values?`}</h3>
-                                <ol className="bordered shadowed mt3">
-                                    <li className="border-bottom">
-                                        <SyncOption
-                                            selected={unsavedDatabase.is_full_sync}
-                                            name={t`Regularly, on a schedule`}
-                                            select={() => this.setIsFullSyncIsOnDemand(true, false)}
-                                        >
-
-                                            <div className="flex align-center">
-                                                <SchedulePicker
-                                                    schedule={!_.isString(unsavedDatabase.schedules && unsavedDatabase.schedules.cache_field_values)
-                                                            ? unsavedDatabase.schedules.cache_field_values
-                                                            : {
-                                                                schedule_day: "mon",
-                                                                schedule_frame: null,
-                                                                schedule_hour: 0,
-                                                                schedule_type: "daily"
-                                                            }
-                                                    }
-                                                    scheduleOptions={["daily", "weekly", "monthly"]}
-                                                    onScheduleChange={this.updateFieldScanSchedule}
-                                                    textBeforeInterval={t`Scan`}
-                                                />
-                                            </div>
-                                        </SyncOption>
-                                    </li>
-                                    <li className="border-bottom pr2">
-                                        <SyncOption
-                                            selected={!unsavedDatabase.is_full_sync && unsavedDatabase.is_on_demand}
-                                            name={t`Only when adding a new filter widget`}
-                                            select={() => this.setIsFullSyncIsOnDemand(false, true)}
-                                        >
-                                            <p className="text-paragraph text-measure">
-                                                {t`When a user adds a new filter to a dashboard or a SQL question, Metabase will
+                <h3
+                >{t`When should Metabase automatically scan and cache field values?`}</h3>
+                <ol className="bordered shadowed mt3">
+                  <li className="border-bottom">
+                    <SyncOption
+                      selected={unsavedDatabase.is_full_sync}
+                      name={t`Regularly, on a schedule`}
+                      select={() => this.setIsFullSyncIsOnDemand(true, false)}
+                    >
+                      <div className="flex align-center">
+                        <SchedulePicker
+                          schedule={
+                            !_.isString(
+                              unsavedDatabase.schedules &&
+                                unsavedDatabase.schedules.cache_field_values,
+                            )
+                              ? unsavedDatabase.schedules.cache_field_values
+                              : {
+                                  schedule_day: "mon",
+                                  schedule_frame: null,
+                                  schedule_hour: 0,
+                                  schedule_type: "daily",
+                                }
+                          }
+                          scheduleOptions={["daily", "weekly", "monthly"]}
+                          onScheduleChange={this.updateFieldScanSchedule}
+                          textBeforeInterval={t`Scan`}
+                        />
+                      </div>
+                    </SyncOption>
+                  </li>
+                  <li className="border-bottom pr2">
+                    <SyncOption
+                      selected={
+                        !unsavedDatabase.is_full_sync &&
+                        unsavedDatabase.is_on_demand
+                      }
+                      name={t`Only when adding a new filter widget`}
+                      select={() => this.setIsFullSyncIsOnDemand(false, true)}
+                    >
+                      <p className="text-paragraph text-measure">
+                        {t`When a user adds a new filter to a dashboard or a SQL question, Metabase will
                                                 scan the field(s) mapped to that filter in order to show the list of selectable values.`}
-                                            </p>
-                                        </SyncOption>
-                                    </li>
-                                    <li>
-                                        <SyncOption
-                                            selected={!unsavedDatabase.is_full_sync && !unsavedDatabase.is_on_demand}
-                                            name={t`Never, I'll do this manually if I need to`}
-                                            select={() => this.setIsFullSyncIsOnDemand(false, false)}
-                                        />
-                                    </li>
-                                </ol>
-                            </div>
-
-                        </div>
-                        <div className="Form-actions mt4">
-                            <button className={"Button Button--primary"} disabled={isSubmitting}>
-                                {isSubmitting ? t`Saving...` : submitButtonText }
-                            </button>
-                            <FormMessage formError={formError} formSuccess={formSuccess}/>
-                        </div>
-                    </form>
-                }
-            </LoadingAndErrorWrapper>
-        )
-    }
+                      </p>
+                    </SyncOption>
+                  </li>
+                  <li>
+                    <SyncOption
+                      selected={
+                        !unsavedDatabase.is_full_sync &&
+                        !unsavedDatabase.is_on_demand
+                      }
+                      name={t`Never, I'll do this manually if I need to`}
+                      select={() => this.setIsFullSyncIsOnDemand(false, false)}
+                    />
+                  </li>
+                </ol>
+              </div>
+            </div>
+            <div className="Form-actions mt4">
+              <button
+                className={"Button Button--primary"}
+                disabled={isSubmitting}
+              >
+                {isSubmitting ? t`Saving...` : submitButtonText}
+              </button>
+              <FormMessage formError={formError} formSuccess={formSuccess} />
+            </div>
+          </form>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
index 17b8cae7a3d766a11939d25f9e32eede6e115c4a..810243a9adaa8d397ae978180cec2826b5e3b3e6 100644
--- a/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
+++ b/frontend/src/metabase/admin/databases/components/DeleteDatabaseModal.jsx
@@ -2,79 +2,91 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 export default class DeleteDatabaseModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            confirmValue: "",
-            error: null
-        };
-    }
-
-    static propTypes = {
-        database: PropTypes.object.isRequired,
-        onClose: PropTypes.func,
-        onDelete: PropTypes.func
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      confirmValue: "",
+      error: null,
     };
+  }
+
+  static propTypes = {
+    database: PropTypes.object.isRequired,
+    onClose: PropTypes.func,
+    onDelete: PropTypes.func,
+  };
 
-    async deleteDatabase() {
-        try {
-            this.props.onDelete(this.props.database);
-            // immediately call on close because database deletion should be non blocking
-            this.props.onClose()
-        } catch (error) {
-            this.setState({ error });
-        }
+  async deleteDatabase() {
+    try {
+      this.props.onDelete(this.props.database);
+      // immediately call on close because database deletion should be non blocking
+      this.props.onClose();
+    } catch (error) {
+      this.setState({ error });
     }
+  }
 
-    render() {
-        const { database } = this.props;
+  render() {
+    const { database } = this.props;
 
-        var formError;
-        if (this.state.error) {
-            var errorMessage = t`Server error encountered`;
-            if (this.state.error.data &&
-                this.state.error.data.message) {
-                errorMessage = this.state.error.data.message;
-            } else {
-                errorMessage = this.state.error.message;
-            }
+    var formError;
+    if (this.state.error) {
+      var errorMessage = t`Server error encountered`;
+      if (this.state.error.data && this.state.error.data.message) {
+        errorMessage = this.state.error.data.message;
+      } else {
+        errorMessage = this.state.error.message;
+      }
 
-            // TODO: timeout display?
-            formError = (
-                <span className="text-error px2">{errorMessage}</span>
-            );
-        }
+      // TODO: timeout display?
+      formError = <span className="text-error px2">{errorMessage}</span>;
+    }
 
-        let confirmed = this.state.confirmValue.toUpperCase() === "DELETE";
+    let confirmed = this.state.confirmValue.toUpperCase() === "DELETE";
 
-        return (
-            <ModalContent
-                title={t`Delete this database?`}
-                onClose={this.props.onClose}
-            >
-                <div className="Form-inputs mb4">
-                    { database.is_sample &&
-                        <p className="text-paragraph">{t`<strong>Just a heads up:</strong> without the Sample Dataset, the Query Builder tutorial won't work. You can always restore the Sample Dataset, but any questions you've saved using this data will be lost.`}</p>
-                    }
-                    <p className="text-paragraph">
-                      {t`All saved questions, metrics, and segments that rely on this database will be lost.`} <strong>{t`This cannot be undone.`}</strong>
-                    </p>
-                    <p className="text-paragraph">
-                      {t`If you're sure, please type`} <strong>{t`DELETE`}</strong> {t`in this box:`}
-                    </p>
-                    <input className="Form-input" type="text" onChange={(e) => this.setState({ confirmValue: e.target.value })} autoFocus />
-                </div>
+    return (
+      <ModalContent
+        title={t`Delete this database?`}
+        onClose={this.props.onClose}
+      >
+        <div className="Form-inputs mb4">
+          {database.is_sample && (
+            <p className="text-paragraph">{t`<strong>Just a heads up:</strong> without the Sample Dataset, the Query Builder tutorial won't work. You can always restore the Sample Dataset, but any questions you've saved using this data will be lost.`}</p>
+          )}
+          <p className="text-paragraph">
+            {t`All saved questions, metrics, and segments that rely on this database will be lost.`}{" "}
+            <strong>{t`This cannot be undone.`}</strong>
+          </p>
+          <p className="text-paragraph">
+            {t`If you're sure, please type`} <strong>{t`DELETE`}</strong>{" "}
+            {t`in this box:`}
+          </p>
+          <input
+            className="Form-input"
+            type="text"
+            onChange={e => this.setState({ confirmValue: e.target.value })}
+            autoFocus
+          />
+        </div>
 
-                <div className="Form-actions ml-auto">
-                    <button className="Button" onClick={this.props.onClose}>{t`Cancel`}</button>
-                    <button className={cx("Button Button--danger ml2", { "disabled": !confirmed })} onClick={() => this.deleteDatabase()}>{t`Delete`}</button>
-                    {formError}
-                </div>
-            </ModalContent>
-        );
-    }
+        <div className="Form-actions ml-auto">
+          <button
+            className="Button"
+            onClick={this.props.onClose}
+          >{t`Cancel`}</button>
+          <button
+            className={cx("Button Button--danger ml2", {
+              disabled: !confirmed,
+            })}
+            onClick={() => this.deleteDatabase()}
+          >{t`Delete`}</button>
+          {formError}
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
index ce7b092b56077daa4102c29d5770853ad615bd91..f8f1cf5b46f43acfbdc0629d2ef0e739c1f16f8e 100644
--- a/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseEditApp.jsx
@@ -8,245 +8,264 @@ import MetabaseSettings from "metabase/lib/settings";
 import DeleteDatabaseModal from "../components/DeleteDatabaseModal.jsx";
 import DatabaseEditForms from "../components/DatabaseEditForms.jsx";
 import DatabaseSchedulingForm from "../components/DatabaseSchedulingForm";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ActionButton from "metabase/components/ActionButton.jsx";
-import Breadcrumbs from "metabase/components/Breadcrumbs.jsx"
+import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 
 import {
-    getEditingDatabase,
-    getFormState,
-    getDatabaseCreationStep
+  getEditingDatabase,
+  getFormState,
+  getDatabaseCreationStep,
 } from "../selectors";
 
 import {
-    reset,
-    initializeDatabase,
-    proceedWithDbCreation,
-    saveDatabase,
-    syncDatabaseSchema,
-    rescanDatabaseFields,
-    discardSavedFieldValues,
-    deleteDatabase,
-    selectEngine
+  reset,
+  initializeDatabase,
+  proceedWithDbCreation,
+  saveDatabase,
+  syncDatabaseSchema,
+  rescanDatabaseFields,
+  discardSavedFieldValues,
+  deleteDatabase,
+  selectEngine,
 } from "../database";
 import ConfirmContent from "metabase/components/ConfirmContent";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
 const mapStateToProps = (state, props) => ({
-    database:  getEditingDatabase(state),
-    databaseCreationStep: getDatabaseCreationStep(state),
-    formState: getFormState(state)
+  database: getEditingDatabase(state),
+  databaseCreationStep: getDatabaseCreationStep(state),
+  formState: getFormState(state),
 });
 
 export const Tab = ({ name, setTab, currentTab }) => {
-    const isCurrentTab = currentTab === name.toLowerCase()
+  const isCurrentTab = currentTab === name.toLowerCase();
 
-    return (
-        <div
-            className={cx('cursor-pointer py2', {'text-brand': isCurrentTab })}
-            // TODO Use css classes instead?
-            style={isCurrentTab ? { borderBottom: "3px solid #509EE3" } : {}}
-            onClick={() => setTab(name)}>
-            <h3>{name}</h3>
-        </div>
-    )
-}
-
-export const Tabs = ({ tabs, currentTab, setTab }) =>
-    <div className="border-bottom">
-        <ol className="Form-offset flex align center">
-            {tabs.map((tab, index) =>
-                <li key={index} className="mr3">
-                    <Tab
-                        name={tab}
-                        setTab={setTab}
-                        currentTab={currentTab}
-                    />
-                </li>
-            )}
-        </ol>
+  return (
+    <div
+      className={cx("cursor-pointer py2", { "text-brand": isCurrentTab })}
+      // TODO Use css classes instead?
+      style={isCurrentTab ? { borderBottom: "3px solid #509EE3" } : {}}
+      onClick={() => setTab(name)}
+    >
+      <h3>{name}</h3>
     </div>
+  );
+};
+
+export const Tabs = ({ tabs, currentTab, setTab }) => (
+  <div className="border-bottom">
+    <ol className="Form-offset flex align center">
+      {tabs.map((tab, index) => (
+        <li key={index} className="mr3">
+          <Tab name={tab} setTab={setTab} currentTab={currentTab} />
+        </li>
+      ))}
+    </ol>
+  </div>
+);
 
 const mapDispatchToProps = {
-    reset,
-    initializeDatabase,
-    proceedWithDbCreation,
-    saveDatabase,
-    syncDatabaseSchema,
-    rescanDatabaseFields,
-    discardSavedFieldValues,
-    deleteDatabase,
-    selectEngine
+  reset,
+  initializeDatabase,
+  proceedWithDbCreation,
+  saveDatabase,
+  syncDatabaseSchema,
+  rescanDatabaseFields,
+  discardSavedFieldValues,
+  deleteDatabase,
+  selectEngine,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 @title(({ database }) => database && database.name)
 export default class DatabaseEditApp extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            currentTab: 'connection'
-        };
-    }
-
-    static propTypes = {
-        database: PropTypes.object,
-        databaseCreationStep: PropTypes.string,
-        formState: PropTypes.object.isRequired,
-        params: PropTypes.object.isRequired,
-        reset: PropTypes.func.isRequired,
-        initializeDatabase: PropTypes.func.isRequired,
-        syncDatabaseSchema: PropTypes.func.isRequired,
-        rescanDatabaseFields: PropTypes.func.isRequired,
-        discardSavedFieldValues: PropTypes.func.isRequired,
-        proceedWithDbCreation: PropTypes.func.isRequired,
-        deleteDatabase: PropTypes.func.isRequired,
-        saveDatabase: PropTypes.func.isRequired,
-        selectEngine: PropTypes.func.isRequired,
-        location: PropTypes.object
+    this.state = {
+      currentTab: "connection",
     };
+  }
 
-    async componentWillMount() {
-        await this.props.reset();
-        await this.props.initializeDatabase(this.props.params.databaseId);
-    }
+  static propTypes = {
+    database: PropTypes.object,
+    databaseCreationStep: PropTypes.string,
+    formState: PropTypes.object.isRequired,
+    params: PropTypes.object.isRequired,
+    reset: PropTypes.func.isRequired,
+    initializeDatabase: PropTypes.func.isRequired,
+    syncDatabaseSchema: PropTypes.func.isRequired,
+    rescanDatabaseFields: PropTypes.func.isRequired,
+    discardSavedFieldValues: PropTypes.func.isRequired,
+    proceedWithDbCreation: PropTypes.func.isRequired,
+    deleteDatabase: PropTypes.func.isRequired,
+    saveDatabase: PropTypes.func.isRequired,
+    selectEngine: PropTypes.func.isRequired,
+    location: PropTypes.object,
+  };
+
+  async componentWillMount() {
+    await this.props.reset();
+    await this.props.initializeDatabase(this.props.params.databaseId);
+  }
 
-    componentWillReceiveProps(nextProps) {
-        const addingNewDatabase = !nextProps.database || !nextProps.database.id
+  componentWillReceiveProps(nextProps) {
+    const addingNewDatabase = !nextProps.database || !nextProps.database.id;
 
-       if (addingNewDatabase) {
-            // Update the current creation step (= active tab) if adding a new database
-            this.setState({ currentTab: nextProps.databaseCreationStep });
-        }
+    if (addingNewDatabase) {
+      // Update the current creation step (= active tab) if adding a new database
+      this.setState({ currentTab: nextProps.databaseCreationStep });
     }
+  }
 
-    render() {
-        let { database, formState } = this.props;
-        const { currentTab } = this.state;
-
-        const editingExistingDatabase = database && database.id != null
-        const addingNewDatabase = !editingExistingDatabase
-
-        const letUserControlScheduling = database && database.details && database.details["let-user-control-scheduling"]
-        const showTabs = editingExistingDatabase && letUserControlScheduling
-
-        return (
-            <div className="wrapper">
-                <Breadcrumbs className="py4" crumbs={[
-                    [t`Databases`, "/admin/databases"],
-                    [addingNewDatabase ? t`Add Database` : database.name]
-                ]} />
-                <section className="Grid Grid--gutters Grid--2-of-3">
-                    <div className="Grid-cell">
-                        <div className="Form-new bordered rounded shadowed pt0">
-                            { showTabs &&
-                                <Tabs
-                                    tabs={[t`Connection`, t`Scheduling`]}
-                                    currentTab={currentTab}
-                                    setTab={tab => this.setState({currentTab: tab.toLowerCase()})}
-                                />
-                            }
-                            <LoadingAndErrorWrapper loading={!database} error={null}>
-                                { () =>
-                                    <div>
-                                        { currentTab === 'connection' &&
-                                        <DatabaseEditForms
-                                            database={database}
-                                            details={database ? database.details : null}
-                                            engines={MetabaseSettings.get('engines')}
-                                            hiddenFields={{ssl: true}}
-                                            formState={formState}
-                                            selectEngine={this.props.selectEngine}
-                                            save={ addingNewDatabase
-                                                ? this.props.proceedWithDbCreation
-                                                : this.props.saveDatabase
-                                            }
-                                        />
-                                        }
-                                        { currentTab === 'scheduling' &&
-                                        <DatabaseSchedulingForm
-                                            database={database}
-                                            formState={formState}
-                                            // Use saveDatabase both for db creation and updating
-                                            save={this.props.saveDatabase}
-                                            submitButtonText={ addingNewDatabase ? t`Save` : t`Save changes` }
-                                        />
-                                        }
-                                    </div>
-                                }
-                            </LoadingAndErrorWrapper>
-                        </div>
-                    </div>
-
-                    { /* Sidebar Actions */ }
-                    { editingExistingDatabase &&
-                        <div className="Grid-cell Cell--1of3">
-                            <div className="Actions bordered rounded shadowed">
-                                <div className="Actions-group">
-                                    <label className="Actions-groupLabel block text-bold">{t`Actions`}</label>
-                                    <ol>
-                                        <li>
-                                            <ActionButton
-                                                actionFn={() => this.props.syncDatabaseSchema(database.id)}
-                                                className="Button Button--syncDbSchema"
-                                                normalText={t`Sync database schema now`}
-                                                activeText={t`Starting…`}
-                                                failedText={t`Failed to sync`}
-                                                successText={t`Sync triggered!`}
-                                            />
-                                        </li>
-                                        <li className="mt2">
-                                            <ActionButton
-                                                actionFn={() => this.props.rescanDatabaseFields(database.id)}
-                                                className="Button Button--rescanFieldValues"
-                                                normalText={t`Re-scan field values now`}
-                                                activeText={t`Starting…`}
-                                                failedText={t`Failed to start scan`}
-                                                successText={t`Scan triggered!`}
-                                            />
-                                        </li>
-                                    </ol>
-                                </div>
-
-                                <div className="Actions-group">
-                                    <label className="Actions-groupLabel block text-bold">{t`Danger Zone`}</label>
-                                    <ol>
-                                        <li>
-                                            <ModalWithTrigger
-                                                ref="discardSavedFieldValuesModal"
-                                                triggerClasses="Button Button--danger Button--discardSavedFieldValues"
-                                                triggerElement={t`Discard saved field values`}
-                                            >
-                                                <ConfirmContent
-                                                    title={t`Discard saved field values`}
-                                                    onClose={() => this.refs.discardSavedFieldValuesModal.toggle()}
-                                                    onAction={() => this.props.discardSavedFieldValues(database.id)}
-                                                />
-                                            </ModalWithTrigger>
-                                        </li>
-
-                                        <li className="mt2">
-                                            <ModalWithTrigger
-                                                ref="deleteDatabaseModal"
-                                                triggerClasses="Button Button--deleteDatabase Button--danger"
-                                                triggerElement={t`Remove this database`}
-                                            >
-                                                <DeleteDatabaseModal
-                                                    database={database}
-                                                    onClose={() => this.refs.deleteDatabaseModal.toggle()}
-                                                    onDelete={() => this.props.deleteDatabase(database.id, true)}
-                                                />
-                                            </ModalWithTrigger>
-                                        </li>
-                                    </ol>
-                                </div>
-                            </div>
-                        </div>
-                    }
-                </section>
+  render() {
+    let { database, formState } = this.props;
+    const { currentTab } = this.state;
+
+    const editingExistingDatabase = database && database.id != null;
+    const addingNewDatabase = !editingExistingDatabase;
+
+    const letUserControlScheduling =
+      database &&
+      database.details &&
+      database.details["let-user-control-scheduling"];
+    const showTabs = editingExistingDatabase && letUserControlScheduling;
+
+    return (
+      <div className="wrapper">
+        <Breadcrumbs
+          className="py4"
+          crumbs={[
+            [t`Databases`, "/admin/databases"],
+            [addingNewDatabase ? t`Add Database` : database.name],
+          ]}
+        />
+        <section className="Grid Grid--gutters Grid--2-of-3">
+          <div className="Grid-cell">
+            <div className="Form-new bordered rounded shadowed pt0">
+              {showTabs && (
+                <Tabs
+                  tabs={[t`Connection`, t`Scheduling`]}
+                  currentTab={currentTab}
+                  setTab={tab =>
+                    this.setState({ currentTab: tab.toLowerCase() })
+                  }
+                />
+              )}
+              <LoadingAndErrorWrapper loading={!database} error={null}>
+                {() => (
+                  <div>
+                    {currentTab === "connection" && (
+                      <DatabaseEditForms
+                        database={database}
+                        details={database ? database.details : null}
+                        engines={MetabaseSettings.get("engines")}
+                        hiddenFields={{ ssl: true }}
+                        formState={formState}
+                        selectEngine={this.props.selectEngine}
+                        save={
+                          addingNewDatabase
+                            ? this.props.proceedWithDbCreation
+                            : this.props.saveDatabase
+                        }
+                      />
+                    )}
+                    {currentTab === "scheduling" && (
+                      <DatabaseSchedulingForm
+                        database={database}
+                        formState={formState}
+                        // Use saveDatabase both for db creation and updating
+                        save={this.props.saveDatabase}
+                        submitButtonText={
+                          addingNewDatabase ? t`Save` : t`Save changes`
+                        }
+                      />
+                    )}
+                  </div>
+                )}
+              </LoadingAndErrorWrapper>
             </div>
-        );
-    }
+          </div>
+
+          {/* Sidebar Actions */}
+          {editingExistingDatabase && (
+            <div className="Grid-cell Cell--1of3">
+              <div className="Actions bordered rounded shadowed">
+                <div className="Actions-group">
+                  <label className="Actions-groupLabel block text-bold">{t`Actions`}</label>
+                  <ol>
+                    <li>
+                      <ActionButton
+                        actionFn={() =>
+                          this.props.syncDatabaseSchema(database.id)
+                        }
+                        className="Button Button--syncDbSchema"
+                        normalText={t`Sync database schema now`}
+                        activeText={t`Starting…`}
+                        failedText={t`Failed to sync`}
+                        successText={t`Sync triggered!`}
+                      />
+                    </li>
+                    <li className="mt2">
+                      <ActionButton
+                        actionFn={() =>
+                          this.props.rescanDatabaseFields(database.id)
+                        }
+                        className="Button Button--rescanFieldValues"
+                        normalText={t`Re-scan field values now`}
+                        activeText={t`Starting…`}
+                        failedText={t`Failed to start scan`}
+                        successText={t`Scan triggered!`}
+                      />
+                    </li>
+                  </ol>
+                </div>
+
+                <div className="Actions-group">
+                  <label className="Actions-groupLabel block text-bold">{t`Danger Zone`}</label>
+                  <ol>
+                    <li>
+                      <ModalWithTrigger
+                        ref="discardSavedFieldValuesModal"
+                        triggerClasses="Button Button--danger Button--discardSavedFieldValues"
+                        triggerElement={t`Discard saved field values`}
+                      >
+                        <ConfirmContent
+                          title={t`Discard saved field values`}
+                          onClose={() =>
+                            this.refs.discardSavedFieldValuesModal.toggle()
+                          }
+                          onAction={() =>
+                            this.props.discardSavedFieldValues(database.id)
+                          }
+                        />
+                      </ModalWithTrigger>
+                    </li>
+
+                    <li className="mt2">
+                      <ModalWithTrigger
+                        ref="deleteDatabaseModal"
+                        triggerClasses="Button Button--deleteDatabase Button--danger"
+                        triggerElement={t`Remove this database`}
+                      >
+                        <DeleteDatabaseModal
+                          database={database}
+                          onClose={() => this.refs.deleteDatabaseModal.toggle()}
+                          onDelete={() =>
+                            this.props.deleteDatabase(database.id, true)
+                          }
+                        />
+                      </ModalWithTrigger>
+                    </li>
+                  </ol>
+                </div>
+              </div>
+            </div>
+          )}
+        </section>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx
index c677b8849fa68f03ba8d0ff939a7e2f6effdedb1..d3d49e0748f15ae8238ef256ea2082e40ae168ac 100644
--- a/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx
+++ b/frontend/src/metabase/admin/databases/containers/DatabaseListApp.jsx
@@ -7,144 +7,170 @@ import cx from "classnames";
 import MetabaseSettings from "metabase/lib/settings";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import CreatedDatabaseModal from "../components/CreatedDatabaseModal.jsx";
 import DeleteDatabaseModal from "../components/DeleteDatabaseModal.jsx";
 
 import {
-    getDatabasesSorted,
-    hasSampleDataset,
-    getDeletes,
-    getDeletionError
+  getDatabasesSorted,
+  hasSampleDataset,
+  getDeletes,
+  getDeletionError,
 } from "../selectors";
 import * as databaseActions from "../database";
 import FormMessage from "metabase/components/form/FormMessage";
 
 const mapStateToProps = (state, props) => {
-    return {
-        created:              props.location.query.created,
-        databases:            getDatabasesSorted(state),
-        hasSampleDataset:     hasSampleDataset(state),
-        engines:              MetabaseSettings.get('engines'),
-        deletes:              getDeletes(state),
-        deletionError:        getDeletionError(state)
-    }
-}
+  return {
+    created: props.location.query.created,
+    databases: getDatabasesSorted(state),
+    hasSampleDataset: hasSampleDataset(state),
+    engines: MetabaseSettings.get("engines"),
+    deletes: getDeletes(state),
+    deletionError: getDeletionError(state),
+  };
+};
 
 const mapDispatchToProps = {
-    ...databaseActions
-}
+  ...databaseActions,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class DatabaseList extends Component {
-    static propTypes = {
-        databases: PropTypes.array,
-        hasSampleDataset: PropTypes.bool,
-        engines: PropTypes.object,
-        deletes: PropTypes.array,
-        deletionError: PropTypes.object
-    };
+  static propTypes = {
+    databases: PropTypes.array,
+    hasSampleDataset: PropTypes.bool,
+    engines: PropTypes.object,
+    deletes: PropTypes.array,
+    deletionError: PropTypes.object,
+  };
 
-    componentWillMount() {
-        this.props.fetchDatabases();
-    }
+  componentWillMount() {
+    this.props.fetchDatabases();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (!this.props.created && newProps.created) {
-            this.refs.createdDatabaseModal.open()
-        }
+  componentWillReceiveProps(newProps) {
+    if (!this.props.created && newProps.created) {
+      this.refs.createdDatabaseModal.open();
     }
+  }
 
-    render() {
-        let { databases, hasSampleDataset, created, engines, deletionError } = this.props;
+  render() {
+    let {
+      databases,
+      hasSampleDataset,
+      created,
+      engines,
+      deletionError,
+    } = this.props;
 
-        return (
-            <div className="wrapper">
-                <section className="PageHeader px2 clearfix">
-                    <Link to="/admin/databases/create" className="Button Button--primary float-right">{t`Add database`}</Link>
-                    <h2 className="PageTitle">{t`Databases`}</h2>
-                </section>
-                { deletionError &&
-                    <section>
-                        <FormMessage formError={deletionError} />
-                    </section>
-                }
-                <section>
-                    <table className="ContentTable">
-                        <thead>
-                            <tr>
-                                <th>{t`Name`}</th>
-                                <th>{t`Engine`}</th>
-                                <th></th>
-                            </tr>
-                        </thead>
-                        <tbody>
-                            { databases ?
-                                [ databases.map(database => {
-                                    const isDeleting = this.props.deletes.indexOf(database.id) !== -1
-                                    return (
-                                        <tr
-                                            key={database.id}
-                                            className={cx({'disabled': isDeleting })}
-                                        >
-                                            <td>
-                                                <Link to={"/admin/databases/"+database.id} className="text-bold link">
-                                                    {database.name}
-                                                </Link>
-                                            </td>
-                                            <td>
-                                                {engines && engines[database.engine] ? engines[database.engine]['driver-name'] : database.engine}
-                                            </td>
-                                            { isDeleting
-                                                ? (<td className="text-right">{t`Deleting...`}</td>)
-                                                : (
-                                                    <td className="Table-actions">
-                                                        <ModalWithTrigger
-                                                            ref={"deleteDatabaseModal_"+database.id}
-                                                            triggerClasses="Button Button--danger"
-                                                            triggerElement={t`Delete`}
-                                                        >
-                                                            <DeleteDatabaseModal
-                                                                database={database}
-                                                                onClose={() => this.refs["deleteDatabaseModal_"+database.id].close()}
-                                                                onDelete={() => this.props.deleteDatabase(database.id)}
-                                                            />
-                                                        </ModalWithTrigger>
-                                                    </td>
-                                                )
-                                            }
-                                        </tr>
-                                    )}),
-                                ]
-                            :
-                                <tr>
-                                    <td colSpan={4}>
-                                        <LoadingSpinner />
-                                        <h3>{t`Loading ...`}</h3>
-                                    </td>
-                                </tr>
-                            }
-                        </tbody>
-                    </table>
-                    { !hasSampleDataset ?
-                        <div className="pt4">
-                            <span className={cx("p2 text-italic", {"border-top": databases && databases.length > 0})}>
-                                <a className="text-grey-2 text-brand-hover no-decoration" onClick={() => this.props.addSampleDataset()}>{t`Bring the sample dataset back`}</a>
-                            </span>
-                        </div>
-                    : null }
-                </section>
-                <ModalWithTrigger
-                    ref="createdDatabaseModal"
-                    isInitiallyOpen={created}
-                >
-                    <CreatedDatabaseModal
-                        databaseId={parseInt(created)}
-                        onDone={() => this.refs.createdDatabaseModal.toggle() }
-                        onClose={() => this.refs.createdDatabaseModal.toggle() }
-                    />
-                </ModalWithTrigger>
+    return (
+      <div className="wrapper">
+        <section className="PageHeader px2 clearfix">
+          <Link
+            to="/admin/databases/create"
+            className="Button Button--primary float-right"
+          >{t`Add database`}</Link>
+          <h2 className="PageTitle">{t`Databases`}</h2>
+        </section>
+        {deletionError && (
+          <section>
+            <FormMessage formError={deletionError} />
+          </section>
+        )}
+        <section>
+          <table className="ContentTable">
+            <thead>
+              <tr>
+                <th>{t`Name`}</th>
+                <th>{t`Engine`}</th>
+                <th />
+              </tr>
+            </thead>
+            <tbody>
+              {databases ? (
+                [
+                  databases.map(database => {
+                    const isDeleting =
+                      this.props.deletes.indexOf(database.id) !== -1;
+                    return (
+                      <tr
+                        key={database.id}
+                        className={cx({ disabled: isDeleting })}
+                      >
+                        <td>
+                          <Link
+                            to={"/admin/databases/" + database.id}
+                            className="text-bold link"
+                          >
+                            {database.name}
+                          </Link>
+                        </td>
+                        <td>
+                          {engines && engines[database.engine]
+                            ? engines[database.engine]["driver-name"]
+                            : database.engine}
+                        </td>
+                        {isDeleting ? (
+                          <td className="text-right">{t`Deleting...`}</td>
+                        ) : (
+                          <td className="Table-actions">
+                            <ModalWithTrigger
+                              ref={"deleteDatabaseModal_" + database.id}
+                              triggerClasses="Button Button--danger"
+                              triggerElement={t`Delete`}
+                            >
+                              <DeleteDatabaseModal
+                                database={database}
+                                onClose={() =>
+                                  this.refs[
+                                    "deleteDatabaseModal_" + database.id
+                                  ].close()
+                                }
+                                onDelete={() =>
+                                  this.props.deleteDatabase(database.id)
+                                }
+                              />
+                            </ModalWithTrigger>
+                          </td>
+                        )}
+                      </tr>
+                    );
+                  }),
+                ]
+              ) : (
+                <tr>
+                  <td colSpan={4}>
+                    <LoadingSpinner />
+                    <h3>{t`Loading ...`}</h3>
+                  </td>
+                </tr>
+              )}
+            </tbody>
+          </table>
+          {!hasSampleDataset ? (
+            <div className="pt4">
+              <span
+                className={cx("p2 text-italic", {
+                  "border-top": databases && databases.length > 0,
+                })}
+              >
+                <a
+                  className="text-grey-2 text-brand-hover no-decoration"
+                  onClick={() => this.props.addSampleDataset()}
+                >{t`Bring the sample dataset back`}</a>
+              </span>
             </div>
-        );
-    }
+          ) : null}
+        </section>
+        <ModalWithTrigger ref="createdDatabaseModal" isInitiallyOpen={created}>
+          <CreatedDatabaseModal
+            databaseId={parseInt(created)}
+            onDone={() => this.refs.createdDatabaseModal.toggle()}
+            onClose={() => this.refs.createdDatabaseModal.toggle()}
+          />
+        </ModalWithTrigger>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/databases/database.js b/frontend/src/metabase/admin/databases/database.js
index 24b55427efa822a9174258c5e55fb7508fee5325..e8eefc1f413c9836eef5d83acdde0114ec5b44c1 100644
--- a/frontend/src/metabase/admin/databases/database.js
+++ b/frontend/src/metabase/admin/databases/database.js
@@ -1,9 +1,13 @@
 import _ from "underscore";
 
 import { createAction } from "redux-actions";
-import { handleActions, combineReducers, createThunkAction } from "metabase/lib/redux";
+import {
+  handleActions,
+  combineReducers,
+  createThunkAction,
+} from "metabase/lib/redux";
 import { push } from "react-router-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
@@ -11,19 +15,19 @@ import { MetabaseApi } from "metabase/services";
 
 // Default schedules for db sync and deep analysis
 export const DEFAULT_SCHEDULES = {
-    "cache_field_values": {
-        "schedule_day": null,
-        "schedule_frame": null,
-        "schedule_hour": 0,
-        "schedule_type": "daily"
-    },
-    "metadata_sync": {
-        "schedule_day": null,
-        "schedule_frame": null,
-        "schedule_hour": null,
-        "schedule_type": "hourly"
-    }
-}
+  cache_field_values: {
+    schedule_day: null,
+    schedule_frame: null,
+    schedule_hour: 0,
+    schedule_type: "daily",
+  },
+  metadata_sync: {
+    schedule_day: null,
+    schedule_frame: null,
+    schedule_hour: null,
+    schedule_type: "hourly",
+  },
+};
 
 export const DB_EDIT_FORM_CONNECTION_TAB = "connection";
 export const DB_EDIT_FORM_SCHEDULING_TAB = "scheduling";
@@ -31,25 +35,39 @@ export const DB_EDIT_FORM_SCHEDULING_TAB = "scheduling";
 export const RESET = "metabase/admin/databases/RESET";
 export const SELECT_ENGINE = "metabase/admin/databases/SELECT_ENGINE";
 export const FETCH_DATABASES = "metabase/admin/databases/FETCH_DATABASES";
-export const INITIALIZE_DATABASE = "metabase/admin/databases/INITIALIZE_DATABASE";
+export const INITIALIZE_DATABASE =
+  "metabase/admin/databases/INITIALIZE_DATABASE";
 export const ADD_SAMPLE_DATASET = "metabase/admin/databases/ADD_SAMPLE_DATASET";
 export const DELETE_DATABASE = "metabase/admin/databases/DELETE_DATABASE";
-export const SYNC_DATABASE_SCHEMA = "metabase/admin/databases/SYNC_DATABASE_SCHEMA";
-export const RESCAN_DATABASE_FIELDS = "metabase/admin/databases/RESCAN_DATABASE_FIELDS";
-export const DISCARD_SAVED_FIELD_VALUES = "metabase/admin/databases/DISCARD_SAVED_FIELD_VALUES";
-export const UPDATE_DATABASE = 'metabase/admin/databases/UPDATE_DATABASE'
-export const UPDATE_DATABASE_STARTED = 'metabase/admin/databases/UPDATE_DATABASE_STARTED'
-export const UPDATE_DATABASE_FAILED = 'metabase/admin/databases/UPDATE_DATABASE_FAILED'
-export const SET_DATABASE_CREATION_STEP = 'metabase/admin/databases/SET_DATABASE_CREATION_STEP'
-export const CREATE_DATABASE = 'metabase/admin/databases/CREATE_DATABASE'
-export const CREATE_DATABASE_STARTED = 'metabase/admin/databases/CREATE_DATABASE_STARTED'
-export const VALIDATE_DATABASE_STARTED = 'metabase/admin/databases/VALIDATE_DATABASE_STARTED'
-export const VALIDATE_DATABASE_FAILED = 'metabase/admin/databases/VALIDATE_DATABASE_FAILED'
-export const CREATE_DATABASE_FAILED = 'metabase/admin/databases/CREATE_DATABASE_FAILED'
-export const DELETE_DATABASE_STARTED = 'metabase/admin/databases/DELETE_DATABASE_STARTED'
-export const DELETE_DATABASE_FAILED = "metabase/admin/databases/DELETE_DATABASE_FAILED";
-export const CLEAR_FORM_STATE = 'metabase/admin/databases/CLEAR_FORM_STATE'
-export const MIGRATE_TO_NEW_SCHEDULING_SETTINGS = 'metabase/admin/databases/MIGRATE_TO_NEW_SCHEDULING_SETTINGS'
+export const SYNC_DATABASE_SCHEMA =
+  "metabase/admin/databases/SYNC_DATABASE_SCHEMA";
+export const RESCAN_DATABASE_FIELDS =
+  "metabase/admin/databases/RESCAN_DATABASE_FIELDS";
+export const DISCARD_SAVED_FIELD_VALUES =
+  "metabase/admin/databases/DISCARD_SAVED_FIELD_VALUES";
+export const UPDATE_DATABASE = "metabase/admin/databases/UPDATE_DATABASE";
+export const UPDATE_DATABASE_STARTED =
+  "metabase/admin/databases/UPDATE_DATABASE_STARTED";
+export const UPDATE_DATABASE_FAILED =
+  "metabase/admin/databases/UPDATE_DATABASE_FAILED";
+export const SET_DATABASE_CREATION_STEP =
+  "metabase/admin/databases/SET_DATABASE_CREATION_STEP";
+export const CREATE_DATABASE = "metabase/admin/databases/CREATE_DATABASE";
+export const CREATE_DATABASE_STARTED =
+  "metabase/admin/databases/CREATE_DATABASE_STARTED";
+export const VALIDATE_DATABASE_STARTED =
+  "metabase/admin/databases/VALIDATE_DATABASE_STARTED";
+export const VALIDATE_DATABASE_FAILED =
+  "metabase/admin/databases/VALIDATE_DATABASE_FAILED";
+export const CREATE_DATABASE_FAILED =
+  "metabase/admin/databases/CREATE_DATABASE_FAILED";
+export const DELETE_DATABASE_STARTED =
+  "metabase/admin/databases/DELETE_DATABASE_STARTED";
+export const DELETE_DATABASE_FAILED =
+  "metabase/admin/databases/DELETE_DATABASE_FAILED";
+export const CLEAR_FORM_STATE = "metabase/admin/databases/CLEAR_FORM_STATE";
+export const MIGRATE_TO_NEW_SCHEDULING_SETTINGS =
+  "metabase/admin/databases/MIGRATE_TO_NEW_SCHEDULING_SETTINGS";
 
 export const reset = createAction(RESET);
 
@@ -58,283 +76,354 @@ export const selectEngine = createAction(SELECT_ENGINE);
 
 // fetchDatabases
 export const fetchDatabases = createThunkAction(FETCH_DATABASES, function() {
-    return async function(dispatch, getState) {
-        try {
-            return await MetabaseApi.db_list();
-        } catch(error) {
-            console.error("error fetching databases", error);
-        }
-    };
+  return async function(dispatch, getState) {
+    try {
+      return await MetabaseApi.db_list();
+    } catch (error) {
+      console.error("error fetching databases", error);
+    }
+  };
 });
 
 // Migrates old "Enable in-depth database analysis" option to new "Let me choose when Metabase syncs and scans" option
 // Migration is run as a separate action because that makes it easy to track in tests
-const migrateDatabaseToNewSchedulingSettings = (database) => {
-    return async function(dispatch, getState) {
-        if (database.details["let-user-control-scheduling"] == undefined) {
-            dispatch.action(MIGRATE_TO_NEW_SCHEDULING_SETTINGS, {
-                ...database,
-                details: {
-                    ...database.details,
-                    // if user has enabled in-depth analysis already, we will run sync&scan in default schedule anyway
-                    // otherwise let the user control scheduling
-                    "let-user-control-scheduling": !database.is_full_sync
-                }
-            })
-        } else {
-            console.log(`${MIGRATE_TO_NEW_SCHEDULING_SETTINGS} is no-op as scheduling settings are already set`)
-        }
+const migrateDatabaseToNewSchedulingSettings = database => {
+  return async function(dispatch, getState) {
+    if (database.details["let-user-control-scheduling"] == undefined) {
+      dispatch.action(MIGRATE_TO_NEW_SCHEDULING_SETTINGS, {
+        ...database,
+        details: {
+          ...database.details,
+          // if user has enabled in-depth analysis already, we will run sync&scan in default schedule anyway
+          // otherwise let the user control scheduling
+          "let-user-control-scheduling": !database.is_full_sync,
+        },
+      });
+    } else {
+      console.log(
+        `${MIGRATE_TO_NEW_SCHEDULING_SETTINGS} is no-op as scheduling settings are already set`,
+      );
     }
-}
+  };
+};
 
 // initializeDatabase
 export const initializeDatabase = function(databaseId) {
-    return async function(dispatch, getState) {
-        if (databaseId) {
-            try {
-                const database = await MetabaseApi.db_get({"dbId": databaseId});
-                dispatch.action(INITIALIZE_DATABASE, database)
-
-                // If the new scheduling toggle isn't set, run the migration
-                if (database.details["let-user-control-scheduling"] == undefined) {
-                    dispatch(migrateDatabaseToNewSchedulingSettings(database))
-                }
-            } catch (error) {
-                if (error.status == 404) {
-                    //$location.path('/admin/databases/');
-                } else {
-                    console.error("error fetching database", databaseId, error);
-                }
-            }
+  return async function(dispatch, getState) {
+    if (databaseId) {
+      try {
+        const database = await MetabaseApi.db_get({ dbId: databaseId });
+        dispatch.action(INITIALIZE_DATABASE, database);
+
+        // If the new scheduling toggle isn't set, run the migration
+        if (database.details["let-user-control-scheduling"] == undefined) {
+          dispatch(migrateDatabaseToNewSchedulingSettings(database));
+        }
+      } catch (error) {
+        if (error.status == 404) {
+          //$location.path('/admin/databases/');
         } else {
-            const newDatabase = {
-                name: '',
-                engine: Object.keys(MetabaseSettings.get('engines'))[0],
-                details: {},
-                created: false
-            }
-            dispatch.action(INITIALIZE_DATABASE, newDatabase);
+          console.error("error fetching database", databaseId, error);
         }
+      }
+    } else {
+      const newDatabase = {
+        name: "",
+        engine: Object.keys(MetabaseSettings.get("engines"))[0],
+        details: {},
+        created: false,
+      };
+      dispatch.action(INITIALIZE_DATABASE, newDatabase);
     }
-}
-
+  };
+};
 
 // addSampleDataset
-export const addSampleDataset = createThunkAction(ADD_SAMPLE_DATASET, function() {
+export const addSampleDataset = createThunkAction(
+  ADD_SAMPLE_DATASET,
+  function() {
     return async function(dispatch, getState) {
-        try {
-            let sampleDataset = await MetabaseApi.db_add_sample_dataset();
-            MetabaseAnalytics.trackEvent("Databases", "Add Sample Data");
-            return sampleDataset;
-        } catch(error) {
-            console.error("error adding sample dataset", error);
-            return error;
-        }
+      try {
+        let sampleDataset = await MetabaseApi.db_add_sample_dataset();
+        MetabaseAnalytics.trackEvent("Databases", "Add Sample Data");
+        return sampleDataset;
+      } catch (error) {
+        console.error("error adding sample dataset", error);
+        return error;
+      }
     };
-});
-
-export const proceedWithDbCreation = function (database) {
-    return async function (dispatch, getState) {
-        if (database.details["let-user-control-scheduling"]) {
-            try {
-                dispatch.action(VALIDATE_DATABASE_STARTED);
-                const { valid } = await MetabaseApi.db_validate({ details: database });
-
-                if (valid) {
-                    dispatch.action(SET_DATABASE_CREATION_STEP, {
-                        // NOTE Atte Keinänen: DatabaseSchedulingForm needs `editingDatabase` with `schedules` so I decided that
-                        // it makes sense to set the value of editingDatabase as part of SET_DATABASE_CREATION_STEP
-                        database: {
-                            ...database,
-                            is_full_sync: true,
-                            schedules: DEFAULT_SCHEDULES
-                        },
-                        step: DB_EDIT_FORM_SCHEDULING_TAB
-                    });
-                } else {
-                    dispatch.action(VALIDATE_DATABASE_FAILED, { error: { data: { message: t`Couldn't connect to the database. Please check the connection details.` } } });
-                }
-            } catch(error) {
-                dispatch.action(VALIDATE_DATABASE_FAILED, { error });
-            }
+  },
+);
+
+export const proceedWithDbCreation = function(database) {
+  return async function(dispatch, getState) {
+    if (database.details["let-user-control-scheduling"]) {
+      try {
+        dispatch.action(VALIDATE_DATABASE_STARTED);
+        const { valid } = await MetabaseApi.db_validate({ details: database });
+
+        if (valid) {
+          dispatch.action(SET_DATABASE_CREATION_STEP, {
+            // NOTE Atte Keinänen: DatabaseSchedulingForm needs `editingDatabase` with `schedules` so I decided that
+            // it makes sense to set the value of editingDatabase as part of SET_DATABASE_CREATION_STEP
+            database: {
+              ...database,
+              is_full_sync: true,
+              schedules: DEFAULT_SCHEDULES,
+            },
+            step: DB_EDIT_FORM_SCHEDULING_TAB,
+          });
         } else {
-            // Skip the scheduling step if user doesn't need precise control over sync and scan
-            dispatch(createDatabase(database));
+          dispatch.action(VALIDATE_DATABASE_FAILED, {
+            error: {
+              data: {
+                message: t`Couldn't connect to the database. Please check the connection details.`,
+              },
+            },
+          });
         }
+      } catch (error) {
+        dispatch.action(VALIDATE_DATABASE_FAILED, { error });
+      }
+    } else {
+      // Skip the scheduling step if user doesn't need precise control over sync and scan
+      dispatch(createDatabase(database));
     }
-}
-
-export const createDatabase = function (database) {
-    return async function (dispatch, getState) {
-        try {
-            dispatch.action(CREATE_DATABASE_STARTED, {})
-            const createdDatabase = await MetabaseApi.db_create(database);
-            MetabaseAnalytics.trackEvent("Databases", "Create", database.engine);
-
-            // update the db metadata already here because otherwise there will be a gap between "Adding..." status
-            // and seeing the db that was just added
-            await dispatch(fetchDatabases())
-
-            dispatch.action(CREATE_DATABASE)
-            dispatch(push('/admin/databases?created=' + createdDatabase.id));
-        } catch (error) {
-            console.error("error creating a database", error);
-            MetabaseAnalytics.trackEvent("Databases", "Create Failed", database.engine);
-            dispatch.action(CREATE_DATABASE_FAILED, { error })
-        }
-    };
-}
+  };
+};
+
+export const createDatabase = function(database) {
+  return async function(dispatch, getState) {
+    try {
+      dispatch.action(CREATE_DATABASE_STARTED, {});
+      const createdDatabase = await MetabaseApi.db_create(database);
+      MetabaseAnalytics.trackEvent("Databases", "Create", database.engine);
+
+      // update the db metadata already here because otherwise there will be a gap between "Adding..." status
+      // and seeing the db that was just added
+      await dispatch(fetchDatabases());
+
+      dispatch.action(CREATE_DATABASE);
+      dispatch(push("/admin/databases?created=" + createdDatabase.id));
+    } catch (error) {
+      console.error("error creating a database", error);
+      MetabaseAnalytics.trackEvent(
+        "Databases",
+        "Create Failed",
+        database.engine,
+      );
+      dispatch.action(CREATE_DATABASE_FAILED, { error });
+    }
+  };
+};
 
 export const updateDatabase = function(database) {
-    return async function(dispatch, getState) {
-        try {
-            dispatch.action(UPDATE_DATABASE_STARTED, { database })
-            const savedDatabase = await MetabaseApi.db_update(database);
-            MetabaseAnalytics.trackEvent("Databases", "Update", database.engine);
-
-            dispatch.action(UPDATE_DATABASE, { database: savedDatabase })
-            setTimeout(() => dispatch.action(CLEAR_FORM_STATE), 3000);
-        } catch (error) {
-            MetabaseAnalytics.trackEvent("Databases", "Update Failed", database.engine);
-            dispatch.action(UPDATE_DATABASE_FAILED, { error });
-        }
-    };
+  return async function(dispatch, getState) {
+    try {
+      dispatch.action(UPDATE_DATABASE_STARTED, { database });
+      const savedDatabase = await MetabaseApi.db_update(database);
+      MetabaseAnalytics.trackEvent("Databases", "Update", database.engine);
+
+      dispatch.action(UPDATE_DATABASE, { database: savedDatabase });
+      setTimeout(() => dispatch.action(CLEAR_FORM_STATE), 3000);
+    } catch (error) {
+      MetabaseAnalytics.trackEvent(
+        "Databases",
+        "Update Failed",
+        database.engine,
+      );
+      dispatch.action(UPDATE_DATABASE_FAILED, { error });
+    }
+  };
 };
 
 // NOTE Atte Keinänen 7/26/17: Original monolithic saveDatabase was broken out to smaller actions
 // but `saveDatabase` action creator is still left here for keeping the interface for React components unchanged
 export const saveDatabase = function(database, details) {
-    // If we don't let user control the scheduling settings, let's override them with Metabase defaults
-    // TODO Atte Keinänen 8/15/17: Implement engine-specific scheduling defaults
-    const letUserControlScheduling = details["let-user-control-scheduling"];
-    const overridesIfNoUserControl = letUserControlScheduling ? {} : {
+  // If we don't let user control the scheduling settings, let's override them with Metabase defaults
+  // TODO Atte Keinänen 8/15/17: Implement engine-specific scheduling defaults
+  const letUserControlScheduling = details["let-user-control-scheduling"];
+  const overridesIfNoUserControl = letUserControlScheduling
+    ? {}
+    : {
         is_full_sync: true,
-        schedules: DEFAULT_SCHEDULES
-    }
-
-    return async function(dispatch, getState) {
-        const databaseWithDetails = {
-            ...database,
-            details,
-            ...overridesIfNoUserControl
-        };
-        const isUnsavedDatabase = !databaseWithDetails.id
-        if (isUnsavedDatabase) {
-            dispatch(createDatabase(databaseWithDetails))
-        } else {
-            dispatch(updateDatabase(databaseWithDetails))
-        }
+        schedules: DEFAULT_SCHEDULES,
+      };
+
+  return async function(dispatch, getState) {
+    const databaseWithDetails = {
+      ...database,
+      details,
+      ...overridesIfNoUserControl,
     };
+    const isUnsavedDatabase = !databaseWithDetails.id;
+    if (isUnsavedDatabase) {
+      dispatch(createDatabase(databaseWithDetails));
+    } else {
+      dispatch(updateDatabase(databaseWithDetails));
+    }
+  };
 };
 
 export const deleteDatabase = function(databaseId, isDetailView = true) {
-    return async function(dispatch, getState) {
-        try {
-            dispatch.action(DELETE_DATABASE_STARTED, { databaseId })
-            dispatch(push('/admin/databases/'));
-            await MetabaseApi.db_delete({"dbId": databaseId});
-            MetabaseAnalytics.trackEvent("Databases", "Delete", isDetailView ? "Using Detail" : "Using List");
-            dispatch.action(DELETE_DATABASE, { databaseId })
-        } catch(error) {
-            console.log('error deleting database', error);
-            dispatch.action(DELETE_DATABASE_FAILED, { databaseId, error })
-        }
-    };
-}
+  return async function(dispatch, getState) {
+    try {
+      dispatch.action(DELETE_DATABASE_STARTED, { databaseId });
+      dispatch(push("/admin/databases/"));
+      await MetabaseApi.db_delete({ dbId: databaseId });
+      MetabaseAnalytics.trackEvent(
+        "Databases",
+        "Delete",
+        isDetailView ? "Using Detail" : "Using List",
+      );
+      dispatch.action(DELETE_DATABASE, { databaseId });
+    } catch (error) {
+      console.log("error deleting database", error);
+      dispatch.action(DELETE_DATABASE_FAILED, { databaseId, error });
+    }
+  };
+};
 
 // syncDatabaseSchema
-export const syncDatabaseSchema = createThunkAction(SYNC_DATABASE_SCHEMA, function(databaseId) {
+export const syncDatabaseSchema = createThunkAction(
+  SYNC_DATABASE_SCHEMA,
+  function(databaseId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.db_sync_schema({"dbId": databaseId});
-            MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
-            return call;
-        } catch(error) {
-            console.log('error syncing database', error);
-        }
+      try {
+        let call = await MetabaseApi.db_sync_schema({ dbId: databaseId });
+        MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
+        return call;
+      } catch (error) {
+        console.log("error syncing database", error);
+      }
     };
-});
+  },
+);
 
 // rescanDatabaseFields
-export const rescanDatabaseFields = createThunkAction(RESCAN_DATABASE_FIELDS, function(databaseId) {
+export const rescanDatabaseFields = createThunkAction(
+  RESCAN_DATABASE_FIELDS,
+  function(databaseId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.db_rescan_values({"dbId": databaseId});
-            MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
-            return call;
-        } catch(error) {
-            console.log('error syncing database', error);
-        }
+      try {
+        let call = await MetabaseApi.db_rescan_values({ dbId: databaseId });
+        MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
+        return call;
+      } catch (error) {
+        console.log("error syncing database", error);
+      }
     };
-});
+  },
+);
 
 // discardSavedFieldValues
-export const discardSavedFieldValues = createThunkAction(DISCARD_SAVED_FIELD_VALUES, function(databaseId) {
+export const discardSavedFieldValues = createThunkAction(
+  DISCARD_SAVED_FIELD_VALUES,
+  function(databaseId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.db_discard_values({"dbId": databaseId});
-            MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
-            return call;
-        } catch(error) {
-            console.log('error syncing database', error);
-        }
+      try {
+        let call = await MetabaseApi.db_discard_values({ dbId: databaseId });
+        MetabaseAnalytics.trackEvent("Databases", "Manual Sync");
+        return call;
+      } catch (error) {
+        console.log("error syncing database", error);
+      }
     };
-});
+  },
+);
 
 // reducers
 
-const databases = handleActions({
+const databases = handleActions(
+  {
     [FETCH_DATABASES]: { next: (state, { payload }) => payload },
-    [ADD_SAMPLE_DATASET]: { next: (state, { payload }) => payload ? [...state, payload] : state },
-    [DELETE_DATABASE]: (state, { payload: { databaseId} }) =>
-        databaseId ? _.reject(state, (d) => d.id === databaseId) : state
-}, null);
-
-const editingDatabase = handleActions({
+    [ADD_SAMPLE_DATASET]: {
+      next: (state, { payload }) => (payload ? [...state, payload] : state),
+    },
+    [DELETE_DATABASE]: (state, { payload: { databaseId } }) =>
+      databaseId ? _.reject(state, d => d.id === databaseId) : state,
+  },
+  null,
+);
+
+const editingDatabase = handleActions(
+  {
     [RESET]: () => null,
     [INITIALIZE_DATABASE]: (state, { payload }) => payload,
     [MIGRATE_TO_NEW_SCHEDULING_SETTINGS]: (state, { payload }) => payload,
     [UPDATE_DATABASE]: (state, { payload }) => payload.database || state,
     [DELETE_DATABASE]: (state, { payload }) => null,
-    [SELECT_ENGINE]: (state, { payload }) => ({...state, engine: payload }),
-    [SET_DATABASE_CREATION_STEP]: (state, { payload: { database } }) => database
-}, null);
-
-const deletes = handleActions({
-    [DELETE_DATABASE_STARTED]: (state, { payload: { databaseId } }) => state.concat([databaseId]),
-    [DELETE_DATABASE_FAILED]: (state, { payload: { databaseId, error } }) => state.filter((dbId) => dbId !== databaseId),
-    [DELETE_DATABASE]: (state, { payload: { databaseId } }) => state.filter((dbId) => dbId !== databaseId)
-}, []);
-
-const deletionError = handleActions({
+    [SELECT_ENGINE]: (state, { payload }) => ({ ...state, engine: payload }),
+    [SET_DATABASE_CREATION_STEP]: (state, { payload: { database } }) =>
+      database,
+  },
+  null,
+);
+
+const deletes = handleActions(
+  {
+    [DELETE_DATABASE_STARTED]: (state, { payload: { databaseId } }) =>
+      state.concat([databaseId]),
+    [DELETE_DATABASE_FAILED]: (state, { payload: { databaseId, error } }) =>
+      state.filter(dbId => dbId !== databaseId),
+    [DELETE_DATABASE]: (state, { payload: { databaseId } }) =>
+      state.filter(dbId => dbId !== databaseId),
+  },
+  [],
+);
+
+const deletionError = handleActions(
+  {
     [DELETE_DATABASE_FAILED]: (state, { payload: { error } }) => error,
-}, null)
+  },
+  null,
+);
 
-const databaseCreationStep = handleActions({
+const databaseCreationStep = handleActions(
+  {
     [RESET]: () => DB_EDIT_FORM_CONNECTION_TAB,
-    [SET_DATABASE_CREATION_STEP] : (state, { payload: { step } }) => step
-}, DB_EDIT_FORM_CONNECTION_TAB)
-
-const DEFAULT_FORM_STATE = { formSuccess: null, formError: null, isSubmitting: false };
+    [SET_DATABASE_CREATION_STEP]: (state, { payload: { step } }) => step,
+  },
+  DB_EDIT_FORM_CONNECTION_TAB,
+);
+
+const DEFAULT_FORM_STATE = {
+  formSuccess: null,
+  formError: null,
+  isSubmitting: false,
+};
 
-const formState = handleActions({
+const formState = handleActions(
+  {
     [RESET]: { next: () => DEFAULT_FORM_STATE },
     [CREATE_DATABASE_STARTED]: () => ({ isSubmitting: true }),
     // not necessarily needed as the page is immediately redirected after db creation
-    [CREATE_DATABASE]: () => ({ formSuccess: { data: { message: t`Successfully created!` } } }),
-    [VALIDATE_DATABASE_FAILED]: (state, { payload: { error } }) => ({ formError: error }),
-    [CREATE_DATABASE_FAILED]: (state, { payload: { error } }) => ({ formError: error }),
+    [CREATE_DATABASE]: () => ({
+      formSuccess: { data: { message: t`Successfully created!` } },
+    }),
+    [VALIDATE_DATABASE_FAILED]: (state, { payload: { error } }) => ({
+      formError: error,
+    }),
+    [CREATE_DATABASE_FAILED]: (state, { payload: { error } }) => ({
+      formError: error,
+    }),
     [UPDATE_DATABASE_STARTED]: () => ({ isSubmitting: true }),
-    [UPDATE_DATABASE]: () => ({ formSuccess: { data: { message: t`Successfully saved!` } } }),
-    [UPDATE_DATABASE_FAILED]: (state, { payload: { error } }) => ({ formError: error }),
-    [CLEAR_FORM_STATE]: () => DEFAULT_FORM_STATE
-}, DEFAULT_FORM_STATE);
+    [UPDATE_DATABASE]: () => ({
+      formSuccess: { data: { message: t`Successfully saved!` } },
+    }),
+    [UPDATE_DATABASE_FAILED]: (state, { payload: { error } }) => ({
+      formError: error,
+    }),
+    [CLEAR_FORM_STATE]: () => DEFAULT_FORM_STATE,
+  },
+  DEFAULT_FORM_STATE,
+);
 
 export default combineReducers({
-    databases,
-    editingDatabase,
-    deletionError,
-    databaseCreationStep,
-    formState,
-    deletes
+  databases,
+  editingDatabase,
+  deletionError,
+  databaseCreationStep,
+  formState,
+  deletes,
 });
diff --git a/frontend/src/metabase/admin/databases/selectors.js b/frontend/src/metabase/admin/databases/selectors.js
index c9d0bbabaee0097c6e8cd25b054ac91006e20f57..5582bccb4326abbe33dfdee5e0d17db488d36c85 100644
--- a/frontend/src/metabase/admin/databases/selectors.js
+++ b/frontend/src/metabase/admin/databases/selectors.js
@@ -1,27 +1,25 @@
 /* @flow weak */
 
 import _ from "underscore";
-import { createSelector } from 'reselect';
-
+import { createSelector } from "reselect";
 
 // Database List
-export const databases         = state => state.admin.databases.databases;
+export const databases = state => state.admin.databases.databases;
 
-export const getDatabasesSorted = createSelector(
-    [databases],
-    (databases) => _.sortBy(databases, 'name')
+export const getDatabasesSorted = createSelector([databases], databases =>
+  _.sortBy(databases, "name"),
 );
 
-export const hasSampleDataset = createSelector(
-    [databases],
-    (databases) => _.some(databases, (d) => d.is_sample)
+export const hasSampleDataset = createSelector([databases], databases =>
+  _.some(databases, d => d.is_sample),
 );
 
-
 // Database Edit
-export const getEditingDatabase      = state => state.admin.databases.editingDatabase;
-export const getFormState            = state => state.admin.databases.formState;
-export const getDatabaseCreationStep = state => state.admin.databases.databaseCreationStep;
-
-export const getDeletes              = state => state.admin.databases.deletes;
-export const getDeletionError        = state => state.admin.databases.deletionError;
+export const getEditingDatabase = state =>
+  state.admin.databases.editingDatabase;
+export const getFormState = state => state.admin.databases.formState;
+export const getDatabaseCreationStep = state =>
+  state.admin.databases.databaseCreationStep;
+
+export const getDeletes = state => state.admin.databases.deletes;
+export const getDeletionError = state => state.admin.databases.deletionError;
diff --git a/frontend/src/metabase/admin/datamodel/components/FormInput.jsx b/frontend/src/metabase/admin/datamodel/components/FormInput.jsx
index bfce1383e6f27116d91c5ada25e3af8a1d47a4a6..775fad44e80e7de90a33bc552369e24d6ced8944 100644
--- a/frontend/src/metabase/admin/datamodel/components/FormInput.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FormInput.jsx
@@ -4,17 +4,21 @@ import cx from "classnames";
 import { formDomOnlyProps } from "metabase/lib/redux";
 
 export default class FormInput extends Component {
-    static propTypes = {};
+  static propTypes = {};
 
-    render() {
-        const { field, className, placeholder } = this.props;
-        return (
-            <input
-                type="text"
-                placeholder={placeholder}
-                className={cx("input full", { "border-error": !field.active && field.visited && field.invalid }, className)}
-                {...formDomOnlyProps(field)}
-            />
-        );
-    }
+  render() {
+    const { field, className, placeholder } = this.props;
+    return (
+      <input
+        type="text"
+        placeholder={placeholder}
+        className={cx(
+          "input full",
+          { "border-error": !field.active && field.visited && field.invalid },
+          className,
+        )}
+        {...formDomOnlyProps(field)}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx b/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx
index c45ee4b1208e5601dab8a96356e20eed93ca81c7..ca5acb143ce0503db8d02b12fa5cd2751ef7cd9f 100644
--- a/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FormLabel.jsx
@@ -2,26 +2,28 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 export default class FormLabel extends Component {
-    static propTypes = {
-        title: PropTypes.string,
-        description: PropTypes.string,
-    };
+  static propTypes = {
+    title: PropTypes.string,
+    description: PropTypes.string,
+  };
 
-    static defaultProps = {
-        title: "",
-        description: ""
-    };
+  static defaultProps = {
+    title: "",
+    description: "",
+  };
 
-    render() {
-        let { title, description, children } = this.props;
-        return (
-            <div className="mb3">
-                <div style={{ maxWidth: "575px" }}>
-                    { title && <label className="h5 text-bold text-uppercase">{ title }</label> }
-                    { description && <p className="mt1 mb2">{description}</p> }
-                </div>
-                {children}
-            </div>
-        );
-    }
+  render() {
+    let { title, description, children } = this.props;
+    return (
+      <div className="mb3">
+        <div style={{ maxWidth: "575px" }}>
+          {title && (
+            <label className="h5 text-bold text-uppercase">{title}</label>
+          )}
+          {description && <p className="mt1 mb2">{description}</p>}
+        </div>
+        {children}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx b/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx
index a2119223ab145de400721a0a35d488909cbcebfa..f304d5e7a3d7db45b82c0d34344d3d1c04a8183d 100644
--- a/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/FormTextArea.jsx
@@ -5,16 +5,20 @@ import cx from "classnames";
 import { formDomOnlyProps } from "metabase/lib/redux";
 
 export default class FormTextArea extends Component {
-    static propTypes = {};
+  static propTypes = {};
 
-    render() {
-        const { field, className, placeholder } = this.props;
-        return (
-            <textarea
-                placeholder={placeholder}
-                className={cx("input full", { "border-error": !field.active && field.visited && field.invalid }, className)}
-                {...formDomOnlyProps(field)}
-            />
-        );
-    }
+  render() {
+    const { field, className, placeholder } = this.props;
+    return (
+      <textarea
+        placeholder={placeholder}
+        className={cx(
+          "input full",
+          { "border-error": !field.active && field.visited && field.invalid },
+          className,
+        )}
+        {...formDomOnlyProps(field)}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx
index 89fb168894039dc2fdb16f4e88d5aac3df3f7ff7..e8f857284c07f64e002cfee662ceca7fb503288e 100644
--- a/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/ObjectActionSelect.jsx
@@ -5,59 +5,77 @@ import { Link } from "react-router";
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ObjectRetireModal from "./ObjectRetireModal.jsx";
 
 import { capitalize } from "metabase/lib/formatting";
 
 export default class ObjectActionsSelect extends Component {
-    static propTypes = {
-        object: PropTypes.object.isRequired,
-        objectType: PropTypes.string.isRequired,
-        onRetire: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    object: PropTypes.object.isRequired,
+    objectType: PropTypes.string.isRequired,
+    onRetire: PropTypes.func.isRequired,
+  };
 
-    async onRetire(object) {
-        await this.props.onRetire(object);
-        this.refs.retireModal.close();
-    }
+  async onRetire(object) {
+    await this.props.onRetire(object);
+    this.refs.retireModal.close();
+  }
 
-    render() {
-        const { object, objectType } = this.props;
-        return (
-            <div>
-                <PopoverWithTrigger
-                    ref="popover"
-                    triggerElement={<span className="text-grey-1 text-grey-4-hover"><Icon name={'ellipsis'}></Icon></span>}
-                >
-                    <ul className="UserActionsSelect">
-                        <li>
-                            <Link to={"/admin/datamodel/" + objectType + "/" + object.id} data-metabase-event={"Data Model;"+objectType+" Edit Page"} className="py1 px2 block bg-brand-hover text-white-hover no-decoration cursor-pointer">
-                                {t`Edit`} {capitalize(objectType)}
-                            </Link>
-                        </li>
-                        <li>
-                            <Link to={"/admin/datamodel/" + objectType + "/" + object.id + "/revisions"} data-metabase-event={"Data Model;"+objectType+" History"} className="py1 px2 block bg-brand-hover text-white-hover no-decoration cursor-pointer">
-                                {t`Revision History`}
-                            </Link>
-                        </li>
-                        <li className="mt1 border-top">
-                            <ModalWithTrigger
-                                ref="retireModal"
-                                triggerElement={"Retire " + capitalize(objectType)}
-                                triggerClasses="block p2 bg-error-hover text-error text-white-hover cursor-pointer"
-                            >
-                                <ObjectRetireModal
-                                    object={object}
-                                    objectType={objectType}
-                                    onRetire={this.onRetire.bind(this)}
-                                    onClose={() => this.refs.retireModal.close()}
-                                />
-                            </ModalWithTrigger>
-                        </li>
-                    </ul>
-                </PopoverWithTrigger>
-            </div>
-        );
-    }
+  render() {
+    const { object, objectType } = this.props;
+    return (
+      <div>
+        <PopoverWithTrigger
+          ref="popover"
+          triggerElement={
+            <span className="text-grey-1 text-grey-4-hover">
+              <Icon name={"ellipsis"} />
+            </span>
+          }
+        >
+          <ul className="UserActionsSelect">
+            <li>
+              <Link
+                to={"/admin/datamodel/" + objectType + "/" + object.id}
+                data-metabase-event={"Data Model;" + objectType + " Edit Page"}
+                className="py1 px2 block bg-brand-hover text-white-hover no-decoration cursor-pointer"
+              >
+                {t`Edit`} {capitalize(objectType)}
+              </Link>
+            </li>
+            <li>
+              <Link
+                to={
+                  "/admin/datamodel/" +
+                  objectType +
+                  "/" +
+                  object.id +
+                  "/revisions"
+                }
+                data-metabase-event={"Data Model;" + objectType + " History"}
+                className="py1 px2 block bg-brand-hover text-white-hover no-decoration cursor-pointer"
+              >
+                {t`Revision History`}
+              </Link>
+            </li>
+            <li className="mt1 border-top">
+              <ModalWithTrigger
+                ref="retireModal"
+                triggerElement={"Retire " + capitalize(objectType)}
+                triggerClasses="block p2 bg-error-hover text-error text-white-hover cursor-pointer"
+              >
+                <ObjectRetireModal
+                  object={object}
+                  objectType={objectType}
+                  onRetire={this.onRetire.bind(this)}
+                  onClose={() => this.refs.retireModal.close()}
+                />
+              </ModalWithTrigger>
+            </li>
+          </ul>
+        </PopoverWithTrigger>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx b/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx
index e7999aeb365bbaa8bc4aecd636988c082d01fc2e..f4df64e127efaebc75c7f3baa2daccb9ac3ab9e9 100644
--- a/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/ObjectRetireModal.jsx
@@ -3,63 +3,66 @@ import ReactDOM from "react-dom";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 export default class ObjectRetireModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            valid: false
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      valid: false,
+    };
+  }
 
-    async handleSubmit() {
-        const { object, objectType } = this.props;
-        let payload = {
-            revision_message: ReactDOM.findDOMNode(this.refs.revision_message).value
-        };
-        payload[objectType+"Id"] = object.id;
+  async handleSubmit() {
+    const { object, objectType } = this.props;
+    let payload = {
+      revision_message: ReactDOM.findDOMNode(this.refs.revision_message).value,
+    };
+    payload[objectType + "Id"] = object.id;
 
-        await this.props.onRetire(payload);
-        this.props.onClose();
-    }
+    await this.props.onRetire(payload);
+    this.props.onClose();
+  }
 
-    render() {
-        const { objectType } = this.props;
-        const { valid } = this.state;
-        return (
-            <ModalContent
-                title={t`Retire this ${objectType}?`}
-                onClose={this.props.onClose}
-            >
-                <form className="flex flex-column flex-full">
-                    <div className="Form-inputs pb4">
-                        <p className="text-paragraph">{t`Saved questions and other things that depend on this ${objectType} will continue to work, but this ${objectType} will no longer be selectable from the query builder.`}</p>
-                        <p className="text-paragraph">{t`If you're sure you want to retire this ${objectType}, please write a quick explanation of why it's being retired:`}</p>
-                        <textarea
-                            ref="revision_message"
-                            className="input full"
-                            placeholder={t`This will show up in the activity feed and in an email that will be sent to anyone on your team who created something that uses this ${objectType}.`}
-                            onChange={(e) => this.setState({ valid: !!e.target.value })}
-                        />
-                    </div>
+  render() {
+    const { objectType } = this.props;
+    const { valid } = this.state;
+    return (
+      <ModalContent
+        title={t`Retire this ${objectType}?`}
+        onClose={this.props.onClose}
+      >
+        <form className="flex flex-column flex-full">
+          <div className="Form-inputs pb4">
+            <p className="text-paragraph">{t`Saved questions and other things that depend on this ${objectType} will continue to work, but this ${objectType} will no longer be selectable from the query builder.`}</p>
+            <p className="text-paragraph">{t`If you're sure you want to retire this ${objectType}, please write a quick explanation of why it's being retired:`}</p>
+            <textarea
+              ref="revision_message"
+              className="input full"
+              placeholder={t`This will show up in the activity feed and in an email that will be sent to anyone on your team who created something that uses this ${objectType}.`}
+              onChange={e => this.setState({ valid: !!e.target.value })}
+            />
+          </div>
 
-                    <div className="Form-actions ml-auto">
-                        <a className="Button" onClick={this.props.onClose}>
-                            {t`Cancel`}
-                        </a>
-                        <ActionButton
-                            actionFn={this.handleSubmit.bind(this)}
-                            className={cx("Button ml2", { "Button--danger": valid, "disabled": !valid })}
-                            normalText={t`Retire`}
-                            activeText={t`Retiring…`}
-                            failedText={t`Failed`}
-                            successText={t`Success`}
-                        />
-                    </div>
-                </form>
-            </ModalContent>
-        );
-    }
+          <div className="Form-actions ml-auto">
+            <a className="Button" onClick={this.props.onClose}>
+              {t`Cancel`}
+            </a>
+            <ActionButton
+              actionFn={this.handleSubmit.bind(this)}
+              className={cx("Button ml2", {
+                "Button--danger": valid,
+                disabled: !valid,
+              })}
+              normalText={t`Retire`}
+              activeText={t`Retiring…`}
+              failedText={t`Failed`}
+              successText={t`Success`}
+            />
+          </div>
+        </form>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
index 917f15c2c99d40d154312b591ecd6c5d64f1f754..7a839543166c94cb994f5dac19ab709071d9037c 100644
--- a/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/PartialQueryBuilder.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import GuiQueryEditor from "metabase/query_builder/components/GuiQueryEditor.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import * as Urls from "metabase/lib/urls";
 
 import cx from "classnames";
@@ -11,87 +11,114 @@ import * as Query from "metabase/lib/query/query";
 import Question from "metabase-lib/lib/Question";
 
 export default class PartialQueryBuilder extends Component {
-    static propTypes = {
-        onChange: PropTypes.func.isRequired,
-        tableMetadata: PropTypes.object.isRequired,
-        updatePreviewSummary: PropTypes.func.isRequired,
-        previewSummary: PropTypes.string
-    };
+  static propTypes = {
+    onChange: PropTypes.func.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+    updatePreviewSummary: PropTypes.func.isRequired,
+    previewSummary: PropTypes.string,
+  };
 
-    componentDidMount() {
-        const { value, tableMetadata } = this.props;
-        this.props.updatePreviewSummary({
-            type: "query",
-            database: tableMetadata.db_id,
-            query: {
-                ...value,
-                source_table: tableMetadata.id
-            }
-        });
-    }
+  componentDidMount() {
+    const { value, tableMetadata } = this.props;
+    this.props.updatePreviewSummary({
+      type: "query",
+      database: tableMetadata.db_id,
+      query: {
+        ...value,
+        source_table: tableMetadata.id,
+      },
+    });
+  }
 
-    setDatasetQuery = (datasetQuery) => {
-        this.props.onChange(datasetQuery.query);
-        this.props.updatePreviewSummary(datasetQuery);
-    }
+  setDatasetQuery = datasetQuery => {
+    this.props.onChange(datasetQuery.query);
+    this.props.updatePreviewSummary(datasetQuery);
+  };
 
-    render() {
-        let { features, value, metadata, tableMetadata, previewSummary } = this.props;
+  render() {
+    let {
+      features,
+      value,
+      metadata,
+      tableMetadata,
+      previewSummary,
+    } = this.props;
 
-        let datasetQuery = {
-            type: "query",
-            database: tableMetadata.db_id,
-            query: {
-                ...value,
-                source_table: tableMetadata.id
-            }
-        };
+    let datasetQuery = {
+      type: "query",
+      database: tableMetadata.db_id,
+      query: {
+        ...value,
+        source_table: tableMetadata.id,
+      },
+    };
 
-        const query = new Question(metadata, { dataset_query: datasetQuery }).query();
+    const query = new Question(metadata, {
+      dataset_query: datasetQuery,
+    }).query();
 
-        let previewCard = {
-            dataset_query: {
-                ...datasetQuery,
-                query: {
-                    aggregation: ["rows"],
-                    breakout: [],
-                    filter: [],
-                    ...datasetQuery.query
-                }
-            }
-        };
-        let previewUrl = Urls.question(null, previewCard);
+    let previewCard = {
+      dataset_query: {
+        ...datasetQuery,
+        query: {
+          aggregation: ["rows"],
+          breakout: [],
+          filter: [],
+          ...datasetQuery.query,
+        },
+      },
+    };
+    let previewUrl = Urls.question(null, previewCard);
 
-        const onChange = (query) => {
-            this.props.onChange(query);
-            this.props.updatePreviewSummary({ ...datasetQuery, query });
-        }
+    const onChange = query => {
+      this.props.onChange(query);
+      this.props.updatePreviewSummary({ ...datasetQuery, query });
+    };
 
-        return (
-            <div className="py1">
-                <GuiQueryEditor
-                    features={features}
-                    query={query}
-                    datasetQuery={datasetQuery}
-                    databases={tableMetadata && [tableMetadata.db]}
-                    setDatasetQuery={this.setDatasetQuery}
-                    isShowingDataReference={false}
-                    supportMultipleAggregations={false}
-                    setDatabaseFn={null}
-                    setSourceTableFn={null}
-                    addQueryFilter={(filter) => onChange(Query.addFilter(datasetQuery.query, filter))}
-                    updateQueryFilter={(index, filter) => onChange(Query.updateFilter(datasetQuery.query, index, filter))}
-                    removeQueryFilter={(index) => onChange(Query.removeFilter(datasetQuery.query, index))}
-                    addQueryAggregation={(aggregation) => onChange(Query.addAggregation(datasetQuery.query, aggregation))}
-                    updateQueryAggregation={(index, aggregation) => onChange(Query.updateAggregation(datasetQuery.query, index, aggregation))}
-                    removeQueryAggregation={(index) => onChange(Query.removeAggregation(datasetQuery.query, index))}
-                >
-                    <div className="flex align-center mx2 my2">
-                        <span className="text-bold px3">{previewSummary}</span>
-                        <a data-metabase-event={"Data Model;Preview Click"} target={window.OSX ? null : "_blank"} className={cx("Button Button--primary")} href={previewUrl}>{t`Preview`}</a>
-                    </div>
-                </GuiQueryEditor>
-            </div>
-        );
-    }
+    return (
+      <div className="py1">
+        <GuiQueryEditor
+          features={features}
+          query={query}
+          datasetQuery={datasetQuery}
+          databases={tableMetadata && [tableMetadata.db]}
+          setDatasetQuery={this.setDatasetQuery}
+          isShowingDataReference={false}
+          supportMultipleAggregations={false}
+          setDatabaseFn={null}
+          setSourceTableFn={null}
+          addQueryFilter={filter =>
+            onChange(Query.addFilter(datasetQuery.query, filter))
+          }
+          updateQueryFilter={(index, filter) =>
+            onChange(Query.updateFilter(datasetQuery.query, index, filter))
+          }
+          removeQueryFilter={index =>
+            onChange(Query.removeFilter(datasetQuery.query, index))
+          }
+          addQueryAggregation={aggregation =>
+            onChange(Query.addAggregation(datasetQuery.query, aggregation))
+          }
+          updateQueryAggregation={(index, aggregation) =>
+            onChange(
+              Query.updateAggregation(datasetQuery.query, index, aggregation),
+            )
+          }
+          removeQueryAggregation={index =>
+            onChange(Query.removeAggregation(datasetQuery.query, index))
+          }
+        >
+          <div className="flex align-center mx2 my2">
+            <span className="text-bold px3">{previewSummary}</span>
+            <a
+              data-metabase-event={"Data Model;Preview Click"}
+              target={window.OSX ? null : "_blank"}
+              className={cx("Button Button--primary")}
+              href={previewUrl}
+            >{t`Preview`}</a>
+          </div>
+        </GuiQueryEditor>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
index fad29ae8c5bb975169cddf4f1eb1c6144af7252b..6e8434da681a65e9d4f630ecb7a93d0db4b6a633 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnItem.jsx
@@ -5,203 +5,240 @@ import { Link, withRouter } from "react-router";
 import Input from "metabase/components/Input.jsx";
 import Select from "metabase/components/Select.jsx";
 import Icon from "metabase/components/Icon";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import * as MetabaseCore from "metabase/lib/core";
 import { titleize, humanize } from "metabase/lib/formatting";
 import { isNumericBaseType } from "metabase/lib/schema_metadata";
 import { TYPE, isa, isFK } from "metabase/lib/types";
 
-import _  from "underscore";
+import _ from "underscore";
 import cx from "classnames";
 
-import type { Field } from "metabase/meta/types/Field"
+import type { Field } from "metabase/meta/types/Field";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 @withRouter
 export default class Column extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.onDescriptionChange = this.onDescriptionChange.bind(this);
-        this.onNameChange = this.onNameChange.bind(this);
-        this.onVisibilityChange = this.onVisibilityChange.bind(this);
+  constructor(props, context) {
+    super(props, context);
+    this.onDescriptionChange = this.onDescriptionChange.bind(this);
+    this.onNameChange = this.onNameChange.bind(this);
+    this.onVisibilityChange = this.onVisibilityChange.bind(this);
+  }
+
+  static propTypes = {
+    field: PropTypes.object,
+    idfields: PropTypes.array.isRequired,
+    updateField: PropTypes.func.isRequired,
+  };
+
+  updateProperty(name, value) {
+    this.props.field[name] = value;
+    this.props.updateField(this.props.field);
+  }
+
+  onNameChange(event) {
+    if (!_.isEmpty(event.target.value)) {
+      this.updateProperty("display_name", event.target.value);
+    } else {
+      // if the user set this to empty then simply reset it because that's not allowed!
+      event.target.value = this.props.field.display_name;
     }
-
-    static propTypes = {
-        field: PropTypes.object,
-        idfields: PropTypes.array.isRequired,
-        updateField: PropTypes.func.isRequired,
-    };
-
-    updateProperty(name, value) {
-        this.props.field[name] = value;
-        this.props.updateField(this.props.field);
-    }
-
-    onNameChange(event) {
-        if (!_.isEmpty(event.target.value)) {
-            this.updateProperty("display_name", event.target.value);
-        } else {
-            // if the user set this to empty then simply reset it because that's not allowed!
-            event.target.value = this.props.field.display_name;
-        }
-    }
-
-    onDescriptionChange(event) {
-        this.updateProperty("description", event.target.value);
-    }
-
-    onVisibilityChange(type) {
-        this.updateProperty("visibility_type", type.id);
-    }
-
-    render() {
-        const { field, idfields, updateField } = this.props;
-
-        return (
-            <li className="mt1 mb3 flex">
-                <div className="flex flex-column flex-full">
-                    <div>
-                        <Input style={{minWidth: 420}} className="AdminInput TableEditor-field-name float-left bordered inline-block rounded text-bold" type="text" value={this.props.field.display_name || ""} onBlurChange={this.onNameChange}/>
-                        <div className="clearfix">
-                            <div className="flex flex-full">
-                                <div className="flex-full px1">
-                                    <FieldVisibilityPicker
-                                        className="block"
-                                        field={field}
-                                        updateField={updateField}
-                                    />
-                                </div>
-                                <div className="flex-full px1">
-                                    <SpecialTypeAndTargetPicker
-                                        className="block"
-                                        field={field}
-                                        updateField={updateField}
-                                        idfields={idfields}
-                                    />
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                    <div className="MetadataTable-title flex flex-column flex-full bordered rounded mt1 mr1">
-                        <Input className="AdminInput TableEditor-field-description" type="text" value={this.props.field.description || ""} onBlurChange={this.onDescriptionChange} placeholder={t`No column description yet`} />
-                    </div>
+  }
+
+  onDescriptionChange(event) {
+    this.updateProperty("description", event.target.value);
+  }
+
+  onVisibilityChange(type) {
+    this.updateProperty("visibility_type", type.id);
+  }
+
+  render() {
+    const { field, idfields, updateField } = this.props;
+
+    return (
+      <li className="mt1 mb3 flex">
+        <div className="flex flex-column flex-full">
+          <div>
+            <Input
+              style={{ minWidth: 420 }}
+              className="AdminInput TableEditor-field-name float-left bordered inline-block rounded text-bold"
+              type="text"
+              value={this.props.field.display_name || ""}
+              onBlurChange={this.onNameChange}
+            />
+            <div className="clearfix">
+              <div className="flex flex-full">
+                <div className="flex-full px1">
+                  <FieldVisibilityPicker
+                    className="block"
+                    field={field}
+                    updateField={updateField}
+                  />
                 </div>
-                <Link to={`${this.props.location.pathname}/${this.props.field.id}`} className="text-brand-hover mx2 mt1">
-                    <Icon name="gear" />
-                </Link>
-            </li>
-        )
-    }
+                <div className="flex-full px1">
+                  <SpecialTypeAndTargetPicker
+                    className="block"
+                    field={field}
+                    updateField={updateField}
+                    idfields={idfields}
+                  />
+                </div>
+              </div>
+            </div>
+          </div>
+          <div className="MetadataTable-title flex flex-column flex-full bordered rounded mt1 mr1">
+            <Input
+              className="AdminInput TableEditor-field-description"
+              type="text"
+              value={this.props.field.description || ""}
+              onBlurChange={this.onDescriptionChange}
+              placeholder={t`No column description yet`}
+            />
+          </div>
+        </div>
+        <Link
+          to={`${this.props.location.pathname}/${this.props.field.id}`}
+          className="text-brand-hover mx2 mt1"
+        >
+          <Icon name="gear" />
+        </Link>
+      </li>
+    );
+  }
 }
 
 // FieldVisibilityPicker and SpecialTypeSelect are also used in FieldApp
 
 export class FieldVisibilityPicker extends Component {
-    props: {
-        field: Field,
-        updateField: (Field) => void,
-        className?: string
-    }
-
-    onVisibilityChange = (visibilityType) => {
-        const { field } = this.props
-        field.visibility_type = visibilityType.id;
-        this.props.updateField(field);
-    }
-
-    render() {
-        const { field, className } = this.props;
-
-        return (
-            <Select
-                className={cx("TableEditor-field-visibility block", className)}
-                placeholder={t`Select a field visibility`}
-                value={_.find(MetabaseCore.field_visibility_types, (type) => { return type.id === field.visibility_type })}
-                options={MetabaseCore.field_visibility_types}
-                onChange={this.onVisibilityChange}
-                triggerClasses={this.props.triggerClasses}
-            />
-        )
-    }
+  props: {
+    field: Field,
+    updateField: Field => void,
+    className?: string,
+  };
+
+  onVisibilityChange = visibilityType => {
+    const { field } = this.props;
+    field.visibility_type = visibilityType.id;
+    this.props.updateField(field);
+  };
+
+  render() {
+    const { field, className } = this.props;
+
+    return (
+      <Select
+        className={cx("TableEditor-field-visibility block", className)}
+        placeholder={t`Select a field visibility`}
+        value={_.find(MetabaseCore.field_visibility_types, type => {
+          return type.id === field.visibility_type;
+        })}
+        options={MetabaseCore.field_visibility_types}
+        onChange={this.onVisibilityChange}
+        triggerClasses={this.props.triggerClasses}
+      />
+    );
+  }
 }
 
 export class SpecialTypeAndTargetPicker extends Component {
-    props: {
-        field: Field,
-        updateField: (Field) => void,
-        className?: string,
-        selectSeparator?: React$Element<any>
+  props: {
+    field: Field,
+    updateField: Field => void,
+    className?: string,
+    selectSeparator?: React$Element<any>,
+  };
+
+  onSpecialTypeChange = async special_type => {
+    const { field, updateField } = this.props;
+    field.special_type = special_type.id;
+
+    // If we are changing the field from a FK to something else, we should delete any FKs present
+    if (field.target && field.target.id != null && isFK(field.special_type)) {
+      // we have something that used to be an FK and is now not an FK
+      // clean up after ourselves
+      field.target = null;
+      field.fk_target_field_id = null;
     }
 
-    onSpecialTypeChange = async (special_type) => {
-        const { field, updateField } = this.props;
-        field.special_type = special_type.id;
+    await updateField(field);
 
-        // If we are changing the field from a FK to something else, we should delete any FKs present
-        if (field.target && field.target.id != null && isFK(field.special_type)) {
-            // we have something that used to be an FK and is now not an FK
-            // clean up after ourselves
-            field.target = null;
-            field.fk_target_field_id = null;
-        }
+    MetabaseAnalytics.trackEvent(
+      "Data Model",
+      "Update Field Special-Type",
+      field.special_type,
+    );
+  };
 
-        await updateField(field);
+  onTargetChange = async target_field => {
+    const { field, updateField } = this.props;
+    field.fk_target_field_id = target_field.id;
 
-        MetabaseAnalytics.trackEvent("Data Model", "Update Field Special-Type", field.special_type);
-    }
+    await updateField(field);
 
-    onTargetChange = async (target_field) => {
-        const { field, updateField } = this.props;
-        field.fk_target_field_id = target_field.id;
+    MetabaseAnalytics.trackEvent("Data Model", "Update Field Target");
+  };
 
-        await updateField(field);
+  render() {
+    const { field, idfields, className, selectSeparator } = this.props;
 
-        MetabaseAnalytics.trackEvent("Data Model", "Update Field Target");
+    let specialTypes = MetabaseCore.field_special_types.slice(0);
+    specialTypes.push({
+      id: null,
+      name: t`No special type`,
+      section: t`Other`,
+    });
+    // if we don't have a numeric base-type then prevent the options for unix timestamp conversion (#823)
+    if (!isNumericBaseType(field)) {
+      specialTypes = specialTypes.filter(f => !isa(f.id, TYPE.UNIXTimestamp));
     }
 
-    render() {
-        const { field, idfields, className, selectSeparator } = this.props;
-
-        let specialTypes = MetabaseCore.field_special_types.slice(0);
-        specialTypes.push({'id': null, 'name': t`No special type`, 'section': t`Other`});
-        // if we don't have a numeric base-type then prevent the options for unix timestamp conversion (#823)
-        if (!isNumericBaseType(field)) {
-            specialTypes = specialTypes.filter((f) => !isa(f.id, TYPE.UNIXTimestamp));
-        }
-
-        const showFKTargetSelect = isFK(field.special_type);
-
-        // If all FK target fields are in the same schema (like `PUBLIC` for sample dataset)
-        // or if there are no schemas at all, omit the schema name
-        const includeSchemaName = _.uniq(idfields.map((idField) => idField.table.schema)).length > 1
-
-        return (
-            <div>
-                <Select
-                    className={cx("TableEditor-field-special-type", className)}
-                    placeholder={t`Select a special type`}
-                    value={_.find(MetabaseCore.field_special_types, (type) => type.id === field.special_type)}
-                    options={specialTypes}
-                    onChange={this.onSpecialTypeChange}
-                    triggerClasses={this.props.triggerClasses}
-                />
-                { showFKTargetSelect && selectSeparator }
-                { showFKTargetSelect && <Select
-                    className={cx("TableEditor-field-target", className)}
-                    triggerClasses={this.props.triggerClasses}
-                    placeholder={t`Select a target`}
-                    value={field.fk_target_field_id && _.find(idfields, (idField) => idField.id === field.fk_target_field_id)}
-                    options={idfields}
-                    optionNameFn={
-                        (idField) => includeSchemaName
-                            ? titleize(humanize(idField.table.schema)) + "." + idField.displayName
-                            : idField.displayName
-                    }
-                    onChange={this.onTargetChange}
-                /> }
-            </div>
-        )
-    }
+    const showFKTargetSelect = isFK(field.special_type);
+
+    // If all FK target fields are in the same schema (like `PUBLIC` for sample dataset)
+    // or if there are no schemas at all, omit the schema name
+    const includeSchemaName =
+      _.uniq(idfields.map(idField => idField.table.schema)).length > 1;
+
+    return (
+      <div>
+        <Select
+          className={cx("TableEditor-field-special-type", className)}
+          placeholder={t`Select a special type`}
+          value={_.find(
+            MetabaseCore.field_special_types,
+            type => type.id === field.special_type,
+          )}
+          options={specialTypes}
+          onChange={this.onSpecialTypeChange}
+          triggerClasses={this.props.triggerClasses}
+        />
+        {showFKTargetSelect && selectSeparator}
+        {showFKTargetSelect && (
+          <Select
+            className={cx("TableEditor-field-target", className)}
+            triggerClasses={this.props.triggerClasses}
+            placeholder={t`Select a target`}
+            value={
+              field.fk_target_field_id &&
+              _.find(
+                idfields,
+                idField => idField.id === field.fk_target_field_id,
+              )
+            }
+            options={idfields}
+            optionNameFn={idField =>
+              includeSchemaName
+                ? titleize(humanize(idField.table.schema)) +
+                  "." +
+                  idField.displayName
+                : idField.displayName
+            }
+            onChange={this.onTargetChange}
+          />
+        )}
+      </div>
+    );
+  }
 }
-
-
diff --git a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx
index a8495d31e07a85cb0a298aa2d45e4d3386c5b71d..bb1f42b6263fdf5c48725ab6b39752ad13ab6615 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/ColumnsList.jsx
@@ -1,38 +1,41 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ColumnItem from "./ColumnItem.jsx";
 
 export default class ColumnsList extends Component {
-    static propTypes = {
-        tableMetadata: PropTypes.object,
-        idfields: PropTypes.array,
-        updateField: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    tableMetadata: PropTypes.object,
+    idfields: PropTypes.array,
+    updateField: PropTypes.func.isRequired,
+  };
 
-    render() {
-        let { tableMetadata } = this.props;
-        return (
-            <div id="ColumnsList" className="my3">
-                <h2 className="px1 text-orange">{t`Columns`}</h2>
-                <div className="text-uppercase text-grey-3 py1">
-                    <div style={{minWidth: 420}} className="float-left px1">{t`Column`}</div>
-                    <div className="flex clearfix">
-                        <div className="flex-half px1">{t`Visibility`}</div>
-                        <div className="flex-half px1">{t`Type`}</div>
-                    </div>
-                </div>
-                <ol className="border-top border-bottom">
-                    {tableMetadata.fields.map((field) =>
-                        <ColumnItem
-                            key={field.id}
-                            field={field}
-                            idfields={this.props.idfields}
-                            updateField={this.props.updateField}
-                        />
-                    )}
-                </ol>
-            </div>
-        );
-    }
+  render() {
+    let { tableMetadata } = this.props;
+    return (
+      <div id="ColumnsList" className="my3">
+        <h2 className="px1 text-orange">{t`Columns`}</h2>
+        <div className="text-uppercase text-grey-3 py1">
+          <div
+            style={{ minWidth: 420 }}
+            className="float-left px1"
+          >{t`Column`}</div>
+          <div className="flex clearfix">
+            <div className="flex-half px1">{t`Visibility`}</div>
+            <div className="flex-half px1">{t`Type`}</div>
+          </div>
+        </div>
+        <ol className="border-top border-bottom">
+          {tableMetadata.fields.map(field => (
+            <ColumnItem
+              key={field.id}
+              field={field}
+              idfields={this.props.idfields}
+              updateField={this.props.updateField}
+            />
+          ))}
+        </ol>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx
index bbbac4fc3c5495d878e3ef2b53ff90e21ec22e0c..01fe92caa4c01f0980b4db39d07bc8a99afe85ca 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataHeader.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link, withRouter } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import SaveStatus from "metabase/components/SaveStatus.jsx";
 import Toggle from "metabase/components/Toggle.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
@@ -10,83 +10,91 @@ import Icon from "metabase/components/Icon.jsx";
 
 @withRouter
 export default class MetadataHeader extends Component {
-    static propTypes = {
-        databaseId: PropTypes.number,
-        databases: PropTypes.array.isRequired,
-        selectDatabase: PropTypes.func.isRequired,
-        isShowingSchema: PropTypes.bool.isRequired,
-        toggleShowSchema: PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    databaseId: PropTypes.number,
+    databases: PropTypes.array.isRequired,
+    selectDatabase: PropTypes.func.isRequired,
+    isShowingSchema: PropTypes.bool.isRequired,
+    toggleShowSchema: PropTypes.func.isRequired,
+  };
 
-    setSaving() {
-        this.refs.status.setSaving.apply(this, arguments);
-    }
+  setSaving() {
+    this.refs.status.setSaving.apply(this, arguments);
+  }
 
-    setSaved() {
-        this.refs.status.setSaved.apply(this, arguments);
-    }
+  setSaved() {
+    this.refs.status.setSaved.apply(this, arguments);
+  }
 
-    setSaveError() {
-        this.refs.status.setSaveError.apply(this, arguments);
-    }
+  setSaveError() {
+    this.refs.status.setSaveError.apply(this, arguments);
+  }
 
-    renderDbSelector() {
-        var database = this.props.databases.filter((db) => db.id === this.props.databaseId)[0];
-        if (database) {
-            var columns = [{
-                selectedItem: database,
-                items: this.props.databases,
-                itemTitleFn: (db) => db.name,
-                itemSelectFn: (db) => {
-                    this.props.selectDatabase(db)
-                    this.refs.databasePopover.toggle();
-                }
-            }];
-            var triggerElement = (
-                <span className="text-bold cursor-pointer text-default">
-                    {database.name}
-                    <Icon className="ml1" name="chevrondown" size={8}/>
-                </span>
-            );
-            return (
-                <PopoverWithTrigger
-                    ref="databasePopover"
-                    triggerElement={triggerElement}
-                >
-                    <ColumnarSelector columns={columns}/>
-                </PopoverWithTrigger>
-            );
-        }
+  renderDbSelector() {
+    var database = this.props.databases.filter(
+      db => db.id === this.props.databaseId,
+    )[0];
+    if (database) {
+      var columns = [
+        {
+          selectedItem: database,
+          items: this.props.databases,
+          itemTitleFn: db => db.name,
+          itemSelectFn: db => {
+            this.props.selectDatabase(db);
+            this.refs.databasePopover.toggle();
+          },
+        },
+      ];
+      var triggerElement = (
+        <span className="text-bold cursor-pointer text-default">
+          {database.name}
+          <Icon className="ml1" name="chevrondown" size={8} />
+        </span>
+      );
+      return (
+        <PopoverWithTrigger
+          ref="databasePopover"
+          triggerElement={triggerElement}
+        >
+          <ColumnarSelector columns={columns} />
+        </PopoverWithTrigger>
+      );
     }
+  }
 
-    // Show a gear to access Table settings page if we're currently looking at a Table. Otherwise show nothing.
-    // TODO - it would be nicer just to disable the gear so the page doesn't jump around once you select a Table.
-    renderTableSettingsButton() {
-        const isViewingTable = this.props.location.pathname.match(/table\/\d+\/?$/);
-        if (!isViewingTable) return null;
+  // Show a gear to access Table settings page if we're currently looking at a Table. Otherwise show nothing.
+  // TODO - it would be nicer just to disable the gear so the page doesn't jump around once you select a Table.
+  renderTableSettingsButton() {
+    const isViewingTable = this.props.location.pathname.match(/table\/\d+\/?$/);
+    if (!isViewingTable) return null;
 
-        return (
-            <span className="ml4 mr3">
-                <Link to={`${this.props.location.pathname}/settings`} >
-                    <Icon name="gear" />
-                </Link>
-            </span>
-        );
-    }
+    return (
+      <span className="ml4 mr3">
+        <Link to={`${this.props.location.pathname}/settings`}>
+          <Icon name="gear" />
+        </Link>
+      </span>
+    );
+  }
 
-    render() {
-        return (
-            <div className="MetadataEditor-header flex align-center flex-no-shrink">
-                <div className="MetadataEditor-headerSection py2 h2">
-                    <span className="text-grey-4">{t`Current database:`}</span> {this.renderDbSelector()}
-                </div>
-                <div className="MetadataEditor-headerSection flex flex-align-right align-center flex-no-shrink">
-                    <SaveStatus ref="status" />
-                    <span className="mr1">{t`Show original schema`}</span>
-                    <Toggle value={this.props.isShowingSchema} onChange={this.props.toggleShowSchema} />
-                    {this.renderTableSettingsButton()}
-                </div>
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className="MetadataEditor-header flex align-center flex-no-shrink">
+        <div className="MetadataEditor-headerSection py2 h2">
+          <span className="text-grey-4">{t`Current database:`}</span>{" "}
+          {this.renderDbSelector()}
+        </div>
+        <div className="MetadataEditor-headerSection flex flex-align-right align-center flex-no-shrink">
+          <SaveStatus ref="status" />
+          <span className="mr1">{t`Show original schema`}</span>
+          <Toggle
+            value={this.props.isShowingSchema}
+            onChange={this.props.toggleShowSchema}
+          />
+          {this.renderTableSettingsButton()}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx
index 2fdc582cc656ef80e9ca7fa8f951936b7181ba6f..678edabf687630f1b3e44020bde4cda6d2b61fbd 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchema.jsx
@@ -1,49 +1,50 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class MetadataSchema extends Component {
-    static propTypes = {
-        tableMetadata: PropTypes.object
-    };
+  static propTypes = {
+    tableMetadata: PropTypes.object,
+  };
 
-    render() {
-        const { tableMetadata } = this.props;
-        if (!tableMetadata) {
-            return false;
-        }
+  render() {
+    const { tableMetadata } = this.props;
+    if (!tableMetadata) {
+      return false;
+    }
 
-        var fields = tableMetadata.fields.map((field) => {
-            return (
-                <li key={field.id} className="px1 py2 flex border-bottom">
-                    <div className="flex-full flex flex-column mr1">
-                        <span className="TableEditor-field-name text-bold">{field.name}</span>
-                    </div>
-                    <div className="flex-half">
-                        <span className="text-bold">{field.base_type}</span>
-                    </div>
-                    <div className="flex-half">
-                    </div>
-                </li>
-            );
-        });
+    var fields = tableMetadata.fields.map(field => {
+      return (
+        <li key={field.id} className="px1 py2 flex border-bottom">
+          <div className="flex-full flex flex-column mr1">
+            <span className="TableEditor-field-name text-bold">
+              {field.name}
+            </span>
+          </div>
+          <div className="flex-half">
+            <span className="text-bold">{field.base_type}</span>
+          </div>
+          <div className="flex-half" />
+        </li>
+      );
+    });
 
-        return (
-            <div className="MetadataTable px2 flex-full">
-                <div className="flex flex-column px1">
-                    <div className="TableEditor-table-name text-bold">{tableMetadata.name}</div>
-                </div>
-                <div className="mt2 ">
-                    <div className="text-uppercase text-grey-3 py1 flex">
-                        <div className="flex-full px1">{t`Column`}</div>
-                        <div className="flex-half px1">{t`Data Type`}</div>
-                        <div className="flex-half px1">{t`Additional Info`}</div>
-                    </div>
-                    <ol className="border-top border-bottom">
-                        {fields}
-                    </ol>
-                </div>
-            </div>
-        );
-    }
+    return (
+      <div className="MetadataTable px2 flex-full">
+        <div className="flex flex-column px1">
+          <div className="TableEditor-table-name text-bold">
+            {tableMetadata.name}
+          </div>
+        </div>
+        <div className="mt2 ">
+          <div className="text-uppercase text-grey-3 py1 flex">
+            <div className="flex-full px1">{t`Column`}</div>
+            <div className="flex-half px1">{t`Data Type`}</div>
+            <div className="flex-half px1">{t`Additional Info`}</div>
+          </div>
+          <ol className="border-top border-bottom">{fields}</ol>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx
index 7ec8647c5b3e1343073c273540646b2a88df9b10..3621d5dcccaa2e95ca4fb31c9c3510bb8821ece7 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataSchemaList.jsx
@@ -1,59 +1,74 @@
 import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { inflect } from "metabase/lib/formatting";
 
 import _ from "underscore";
 import cx from "classnames";
 
 export default class MetadataSchemaList extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            searchText: "",
-            searchRegex: null
-        };
-
-        _.bindAll(this, "updateSearchText");
-    }
-
-    updateSearchText(event) {
-        this.setState({
-            searchText: event.target.value,
-            searchRegex: event.target.value ? new RegExp(RegExp.escape(event.target.value), "i") : null
-        });
-    }
-
-    render() {
-        const { schemas, selectedSchema } = this.props;
-        const { searchRegex } = this.state;
-
-        let filteredSchemas = searchRegex ? schemas.filter((s) => searchRegex.test(s.name)) : schemas;
-        return (
-            <div className="MetadataEditor-table-list AdminList flex-no-shrink">
-                <div className="AdminList-search">
-                    <Icon name="search" size={16}/>
-                    <input
-                        className="AdminInput pl4 border-bottom"
-                        type="text"
-                        placeholder={t`Find a schema`}
-                        value={this.state.searchText}
-                        onChange={this.updateSearchText}
-                    />
-                </div>
-                <ul className="AdminList-items">
-                    <li className="AdminList-section">{filteredSchemas.length} {inflect("schema", filteredSchemas.length)}</li>
-                    {filteredSchemas.map(schema =>
-                        <li key={schema.name}>
-                            <a className={cx("AdminList-item flex align-center no-decoration", { selected: selectedSchema && selectedSchema.name === schema.name })} onClick={() => this.props.onChangeSchema(schema)}>
-                                {schema.name}
-                            </a>
-                        </li>
-                    )}
-                </ul>
-            </div>
-        );
-    }
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      searchText: "",
+      searchRegex: null,
+    };
+
+    _.bindAll(this, "updateSearchText");
+  }
+
+  updateSearchText(event) {
+    this.setState({
+      searchText: event.target.value,
+      searchRegex: event.target.value
+        ? new RegExp(RegExp.escape(event.target.value), "i")
+        : null,
+    });
+  }
+
+  render() {
+    const { schemas, selectedSchema } = this.props;
+    const { searchRegex } = this.state;
+
+    let filteredSchemas = searchRegex
+      ? schemas.filter(s => searchRegex.test(s.name))
+      : schemas;
+    return (
+      <div className="MetadataEditor-table-list AdminList flex-no-shrink">
+        <div className="AdminList-search">
+          <Icon name="search" size={16} />
+          <input
+            className="AdminInput pl4 border-bottom"
+            type="text"
+            placeholder={t`Find a schema`}
+            value={this.state.searchText}
+            onChange={this.updateSearchText}
+          />
+        </div>
+        <ul className="AdminList-items">
+          <li className="AdminList-section">
+            {filteredSchemas.length} {inflect("schema", filteredSchemas.length)}
+          </li>
+          {filteredSchemas.map(schema => (
+            <li key={schema.name}>
+              <a
+                className={cx(
+                  "AdminList-item flex align-center no-decoration",
+                  {
+                    selected:
+                      selectedSchema && selectedSchema.name === schema.name,
+                  },
+                )}
+                onClick={() => this.props.onChangeSchema(schema)}
+              >
+                {schema.name}
+              </a>
+            </li>
+          ))}
+        </ul>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx
index 9d91bdefbdffb6ad48d972cfed017fe012ba4cae..071b7c245bc6ea34d97d457119e642db69a024eb 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTable.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import MetricsList from "./MetricsList.jsx";
 import ColumnsList from "./ColumnsList.jsx";
 import SegmentsList from "./SegmentsList.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Input from "metabase/components/Input.jsx";
 import ProgressBar from "metabase/components/ProgressBar.jsx";
 
@@ -12,106 +12,133 @@ import _ from "underscore";
 import cx from "classnames";
 
 export default class MetadataTable extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.onDescriptionChange = this.onDescriptionChange.bind(this);
-        this.onNameChange = this.onNameChange.bind(this);
-        this.updateProperty = this.updateProperty.bind(this);
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.onDescriptionChange = this.onDescriptionChange.bind(this);
+    this.onNameChange = this.onNameChange.bind(this);
+    this.updateProperty = this.updateProperty.bind(this);
+  }
 
-    static propTypes = {
-        tableMetadata: PropTypes.object,
-        idfields: PropTypes.array.isRequired,
-        updateTable: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    tableMetadata: PropTypes.object,
+    idfields: PropTypes.array.isRequired,
+    updateTable: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+  };
 
-    isHidden() {
-        return !!this.props.tableMetadata.visibility_type;
-    }
+  isHidden() {
+    return !!this.props.tableMetadata.visibility_type;
+  }
 
-    updateProperty(name, value) {
-        this.props.tableMetadata[name] = value;
-        this.setState({ saving: true });
-        this.props.updateTable(this.props.tableMetadata);
-    }
+  updateProperty(name, value) {
+    this.props.tableMetadata[name] = value;
+    this.setState({ saving: true });
+    this.props.updateTable(this.props.tableMetadata);
+  }
 
-    onNameChange(event) {
-        if (!_.isEmpty(event.target.value)) {
-            this.updateProperty("display_name", event.target.value);
-        } else {
-            // if the user set this to empty then simply reset it because that's not allowed!
-            event.target.value = this.props.tableMetadata.display_name;
-        }
+  onNameChange(event) {
+    if (!_.isEmpty(event.target.value)) {
+      this.updateProperty("display_name", event.target.value);
+    } else {
+      // if the user set this to empty then simply reset it because that's not allowed!
+      event.target.value = this.props.tableMetadata.display_name;
     }
+  }
 
-    onDescriptionChange(event) {
-        this.updateProperty("description", event.target.value);
-    }
+  onDescriptionChange(event) {
+    this.updateProperty("description", event.target.value);
+  }
 
-    renderVisibilityType(text, type, any) {
-        var classes = cx("mx1", "text-bold", "text-brand-hover", "cursor-pointer", "text-default", {
-            "text-brand": this.props.tableMetadata.visibility_type === type || (any && this.props.tableMetadata.visibility_type)
-        });
-        return <span className={classes} onClick={this.updateProperty.bind(null, "visibility_type", type)}>{text}</span>;
-    }
+  renderVisibilityType(text, type, any) {
+    var classes = cx(
+      "mx1",
+      "text-bold",
+      "text-brand-hover",
+      "cursor-pointer",
+      "text-default",
+      {
+        "text-brand":
+          this.props.tableMetadata.visibility_type === type ||
+          (any && this.props.tableMetadata.visibility_type),
+      },
+    );
+    return (
+      <span
+        className={classes}
+        onClick={this.updateProperty.bind(null, "visibility_type", type)}
+      >
+        {text}
+      </span>
+    );
+  }
 
-    renderVisibilityWidget() {
-        var subTypes;
-        if (this.props.tableMetadata.visibility_type) {
-            subTypes = (
-                <span id="VisibilitySubTypes" className="border-left mx2">
-                    <span className="mx2 text-uppercase text-grey-3">{t`Why Hide?`}</span>
-                    {this.renderVisibilityType(t`Technical Data`, "technical")}
-                    {this.renderVisibilityType(t`Irrelevant/Cruft`, "cruft")}
-                </span>
-            );
-        }
-        return (
-            <span id="VisibilityTypes">
-                {this.renderVisibilityType(t`Queryable`, null)}
-                {this.renderVisibilityType(t`Hidden`, "hidden", true)}
-                {subTypes}
-            </span>
-        );
+  renderVisibilityWidget() {
+    var subTypes;
+    if (this.props.tableMetadata.visibility_type) {
+      subTypes = (
+        <span id="VisibilitySubTypes" className="border-left mx2">
+          <span className="mx2 text-uppercase text-grey-3">{t`Why Hide?`}</span>
+          {this.renderVisibilityType(t`Technical Data`, "technical")}
+          {this.renderVisibilityType(t`Irrelevant/Cruft`, "cruft")}
+        </span>
+      );
     }
+    return (
+      <span id="VisibilityTypes">
+        {this.renderVisibilityType(t`Queryable`, null)}
+        {this.renderVisibilityType(t`Hidden`, "hidden", true)}
+        {subTypes}
+      </span>
+    );
+  }
 
-    render() {
-        const { tableMetadata } = this.props;
-        if (!tableMetadata) {
-            return false;
-        }
-
-        return (
-            <div className="MetadataTable px3 flex-full">
-                <div className="MetadataTable-title flex flex-column bordered rounded">
-                    <Input className="AdminInput TableEditor-table-name text-bold border-bottom rounded-top" type="text" value={tableMetadata.display_name || ""} onBlurChange={this.onNameChange}/>
-                    <Input className="AdminInput TableEditor-table-description rounded-bottom" type="text" value={tableMetadata.description || ""} onBlurChange={this.onDescriptionChange} placeholder={t`No table description yet`} />
-                </div>
-                <div className="MetadataTable-header flex align-center py2 text-grey-3">
-                    <span className="mx1 text-uppercase">{t`Visibility`}</span>
-                    {this.renderVisibilityWidget()}
-                    <span className="flex-align-right flex align-center">
-                        <span className="text-uppercase mr1">{t`Metadata Strength`}</span>
-                        <ProgressBar percentage={tableMetadata.metadataStrength} />
-                    </span>
-                </div>
-                <div className={"mt2 " + (this.isHidden() ? "disabled" : "")}>
-                    <SegmentsList
-                        tableMetadata={tableMetadata}
-                        onRetire={this.props.onRetireSegment}
-                    />
-                    <MetricsList
-                        tableMetadata={tableMetadata}
-                        onRetire={this.props.onRetireMetric}
-                    />
-                    <ColumnsList
-                        tableMetadata={tableMetadata}
-                        idfields={this.props.idfields}
-                        updateField={this.props.updateField}
-                    />
-                </div>
-            </div>
-        );
+  render() {
+    const { tableMetadata } = this.props;
+    if (!tableMetadata) {
+      return false;
     }
+
+    return (
+      <div className="MetadataTable px3 flex-full">
+        <div className="MetadataTable-title flex flex-column bordered rounded">
+          <Input
+            className="AdminInput TableEditor-table-name text-bold border-bottom rounded-top"
+            type="text"
+            value={tableMetadata.display_name || ""}
+            onBlurChange={this.onNameChange}
+          />
+          <Input
+            className="AdminInput TableEditor-table-description rounded-bottom"
+            type="text"
+            value={tableMetadata.description || ""}
+            onBlurChange={this.onDescriptionChange}
+            placeholder={t`No table description yet`}
+          />
+        </div>
+        <div className="MetadataTable-header flex align-center py2 text-grey-3">
+          <span className="mx1 text-uppercase">{t`Visibility`}</span>
+          {this.renderVisibilityWidget()}
+          <span className="flex-align-right flex align-center">
+            <span className="text-uppercase mr1">{t`Metadata Strength`}</span>
+            <ProgressBar percentage={tableMetadata.metadataStrength} />
+          </span>
+        </div>
+        <div className={"mt2 " + (this.isHidden() ? "disabled" : "")}>
+          <SegmentsList
+            tableMetadata={tableMetadata}
+            onRetire={this.props.onRetireSegment}
+          />
+          <MetricsList
+            tableMetadata={tableMetadata}
+            onRetire={this.props.onRetireMetric}
+          />
+          <ColumnsList
+            tableMetadata={tableMetadata}
+            idfields={this.props.idfields}
+            updateField={this.props.updateField}
+          />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx
index c8914a0388009e6e91e83a704443c19acd9e79ad..ae85f8470de77c1e2a7fe8bb42d5a8455b1f721e 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTableList.jsx
@@ -3,103 +3,133 @@ import PropTypes from "prop-types";
 
 import ProgressBar from "metabase/components/ProgressBar.jsx";
 import Icon from "metabase/components/Icon.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { inflect } from "metabase/lib/formatting";
 
 import _ from "underscore";
 import cx from "classnames";
 
 export default class MetadataTableList extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            searchText: "",
-            searchRegex: null
-        };
+    this.state = {
+      searchText: "",
+      searchRegex: null,
+    };
 
-        _.bindAll(this, "updateSearchText");
-    }
+    _.bindAll(this, "updateSearchText");
+  }
 
-    static propTypes = {
-        tableId: PropTypes.number,
-        tables: PropTypes.array.isRequired,
-        selectTable: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    tableId: PropTypes.number,
+    tables: PropTypes.array.isRequired,
+    selectTable: PropTypes.func.isRequired,
+  };
 
-    updateSearchText(event) {
-        this.setState({
-            searchText: event.target.value,
-            searchRegex: event.target.value ? new RegExp(RegExp.escape(event.target.value), "i") : null
-        });
-    }
+  updateSearchText(event) {
+    this.setState({
+      searchText: event.target.value,
+      searchRegex: event.target.value
+        ? new RegExp(RegExp.escape(event.target.value), "i")
+        : null,
+    });
+  }
 
-    render() {
-        var queryableTablesHeader, hiddenTablesHeader;
-        var queryableTables = [];
-        var hiddenTables = [];
+  render() {
+    var queryableTablesHeader, hiddenTablesHeader;
+    var queryableTables = [];
+    var hiddenTables = [];
 
-        if (this.props.tables) {
-            var tables = _.sortBy(this.props.tables, "display_name");
-            _.each(tables, (table) => {
-                var row = (
-                    <li key={table.id}>
-                        <a className={cx("AdminList-item flex align-center no-decoration", { selected: this.props.tableId === table.id })} onClick={this.props.selectTable.bind(null, table)}>
-                            {table.display_name}
-                            <ProgressBar className="ProgressBar ProgressBar--mini flex-align-right" percentage={table.metadataStrength} />
-                        </a>
-                    </li>
-                );
-                var regex = this.state.searchRegex;
-                if (!regex || regex.test(table.display_name) || regex.test(table.name)) {
-                    if (table.visibility_type) {
-                        hiddenTables.push(row);
-                    } else {
-                        queryableTables.push(row);
-                    }
-                }
-            });
+    if (this.props.tables) {
+      var tables = _.sortBy(this.props.tables, "display_name");
+      _.each(tables, table => {
+        var row = (
+          <li key={table.id}>
+            <a
+              className={cx("AdminList-item flex align-center no-decoration", {
+                selected: this.props.tableId === table.id,
+              })}
+              onClick={this.props.selectTable.bind(null, table)}
+            >
+              {table.display_name}
+              <ProgressBar
+                className="ProgressBar ProgressBar--mini flex-align-right"
+                percentage={table.metadataStrength}
+              />
+            </a>
+          </li>
+        );
+        var regex = this.state.searchRegex;
+        if (
+          !regex ||
+          regex.test(table.display_name) ||
+          regex.test(table.name)
+        ) {
+          if (table.visibility_type) {
+            hiddenTables.push(row);
+          } else {
+            queryableTables.push(row);
+          }
         }
+      });
+    }
 
-        if (queryableTables.length > 0) {
-            queryableTablesHeader = <li className="AdminList-section">{queryableTables.length} Queryable {inflect("Table", queryableTables.length)}</li>;
-        }
-        if (hiddenTables.length > 0) {
-            hiddenTablesHeader = <li className="AdminList-section">{hiddenTables.length} Hidden {inflect("Table", hiddenTables.length)}</li>;
-        }
-        if (queryableTables.length === 0 && hiddenTables.length === 0) {
-            queryableTablesHeader = <li className="AdminList-section">0 Tables</li>;
-        }
+    if (queryableTables.length > 0) {
+      queryableTablesHeader = (
+        <li className="AdminList-section">
+          {queryableTables.length} Queryable{" "}
+          {inflect("Table", queryableTables.length)}
+        </li>
+      );
+    }
+    if (hiddenTables.length > 0) {
+      hiddenTablesHeader = (
+        <li className="AdminList-section">
+          {hiddenTables.length} Hidden {inflect("Table", hiddenTables.length)}
+        </li>
+      );
+    }
+    if (queryableTables.length === 0 && hiddenTables.length === 0) {
+      queryableTablesHeader = <li className="AdminList-section">0 Tables</li>;
+    }
 
-        return (
-            <div className="MetadataEditor-table-list AdminList flex-no-shrink">
-                <div className="AdminList-search">
-                    <Icon name="search" size={16}/>
-                    <input
-                        className="AdminInput pl4 border-bottom"
-                        type="text"
-                        placeholder={t`Find a table`}
-                        value={this.state.searchText}
-                        onChange={this.updateSearchText}
-                    />
-                </div>
-                { (this.props.onBack || this.props.schema) &&
-                    <h4 className="p2 border-bottom">
-                        { this.props.onBack &&
-                            <span className="text-brand cursor-pointer" onClick={this.props.onBack}><Icon name="chevronleft" size={10}/>{t`Schemas`}</span>
-                        }
-                        { this.props.onBack && this.props.schema && <span className="mx1">-</span>}
-                        { this.props.schema && <span> {this.props.schema.name}</span>}
-                    </h4>
-                }
+    return (
+      <div className="MetadataEditor-table-list AdminList flex-no-shrink">
+        <div className="AdminList-search">
+          <Icon name="search" size={16} />
+          <input
+            className="AdminInput pl4 border-bottom"
+            type="text"
+            placeholder={t`Find a table`}
+            value={this.state.searchText}
+            onChange={this.updateSearchText}
+          />
+        </div>
+        {(this.props.onBack || this.props.schema) && (
+          <h4 className="p2 border-bottom">
+            {this.props.onBack && (
+              <span
+                className="text-brand cursor-pointer"
+                onClick={this.props.onBack}
+              >
+                <Icon name="chevronleft" size={10} />
+                {t`Schemas`}
+              </span>
+            )}
+            {this.props.onBack &&
+              this.props.schema && <span className="mx1">-</span>}
+            {this.props.schema && <span> {this.props.schema.name}</span>}
+          </h4>
+        )}
 
-                <ul className="AdminList-items">
-                    {queryableTablesHeader}
-                    {queryableTables}
-                    {hiddenTablesHeader}
-                    {hiddenTables}
-                </ul>
-            </div>
-        );
-    }
+        <ul className="AdminList-items">
+          {queryableTablesHeader}
+          {queryableTables}
+          {hiddenTablesHeader}
+          {hiddenTables}
+        </ul>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx
index 6c6df516e693a1195e1280cfd8d562821eb2c456..0d5e3827ff1d4c4ae984bcab083db5f532800dc6 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetadataTablePicker.jsx
@@ -7,61 +7,72 @@ import MetadataSchemaList from "./MetadataSchemaList.jsx";
 import { titleize, humanize } from "metabase/lib/formatting";
 
 export default class MetadataTablePicker extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            schemas: null,
-            selectedSchema: null,
-            showTablePicker: true
-        };
-    }
-
-    static propTypes = {
-        tableId: PropTypes.number,
-        tables: PropTypes.array.isRequired,
-        selectTable: PropTypes.func.isRequired
+    this.state = {
+      schemas: null,
+      selectedSchema: null,
+      showTablePicker: true,
     };
+  }
 
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
-    }
+  static propTypes = {
+    tableId: PropTypes.number,
+    tables: PropTypes.array.isRequired,
+    selectTable: PropTypes.func.isRequired,
+  };
 
-    componentWillReceiveProps(newProps) {
-        const { tables } = newProps;
-        let schemas = {};
-        let selectedSchema;
-        for (let table of tables) {
-            let name = table.schema || ""; // possibly null
-            schemas[name] = schemas[name] || {
-                name: titleize(humanize(name)),
-                tables: []
-            }
-            schemas[name].tables.push(table);
-            if (table.id === newProps.tableId) {
-                selectedSchema = schemas[name];
-            }
-        }
-        this.setState({
-            schemas: Object.values(schemas).sort((a, b) => a.name.localeCompare(b.name)),
-            selectedSchema: selectedSchema
-        });
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
+
+  componentWillReceiveProps(newProps) {
+    const { tables } = newProps;
+    let schemas = {};
+    let selectedSchema;
+    for (let table of tables) {
+      let name = table.schema || ""; // possibly null
+      schemas[name] = schemas[name] || {
+        name: titleize(humanize(name)),
+        tables: [],
+      };
+      schemas[name].tables.push(table);
+      if (table.id === newProps.tableId) {
+        selectedSchema = schemas[name];
+      }
     }
+    this.setState({
+      schemas: Object.values(schemas).sort((a, b) =>
+        a.name.localeCompare(b.name),
+      ),
+      selectedSchema: selectedSchema,
+    });
+  }
 
-    render() {
-        const { schemas } = this.state;
-        if (schemas.length === 1) {
-            return <MetadataTableList {...this.props} tables={schemas[0].tables} />;
-        }
-        if (this.state.selectedSchema && this.state.showTablePicker) {
-            return <MetadataTableList {...this.props} tables={this.state.selectedSchema.tables} schema={this.state.selectedSchema} onBack={() => this.setState({ showTablePicker: false })} />;
-        }
-        return (
-            <MetadataSchemaList
-                schemas={schemas}
-                selectedSchema={this.state.schema}
-                onChangeSchema={(schema) => this.setState({ selectedSchema: schema, showTablePicker: true })}
-            />
-        );
+  render() {
+    const { schemas } = this.state;
+    if (schemas.length === 1) {
+      return <MetadataTableList {...this.props} tables={schemas[0].tables} />;
     }
+    if (this.state.selectedSchema && this.state.showTablePicker) {
+      return (
+        <MetadataTableList
+          {...this.props}
+          tables={this.state.selectedSchema.tables}
+          schema={this.state.selectedSchema}
+          onBack={() => this.setState({ showTablePicker: false })}
+        />
+      );
+    }
+    return (
+      <MetadataSchemaList
+        schemas={schemas}
+        selectedSchema={this.state.schema}
+        onChangeSchema={schema =>
+          this.setState({ selectedSchema: schema, showTablePicker: true })
+        }
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx
index fa8ce0053fcab6d515d178cad42bd285d34eb43e..54c94851b5de015aab713cb4c8d128d0d516dd4a 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetricItem.jsx
@@ -6,32 +6,32 @@ import ObjectActionSelect from "../ObjectActionSelect.jsx";
 import Query from "metabase/lib/query";
 
 export default class MetricItem extends Component {
-    static propTypes = {
-        metric: PropTypes.object.isRequired,
-        onRetire: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    metric: PropTypes.object.isRequired,
+    onRetire: PropTypes.func.isRequired,
+  };
 
-    render() {
-        let { metric, tableMetadata } = this.props;
+  render() {
+    let { metric, tableMetadata } = this.props;
 
-        let description = Query.generateQueryDescription(tableMetadata, metric.definition, { sections: ["aggregation", "filter"], jsx: true });
+    let description = Query.generateQueryDescription(
+      tableMetadata,
+      metric.definition,
+      { sections: ["aggregation", "filter"], jsx: true },
+    );
 
-        return (
-            <tr className="mt1 mb3">
-                <td className="px1">
-                    {metric.name}
-                </td>
-                <td className="px1 text-ellipsis">
-                    {description}
-                </td>
-                <td className="px1 text-centered">
-                    <ObjectActionSelect
-                        object={metric}
-                        objectType="metric"
-                        onRetire={this.props.onRetire}
-                    />
-                </td>
-            </tr>
-        )
-    }
+    return (
+      <tr className="mt1 mb3">
+        <td className="px1">{metric.name}</td>
+        <td className="px1 text-ellipsis">{description}</td>
+        <td className="px1 text-centered">
+          <ObjectActionSelect
+            object={metric}
+            objectType="metric"
+            onRetire={this.props.onRetire}
+          />
+        </td>
+      </tr>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx
index 35bd0c41590e8f43ef9e5ff54dd89d25528cb2e7..78e3f593c3075159b1c3b56b0ba5d2b5da87de64 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/MetricsList.jsx
@@ -1,52 +1,60 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import MetricItem from "./MetricItem.jsx";
 
 export default class MetricsList extends Component {
-    static propTypes = {
-        tableMetadata: PropTypes.object.isRequired,
-        onRetire: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    tableMetadata: PropTypes.object.isRequired,
+    onRetire: PropTypes.func.isRequired,
+  };
 
-    render() {
-        let { tableMetadata } = this.props;
+  render() {
+    let { tableMetadata } = this.props;
 
-        tableMetadata.metrics = tableMetadata.metrics || [];
-        tableMetadata.metrics = tableMetadata.metrics.filter((mtrc) => mtrc.is_active === true);
+    tableMetadata.metrics = tableMetadata.metrics || [];
+    tableMetadata.metrics = tableMetadata.metrics.filter(
+      mtrc => mtrc.is_active === true,
+    );
 
-        return (
-            <div id="MetricsList" className="my3">
-                <div className="flex mb1">
-                    <h2 className="px1 text-green">{t`Metrics`}</h2>
-                    <Link to={"/admin/datamodel/metric/create?table="+tableMetadata.id} data-metabase-event="Data Model;Add Metric Page" className="flex-align-right float-right text-bold text-brand no-decoration">+ {t`Add a Metric`}</Link>
-                </div>
-                <table className="AdminTable">
-                    <thead>
-                        <tr>
-                            <th style={{ minWidth: "200px" }}>{t`Name`}</th>
-                            <th className="full">{t`Definition`}</th>
-                            <th>{t`Actions`}</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        {tableMetadata.metrics.map(metric =>
-                            <MetricItem
-                                key={metric.id}
-                                metric={metric}
-                                tableMetadata={tableMetadata}
-                                onRetire={this.props.onRetire}
-                            />
-                        )}
-                    </tbody>
-                </table>
-                { tableMetadata.metrics.length === 0 &&
-                    <div className="flex layout-centered m4 text-grey-3">
-                        {t`Create metrics to add them to the View dropdown in the query builder`}
-                    </div>
-                }
-            </div>
-        );
-    }
+    return (
+      <div id="MetricsList" className="my3">
+        <div className="flex mb1">
+          <h2 className="px1 text-green">{t`Metrics`}</h2>
+          <Link
+            to={"/admin/datamodel/metric/create?table=" + tableMetadata.id}
+            data-metabase-event="Data Model;Add Metric Page"
+            className="flex-align-right float-right text-bold text-brand no-decoration"
+          >
+            + {t`Add a Metric`}
+          </Link>
+        </div>
+        <table className="AdminTable">
+          <thead>
+            <tr>
+              <th style={{ minWidth: "200px" }}>{t`Name`}</th>
+              <th className="full">{t`Definition`}</th>
+              <th>{t`Actions`}</th>
+            </tr>
+          </thead>
+          <tbody>
+            {tableMetadata.metrics.map(metric => (
+              <MetricItem
+                key={metric.id}
+                metric={metric}
+                tableMetadata={tableMetadata}
+                onRetire={this.props.onRetire}
+              />
+            ))}
+          </tbody>
+        </table>
+        {tableMetadata.metrics.length === 0 && (
+          <div className="flex layout-centered m4 text-grey-3">
+            {t`Create metrics to add them to the View dropdown in the query builder`}
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx
index 529d3b5e9377d526ab7e348cd3abfb913dc02e0e..18158962dbe6d03e0fcc4abc968329dbc37f48de 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentItem.jsx
@@ -6,35 +6,43 @@ import ObjectActionSelect from "../ObjectActionSelect.jsx";
 import Query from "metabase/lib/query";
 
 export default class SegmentItem extends Component {
-    static propTypes = {
-        segment: PropTypes.object.isRequired,
-        tableMetadata: PropTypes.object.isRequired,
-        onRetire: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    segment: PropTypes.object.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+    onRetire: PropTypes.func.isRequired,
+  };
 
-    render() {
-        let { segment, tableMetadata } = this.props;
+  render() {
+    let { segment, tableMetadata } = this.props;
 
-        let description = Query.generateQueryDescription(tableMetadata, segment.definition, { sections: ["filter"], jsx: true });
+    let description = Query.generateQueryDescription(
+      tableMetadata,
+      segment.definition,
+      { sections: ["filter"], jsx: true },
+    );
 
-        return (
-            <tr className="mt1 mb3">
-                <td className="px1">
-                    {segment.name}
-                </td>
-                <td className="px1 text-ellipsis">
-                    <div style={{maxWidth: 400, overflow: 'hidden', textOverflow: 'ellipsis' }}>
-                        {description}
-                    </div>
-                </td>
-                <td className="px1 text-centered">
-                    <ObjectActionSelect
-                        object={segment}
-                        objectType="segment"
-                        onRetire={this.props.onRetire}
-                    />
-                </td>
-            </tr>
-        )
-    }
+    return (
+      <tr className="mt1 mb3">
+        <td className="px1">{segment.name}</td>
+        <td className="px1 text-ellipsis">
+          <div
+            style={{
+              maxWidth: 400,
+              overflow: "hidden",
+              textOverflow: "ellipsis",
+            }}
+          >
+            {description}
+          </div>
+        </td>
+        <td className="px1 text-centered">
+          <ObjectActionSelect
+            object={segment}
+            objectType="segment"
+            onRetire={this.props.onRetire}
+          />
+        </td>
+      </tr>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx
index 2f806afc9e3b6073e600693222367d9f6aa7bbdd..391cb35f983142060e383c0c8787ad9f9ed85841 100644
--- a/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/database/SegmentsList.jsx
@@ -1,52 +1,60 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import SegmentItem from "./SegmentItem.jsx";
 
 export default class SegmentsList extends Component {
-    static propTypes = {
-        tableMetadata: PropTypes.object.isRequired,
-        onRetire: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    tableMetadata: PropTypes.object.isRequired,
+    onRetire: PropTypes.func.isRequired,
+  };
 
-    render() {
-        let { tableMetadata } = this.props;
+  render() {
+    let { tableMetadata } = this.props;
 
-        tableMetadata.segments = tableMetadata.segments || [];
-        tableMetadata.segments = tableMetadata.segments.filter((sgmt) => sgmt.is_active === true);
+    tableMetadata.segments = tableMetadata.segments || [];
+    tableMetadata.segments = tableMetadata.segments.filter(
+      sgmt => sgmt.is_active === true,
+    );
 
-        return (
-            <div id="SegmentsList" className="my3">
-                <div className="flex mb1">
-                    <h2 className="px1 text-purple">{t`Segments`}</h2>
-                    <Link to={"/admin/datamodel/segment/create?table="+tableMetadata.id} data-metabase-event="Data Model;Add Segment Page" className="flex-align-right float-right text-bold text-brand no-decoration">+ {t`Add a Segment`}</Link>
-                </div>
-                <table className="AdminTable">
-                    <thead>
-                        <tr>
-                            <th style={{ minWidth: "200px" }}>{t`Name`}</th>
-                            <th className="full">{t`Definition`}</th>
-                            <th>{t`Actions`}</th>
-                        </tr>
-                    </thead>
-                    <tbody>
-                        {tableMetadata.segments.map(segment =>
-                            <SegmentItem
-                                key={segment.id}
-                                segment={segment}
-                                tableMetadata={tableMetadata}
-                                onRetire={this.props.onRetire}
-                            />
-                        )}
-                    </tbody>
-                </table>
-                { tableMetadata.segments.length === 0 &&
-                    <div className="flex layout-centered m4 text-grey-3">
-                        {t`Create segments to add them to the Filter dropdown in the query builder`}
-                    </div>
-                }
-            </div>
-        );
-    }
+    return (
+      <div id="SegmentsList" className="my3">
+        <div className="flex mb1">
+          <h2 className="px1 text-purple">{t`Segments`}</h2>
+          <Link
+            to={"/admin/datamodel/segment/create?table=" + tableMetadata.id}
+            data-metabase-event="Data Model;Add Segment Page"
+            className="flex-align-right float-right text-bold text-brand no-decoration"
+          >
+            + {t`Add a Segment`}
+          </Link>
+        </div>
+        <table className="AdminTable">
+          <thead>
+            <tr>
+              <th style={{ minWidth: "200px" }}>{t`Name`}</th>
+              <th className="full">{t`Definition`}</th>
+              <th>{t`Actions`}</th>
+            </tr>
+          </thead>
+          <tbody>
+            {tableMetadata.segments.map(segment => (
+              <SegmentItem
+                key={segment.id}
+                segment={segment}
+                tableMetadata={tableMetadata}
+                onRetire={this.props.onRetire}
+              />
+            ))}
+          </tbody>
+        </table>
+        {tableMetadata.segments.length === 0 && (
+          <div className="flex layout-centered m4 text-grey-3">
+            {t`Create segments to add them to the Filter dropdown in the query builder`}
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx
index 9d6e29ef3e947177df5ec253b4604fa7e0543568..b442f95d6598fc5f67893658d2beca36e9482357 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/QueryDiff.jsx
@@ -9,37 +9,37 @@ import AggregationWidget from "metabase/query_builder/components/AggregationWidg
 import Query from "metabase/lib/query";
 
 export default class QueryDiff extends Component {
-    static propTypes = {
-        diff: PropTypes.object.isRequired,
-        tableMetadata: PropTypes.object.isRequired
-    };
+  static propTypes = {
+    diff: PropTypes.object.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+  };
 
-    render() {
-        const { diff: { before, after }, tableMetadata} = this.props;
-        const defintion = after || before;
+  render() {
+    const { diff: { before, after }, tableMetadata } = this.props;
+    const defintion = after || before;
 
-        const filters = Query.getFilters(defintion);
+    const filters = Query.getFilters(defintion);
 
-        return (
-            <LoadingAndErrorWrapper loading={!tableMetadata}>
-            {() =>
-                <div className="my1" style={{ pointerEvents: "none" }}>
-                    { defintion.aggregation &&
-                        <AggregationWidget
-                            aggregation={defintion.aggregation}
-                            tableMetadata={tableMetadata}
-                        />
-                    }
-                    { filters.length > 0 &&
-                        <FilterList
-                            filters={filters}
-                            tableMetadata={tableMetadata}
-                            maxDisplayValues={Infinity}
-                        />
-                    }
-                </div>
-            }
-            </LoadingAndErrorWrapper>
-        )
-    }
+    return (
+      <LoadingAndErrorWrapper loading={!tableMetadata}>
+        {() => (
+          <div className="my1" style={{ pointerEvents: "none" }}>
+            {defintion.aggregation && (
+              <AggregationWidget
+                aggregation={defintion.aggregation}
+                tableMetadata={tableMetadata}
+              />
+            )}
+            {filters.length > 0 && (
+              <FilterList
+                filters={filters}
+                tableMetadata={tableMetadata}
+                maxDisplayValues={Infinity}
+              />
+            )}
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx
index 4671c47d44e02c748e1c5ffc1dc7d7cc23620b5b..688c901b64c01bc90bdbaaa018559024af7f334f 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/Revision.jsx
@@ -2,8 +2,8 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import RevisionDiff from "./RevisionDiff.jsx";
-import { t } from 'c-3po';
-import UserAvatar from "metabase/components/UserAvatar.jsx"
+import { t } from "c-3po";
+import UserAvatar from "metabase/components/UserAvatar.jsx";
 
 import moment from "moment";
 
@@ -11,83 +11,86 @@ import moment from "moment";
 // TODO: show different color avatars for users that aren't me
 
 export default class Revision extends Component {
-    static propTypes = {
-        objectName: PropTypes.string.isRequired,
-        revision: PropTypes.object.isRequired,
-        currentUser: PropTypes.object.isRequired,
-        tableMetadata: PropTypes.object.isRequired
-    };
+  static propTypes = {
+    objectName: PropTypes.string.isRequired,
+    revision: PropTypes.object.isRequired,
+    currentUser: PropTypes.object.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+  };
 
-    getAction() {
-        const { revision, objectName } = this.props;
-        if (revision.is_creation) {
-            return t`created` + " \"" + revision.diff.name.after + "\"";
-        }
-        if (revision.is_reversion) {
-            return t`reverted to a previous version`;
-        }
-        let changedKeys = Object.keys(revision.diff);
-        if (changedKeys.length === 1) {
-            switch (changedKeys[0]) {
-                case "name":
-                    return t`edited the title`;
-                case "description":
-                    return t`edited the description`;
-                case "defintion":
-                    return t`edited the ` + objectName;
-            }
-        }
-        return t`made some changes`;
+  getAction() {
+    const { revision, objectName } = this.props;
+    if (revision.is_creation) {
+      return t`created` + ' "' + revision.diff.name.after + '"';
     }
-
-    getName() {
-        const { revision: { user }, currentUser } = this.props;
-        if (user.id === currentUser.id) {
-            return t`You`
-        } else {
-            return user.first_name;
-        }
+    if (revision.is_reversion) {
+      return t`reverted to a previous version`;
+    }
+    let changedKeys = Object.keys(revision.diff);
+    if (changedKeys.length === 1) {
+      switch (changedKeys[0]) {
+        case "name":
+          return t`edited the title`;
+        case "description":
+          return t`edited the description`;
+        case "defintion":
+          return t`edited the ` + objectName;
+      }
     }
+    return t`made some changes`;
+  }
 
-    render() {
-        const { revision, tableMetadata, userColor } = this.props;
-        let message = revision.message;
-        let diffKeys = Object.keys(revision.diff);
+  getName() {
+    const { revision: { user }, currentUser } = this.props;
+    if (user.id === currentUser.id) {
+      return t`You`;
+    } else {
+      return user.first_name;
+    }
+  }
 
-        if (revision.is_creation) {
-            // these are included in the
-            message = revision.diff.description.after;
-            diffKeys = diffKeys.filter(k => k !== "name" && k !== "description");
-        }
+  render() {
+    const { revision, tableMetadata, userColor } = this.props;
+    let message = revision.message;
+    let diffKeys = Object.keys(revision.diff);
 
-        return (
-            <li className="flex flex-row">
-                <div className="flex flex-column align-center mr2">
-                    <div className="text-white">
-                        <UserAvatar user={revision.user} background={userColor}/>
-                    </div>
-                    <div className="flex-full my1 border-left" style={{borderWidth: 2}} />
-                </div>
-                <div className="flex-full mt1 mb4">
-                    <div className="flex mb1 text-grey-4">
-                        <span className="">
-                            <strong>{this.getName()}</strong> {this.getAction()}
-                        </span>
-                        <span className="flex-align-right h5">
-                            {moment(revision.timestamp).format("MMMM DD, YYYY")}
-                        </span>
-                    </div>
-                    { message && <p>"{message}"</p> }
-                    { diffKeys.map(key =>
-                        <RevisionDiff
-                            key={key}
-                            property={key}
-                            diff={revision.diff[key]}
-                            tableMetadata={tableMetadata}
-                        />
-                    )}
-                </div>
-            </li>
-        );
+    if (revision.is_creation) {
+      // these are included in the
+      message = revision.diff.description.after;
+      diffKeys = diffKeys.filter(k => k !== "name" && k !== "description");
     }
+
+    return (
+      <li className="flex flex-row">
+        <div className="flex flex-column align-center mr2">
+          <div className="text-white">
+            <UserAvatar user={revision.user} background={userColor} />
+          </div>
+          <div
+            className="flex-full my1 border-left"
+            style={{ borderWidth: 2 }}
+          />
+        </div>
+        <div className="flex-full mt1 mb4">
+          <div className="flex mb1 text-grey-4">
+            <span className="">
+              <strong>{this.getName()}</strong> {this.getAction()}
+            </span>
+            <span className="flex-align-right h5">
+              {moment(revision.timestamp).format("MMMM DD, YYYY")}
+            </span>
+          </div>
+          {message && <p>"{message}"</p>}
+          {diffKeys.map(key => (
+            <RevisionDiff
+              key={key}
+              property={key}
+              diff={revision.diff[key]}
+              tableMetadata={tableMetadata}
+            />
+          ))}
+        </div>
+      </li>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx
index 410bab3eab61013192e072fc21890bc8848f3813..fb8d40ac7ffe5d5d6d3fe5bb769c669681f0e328 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionDiff.jsx
@@ -7,40 +7,43 @@ import QueryDiff from "./QueryDiff.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
 export default class RevisionDiff extends Component {
-    static propTypes = {
-        property: PropTypes.string.isRequired,
-        diff: PropTypes.object.isRequired,
-        tableMetadata: PropTypes.object.isRequired
-    };
+  static propTypes = {
+    property: PropTypes.string.isRequired,
+    diff: PropTypes.object.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+  };
 
-    render() {
-        let { diff: { before, after }, tableMetadata} = this.props;
+  render() {
+    let { diff: { before, after }, tableMetadata } = this.props;
 
-        let icon;
-        if (before != null && after != null) {
-            icon = <Icon name="pencil" className="text-brand" size={16} />
-        } else if (before != null) {
-            icon = <Icon name="add" className="text-error" size={16} />
-        } else {
-            // TODO: "minus" icon
-            icon = <Icon name="add" className="text-green" size={16} />
-        }
-
-        return (
-            <div className="bordered rounded my2" style={{borderWidth: 2, overflow: 'hidden', maxWidth: 860}}>
-                <div className="flex align-center scroll-x scroll-show scroll-show-horizontal">
-                    <div className="m3" style={{lineHeight: 0}}>
-                        {icon}
-                    </div>
-                    <div>
-                        { this.props.property === "definition" ?
-                            <QueryDiff diff={this.props.diff} tableMetadata={tableMetadata}/>
-                        :
-                            <TextDiff diff={this.props.diff}/>
-                        }
-                    </div>
-                </div>
-            </div>
-        );
+    let icon;
+    if (before != null && after != null) {
+      icon = <Icon name="pencil" className="text-brand" size={16} />;
+    } else if (before != null) {
+      icon = <Icon name="add" className="text-error" size={16} />;
+    } else {
+      // TODO: "minus" icon
+      icon = <Icon name="add" className="text-green" size={16} />;
     }
+
+    return (
+      <div
+        className="bordered rounded my2"
+        style={{ borderWidth: 2, overflow: "hidden", maxWidth: 860 }}
+      >
+        <div className="flex align-center scroll-x scroll-show scroll-show-horizontal">
+          <div className="m3" style={{ lineHeight: 0 }}>
+            {icon}
+          </div>
+          <div>
+            {this.props.property === "definition" ? (
+              <QueryDiff diff={this.props.diff} tableMetadata={tableMetadata} />
+            ) : (
+              <TextDiff diff={this.props.diff} />
+            )}
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx
index af1c5e6ef1f5cd9162223c6408fa072a94996dd2..72916be26ef7d7ec36e018559d94f97bdb7565f5 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/RevisionHistory.jsx
@@ -2,52 +2,66 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import Revision from "./Revision.jsx";
-import { t } from 'c-3po';
-import Breadcrumbs from "metabase/components/Breadcrumbs.jsx"
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx"
+import { t } from "c-3po";
+import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
 import { assignUserColors } from "metabase/lib/formatting";
 
 export default class RevisionHistory extends Component {
-    static propTypes = {
-        object: PropTypes.object,
-        revisions: PropTypes.array,
-        tableMetadata: PropTypes.object
-    };
+  static propTypes = {
+    object: PropTypes.object,
+    revisions: PropTypes.array,
+    tableMetadata: PropTypes.object,
+  };
 
-    render() {
-        const { object, revisions, tableMetadata, user } = this.props;
+  render() {
+    const { object, revisions, tableMetadata, user } = this.props;
 
-        let userColorAssignments = {};
-        if (revisions) {
-            userColorAssignments = assignUserColors(revisions.map(r => r.user.id), user.id)
-        }
-
-        return (
-            <LoadingAndErrorWrapper loading={!object || !revisions}>
-            {() =>
-                <div className="wrapper">
-                    <Breadcrumbs className="py4" crumbs={[
-                        [t`Datamodel`, "/admin/datamodel/database/" + tableMetadata.db_id + "/table/" + tableMetadata.id],
-                        [this.props.objectType + t` History`]
-                    ]}/>
-                    <div className="wrapper py4" style={{maxWidth: 950}}>
-                        <h2 className="mb4">{t`Revision History for`} "{object.name}"</h2>
-                        <ol>
-                        {revisions.map(revision =>
-                            <Revision
-                                revision={revision}
-                                objectName={name}
-                                currentUser={user}
-                                tableMetadata={tableMetadata}
-                                userColor={userColorAssignments[revision.user.id]}
-                            />
-                        )}
-                        </ol>
-                    </div>
-                </div>
-            }
-            </LoadingAndErrorWrapper>
-        );
+    let userColorAssignments = {};
+    if (revisions) {
+      userColorAssignments = assignUserColors(
+        revisions.map(r => r.user.id),
+        user.id,
+      );
     }
+
+    return (
+      <LoadingAndErrorWrapper loading={!object || !revisions}>
+        {() => (
+          <div className="wrapper">
+            <Breadcrumbs
+              className="py4"
+              crumbs={[
+                [
+                  t`Datamodel`,
+                  "/admin/datamodel/database/" +
+                    tableMetadata.db_id +
+                    "/table/" +
+                    tableMetadata.id,
+                ],
+                [this.props.objectType + t` History`],
+              ]}
+            />
+            <div className="wrapper py4" style={{ maxWidth: 950 }}>
+              <h2 className="mb4">
+                {t`Revision History for`} "{object.name}"
+              </h2>
+              <ol>
+                {revisions.map(revision => (
+                  <Revision
+                    revision={revision}
+                    objectName={name}
+                    currentUser={user}
+                    tableMetadata={tableMetadata}
+                    userColor={userColorAssignments[revision.user.id]}
+                  />
+                ))}
+              </ol>
+            </div>
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx b/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx
index 507eb67c54bbfc3c2a8ea3f390fd845cc16a5d3a..9035ba5bca0eca7c11b6a0b2eba6797776bcb9d9 100644
--- a/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx
+++ b/frontend/src/metabase/admin/datamodel/components/revisions/TextDiff.jsx
@@ -3,37 +3,39 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
-import { diffWords } from 'diff';
+import { diffWords } from "diff";
 
 export default class TextDiff extends Component {
-    static propTypes = {
-        diff: PropTypes.object.isRequired
-    };
+  static propTypes = {
+    diff: PropTypes.object.isRequired,
+  };
 
-    render() {
-        let { diff: { before, after }} = this.props;
-        return (
-            <div>
-                "
-                {before != null &&  after != null ?
-                    diffWords(before, after).map((section, index) =>
-                        <span>
-                        {section.added ?
-                            <strong key={index}>{section.value}</strong>
-                        : section.removed ?
-                            <span key={index} style={{ textDecoration: "line-through"}}>{section.value}</span>
-                        :
-                            <span key={index}>{section.value}</span>
-                        }{" "}
-                        </span>
-                    )
-                : before != null ?
-                    <span style={{ textDecoration: "line-through"}}>{before}</span>
-                :
-                    <strong>{after}</strong>
-                }
-                "
-            </div>
-        );
-    }
+  render() {
+    let { diff: { before, after } } = this.props;
+    return (
+      <div>
+        "
+        {before != null && after != null ? (
+          diffWords(before, after).map((section, index) => (
+            <span>
+              {section.added ? (
+                <strong key={index}>{section.value}</strong>
+              ) : section.removed ? (
+                <span key={index} style={{ textDecoration: "line-through" }}>
+                  {section.value}
+                </span>
+              ) : (
+                <span key={index}>{section.value}</span>
+              )}{" "}
+            </span>
+          ))
+        ) : before != null ? (
+          <span style={{ textDecoration: "line-through" }}>{before}</span>
+        ) : (
+          <strong>{after}</strong>
+        )}
+        "
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx b/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx
index 2b3322314879d73b44243ea35b241b65a858b365..611de99bc2643d160eac1083849e48b3272fd69b 100644
--- a/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/FieldApp.jsx
@@ -4,15 +4,15 @@
  * TODO Atte Keinänen 7/6/17: This uses the standard metadata API; we should migrate also other parts of admin section
  */
 
-import React, { Component } from 'react'
-import { Link } from 'react-router'
+import React, { Component } from "react";
+import { Link } from "react-router";
 import { connect } from "react-redux";
 import _ from "underscore";
 import cx from "classnames";
-import { t } from 'c-3po';
-import Icon from 'metabase/components/Icon'
-import Input from 'metabase/components/Input'
-import Select from 'metabase/components/Select'
+import { t } from "c-3po";
+import Icon from "metabase/components/Icon";
+import Input from "metabase/components/Input";
+import Select from "metabase/components/Select";
 import SaveStatus from "metabase/components/SaveStatus";
 import Breadcrumbs from "metabase/components/Breadcrumbs";
 import ButtonWithStatus from "metabase/components/ButtonWithStatus";
@@ -20,7 +20,7 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 
 import { getMetadata } from "metabase/selectors/metadata";
 import * as metadataActions from "metabase/redux/metadata";
-import * as datamodelActions from "../datamodel"
+import * as datamodelActions from "../datamodel";
 
 import ActionButton from "metabase/components/ActionButton.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
@@ -28,643 +28,771 @@ import SelectButton from "metabase/components/SelectButton";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import FieldList from "metabase/query_builder/components/FieldList";
 import {
-    FieldVisibilityPicker,
-    SpecialTypeAndTargetPicker
+  FieldVisibilityPicker,
+  SpecialTypeAndTargetPicker,
 } from "metabase/admin/datamodel/components/database/ColumnItem";
 import { getDatabaseIdfields } from "metabase/admin/datamodel/selectors";
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 import Question from "metabase-lib/lib/Question";
 import { DatetimeFieldDimension } from "metabase-lib/lib/Dimension";
 
-import {
-    rescanFieldValues,
-    discardFieldValues
-} from "../field";
+import { rescanFieldValues, discardFieldValues } from "../field";
+
+const HAS_FIELD_VALUES_OPTIONS = [
+  { name: "Search box", value: "search" },
+  { name: "A list of all values", value: "list" },
+  { name: "Plain input box", value: "none" },
+];
 
-const SelectClasses = 'h3 bordered border-dark shadowed p2 inline-block flex align-center rounded text-bold'
+const SelectClasses =
+  "h3 bordered border-dark shadowed p2 inline-block flex align-center rounded text-bold";
 
 const mapStateToProps = (state, props) => {
-    return {
-        databaseId: parseInt(props.params.databaseId),
-        tableId: parseInt(props.params.tableId),
-        fieldId: parseInt(props.params.fieldId),
-        metadata: getMetadata(state),
-        idfields: getDatabaseIdfields(state)
-    }
-}
+  return {
+    databaseId: parseInt(props.params.databaseId),
+    tableId: parseInt(props.params.tableId),
+    fieldId: parseInt(props.params.fieldId),
+    metadata: getMetadata(state),
+    idfields: getDatabaseIdfields(state),
+  };
+};
 
 const mapDispatchToProps = {
-    fetchDatabaseMetadata: metadataActions.fetchDatabaseMetadata,
-    fetchTableMetadata: metadataActions.fetchTableMetadata,
-    updateField: metadataActions.updateField,
-    updateFieldValues: metadataActions.updateFieldValues,
-    updateFieldDimension: metadataActions.updateFieldDimension,
-    deleteFieldDimension: metadataActions.deleteFieldDimension,
-    fetchDatabaseIdfields: datamodelActions.fetchDatabaseIdfields,
-    rescanFieldValues,
-    discardFieldValues
-}
+  fetchDatabaseMetadata: metadataActions.fetchDatabaseMetadata,
+  fetchTableMetadata: metadataActions.fetchTableMetadata,
+  fetchFieldValues: metadataActions.fetchFieldValues,
+  updateField: metadataActions.updateField,
+  updateFieldValues: metadataActions.updateFieldValues,
+  updateFieldDimension: metadataActions.updateFieldDimension,
+  deleteFieldDimension: metadataActions.deleteFieldDimension,
+  fetchDatabaseIdfields: datamodelActions.fetchDatabaseIdfields,
+  rescanFieldValues,
+  discardFieldValues,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class FieldApp extends Component {
-    saveStatus: null
-
-    props: {
-        databaseId: number,
-        tableId: number,
-        fieldId: number,
-        metadata: Metadata,
-        idfields: Object[],
-
-        fetchDatabaseMetadata: (number) => Promise<void>,
-        fetchTableMetadata: (number) => Promise<void>,
-        updateField: (any) => Promise<void>,
-        updateFieldValues: (any) => Promise<void>,
-        updateFieldDimension: (any) => Promise<void>,
-        deleteFieldDimension: (any) => Promise<void>,
-        fetchDatabaseIdfields: (number) => Promise<void>
+  saveStatus: null;
+
+  props: {
+    databaseId: number,
+    tableId: number,
+    fieldId: number,
+    metadata: Metadata,
+    idfields: Object[],
+
+    fetchDatabaseMetadata: number => Promise<void>,
+    fetchTableMetadata: number => Promise<void>,
+    fetchFieldValues: number => Promise<void>,
+    updateField: any => Promise<void>,
+    updateFieldValues: any => Promise<void>,
+    updateFieldDimension: any => Promise<void>,
+    deleteFieldDimension: any => Promise<void>,
+    fetchDatabaseIdfields: number => Promise<void>,
+  };
+
+  async componentWillMount() {
+    const {
+      databaseId,
+      tableId,
+      fieldId,
+      fetchDatabaseMetadata,
+      fetchTableMetadata,
+      fetchDatabaseIdfields,
+      fetchFieldValues,
+    } = this.props;
+
+    // A complete database metadata is needed in case that foreign key is changed
+    // and then we need to show FK remapping options for a new table
+    await fetchDatabaseMetadata(databaseId);
+
+    // Only fetchTableMetadata hydrates `dimension` in the field object
+    // Force reload to ensure that we are not showing stale information
+    await fetchTableMetadata(tableId, true);
+
+    // load field values if has_field_values === "list"
+    const field = this.props.metadata.field(fieldId);
+    if (field && field.has_field_values === "list") {
+      await fetchFieldValues(fieldId);
     }
 
-    async componentWillMount() {
-        const {databaseId, tableId, fetchDatabaseMetadata, fetchTableMetadata, fetchDatabaseIdfields} = this.props;
-
-        // A complete database metadata is needed in case that foreign key is changed
-        // and then we need to show FK remapping options for a new table
-        await fetchDatabaseMetadata(databaseId);
-
-        // Only fetchTableMetadata hydrates `dimension` in the field object
-        // Force reload to ensure that we are not showing stale information
-        await fetchTableMetadata(tableId, true);
-
-        // TODO Atte Keinänen 7/10/17: Migrate this to redux/metadata
-        await fetchDatabaseIdfields(databaseId);
+    // TODO Atte Keinänen 7/10/17: Migrate this to redux/metadata
+    await fetchDatabaseIdfields(databaseId);
+  }
+
+  linkWithSaveStatus = saveMethod => {
+    const self = this;
+    return async (...params) => {
+      self.saveStatus && self.saveStatus.setSaving();
+      await saveMethod(...params);
+      self.saveStatus && self.saveStatus.setSaved();
+    };
+  };
+
+  onUpdateField = this.linkWithSaveStatus(this.props.updateField);
+  onUpdateFieldProperties = this.linkWithSaveStatus(async fieldProps => {
+    const { metadata, fieldId } = this.props;
+    const field = metadata.fields[fieldId];
+
+    if (field) {
+      // `table` and `target` propertes is part of the fully connected metadata graph; drop it because it
+      // makes conversion to JSON impossible due to cyclical data structure
+      await this.props.updateField({
+        ...field.getPlainObject(),
+        ...fieldProps,
+      });
+    } else {
+      console.warn(
+        "Updating field properties in fields settings failed because of missing field metadata",
+      );
     }
+  });
+  onUpdateFieldValues = this.linkWithSaveStatus(this.props.updateFieldValues);
+  onUpdateFieldDimension = this.linkWithSaveStatus(
+    this.props.updateFieldDimension,
+  );
+  onDeleteFieldDimension = this.linkWithSaveStatus(
+    this.props.deleteFieldDimension,
+  );
+
+  render() {
+    const {
+      metadata,
+      fieldId,
+      databaseId,
+      tableId,
+      idfields,
+      fetchTableMetadata,
+    } = this.props;
+
+    const db = metadata.databases[databaseId];
+    const field = metadata.fields[fieldId];
+    const table = metadata.tables[tableId];
+
+    const isLoading = !field || !table || !idfields;
+
+    return (
+      <LoadingAndErrorWrapper loading={isLoading} error={null} noWrapper>
+        {() => (
+          <div className="relative">
+            <div className="wrapper wrapper--trim">
+              <BackButton databaseId={databaseId} tableId={tableId} />
+              <div className="my4 py1 ml-auto mr-auto">
+                <Breadcrumbs
+                  crumbs={[
+                    [db.name, `/admin/datamodel/database/${db.id}`],
+                    [
+                      table.display_name,
+                      `/admin/datamodel/database/${db.id}/table/${table.id}`,
+                    ],
+                    t`${field.display_name} – Field Settings`,
+                  ]}
+                />
+              </div>
+              <div className="absolute top right mt4 mr4">
+                <SaveStatus ref={ref => (this.saveStatus = ref)} />
+              </div>
+
+              <Section>
+                <FieldHeader
+                  field={field}
+                  updateFieldProperties={this.onUpdateFieldProperties}
+                  updateFieldDimension={this.onUpdateFieldDimension}
+                />
+              </Section>
 
-    linkWithSaveStatus = (saveMethod) => {
-        const self = this;
-        return async (...params) => {
-            self.saveStatus && self.saveStatus.setSaving();
-            await saveMethod(...params);
-            self.saveStatus && self.saveStatus.setSaved();
-        }
-    }
+              <Section>
+                <SectionHeader
+                  title={t`Visibility`}
+                  description={t`Where this field will appear throughout Metabase`}
+                />
+                <FieldVisibilityPicker
+                  triggerClasses={SelectClasses}
+                  field={field.getPlainObject()}
+                  updateField={this.onUpdateField}
+                />
+              </Section>
+
+              <Section>
+                <SectionHeader title={t`Type`} />
+                <SpecialTypeAndTargetPicker
+                  triggerClasses={SelectClasses}
+                  field={field.getPlainObject()}
+                  updateField={this.onUpdateField}
+                  idfields={idfields}
+                  selectSeparator={<SelectSeparator />}
+                />
+              </Section>
 
-    onUpdateField = this.linkWithSaveStatus(this.props.updateField)
-    onUpdateFieldProperties = this.linkWithSaveStatus(async (fieldProps) => {
-        const { metadata, fieldId } = this.props;
-        const field = metadata.fields[fieldId];
-
-        if (field) {
-            // `table` and `target` propertes is part of the fully connected metadata graph; drop it because it
-            // makes conversion to JSON impossible due to cyclical data structure
-            await this.props.updateField({ ...field.getPlainObject(), ...fieldProps });
-        } else {
-            console.warn("Updating field properties in fields settings failed because of missing field metadata")
-        }
-    })
-    onUpdateFieldValues = this.linkWithSaveStatus(this.props.updateFieldValues)
-    onUpdateFieldDimension = this.linkWithSaveStatus(this.props.updateFieldDimension)
-    onDeleteFieldDimension = this.linkWithSaveStatus(this.props.deleteFieldDimension)
-
-    render () {
-        const {
-            metadata,
-            fieldId,
-            databaseId,
-            tableId,
-            idfields,
-            fetchTableMetadata
-        } = this.props;
-
-        const db = metadata.databases[databaseId]
-        const field = metadata.fields[fieldId]
-        const table = metadata.tables[tableId]
-
-        const isLoading = !field || !table || !idfields
-
-        return (
-            <LoadingAndErrorWrapper loading={isLoading} error={null} noWrapper>
-                { () =>
-                    <div className="relative">
-                        <div className="wrapper wrapper--trim">
-                            <BackButton databaseId={databaseId} tableId={tableId} />
-                            <div className="my4 py1 ml-auto mr-auto">
-                                <Breadcrumbs
-                                    crumbs={[
-                                        [db.name, `/admin/datamodel/database/${db.id}`],
-                                        [table.display_name, `/admin/datamodel/database/${db.id}/table/${table.id}`],
-                                        t`${field.display_name} – Field Settings`,
-                                    ]}
-                                />
-                            </div>
-                            <div className="absolute top right mt4 mr4">
-                                <SaveStatus ref={(ref) => this.saveStatus = ref}/>
-                            </div>
-
-                            <Section>
-                                <FieldHeader
-                                    field={field}
-                                    updateFieldProperties={this.onUpdateFieldProperties}
-                                    updateFieldDimension={this.onUpdateFieldDimension}
-                                />
-                            </Section>
-
-                            <Section>
-                                <SectionHeader title={t`Visibility`}
-                                               description={t`Where this field will appear throughout Metabase`}/>
-                                <FieldVisibilityPicker
-                                    triggerClasses={SelectClasses}
-                                    field={field.getPlainObject()}
-                                    updateField={this.onUpdateField}
-                                />
-                            </Section>
-
-                            <Section>
-                                <SectionHeader title={t`Type`} />
-                                <SpecialTypeAndTargetPicker
-                                    triggerClasses={SelectClasses}
-                                    field={field.getPlainObject()}
-                                    updateField={this.onUpdateField}
-                                    idfields={idfields}
-                                    selectSeparator={<SelectSeparator />}
-                                />
-                            </Section>
-
-                            <Section>
-                                <FieldRemapping
-                                    field={field}
-                                    table={table}
-                                    fields={metadata.fields}
-                                    updateFieldProperties={this.onUpdateFieldProperties}
-                                    updateFieldValues={this.onUpdateFieldValues}
-                                    updateFieldDimension={this.onUpdateFieldDimension}
-                                    deleteFieldDimension={this.onDeleteFieldDimension}
-                                    fetchTableMetadata={fetchTableMetadata}
-                                />
-                            </Section>
-
-                            <Section>
-                                <UpdateCachedFieldValues
-                                    rescanFieldValues={() => this.props.rescanFieldValues(field.id)}
-                                    discardFieldValues={() => this.props.discardFieldValues(field.id)}
-                                />
-                            </Section>
-                        </div>
-                    </div>
-                }
-            </LoadingAndErrorWrapper>
-        )
-    }
+              <Section>
+                <SectionHeader
+                  title={t`Filtering on this field`}
+                  description={t`When this field is used in a filter, what should people use to enter the value they want to filter on?`}
+                />
+                <Select
+                  triggerClasses={SelectClasses}
+                  value={_.findWhere(HAS_FIELD_VALUES_OPTIONS, {
+                    value: field.has_field_values,
+                  })}
+                  onChange={option =>
+                    this.onUpdateFieldProperties({
+                      has_field_values: option.value,
+                    })
+                  }
+                  options={HAS_FIELD_VALUES_OPTIONS}
+                />
+              </Section>
+
+              <Section>
+                <FieldRemapping
+                  field={field}
+                  table={table}
+                  fields={metadata.fields}
+                  updateFieldProperties={this.onUpdateFieldProperties}
+                  updateFieldValues={this.onUpdateFieldValues}
+                  updateFieldDimension={this.onUpdateFieldDimension}
+                  deleteFieldDimension={this.onDeleteFieldDimension}
+                  fetchTableMetadata={fetchTableMetadata}
+                />
+              </Section>
+
+              <Section>
+                <UpdateCachedFieldValues
+                  rescanFieldValues={() =>
+                    this.props.rescanFieldValues(field.id)
+                  }
+                  discardFieldValues={() =>
+                    this.props.discardFieldValues(field.id)
+                  }
+                />
+              </Section>
+            </div>
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
 // TODO: Should this invoke goBack() instead?
 // not sure if it's possible to do that neatly with Link component
-export const BackButton = ({ databaseId, tableId }) =>
-    <Link
-        to={`/admin/datamodel/database/${databaseId}/table/${tableId}`}
-        className="circle text-white p2 mt3 ml3 flex align-center justify-center  absolute top left"
-        style={{ backgroundColor: '#8091AB' }}
-    >
-        <Icon name="backArrow" />
-    </Link>
-
-const SelectSeparator = () =>
-    <Icon
-        name="chevronright"
-        size={12}
-        className="mx2 text-grey-3"
-    />
+export const BackButton = ({ databaseId, tableId }) => (
+  <Link
+    to={`/admin/datamodel/database/${databaseId}/table/${tableId}`}
+    className="circle text-white p2 mt3 ml3 flex align-center justify-center  absolute top left"
+    style={{ backgroundColor: "#8091AB" }}
+  >
+    <Icon name="backArrow" />
+  </Link>
+);
+
+const SelectSeparator = () => (
+  <Icon name="chevronright" size={12} className="mx2 text-grey-3" />
+);
 
 export class FieldHeader extends Component {
-    onNameChange = (e) => {
-        this.updateNameDebounced(e.target.value);
-    }
-    onDescriptionChange = (e) => {
-        this.updateDescriptionDebounced(e.target.value)
+  onNameChange = e => {
+    this.updateNameDebounced(e.target.value);
+  };
+  onDescriptionChange = e => {
+    this.updateDescriptionDebounced(e.target.value);
+  };
+
+  // Separate update methods because of throttling the input
+  updateNameDebounced = _.debounce(async name => {
+    const { field, updateFieldProperties, updateFieldDimension } = this.props;
+
+    // Update the dimension name if it exists
+    // TODO: Have a separate input field for the dimension name?
+    if (!_.isEmpty(field.dimensions)) {
+      await updateFieldDimension(field.id, {
+        type: field.dimensions.type,
+        human_readable_field_id: field.dimensions.human_readable_field_id,
+        name,
+      });
     }
 
-    // Separate update methods because of throttling the input
-    updateNameDebounced = _.debounce(async (name) => {
-        const { field, updateFieldProperties, updateFieldDimension } = this.props;
-
-        // Update the dimension name if it exists
-        // TODO: Have a separate input field for the dimension name?
-        if (!_.isEmpty(field.dimensions)) {
-            await updateFieldDimension(field.id, {
-                type: field.dimensions.type,
-                human_readable_field_id: field.dimensions.human_readable_field_id,
-                name
-            })
-        }
-
-        // todo: how to treat empty / too long strings? see how this is done in Column
-        updateFieldProperties({ display_name: name })
-    }, 300)
-
-    updateDescriptionDebounced = _.debounce((description) => {
-        const { updateFieldProperties } = this.props
-        updateFieldProperties({ description })
-    }, 300);
-
-
-    render () {
-        return (
-            <div>
-                <Input
-                    className="h1 AdminInput bordered rounded border-dark block mb1"
-                    value={this.props.field.display_name}
-                    onChange={this.onNameChange}
-                    placeholder={this.props.field.name}
-                />
-                <Input
-                    className="text AdminInput bordered input text-measure block full"
-                    value={this.props.field.description}
-                    onChange={this.onDescriptionChange}
-                    placeholder={t`No description for this field yet`}
-                />
-            </div>
-        )
-    }
+    // todo: how to treat empty / too long strings? see how this is done in Column
+    updateFieldProperties({ display_name: name });
+  }, 300);
+
+  updateDescriptionDebounced = _.debounce(description => {
+    const { updateFieldProperties } = this.props;
+    updateFieldProperties({ description });
+  }, 300);
+
+  render() {
+    return (
+      <div>
+        <Input
+          className="h1 AdminInput bordered rounded border-dark block mb1"
+          value={this.props.field.display_name}
+          onChange={this.onNameChange}
+          placeholder={this.props.field.name}
+        />
+        <Input
+          className="text AdminInput bordered input text-measure block full"
+          value={this.props.field.description}
+          onChange={this.onDescriptionChange}
+          placeholder={t`No description for this field yet`}
+        />
+      </div>
+    );
+  }
 }
 
 // consider renaming this component to something more descriptive
 export class ValueRemappings extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        const editingRemappings = new Map([...props.remappings]
-            .map(([original, mappedOrUndefined]) => {
-                // Use currently the original value as the "default custom mapping" as the current backend implementation
-                // requires that all original values must have corresponding mappings
-
-                // Additionally, the defensive `.toString` ensures that the mapped value definitely will be string
-                const mappedString =
-                    mappedOrUndefined !== undefined ? mappedOrUndefined.toString() : original.toString();
-
-                return [original, mappedString]
-            })
-        )
-
-        const containsUnsetMappings = [...props.remappings].some(([_, mappedOrUndefined]) => {
-            return mappedOrUndefined === undefined;
-        })
-        if (containsUnsetMappings) {
-            // Save the initial values to make sure that we aren't left in a potentially broken state where
-            // the dimension type is "internal" but we don't have any values in metabase_fieldvalues
-            this.props.updateRemappings(editingRemappings);
-        }
-
-        this.state = {
-            editingRemappings
-        }
-    }
+  state = {
+    editingRemappings: new Map(),
+  };
 
-    onSetRemapping(original, newMapped) {
-        this.setState({
-            editingRemappings: new Map([
-                ...this.state.editingRemappings,
-                [original, newMapped]
-            ])
-        });
-    }
-
-    onSaveClick = () => {
-        MetabaseAnalytics.trackEvent("Data Model", "Update Custom Remappings");
-        // Returns the promise so that ButtonWithStatus can show the saving status
-        return this.props.updateRemappings(this.state.editingRemappings);
-    }
+  componentWillMount() {
+    this._updateEditingRemappings(this.props.remappings);
+  }
 
-    customValuesAreNonEmpty = () => {
-        return Array.from(this.state.editingRemappings.values())
-            .every((value) => value !== "")
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.remappings !== this.props.remappings) {
+      this._updateEditingRemappings(nextProps.remappings);
     }
-
-    render () {
-        const { editingRemappings } = this.state;
-
-        return (
-            <div className="bordered rounded py2 px4 border-dark">
-                <div className="flex align-center my1 pb2 border-bottom">
-                    <h3>{t`Original value`}</h3>
-                    <h3 className="ml-auto">{t`Mapped value`}</h3>
-                </div>
-                <ol>
-                    { [...editingRemappings].map(([original, mapped]) =>
-                        <li className="mb1">
-                            <FieldValueMapping
-                                original={original}
-                                mapped={mapped}
-                                setMapping={(newMapped) => this.onSetRemapping(original, newMapped) }
-                            />
-                        </li>
-                    )}
-                </ol>
-                <div className="flex align-center">
-                    <ButtonWithStatus
-                        className="ml-auto"
-                        disabled={!this.customValuesAreNonEmpty()}
-                        onClickOperation={this.onSaveClick}
-                    >
-                        {t`Save`}
-                    </ButtonWithStatus>
-                </div>
-            </div>
-        )
+  }
+
+  _updateEditingRemappings(remappings) {
+    const editingRemappings = new Map(
+      [...remappings].map(([original, mappedOrUndefined]) => {
+        // Use currently the original value as the "default custom mapping" as the current backend implementation
+        // requires that all original values must have corresponding mappings
+
+        // Additionally, the defensive `.toString` ensures that the mapped value definitely will be string
+        const mappedString =
+          mappedOrUndefined !== undefined
+            ? mappedOrUndefined.toString()
+            : original.toString();
+
+        return [original, mappedString];
+      }),
+    );
+
+    const containsUnsetMappings = [...remappings].some(
+      ([_, mappedOrUndefined]) => {
+        return mappedOrUndefined === undefined;
+      },
+    );
+    if (containsUnsetMappings) {
+      // Save the initial values to make sure that we aren't left in a potentially broken state where
+      // the dimension type is "internal" but we don't have any values in metabase_fieldvalues
+      this.props.updateRemappings(editingRemappings);
     }
+    this.setState({ editingRemappings });
+  }
+
+  onSetRemapping(original, newMapped) {
+    this.setState({
+      editingRemappings: new Map([
+        ...this.state.editingRemappings,
+        [original, newMapped],
+      ]),
+    });
+  }
+
+  onSaveClick = () => {
+    MetabaseAnalytics.trackEvent("Data Model", "Update Custom Remappings");
+    // Returns the promise so that ButtonWithStatus can show the saving status
+    return this.props.updateRemappings(this.state.editingRemappings);
+  };
+
+  customValuesAreNonEmpty = () => {
+    return Array.from(this.state.editingRemappings.values()).every(
+      value => value !== "",
+    );
+  };
+
+  render() {
+    const { editingRemappings } = this.state;
+
+    return (
+      <div className="bordered rounded py2 px4 border-dark">
+        <div className="flex align-center my1 pb2 border-bottom">
+          <h3>{t`Original value`}</h3>
+          <h3 className="ml-auto">{t`Mapped value`}</h3>
+        </div>
+        <ol>
+          {[...editingRemappings].map(([original, mapped]) => (
+            <li className="mb1">
+              <FieldValueMapping
+                original={original}
+                mapped={mapped}
+                setMapping={newMapped =>
+                  this.onSetRemapping(original, newMapped)
+                }
+              />
+            </li>
+          ))}
+        </ol>
+        <div className="flex align-center">
+          <ButtonWithStatus
+            className="ml-auto"
+            disabled={!this.customValuesAreNonEmpty()}
+            onClickOperation={this.onSaveClick}
+          >
+            {t`Save`}
+          </ButtonWithStatus>
+        </div>
+      </div>
+    );
+  }
 }
 
 export class FieldValueMapping extends Component {
-    onInputChange = (e) => {
-        this.props.setMapping(e.target.value)
-    }
-
-    render () {
-        const { original, mapped } = this.props
-        return (
-            <div className="flex align-center">
-                <h3>{original}</h3>
-                <Input
-                    className="AdminInput input ml-auto"
-                    value={mapped}
-                    onChange={this.onInputChange}
-                    placeholder={t`Enter value`}
-                />
-            </div>
-        )
-    }
+  onInputChange = e => {
+    this.props.setMapping(e.target.value);
+  };
+
+  render() {
+    const { original, mapped } = this.props;
+    return (
+      <div className="flex align-center">
+        <h3>{original}</h3>
+        <Input
+          className="AdminInput input ml-auto"
+          value={mapped}
+          onChange={this.onInputChange}
+          placeholder={t`Enter value`}
+        />
+      </div>
+    );
+  }
 }
 
-export const Section = ({ children }) => <section className="my3">{children}</section>
-
-export const SectionHeader = ({ title, description }) =>
-    <div className="border-bottom py2 mb2">
-        <h2 className="text-italic">{title}</h2>
-        { description && <p className="mb0 text-grey-4 mt1 text-paragraph text-measure">{description}</p> }
-    </div>
+export const Section = ({ children }) => (
+  <section className="my3">{children}</section>
+);
+
+export const SectionHeader = ({ title, description }) => (
+  <div className="border-bottom py2 mb2">
+    <h2 className="text-italic">{title}</h2>
+    {description && (
+      <p className="mb0 text-grey-4 mt1 text-paragraph text-measure">
+        {description}
+      </p>
+    )}
+  </div>
+);
 
 const MAP_OPTIONS = {
-    original: { type: "original", name: t`Use original value` },
-    foreign:  { type: "foreign", name: t`Use foreign key` },
-    custom:   { type: "custom", name: t`Custom mapping` }
-}
+  original: { type: "original", name: t`Use original value` },
+  foreign: { type: "foreign", name: t`Use foreign key` },
+  custom: { type: "custom", name: t`Custom mapping` },
+};
 
 export class FieldRemapping extends Component {
-    state = {
-        isChoosingInitialFkTarget: false,
-        dismissedInitialFkTargetPopover: false
-    }
-
-    constructor(props, context) {
-        super(props, context);
-    }
-
-    getMappingTypeForField = (field) => {
-        if (this.state.isChoosingInitialFkTarget) return MAP_OPTIONS.foreign;
-
-        if (_.isEmpty(field.dimensions)) return MAP_OPTIONS.original;
-        if (field.dimensions.type === "external") return MAP_OPTIONS.foreign;
-        if (field.dimensions.type === "internal") return MAP_OPTIONS.custom;
-
-        throw new Error(t`Unrecognized mapping type`);
-    }
-
-    getAvailableMappingTypes = () => {
-        const { field } = this.props;
-
-        const hasForeignKeys = field.special_type === "type/FK" && this.getForeignKeys().length > 0;
-
-        // Only show the "custom" option if we have some values that can be mapped to user-defined custom values
-        // (for a field without user-defined remappings, every key of `field.remappings` has value `undefined`)
-        const hasMappableNumeralValues =
-            field.remapping.size > 0 &&
-            [...field.remapping.keys()].every((key) => typeof key === "number" );
-
-        return [
-            MAP_OPTIONS.original,
-            ...(hasForeignKeys ? [MAP_OPTIONS.foreign] : []),
-            ...(hasMappableNumeralValues > 0 ? [MAP_OPTIONS.custom] : [])
-        ]
-    }
-
-    getFKTargetTableEntityNameOrNull = () => {
-        const fks = this.getForeignKeys()
-        const fkTargetFields = fks[0] && fks[0].dimensions.map((dim) => dim.field());
-
-        if (fkTargetFields) {
-            // TODO Atte Keinänen 7/11/17: Should there be `isName(field)` in Field.js?
-            const nameField = fkTargetFields.find((field) => field.special_type === "type/Name")
-            return nameField ? nameField.id : null;
-        } else {
-            throw new Error(t`Current field isn't a foreign key or FK target table metadata is missing`)
-        }
-    }
-
-    clearEditingStates = () => {
-        this.setState({ isChoosingInitialFkTarget: false, dismissedInitialFkTargetPopover: false });
-    }
-
-    onSetMappingType = async (mappingType) => {
-        const { table, field, fetchTableMetadata, updateFieldDimension, deleteFieldDimension } = this.props;
-
-        this.clearEditingStates();
-
-
-        if (mappingType.type === "original") {
-            MetabaseAnalytics.trackEvent("Data Model", "Change Remapping Type", "No Remapping");
-            await deleteFieldDimension(field.id)
-            this.setState({ hasChanged: false })
-        } else if (mappingType.type === "foreign") {
-            // Try to find a entity name field from target table and choose it as remapping target field if it exists
-            const entityNameFieldId = this.getFKTargetTableEntityNameOrNull();
-
-            if (entityNameFieldId) {
-                MetabaseAnalytics.trackEvent("Data Model", "Change Remapping Type", "Foreign Key");
-                await updateFieldDimension(field.id, {
-                    type: "external",
-                    name: field.display_name,
-                    human_readable_field_id: entityNameFieldId
-                })
-            } else {
-                // Enter a special state where we are choosing an initial value for FK target
-                this.setState({
-                    hasChanged: true,
-                    isChoosingInitialFkTarget: true
-                });
-            }
-
-        } else if (mappingType.type === "custom") {
-            MetabaseAnalytics.trackEvent("Data Model", "Change Remapping Type", "Custom Remappings");
-            await updateFieldDimension(field.id, {
-                type: "internal",
-                name: field.display_name,
-                human_readable_field_id: null
-            })
-            this.setState({ hasChanged: true })
-        } else {
-            throw new Error(t`Unrecognized mapping type`);
-        }
-
-        // TODO Atte Keinänen 7/11/17: It's a pretty heavy approach to reload the whole table after a single field
-        // has been updated; would be nicer to just fetch a single field. MetabaseApi.field_get seems to exist for that
-        await fetchTableMetadata(table.id, true);
+  state = {
+    isChoosingInitialFkTarget: false,
+    dismissedInitialFkTargetPopover: false,
+  };
+
+  constructor(props, context) {
+    super(props, context);
+  }
+
+  getMappingTypeForField = field => {
+    if (this.state.isChoosingInitialFkTarget) return MAP_OPTIONS.foreign;
+
+    if (_.isEmpty(field.dimensions)) return MAP_OPTIONS.original;
+    if (field.dimensions.type === "external") return MAP_OPTIONS.foreign;
+    if (field.dimensions.type === "internal") return MAP_OPTIONS.custom;
+
+    throw new Error(t`Unrecognized mapping type`);
+  };
+
+  getAvailableMappingTypes = () => {
+    const { field } = this.props;
+
+    const hasForeignKeys =
+      field.special_type === "type/FK" && this.getForeignKeys().length > 0;
+
+    // Only show the "custom" option if we have some values that can be mapped to user-defined custom values
+    // (for a field without user-defined remappings, every key of `field.remappings` has value `undefined`)
+    const hasMappableNumeralValues =
+      field.remapping.size > 0 &&
+      [...field.remapping.keys()].every(key => typeof key === "number");
+
+    return [
+      MAP_OPTIONS.original,
+      ...(hasForeignKeys ? [MAP_OPTIONS.foreign] : []),
+      ...(hasMappableNumeralValues > 0 ? [MAP_OPTIONS.custom] : []),
+    ];
+  };
+
+  getFKTargetTableEntityNameOrNull = () => {
+    const fks = this.getForeignKeys();
+    const fkTargetFields = fks[0] && fks[0].dimensions.map(dim => dim.field());
+
+    if (fkTargetFields) {
+      // TODO Atte Keinänen 7/11/17: Should there be `isName(field)` in Field.js?
+      const nameField = fkTargetFields.find(
+        field => field.special_type === "type/Name",
+      );
+      return nameField ? nameField.id : null;
+    } else {
+      throw new Error(
+        t`Current field isn't a foreign key or FK target table metadata is missing`,
+      );
     }
-
-    onForeignKeyFieldChange = async (foreignKeyClause) => {
-        const { table, field, fetchTableMetadata, updateFieldDimension } = this.props;
-
-        this.clearEditingStates();
-
-        // TODO Atte Keinänen 7/10/17: Use Dimension class when migrating to metabase-lib
-        if (foreignKeyClause.length === 3 && foreignKeyClause[0] === "fk->") {
-            MetabaseAnalytics.trackEvent("Data Model", "Update FK Remapping Target");
-            await updateFieldDimension(field.id, {
-                type: "external",
-                name: field.display_name,
-                human_readable_field_id: foreignKeyClause[2]
-            })
-
-            await fetchTableMetadata(table.id, true);
-
-            this.refs.fkPopover.close()
-        } else {
-            throw new Error(t`The selected field isn't a foreign key`)
-        }
-
+  };
+
+  clearEditingStates = () => {
+    this.setState({
+      isChoosingInitialFkTarget: false,
+      dismissedInitialFkTargetPopover: false,
+    });
+  };
+
+  onSetMappingType = async mappingType => {
+    const {
+      table,
+      field,
+      fetchTableMetadata,
+      updateFieldDimension,
+      deleteFieldDimension,
+    } = this.props;
+
+    this.clearEditingStates();
+
+    if (mappingType.type === "original") {
+      MetabaseAnalytics.trackEvent(
+        "Data Model",
+        "Change Remapping Type",
+        "No Remapping",
+      );
+      await deleteFieldDimension(field.id);
+      this.setState({ hasChanged: false });
+    } else if (mappingType.type === "foreign") {
+      // Try to find a entity name field from target table and choose it as remapping target field if it exists
+      const entityNameFieldId = this.getFKTargetTableEntityNameOrNull();
+
+      if (entityNameFieldId) {
+        MetabaseAnalytics.trackEvent(
+          "Data Model",
+          "Change Remapping Type",
+          "Foreign Key",
+        );
+        await updateFieldDimension(field.id, {
+          type: "external",
+          name: field.display_name,
+          human_readable_field_id: entityNameFieldId,
+        });
+      } else {
+        // Enter a special state where we are choosing an initial value for FK target
+        this.setState({
+          hasChanged: true,
+          isChoosingInitialFkTarget: true,
+        });
+      }
+    } else if (mappingType.type === "custom") {
+      MetabaseAnalytics.trackEvent(
+        "Data Model",
+        "Change Remapping Type",
+        "Custom Remappings",
+      );
+      await updateFieldDimension(field.id, {
+        type: "internal",
+        name: field.display_name,
+        human_readable_field_id: null,
+      });
+      this.setState({ hasChanged: true });
+    } else {
+      throw new Error(t`Unrecognized mapping type`);
     }
 
-    onUpdateRemappings = (remappings) => {
-        const { field, updateFieldValues } = this.props;
-        return updateFieldValues(field.id, Array.from(remappings));
+    // TODO Atte Keinänen 7/11/17: It's a pretty heavy approach to reload the whole table after a single field
+    // has been updated; would be nicer to just fetch a single field. MetabaseApi.field_get seems to exist for that
+    await fetchTableMetadata(table.id, true);
+  };
+
+  onForeignKeyFieldChange = async foreignKeyClause => {
+    const {
+      table,
+      field,
+      fetchTableMetadata,
+      updateFieldDimension,
+    } = this.props;
+
+    this.clearEditingStates();
+
+    // TODO Atte Keinänen 7/10/17: Use Dimension class when migrating to metabase-lib
+    if (foreignKeyClause.length === 3 && foreignKeyClause[0] === "fk->") {
+      MetabaseAnalytics.trackEvent("Data Model", "Update FK Remapping Target");
+      await updateFieldDimension(field.id, {
+        type: "external",
+        name: field.display_name,
+        human_readable_field_id: foreignKeyClause[2],
+      });
+
+      await fetchTableMetadata(table.id, true);
+
+      this.refs.fkPopover.close();
+    } else {
+      throw new Error(t`The selected field isn't a foreign key`);
     }
-
-    // TODO Atte Keinänen 7/11/17: Should we have stricter criteria for valid remapping targets?
-    isValidFKRemappingTarget = (dimension) => !(dimension.defaultDimension() instanceof DatetimeFieldDimension)
-
-    getForeignKeys = () => {
-        const { table, field } = this.props;
-
-        // this method has a little odd structure due to using fieldOptions(); basically filteredFKs should
-        // always be an array with a single value
-        const metadata = table.metadata;
-        const fieldOptions = Question.create({ metadata, databaseId: table.db.id, tableId: table.id }).query().fieldOptions();
-        const unfilteredFks = fieldOptions.fks
-        const filteredFKs = unfilteredFks.filter(fk => fk.field.id === field.id);
-
-        return filteredFKs.map(filteredFK => ({
-            field: filteredFK.field,
-            dimension: filteredFK.dimension,
-            dimensions: filteredFK.dimensions.filter(this.isValidFKRemappingTarget)
-        }));
-    }
-
-    onFkPopoverDismiss = () => {
-        const { isChoosingInitialFkTarget } = this.state;
-
-        if (isChoosingInitialFkTarget) {
-            this.setState({ dismissedInitialFkTargetPopover: true })
-        }
+  };
+
+  onUpdateRemappings = remappings => {
+    const { field, updateFieldValues } = this.props;
+    return updateFieldValues(field.id, Array.from(remappings));
+  };
+
+  // TODO Atte Keinänen 7/11/17: Should we have stricter criteria for valid remapping targets?
+  isValidFKRemappingTarget = dimension =>
+    !(dimension.defaultDimension() instanceof DatetimeFieldDimension);
+
+  getForeignKeys = () => {
+    const { table, field } = this.props;
+
+    // this method has a little odd structure due to using fieldOptions(); basically filteredFKs should
+    // always be an array with a single value
+    const metadata = table.metadata;
+    const fieldOptions = Question.create({
+      metadata,
+      databaseId: table.db.id,
+      tableId: table.id,
+    })
+      .query()
+      .fieldOptions();
+    const unfilteredFks = fieldOptions.fks;
+    const filteredFKs = unfilteredFks.filter(fk => fk.field.id === field.id);
+
+    return filteredFKs.map(filteredFK => ({
+      field: filteredFK.field,
+      dimension: filteredFK.dimension,
+      dimensions: filteredFK.dimensions.filter(this.isValidFKRemappingTarget),
+    }));
+  };
+
+  onFkPopoverDismiss = () => {
+    const { isChoosingInitialFkTarget } = this.state;
+
+    if (isChoosingInitialFkTarget) {
+      this.setState({ dismissedInitialFkTargetPopover: true });
     }
-
-    render () {
-        const { field, table, fields} = this.props;
-        const { isChoosingInitialFkTarget, hasChanged, dismissedInitialFkTargetPopover } = this.state;
-
-        const mappingType = this.getMappingTypeForField(field)
-        const isFKMapping = mappingType === MAP_OPTIONS.foreign;
-        const hasFKMappingValue = isFKMapping && field.dimensions.human_readable_field_id !== null;
-        const fkMappingField = hasFKMappingValue && fields[field.dimensions.human_readable_field_id];
-
-        return (
-            <div>
-                <SectionHeader
-                    title={t`Display values`}
-                    description={t`Choose to show the original value from the database, or have this field display associated or custom information.`}
-                />
-                <Select
-                    triggerClasses={SelectClasses}
-                    value={mappingType}
-                    onChange={this.onSetMappingType}
-                    options={this.getAvailableMappingTypes()}
-                />
-                { mappingType === MAP_OPTIONS.foreign && [
-                    <SelectSeparator key="foreignKeySeparator" />,
-                    <PopoverWithTrigger
-                        ref="fkPopover"
-                        triggerElement={
-                            <SelectButton
-                                hasValue={hasFKMappingValue}
-                                className={cx(
-                                    "flex inline-block no-decoration h3 p2 shadowed",
-                                    {
-                                        "border-error": dismissedInitialFkTargetPopover,
-                                        "border-dark": !dismissedInitialFkTargetPopover
-                                    }
-                                )}
-                            >
-                                {fkMappingField ? fkMappingField.display_name : <span className="text-grey-1">{t`Choose a field`}</span>}
-                            </SelectButton>
-                        }
-                        isInitiallyOpen={isChoosingInitialFkTarget}
-                        onClose={this.onFkPopoverDismiss}
-                    >
-                        <FieldList
-                            className="text-purple"
-                            field={fkMappingField}
-                            fieldOptions={{ count: 0, dimensions: [], fks: this.getForeignKeys() }}
-                            tableMetadata={table}
-                            onFieldChange={this.onForeignKeyFieldChange}
-                            hideSectionHeader
-                        />
-                    </PopoverWithTrigger>,
-                    dismissedInitialFkTargetPopover && <div className="text-danger my2">{t`Please select a column to use for display.`}</div>,
-                    hasChanged && hasFKMappingValue && <RemappingNamingTip />
-                ]}
-                { mappingType === MAP_OPTIONS.custom && (
-                    <div className="mt3">
-                        { hasChanged && <RemappingNamingTip /> }
-                        <ValueRemappings
-                            remappings={field && field.remapping}
-                            updateRemappings={this.onUpdateRemappings}
-                        />
-                    </div>
+  };
+
+  render() {
+    const { field, table, fields } = this.props;
+    const {
+      isChoosingInitialFkTarget,
+      hasChanged,
+      dismissedInitialFkTargetPopover,
+    } = this.state;
+
+    const mappingType = this.getMappingTypeForField(field);
+    const isFKMapping = mappingType === MAP_OPTIONS.foreign;
+    const hasFKMappingValue =
+      isFKMapping && field.dimensions.human_readable_field_id !== null;
+    const fkMappingField =
+      hasFKMappingValue && fields[field.dimensions.human_readable_field_id];
+
+    return (
+      <div>
+        <SectionHeader
+          title={t`Display values`}
+          description={t`Choose to show the original value from the database, or have this field display associated or custom information.`}
+        />
+        <Select
+          triggerClasses={SelectClasses}
+          value={mappingType}
+          onChange={this.onSetMappingType}
+          options={this.getAvailableMappingTypes()}
+        />
+        {mappingType === MAP_OPTIONS.foreign && [
+          <SelectSeparator key="foreignKeySeparator" />,
+          <PopoverWithTrigger
+            ref="fkPopover"
+            triggerElement={
+              <SelectButton
+                hasValue={hasFKMappingValue}
+                className={cx(
+                  "flex inline-block no-decoration h3 p2 shadowed",
+                  {
+                    "border-error": dismissedInitialFkTargetPopover,
+                    "border-dark": !dismissedInitialFkTargetPopover,
+                  },
                 )}
-            </div>
-        )
-    }
+              >
+                {fkMappingField ? (
+                  fkMappingField.display_name
+                ) : (
+                  <span className="text-grey-1">{t`Choose a field`}</span>
+                )}
+              </SelectButton>
+            }
+            isInitiallyOpen={isChoosingInitialFkTarget}
+            onClose={this.onFkPopoverDismiss}
+          >
+            <FieldList
+              className="text-purple"
+              field={fkMappingField}
+              fieldOptions={{
+                count: 0,
+                dimensions: [],
+                fks: this.getForeignKeys(),
+              }}
+              tableMetadata={table}
+              onFieldChange={this.onForeignKeyFieldChange}
+              hideSectionHeader
+            />
+          </PopoverWithTrigger>,
+          dismissedInitialFkTargetPopover && (
+            <div className="text-danger my2">{t`Please select a column to use for display.`}</div>
+          ),
+          hasChanged && hasFKMappingValue && <RemappingNamingTip />,
+        ]}
+        {mappingType === MAP_OPTIONS.custom && (
+          <div className="mt3">
+            {hasChanged && <RemappingNamingTip />}
+            <ValueRemappings
+              remappings={field && field.remapping}
+              updateRemappings={this.onUpdateRemappings}
+            />
+          </div>
+        )}
+      </div>
+    );
+  }
 }
 
-export const RemappingNamingTip = () =>
-    <div className="bordered rounded p1 mt1 mb2 border-brand">
-        <span className="text-brand text-bold">{t`Tip:`}</span>
-        {t`You might want to update the field name to make sure it still makes sense based on your remapping choices.`}
-    </div>
-
+export const RemappingNamingTip = () => (
+  <div className="bordered rounded p1 mt1 mb2 border-brand">
+    <span className="text-brand text-bold">{t`Tip:`}</span>
+    {t`You might want to update the field name to make sure it still makes sense based on your remapping choices.`}
+  </div>
+);
 
 export class UpdateCachedFieldValues extends Component {
-    render () {
-        return (
-            <div>
-                <SectionHeader
-                    title={t`Cached field values`}
-                    description={t`Metabase can scan the values for this field to enable checkbox filters in dashboards and questions.`}
-                />
-                <ActionButton
-                    className="Button mr2"
-                    actionFn={this.props.rescanFieldValues}
-                    normalText={t`Re-scan this field`}
-                    activeText={t`Starting…`}
-                    failedText={t`Failed to start scan`}
-                    successText={t`Scan triggered!`}
-                />
-                <ActionButton
-                    className="Button Button--danger"
-                    actionFn={this.props.discardFieldValues}
-                    normalText={t`Discard cached field values`}
-                    activeText={t`Starting…`}
-                    failedText={t`Failed to discard values`}
-                    successText={t`Discard triggered!`}
-                />
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div>
+        <SectionHeader
+          title={t`Cached field values`}
+          description={t`Metabase can scan the values for this field to enable checkbox filters in dashboards and questions.`}
+        />
+        <ActionButton
+          className="Button mr2"
+          actionFn={this.props.rescanFieldValues}
+          normalText={t`Re-scan this field`}
+          activeText={t`Starting…`}
+          failedText={t`Failed to start scan`}
+          successText={t`Scan triggered!`}
+        />
+        <ActionButton
+          className="Button Button--danger"
+          actionFn={this.props.discardFieldValues}
+          normalText={t`Discard cached field values`}
+          activeText={t`Starting…`}
+          failedText={t`Failed to discard values`}
+          successText={t`Discard triggered!`}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
index 64fa9dfbe98b6af59a68ce45efde9fe12d0dbf04..d3bbf25846e088142dc3ae04053b4c890d846ccf 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetadataEditorApp.jsx
@@ -3,117 +3,138 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 import AdminEmptyText from "metabase/components/AdminEmptyText.jsx";
-import MetadataHeader from '../components/database/MetadataHeader.jsx';
-import MetadataTablePicker from '../components/database/MetadataTablePicker.jsx';
-import MetadataTable from '../components/database/MetadataTable.jsx';
-import MetadataSchema from '../components/database/MetadataSchema.jsx';
+import MetadataHeader from "../components/database/MetadataHeader.jsx";
+import MetadataTablePicker from "../components/database/MetadataTablePicker.jsx";
+import MetadataTable from "../components/database/MetadataTable.jsx";
+import MetadataSchema from "../components/database/MetadataSchema.jsx";
 
 import {
-    getDatabases,
-    getDatabaseIdfields,
-    getEditingDatabaseWithTableMetadataStrengths,
-    getEditingTable
+  getDatabases,
+  getDatabaseIdfields,
+  getEditingDatabaseWithTableMetadataStrengths,
+  getEditingTable,
 } from "../selectors";
 import * as metadataActions from "../datamodel";
 
 const mapStateToProps = (state, props) => {
-    return {
-        databaseId:           parseInt(props.params.databaseId),
-        tableId:              parseInt(props.params.tableId),
-        databases:            getDatabases(state, props),
-        idfields:             getDatabaseIdfields(state, props),
-        databaseMetadata:     getEditingDatabaseWithTableMetadataStrengths(state, props),
-        editingTable:         getEditingTable(state, props)
-    }
-}
+  return {
+    databaseId: parseInt(props.params.databaseId),
+    tableId: parseInt(props.params.tableId),
+    databases: getDatabases(state, props),
+    idfields: getDatabaseIdfields(state, props),
+    databaseMetadata: getEditingDatabaseWithTableMetadataStrengths(
+      state,
+      props,
+    ),
+    editingTable: getEditingTable(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    ...metadataActions,
-}
+  ...metadataActions,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetadataEditor extends Component {
+  constructor(props, context) {
+    super(props, context);
+    this.toggleShowSchema = this.toggleShowSchema.bind(this);
 
-    constructor(props, context) {
-        super(props, context);
-        this.toggleShowSchema = this.toggleShowSchema.bind(this);
-
-        this.state = {
-            isShowingSchema: false
-        };
-    }
-
-    static propTypes = {
-        databaseId: PropTypes.number,
-        tableId: PropTypes.number,
-        databases: PropTypes.array.isRequired,
-        selectDatabase: PropTypes.func.isRequired,
-        databaseMetadata: PropTypes.object,
-        selectTable: PropTypes.func.isRequired,
-        idfields: PropTypes.array.isRequired,
-        editingTable: PropTypes.number,
-        updateTable: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired
+    this.state = {
+      isShowingSchema: false,
     };
+  }
 
-    componentWillMount() {
-        // if we know what database we are initialized with, include that
-        this.props.initializeMetadata(this.props.databaseId, this.props.tableId);
-    }
+  static propTypes = {
+    databaseId: PropTypes.number,
+    tableId: PropTypes.number,
+    databases: PropTypes.array.isRequired,
+    selectDatabase: PropTypes.func.isRequired,
+    databaseMetadata: PropTypes.object,
+    selectTable: PropTypes.func.isRequired,
+    idfields: PropTypes.array.isRequired,
+    editingTable: PropTypes.number,
+    updateTable: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+  };
 
-    toggleShowSchema() {
-        this.setState({ isShowingSchema: !this.state.isShowingSchema });
-        MetabaseAnalytics.trackEvent("Data Model", "Show OG Schema", !this.state.isShowingSchema);
-    }
+  componentWillMount() {
+    // if we know what database we are initialized with, include that
+    this.props.initializeMetadata(this.props.databaseId, this.props.tableId);
+  }
 
-    render() {
-        var tableMetadata = (this.props.databaseMetadata) ? _.findWhere(this.props.databaseMetadata.tables, {id: this.props.editingTable}) : null;
-        var content;
-        if (tableMetadata) {
-            if (this.state.isShowingSchema) {
-                content = (<MetadataSchema tableMetadata={tableMetadata} />);
-            } else {
-                content = (
-                    <MetadataTable
-                        tableMetadata={tableMetadata}
-                        idfields={this.props.idfields}
-                        updateTable={(table) => this.props.updateTable(table)}
-                        updateField={(field) => this.props.updateField(field)}
-                        onRetireSegment={this.props.onRetireSegment}
-                        onRetireMetric={this.props.onRetireMetric}
-                    />
-                );
-            }
-        } else {
-            content = (
-                <div style={{paddingTop: "10rem"}} className="full text-centered">
-                    <AdminEmptyText message={t`Select any table to see its schema and add or edit metadata.`} />
-                </div>
-            );
-        }
-        return (
-            <div className="p3">
-                <MetadataHeader
-                    ref="header"
-                    databaseId={this.props.databaseMetadata ? this.props.databaseMetadata.id : null}
-                    databases={this.props.databases}
-                    selectDatabase={this.props.selectDatabase}
-                    isShowingSchema={this.state.isShowingSchema}
-                    toggleShowSchema={this.toggleShowSchema}
-                />
-              <div style={{minHeight: "60vh"}} className="flex flex-row flex-full mt2 full-height">
-                    <MetadataTablePicker
-                        tableId={this.props.editingTable}
-                        tables={(this.props.databaseMetadata) ? this.props.databaseMetadata.tables : []}
-                        selectTable={this.props.selectTable}
-                    />
-                    {content}
-                </div>
-            </div>
+  toggleShowSchema() {
+    this.setState({ isShowingSchema: !this.state.isShowingSchema });
+    MetabaseAnalytics.trackEvent(
+      "Data Model",
+      "Show OG Schema",
+      !this.state.isShowingSchema,
+    );
+  }
+
+  render() {
+    var tableMetadata = this.props.databaseMetadata
+      ? _.findWhere(this.props.databaseMetadata.tables, {
+          id: this.props.editingTable,
+        })
+      : null;
+    var content;
+    if (tableMetadata) {
+      if (this.state.isShowingSchema) {
+        content = <MetadataSchema tableMetadata={tableMetadata} />;
+      } else {
+        content = (
+          <MetadataTable
+            tableMetadata={tableMetadata}
+            idfields={this.props.idfields}
+            updateTable={table => this.props.updateTable(table)}
+            updateField={field => this.props.updateField(field)}
+            onRetireSegment={this.props.onRetireSegment}
+            onRetireMetric={this.props.onRetireMetric}
+          />
         );
+      }
+    } else {
+      content = (
+        <div style={{ paddingTop: "10rem" }} className="full text-centered">
+          <AdminEmptyText
+            message={t`Select any table to see its schema and add or edit metadata.`}
+          />
+        </div>
+      );
     }
+    return (
+      <div className="p3">
+        <MetadataHeader
+          ref="header"
+          databaseId={
+            this.props.databaseMetadata ? this.props.databaseMetadata.id : null
+          }
+          databases={this.props.databases}
+          selectDatabase={this.props.selectDatabase}
+          isShowingSchema={this.state.isShowingSchema}
+          toggleShowSchema={this.toggleShowSchema}
+        />
+        <div
+          style={{ minHeight: "60vh" }}
+          className="flex flex-row flex-full mt2 full-height"
+        >
+          <MetadataTablePicker
+            tableId={this.props.editingTable}
+            tables={
+              this.props.databaseMetadata
+                ? this.props.databaseMetadata.tables
+                : []
+            }
+            selectTable={this.props.selectTable}
+          />
+          {content}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
index af8128a08855971fafc935681871233485d9de90..57d46b7677c7214e01cf1c036b74f5eeab6fc280 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetricApp.jsx
@@ -13,61 +13,63 @@ import { fetchTableMetadata } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
 
 const mapDispatchToProps = {
-    ...actions,
-    fetchTableMetadata,
-    clearRequestState,
-    onChangeLocation: push,
+  ...actions,
+  fetchTableMetadata,
+  clearRequestState,
+  onChangeLocation: push,
 };
 
 const mapStateToProps = (state, props) => ({
-    ...metricEditSelectors(state, props),
-    metadata: getMetadata(state, props)
-})
+  ...metricEditSelectors(state, props),
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricApp extends Component {
-    async componentWillMount() {
-        const { params, location } = this.props;
+  async componentWillMount() {
+    const { params, location } = this.props;
 
-        let tableId;
-        if (params.id) {
-            const metricId = parseInt(params.id);
-            const { payload: metric } = await this.props.getMetric({ metricId });
-            tableId = metric.table_id;
-        } else if (location.query.table) {
-            tableId = parseInt(location.query.table);
-        }
-
-        if (tableId != null) {
-            // TODO Atte Keinänen 6/8/17: Use only global metadata (`fetchTableMetadata`)
-            this.props.loadTableMetadata(tableId);
-            this.props.fetchTableMetadata(tableId);
-        }
+    let tableId;
+    if (params.id) {
+      const metricId = parseInt(params.id);
+      const { payload: metric } = await this.props.getMetric({ metricId });
+      tableId = metric.table_id;
+    } else if (location.query.table) {
+      tableId = parseInt(location.query.table);
     }
 
-    async onSubmit(metric, f) {
-        let { tableMetadata } = this.props;
-        if (metric.id != null) {
-            await this.props.updateMetric(metric);
-            this.props.clearRequestState({statePath: ['metadata', 'metrics']});
-            MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
-        } else {
-            await this.props.createMetric(metric);
-            this.props.clearRequestState({statePath: ['metadata', 'metrics']});
-            MetabaseAnalytics.trackEvent("Data Model", "Metric Created");
-        }
-
-        this.props.onChangeLocation("/admin/datamodel/database/" + tableMetadata.db_id + "/table/" + tableMetadata.id);
+    if (tableId != null) {
+      // TODO Atte Keinänen 6/8/17: Use only global metadata (`fetchTableMetadata`)
+      this.props.loadTableMetadata(tableId);
+      this.props.fetchTableMetadata(tableId);
     }
+  }
 
-    render() {
-        return (
-            <div>
-                <MetricForm
-                    {...this.props}
-                    onSubmit={this.onSubmit.bind(this)}
-                />
-            </div>
-        );
+  async onSubmit(metric, f) {
+    let { tableMetadata } = this.props;
+    if (metric.id != null) {
+      await this.props.updateMetric(metric);
+      this.props.clearRequestState({ statePath: ["metadata", "metrics"] });
+      MetabaseAnalytics.trackEvent("Data Model", "Metric Updated");
+    } else {
+      await this.props.createMetric(metric);
+      this.props.clearRequestState({ statePath: ["metadata", "metrics"] });
+      MetabaseAnalytics.trackEvent("Data Model", "Metric Created");
     }
+
+    this.props.onChangeLocation(
+      "/admin/datamodel/database/" +
+        tableMetadata.db_id +
+        "/table/" +
+        tableMetadata.id,
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        <MetricForm {...this.props} onSubmit={this.onSubmit.bind(this)} />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
index 1e4dea5075ba32b71d122d26fbf654a686a5f815..c9c32f4598307c3f6085120648746e859e1d2a15 100644
--- a/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/MetricForm.jsx
@@ -7,7 +7,7 @@ import FormTextArea from "../components/FormTextArea.jsx";
 import FieldSet from "metabase/components/FieldSet.jsx";
 import PartialQueryBuilder from "../components/PartialQueryBuilder.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { formatValue } from "metabase/lib/formatting";
 
 import { metricFormSelectors } from "../selectors";
@@ -19,134 +19,184 @@ import cx from "classnames";
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 import Table from "metabase-lib/lib/metadata/Table";
 
-@reduxForm({
+@reduxForm(
+  {
     form: "metric",
-    fields: ["id", "name", "description", "table_id", "definition", "revision_message", "show_in_getting_started"],
-    validate: (values) => {
-        const errors = {};
-        if (!values.name) {
-            errors.name = t`Name is required`;
+    fields: [
+      "id",
+      "name",
+      "description",
+      "table_id",
+      "definition",
+      "revision_message",
+      "show_in_getting_started",
+    ],
+    validate: values => {
+      const errors = {};
+      if (!values.name) {
+        errors.name = t`Name is required`;
+      }
+      if (!values.description) {
+        errors.description = t`Description is required`;
+      }
+      if (values.id != null) {
+        if (!values.revision_message) {
+          errors.revision_message = t`Revision message is required`;
         }
-        if (!values.description) {
-            errors.description = t`Description is required`;
-        }
-        if (values.id != null) {
-            if (!values.revision_message) {
-                errors.revision_message = t`Revision message is required`;
-            }
-        }
-        let aggregations = values.definition && Query.getAggregations(values.definition);
-        if (!aggregations || aggregations.length === 0) {
-            errors.definition = t`Aggregation is required`;
-        }
-        return errors;
-    }
-},
-(state, props) => metricFormSelectors(state, props))
+      }
+      let aggregations =
+        values.definition && Query.getAggregations(values.definition);
+      if (!aggregations || aggregations.length === 0) {
+        errors.definition = t`Aggregation is required`;
+      }
+      return errors;
+    },
+  },
+  (state, props) => metricFormSelectors(state, props),
+)
 export default class MetricForm extends Component {
-    updatePreviewSummary(datasetQuery) {
-        this.props.updatePreviewSummary({
-            ...datasetQuery,
-            query: {
-                aggregation: ["count"],
-                ...datasetQuery.query,
-            }
-        })
-    }
-
-    renderActionButtons() {
-        const { invalid, handleSubmit, tableMetadata } = this.props;
-        return (
-            <div>
-                <button className={cx("Button", { "Button--primary": !invalid, "disabled": invalid })} onClick={handleSubmit}>{t`Save changes`}</button>
-                <Link to={"/admin/datamodel/database/" + tableMetadata.db_id + "/table/" + tableMetadata.id} className="Button ml2">{t`Cancel`}</Link>
-            </div>
-        )
-    }
+  updatePreviewSummary(datasetQuery) {
+    this.props.updatePreviewSummary({
+      ...datasetQuery,
+      query: {
+        aggregation: ["count"],
+        ...datasetQuery.query,
+      },
+    });
+  }
 
+  renderActionButtons() {
+    const { invalid, handleSubmit, tableMetadata } = this.props;
+    return (
+      <div>
+        <button
+          className={cx("Button", {
+            "Button--primary": !invalid,
+            disabled: invalid,
+          })}
+          onClick={handleSubmit}
+        >{t`Save changes`}</button>
+        <Link
+          to={
+            "/admin/datamodel/database/" +
+            tableMetadata.db_id +
+            "/table/" +
+            tableMetadata.id
+          }
+          className="Button ml2"
+        >{t`Cancel`}</Link>
+      </div>
+    );
+  }
 
-    render() {
-        const { fields: { id, name, description, definition, revision_message }, metric, metadata, tableMetadata, handleSubmit, previewSummary } = this.props;
+  render() {
+    const {
+      fields: { id, name, description, definition, revision_message },
+      metric,
+      metadata,
+      tableMetadata,
+      handleSubmit,
+      previewSummary,
+    } = this.props;
 
-        return (
-            <LoadingAndErrorWrapper loading={!tableMetadata}>
-            { () =>
-                <form className="full" onSubmit={handleSubmit}>
-                    <div className="wrapper py4">
-                        <FormLabel
-                            title={(metric && metric.id != null ? t`Edit Your Metric` : t`Create Your Metric`)}
-                            description={metric && metric.id != null ?
-                                t`Make changes to your metric and leave an explanatory note.` :
-                                t`You can create saved metrics to add a named metric option to this table. Saved metrics include the aggregation type, the aggregated field, and optionally any filter you add. As an example, you might use this to create something like the official way of calculating "Average Price" for an Orders table.`
-                            }
-                        >
-                        <PartialQueryBuilder
-                            features={{
-                                filter: true,
-                                aggregation: true
-                            }}
-                            metadata={
-                                metadata && tableMetadata && metadata.tables && metadata.tables[tableMetadata.id].fields && Object.assign(new Metadata(), metadata, {
-                                    tables: {
-                                        ...metadata.tables,
-                                        [tableMetadata.id]: Object.assign(new Table(), metadata.tables[tableMetadata.id], {
-                                            aggregation_options: tableMetadata.aggregation_options.filter(a => a.short !== "rows"),
-                                            metrics: []
-                                        })
-                                    }
-                                })
-                            }
-                            tableMetadata={tableMetadata}
-                            previewSummary={previewSummary == null ? "" : t`Result: ` + formatValue(previewSummary)}
-                            updatePreviewSummary={this.updatePreviewSummary.bind(this)}
-                            {...definition}
-                        />
-                        </FormLabel>
-                        <div style={{ maxWidth: "575px" }}>
-                            <FormLabel
-                                title={t`Name Your Metric`}
-                                description={t`Give your metric a name to help others find it.`}
-                            >
-                                <FormInput
-                                    field={name}
-                                    placeholder={t`Something descriptive but not too long`}
-                                />
-                            </FormLabel>
-                            <FormLabel
-                                title={t`Describe Your Metric`}
-                                description={t`Give your metric a description to help others understand what it's about.`}
-                            >
-                                <FormTextArea
-                                    field={description}
-                                    placeholder={t`This is a good place to be more specific about less obvious metric rules`}
-                                />
-                            </FormLabel>
-                            { id.value != null &&
-                                <FieldSet legend={t`Reason For Changes`}>
-                                    <FormLabel description={t`Leave a note to explain what changes you made and why they were required.`}>
-                                        <FormTextArea
-                                            field={revision_message}
-                                            placeholder={t`This will show up in the revision history for this metric to help everyone remember why things changed`}
-                                        />
-                                    </FormLabel>
-                                    <div className="flex align-center">
-                                        {this.renderActionButtons()}
-                                    </div>
-                                </FieldSet>
-                            }
-                        </div>
+    return (
+      <LoadingAndErrorWrapper loading={!tableMetadata}>
+        {() => (
+          <form className="full" onSubmit={handleSubmit}>
+            <div className="wrapper py4">
+              <FormLabel
+                title={
+                  metric && metric.id != null
+                    ? t`Edit Your Metric`
+                    : t`Create Your Metric`
+                }
+                description={
+                  metric && metric.id != null
+                    ? t`Make changes to your metric and leave an explanatory note.`
+                    : t`You can create saved metrics to add a named metric option to this table. Saved metrics include the aggregation type, the aggregated field, and optionally any filter you add. As an example, you might use this to create something like the official way of calculating "Average Price" for an Orders table.`
+                }
+              >
+                <PartialQueryBuilder
+                  features={{
+                    filter: true,
+                    aggregation: true,
+                  }}
+                  metadata={
+                    metadata &&
+                    tableMetadata &&
+                    metadata.tables &&
+                    metadata.tables[tableMetadata.id].fields &&
+                    Object.assign(new Metadata(), metadata, {
+                      tables: {
+                        ...metadata.tables,
+                        [tableMetadata.id]: Object.assign(
+                          new Table(),
+                          metadata.tables[tableMetadata.id],
+                          {
+                            aggregation_options: tableMetadata.aggregation_options.filter(
+                              a => a.short !== "rows",
+                            ),
+                            metrics: [],
+                          },
+                        ),
+                      },
+                    })
+                  }
+                  tableMetadata={tableMetadata}
+                  previewSummary={
+                    previewSummary == null
+                      ? ""
+                      : t`Result: ` + formatValue(previewSummary)
+                  }
+                  updatePreviewSummary={this.updatePreviewSummary.bind(this)}
+                  {...definition}
+                />
+              </FormLabel>
+              <div style={{ maxWidth: "575px" }}>
+                <FormLabel
+                  title={t`Name Your Metric`}
+                  description={t`Give your metric a name to help others find it.`}
+                >
+                  <FormInput
+                    field={name}
+                    placeholder={t`Something descriptive but not too long`}
+                  />
+                </FormLabel>
+                <FormLabel
+                  title={t`Describe Your Metric`}
+                  description={t`Give your metric a description to help others understand what it's about.`}
+                >
+                  <FormTextArea
+                    field={description}
+                    placeholder={t`This is a good place to be more specific about less obvious metric rules`}
+                  />
+                </FormLabel>
+                {id.value != null && (
+                  <FieldSet legend={t`Reason For Changes`}>
+                    <FormLabel
+                      description={t`Leave a note to explain what changes you made and why they were required.`}
+                    >
+                      <FormTextArea
+                        field={revision_message}
+                        placeholder={t`This will show up in the revision history for this metric to help everyone remember why things changed`}
+                      />
+                    </FormLabel>
+                    <div className="flex align-center">
+                      {this.renderActionButtons()}
                     </div>
+                  </FieldSet>
+                )}
+              </div>
+            </div>
 
-                    { id.value == null &&
-                        <div className="border-top py4">
-                            <div className="wrapper">
-                                {this.renderActionButtons()}
-                            </div>
-                        </div>
-                    }
-                </form>
-            }
-            </LoadingAndErrorWrapper>
-        );
-    }
+            {id.value == null && (
+              <div className="border-top py4">
+                <div className="wrapper">{this.renderActionButtons()}</div>
+              </div>
+            )}
+          </form>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx b/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx
index bf4043b2a553838b40ebf49b8466f27d64e5664f..b644b3fd5de26e84da524dc9d118b52b6c41e397 100644
--- a/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/RevisionHistoryApp.jsx
@@ -7,27 +7,25 @@ import { revisionHistorySelectors } from "../selectors";
 import * as actions from "../datamodel";
 
 const mapStateToProps = (state, props) => {
-    return {
-        ...revisionHistorySelectors(state, props),
-        entity: props.params.entity,
-        id:     props.params.id
-    }
-}
+  return {
+    ...revisionHistorySelectors(state, props),
+    entity: props.params.entity,
+    id: props.params.id,
+  };
+};
 
 const mapDispatchToProps = {
-    ...actions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class RevisionHistoryApp extends Component {
-    componentWillMount() {
-        let { entity, id } = this.props;
+  componentWillMount() {
+    let { entity, id } = this.props;
 
-        this.props.fetchRevisions({ entity, id })
-    }
-    render() {
-        return (
-            <RevisionHistory {...this.props} objectType={this.props.entity} />
-        );
-    }
+    this.props.fetchRevisions({ entity, id });
+  }
+  render() {
+    return <RevisionHistory {...this.props} objectType={this.props.entity} />;
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
index 778c0d8e36bda4b8101ac631fc8b55550de06954..f380ee49530cbc94a6f83c372ff0c1a260e104c9 100644
--- a/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/SegmentApp.jsx
@@ -13,61 +13,63 @@ import { getMetadata } from "metabase/selectors/metadata";
 import { fetchTableMetadata } from "metabase/redux/metadata";
 
 const mapDispatchToProps = {
-    ...actions,
-    fetchTableMetadata,
-    clearRequestState,
-    onChangeLocation: push
+  ...actions,
+  fetchTableMetadata,
+  clearRequestState,
+  onChangeLocation: push,
 };
 
 const mapStateToProps = (state, props) => ({
-    ...segmentEditSelectors(state, props),
-    metadata: getMetadata(state, props)
-})
+  ...segmentEditSelectors(state, props),
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentApp extends Component {
-    async componentWillMount() {
-        const { params, location } = this.props;
+  async componentWillMount() {
+    const { params, location } = this.props;
 
-        let tableId;
-        if (params.id) {
-            const segmentId = parseInt(params.id);
-            const { payload: segment } = await this.props.getSegment({ segmentId });
-            tableId = segment.table_id;
-        } else if (location.query.table) {
-            tableId = parseInt(location.query.table);
-        }
-
-        if (tableId != null) {
-            // TODO Atte Keinänen 6/8/17: Use only global metadata (`fetchTableMetadata`)
-            this.props.loadTableMetadata(tableId);
-            this.props.fetchTableMetadata(tableId);
-        }
+    let tableId;
+    if (params.id) {
+      const segmentId = parseInt(params.id);
+      const { payload: segment } = await this.props.getSegment({ segmentId });
+      tableId = segment.table_id;
+    } else if (location.query.table) {
+      tableId = parseInt(location.query.table);
     }
 
-    async onSubmit(segment, f) {
-        let { tableMetadata } = this.props;
-        if (segment.id != null) {
-            await this.props.updateSegment(segment);
-            this.props.clearRequestState({statePath: ['metadata', 'segments']});
-            MetabaseAnalytics.trackEvent("Data Model", "Segment Updated");
-        } else {
-            await this.props.createSegment(segment);
-            this.props.clearRequestState({statePath: ['metadata', 'segments']});
-            MetabaseAnalytics.trackEvent("Data Model", "Segment Created");
-        }
-
-        this.props.onChangeLocation("/admin/datamodel/database/" + tableMetadata.db_id + "/table/" + tableMetadata.id);
+    if (tableId != null) {
+      // TODO Atte Keinänen 6/8/17: Use only global metadata (`fetchTableMetadata`)
+      this.props.loadTableMetadata(tableId);
+      this.props.fetchTableMetadata(tableId);
     }
+  }
 
-    render() {
-        return (
-            <div>
-                <SegmentForm
-                    {...this.props}
-                    onSubmit={this.onSubmit.bind(this)}
-                />
-            </div>
-        );
+  async onSubmit(segment, f) {
+    let { tableMetadata } = this.props;
+    if (segment.id != null) {
+      await this.props.updateSegment(segment);
+      this.props.clearRequestState({ statePath: ["metadata", "segments"] });
+      MetabaseAnalytics.trackEvent("Data Model", "Segment Updated");
+    } else {
+      await this.props.createSegment(segment);
+      this.props.clearRequestState({ statePath: ["metadata", "segments"] });
+      MetabaseAnalytics.trackEvent("Data Model", "Segment Created");
     }
+
+    this.props.onChangeLocation(
+      "/admin/datamodel/database/" +
+        tableMetadata.db_id +
+        "/table/" +
+        tableMetadata.id,
+    );
+  }
+
+  render() {
+    return (
+      <div>
+        <SegmentForm {...this.props} onSubmit={this.onSubmit.bind(this)} />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
index f6e870d6d9835d50386850c821c11c4be275780b..42d18296299b8ab87309d4207dfb4c9ddb242f76 100644
--- a/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/SegmentForm.jsx
@@ -7,7 +7,7 @@ import FormTextArea from "../components/FormTextArea.jsx";
 import FieldSet from "metabase/components/FieldSet.jsx";
 import PartialQueryBuilder from "../components/PartialQueryBuilder.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { formatValue } from "metabase/lib/formatting";
 
 import { segmentFormSelectors } from "../selectors";
@@ -17,131 +17,190 @@ import cx from "classnames";
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 import Table from "metabase-lib/lib/metadata/Table";
 
-@reduxForm({
+@reduxForm(
+  {
     form: "segment",
-    fields: ["id", "name", "description", "table_id", "definition", "revision_message"],
-    validate: (values) => {
-        const errors = {};
-        if (!values.name) {
-            errors.name = t`Name is required`;
+    fields: [
+      "id",
+      "name",
+      "description",
+      "table_id",
+      "definition",
+      "revision_message",
+    ],
+    validate: values => {
+      const errors = {};
+      if (!values.name) {
+        errors.name = t`Name is required`;
+      }
+      if (!values.description) {
+        errors.description = t`Description is required`;
+      }
+      if (values.id != null) {
+        if (!values.revision_message) {
+          errors.revision_message = t`Revision message is required`;
         }
-        if (!values.description) {
-            errors.description = t`Description is required`;
-        }
-        if (values.id != null) {
-            if (!values.revision_message) {
-                errors.revision_message = t`Revision message is required`;
-            }
-        }
-        if (!values.definition || !values.definition.filter || values.definition.filter.length < 1) {
-            errors.definition = t`At least one filter is required`;
-        }
-        return errors;
+      }
+      if (
+        !values.definition ||
+        !values.definition.filter ||
+        values.definition.filter.length < 1
+      ) {
+        errors.definition = t`At least one filter is required`;
+      }
+      return errors;
     },
-    initialValues: { name: "", description: "", table_id: null, definition: { filter: [] }, revision_message: null }
-},
-(state, props) => segmentFormSelectors(state, props))
+    initialValues: {
+      name: "",
+      description: "",
+      table_id: null,
+      definition: { filter: [] },
+      revision_message: null,
+    },
+  },
+  (state, props) => segmentFormSelectors(state, props),
+)
 export default class SegmentForm extends Component {
-    updatePreviewSummary(datasetQuery) {
-        this.props.updatePreviewSummary({
-            ...datasetQuery,
-            query: {
-                ...datasetQuery.query,
-                aggregation: ["count"]
-            }
-        })
-    }
+  updatePreviewSummary(datasetQuery) {
+    this.props.updatePreviewSummary({
+      ...datasetQuery,
+      query: {
+        ...datasetQuery.query,
+        aggregation: ["count"],
+      },
+    });
+  }
 
-    renderActionButtons() {
-        const { invalid, handleSubmit, tableMetadata } = this.props;
-        return (
-            <div>
-                <button className={cx("Button", { "Button--primary": !invalid, "disabled": invalid })} onClick={handleSubmit}>{t`Save changes`}</button>
-                <Link to={"/admin/datamodel/database/" + tableMetadata.db_id + "/table/" + tableMetadata.id} className="Button ml2">{t`Cancel`}</Link>
-            </div>
-        )
-    }
+  renderActionButtons() {
+    const { invalid, handleSubmit, tableMetadata } = this.props;
+    return (
+      <div>
+        <button
+          className={cx("Button", {
+            "Button--primary": !invalid,
+            disabled: invalid,
+          })}
+          onClick={handleSubmit}
+        >{t`Save changes`}</button>
+        <Link
+          to={
+            "/admin/datamodel/database/" +
+            tableMetadata.db_id +
+            "/table/" +
+            tableMetadata.id
+          }
+          className="Button ml2"
+        >{t`Cancel`}</Link>
+      </div>
+    );
+  }
 
-    render() {
-        const { fields: { id, name, description, definition, revision_message }, segment, metadata, tableMetadata, handleSubmit, previewSummary } = this.props;
+  render() {
+    const {
+      fields: { id, name, description, definition, revision_message },
+      segment,
+      metadata,
+      tableMetadata,
+      handleSubmit,
+      previewSummary,
+    } = this.props;
 
-        return (
-            <LoadingAndErrorWrapper loading={!tableMetadata}>
-            { () =>
-                <form className="full" onSubmit={handleSubmit}>
-                    <div className="wrapper py4">
-                        <FormLabel
-                            title={(segment && segment.id != null ? t`Edit Your Segment` : t`Create Your Segment`)}
-                            description={segment && segment.id != null ?
-                                t`Make changes to your segment and leave an explanatory note.` :
-                                t`Select and add filters to create your new segment for the ${tableMetadata.display_name} table`
-                            }
-                        >
-                            <PartialQueryBuilder
-                                features={{
-                                    filter: true
-                                }}
-                                metadata={
-                                    metadata && tableMetadata && metadata.tables && metadata.tables[tableMetadata.id].fields && Object.assign(new Metadata(), metadata, {
-                                        tables: {
-                                            ...metadata.tables,
-                                            [tableMetadata.id]: Object.assign(new Table(), metadata.tables[tableMetadata.id], {
-                                                segments: []
-                                            })
-                                        }
-                                    })
-                                }
-                                tableMetadata={tableMetadata}
-                                previewSummary={previewSummary == null ? "" : formatValue(previewSummary) + " rows"}
-                                updatePreviewSummary={this.updatePreviewSummary.bind(this)}
-                                {...definition}
-                            />
-                        </FormLabel>
-                        <div style={{ maxWidth: "575px" }}>
-                            <FormLabel
-                                title={t`Name Your Segment`}
-                                description={t`Give your segment a name to help others find it.`}
-                            >
-                                <FormInput
-                                    field={name}
-                                    placeholder={t`Something descriptive but not too long`}
-                                />
-                            </FormLabel>
-                            <FormLabel
-                                title={t`Describe Your Segment`}
-                                description={t`Give your segment a description to help others understand what it's about.`}
-                            >
-                                <FormTextArea
-                                    field={description}
-                                    placeholder={t`This is a good place to be more specific about less obvious segment rules`}
-                                />
-                            </FormLabel>
-                            { id.value != null &&
-                                <FieldSet legend={t`Reason For Changes`}>
-                                    <FormLabel description={t`Leave a note to explain what changes you made and why they were required.`}>
-                                        <FormTextArea
-                                            field={revision_message}
-                                            placeholder={t`This will show up in the revision history for this segment to help everyone remember why things changed`}
-                                        />
-                                    </FormLabel>
-                                    <div className="flex align-center">
-                                        {this.renderActionButtons()}
-                                    </div>
-                                </FieldSet>
-                            }
-                        </div>
+    return (
+      <LoadingAndErrorWrapper loading={!tableMetadata}>
+        {() => (
+          <form className="full" onSubmit={handleSubmit}>
+            <div className="wrapper py4">
+              <FormLabel
+                title={
+                  segment && segment.id != null
+                    ? t`Edit Your Segment`
+                    : t`Create Your Segment`
+                }
+                description={
+                  segment && segment.id != null
+                    ? t`Make changes to your segment and leave an explanatory note.`
+                    : t`Select and add filters to create your new segment for the ${
+                        tableMetadata.display_name
+                      } table`
+                }
+              >
+                <PartialQueryBuilder
+                  features={{
+                    filter: true,
+                  }}
+                  metadata={
+                    metadata &&
+                    tableMetadata &&
+                    metadata.tables &&
+                    metadata.tables[tableMetadata.id].fields &&
+                    Object.assign(new Metadata(), metadata, {
+                      tables: {
+                        ...metadata.tables,
+                        [tableMetadata.id]: Object.assign(
+                          new Table(),
+                          metadata.tables[tableMetadata.id],
+                          {
+                            segments: [],
+                          },
+                        ),
+                      },
+                    })
+                  }
+                  tableMetadata={tableMetadata}
+                  previewSummary={
+                    previewSummary == null
+                      ? ""
+                      : formatValue(previewSummary) + " rows"
+                  }
+                  updatePreviewSummary={this.updatePreviewSummary.bind(this)}
+                  {...definition}
+                />
+              </FormLabel>
+              <div style={{ maxWidth: "575px" }}>
+                <FormLabel
+                  title={t`Name Your Segment`}
+                  description={t`Give your segment a name to help others find it.`}
+                >
+                  <FormInput
+                    field={name}
+                    placeholder={t`Something descriptive but not too long`}
+                  />
+                </FormLabel>
+                <FormLabel
+                  title={t`Describe Your Segment`}
+                  description={t`Give your segment a description to help others understand what it's about.`}
+                >
+                  <FormTextArea
+                    field={description}
+                    placeholder={t`This is a good place to be more specific about less obvious segment rules`}
+                  />
+                </FormLabel>
+                {id.value != null && (
+                  <FieldSet legend={t`Reason For Changes`}>
+                    <FormLabel
+                      description={t`Leave a note to explain what changes you made and why they were required.`}
+                    >
+                      <FormTextArea
+                        field={revision_message}
+                        placeholder={t`This will show up in the revision history for this segment to help everyone remember why things changed`}
+                      />
+                    </FormLabel>
+                    <div className="flex align-center">
+                      {this.renderActionButtons()}
                     </div>
+                  </FieldSet>
+                )}
+              </div>
+            </div>
 
-                    { id.value == null &&
-                        <div className="border-top py4">
-                            <div className="wrapper">
-                                {this.renderActionButtons()}
-                            </div>
-                        </div>
-                    }
-                </form>
-            }
-            </LoadingAndErrorWrapper>
-        );
-    }
+            {id.value == null && (
+              <div className="border-top py4">
+                <div className="wrapper">{this.renderActionButtons()}</div>
+              </div>
+            )}
+          </form>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/containers/TableSettingsApp.jsx b/frontend/src/metabase/admin/datamodel/containers/TableSettingsApp.jsx
index 6289a7734a56ff0594f135867b350ec38181470c..7f2370666466cfbd6c35f3bd9ef6420b42303583 100644
--- a/frontend/src/metabase/admin/datamodel/containers/TableSettingsApp.jsx
+++ b/frontend/src/metabase/admin/datamodel/containers/TableSettingsApp.jsx
@@ -1,116 +1,127 @@
-import React, { Component } from 'react'
+import React, { Component } from "react";
 import { connect } from "react-redux";
 
 import * as metadataActions from "metabase/redux/metadata";
 
 import { getMetadata } from "metabase/selectors/metadata";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import { BackButton, Section, SectionHeader } from "metabase/admin/datamodel/containers/FieldApp";
-import ActionButton from "metabase/components/ActionButton.jsx";
-
 import {
-    rescanTableFieldValues,
-    discardTableFieldValues
-} from "../table";
+  BackButton,
+  Section,
+  SectionHeader,
+} from "metabase/admin/datamodel/containers/FieldApp";
+import ActionButton from "metabase/components/ActionButton.jsx";
 
+import { rescanTableFieldValues, discardTableFieldValues } from "../table";
 
 const mapStateToProps = (state, props) => {
-    return {
-        databaseId: parseInt(props.params.databaseId),
-        tableId: parseInt(props.params.tableId),
-        metadata: getMetadata(state)
-    };
+  return {
+    databaseId: parseInt(props.params.databaseId),
+    tableId: parseInt(props.params.tableId),
+    metadata: getMetadata(state),
+  };
 };
 
 const mapDispatchToProps = {
-    fetchDatabaseMetadata: metadataActions.fetchDatabaseMetadata,
-    fetchTableMetadata: metadataActions.fetchTableMetadata,
-    rescanTableFieldValues,
-    discardTableFieldValues
+  fetchDatabaseMetadata: metadataActions.fetchDatabaseMetadata,
+  fetchTableMetadata: metadataActions.fetchTableMetadata,
+  rescanTableFieldValues,
+  discardTableFieldValues,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class TableSettingsApp extends Component {
+  async componentWillMount() {
+    const {
+      databaseId,
+      tableId,
+      fetchDatabaseMetadata,
+      fetchTableMetadata,
+    } = this.props;
 
-    async componentWillMount() {
-        const {databaseId, tableId, fetchDatabaseMetadata, fetchTableMetadata} = this.props;
-
-        await fetchDatabaseMetadata(databaseId);
-        await fetchTableMetadata(tableId, true);
-    }
+    await fetchDatabaseMetadata(databaseId);
+    await fetchTableMetadata(tableId, true);
+  }
 
-    render() {
-        const { metadata, databaseId, tableId } = this.props;
+  render() {
+    const { metadata, databaseId, tableId } = this.props;
 
-        const db = metadata && metadata.databases[databaseId];
-        const table = metadata && metadata.tables[tableId];
-        const isLoading = !table;
+    const db = metadata && metadata.databases[databaseId];
+    const table = metadata && metadata.tables[tableId];
+    const isLoading = !table;
 
-        return (
-            <LoadingAndErrorWrapper loading={isLoading} error={null} noWrapper>
-                { () =>
-                    <div className="relative">
-                        <div className="wrapper wrapper--trim">
-                            <Nav db={db} table={table} />
-                            <UpdateFieldValues
-                                rescanTableFieldValues={() => this.props.rescanTableFieldValues(table.id)}
-                                discardTableFieldValues={() => this.props.discardTableFieldValues(table.id)}
-                            />
-                        </div>
-                    </div>
+    return (
+      <LoadingAndErrorWrapper loading={isLoading} error={null} noWrapper>
+        {() => (
+          <div className="relative">
+            <div className="wrapper wrapper--trim">
+              <Nav db={db} table={table} />
+              <UpdateFieldValues
+                rescanTableFieldValues={() =>
+                  this.props.rescanTableFieldValues(table.id)
                 }
-            </LoadingAndErrorWrapper>
-        );
-    }
+                discardTableFieldValues={() =>
+                  this.props.discardTableFieldValues(table.id)
+                }
+              />
+            </div>
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
 class Nav extends Component {
-    render () {
-        const { db, table } = this.props;
-        return (
-            <div>
-                <BackButton databaseId={db.id} tableId={table.id} />
-                <div className="my4 py1 ml-auto mr-auto">
-                    <Breadcrumbs
-                        crumbs={[
-                            db && [db.name, `/admin/datamodel/database/${db.id}`],
-                            table && [table.display_name, `/admin/datamodel/database/${db.id}/table/${table.id}`],
-                            t`Settings`
-                        ]}
-                    />
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const { db, table } = this.props;
+    return (
+      <div>
+        <BackButton databaseId={db.id} tableId={table.id} />
+        <div className="my4 py1 ml-auto mr-auto">
+          <Breadcrumbs
+            crumbs={[
+              db && [db.name, `/admin/datamodel/database/${db.id}`],
+              table && [
+                table.display_name,
+                `/admin/datamodel/database/${db.id}/table/${table.id}`,
+              ],
+              t`Settings`,
+            ]}
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
 class UpdateFieldValues extends Component {
-    render () {
-        return (
-            <Section>
-                <SectionHeader
-                    title={t`Cached field values`}
-                    description={t`Metabase can scan the values in this table to enable checkbox filters in dashboards and questions.`}
-                />
-                <ActionButton
-                    className="Button mr2"
-                    actionFn={this.props.rescanTableFieldValues}
-                    normalText={t`Re-scan this table`}
-                    activeText={t`Starting…`}
-                    failedText={t`Failed to start scan`}
-                    successText={t`Scan triggered!`}
-                />
-                <ActionButton
-                    className="Button Button--danger"
-                    actionFn={this.props.discardTableFieldValues}
-                    normalText={t`Discard cached field values`}
-                    activeText={t`Starting…`}
-                    failedText={t`Failed to discard values`}
-                    successText={t`Discard triggered!`}
-                />
-            </Section>
-        );
-    }
+  render() {
+    return (
+      <Section>
+        <SectionHeader
+          title={t`Cached field values`}
+          description={t`Metabase can scan the values in this table to enable checkbox filters in dashboards and questions.`}
+        />
+        <ActionButton
+          className="Button mr2"
+          actionFn={this.props.rescanTableFieldValues}
+          normalText={t`Re-scan this table`}
+          activeText={t`Starting…`}
+          failedText={t`Failed to start scan`}
+          successText={t`Scan triggered!`}
+        />
+        <ActionButton
+          className="Button Button--danger"
+          actionFn={this.props.discardTableFieldValues}
+          normalText={t`Discard cached field values`}
+          activeText={t`Starting…`}
+          failedText={t`Failed to discard values`}
+          successText={t`Discard triggered!`}
+        />
+      </Section>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/datamodel/datamodel.js b/frontend/src/metabase/admin/datamodel/datamodel.js
index dec914c348600a48df918dc1c2f3a8c92ad5397a..7ec91a8f626a9d985b2585ab9da7763777a7a743 100644
--- a/frontend/src/metabase/admin/datamodel/datamodel.js
+++ b/frontend/src/metabase/admin/datamodel/datamodel.js
@@ -1,176 +1,213 @@
 import _ from "underscore";
 
-import { handleActions, combineReducers, createAction, createThunkAction, momentifyTimestamps } from "metabase/lib/redux";
+import {
+  handleActions,
+  combineReducers,
+  createAction,
+  createThunkAction,
+  momentifyTimestamps,
+} from "metabase/lib/redux";
 import { push } from "react-router-redux";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 import { loadTableAndForeignKeys } from "metabase/lib/table";
 
-import { MetabaseApi, SegmentApi, MetricApi, RevisionsApi } from "metabase/services";
+import {
+  MetabaseApi,
+  SegmentApi,
+  MetricApi,
+  RevisionsApi,
+} from "metabase/services";
 
 import { getEditingDatabase } from "./selectors";
 
 function loadDatabaseMetadata(databaseId) {
-    return MetabaseApi.db_metadata({ 'dbId': databaseId });
+  return MetabaseApi.db_metadata({ dbId: databaseId });
 }
 
 // initializeMetadata
-export const INITIALIZE_METADATA = "metabase/admin/datamodel/INITIALIZE_METADATA";
-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 {
-            databases = await MetabaseApi.db_list();
-        } catch(error) {
-            console.log("error fetching databases", error);
-        }
-
-        // initialize a database
-        if (databases && !_.isEmpty(databases)) {
-            let db = databaseId ? _.findWhere(databases, {id: databaseId}) : databases[0];
-
-            database = await loadDatabaseMetadata(db.id);
-        }
-
-        if (database) {
-            dispatch(fetchDatabaseIdfields(database.id));
-        }
-
-        return {
-            databases,
-            database,
-            tableId
-        }
+      let databases, database;
+      try {
+        databases = await MetabaseApi.db_list();
+      } catch (error) {
+        console.log("error fetching databases", error);
+      }
+
+      // initialize a database
+      if (databases && !_.isEmpty(databases)) {
+        let db = databaseId
+          ? _.findWhere(databases, { id: databaseId })
+          : databases[0];
+
+        database = await loadDatabaseMetadata(db.id);
+      }
+
+      if (database) {
+        dispatch(fetchDatabaseIdfields(database.id));
+      }
+
+      return {
+        databases,
+        database,
+        tableId,
+      };
     };
-});
+  },
+);
 
 // fetchDatabaseIdfields
 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 });
-            return idfields.map(function(field) {
-                field.displayName = field.table.display_name + " → " + field.display_name;
-                return field;
-            });
-        } catch (error) {
-            console.warn("error getting idfields", databaseId, error);
-        }
-    };
+export const fetchDatabaseIdfields = createThunkAction(FETCH_IDFIELDS, function(
+  databaseId,
+) {
+  return async function(dispatch, getState) {
+    try {
+      let idfields = await MetabaseApi.db_idfields({ dbId: databaseId });
+      return idfields.map(function(field) {
+        field.displayName =
+          field.table.display_name + " → " + field.display_name;
+        return field;
+      });
+    } catch (error) {
+      console.warn("error getting idfields", databaseId, error);
+    }
+  };
 });
 
 // selectDatabase
 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);
+  return async function(dispatch, getState) {
+    try {
+      let database = await loadDatabaseMetadata(db.id);
 
-            dispatch(fetchDatabaseIdfields(db.id));
+      dispatch(fetchDatabaseIdfields(db.id));
 
-            // we also want to update our url to match our new state
-            dispatch(push('/admin/datamodel/database/'+db.id));
+      // we also want to update our url to match our new state
+      dispatch(push("/admin/datamodel/database/" + db.id));
 
-            return database;
-        } catch (error) {
-            console.log("error fetching tables", db.id, error);
-        }
-    };
+      return database;
+    } catch (error) {
+      console.log("error fetching tables", db.id, error);
+    }
+  };
 });
 
 // selectTable
 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));
-
-        return table.id;
-    };
+  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),
+    );
+
+    return table.id;
+  };
 });
 
 // updateTable
 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
-            let slimTable = { ...table };
-            slimTable = _.omit(slimTable, "fields", "fields_lookup", "aggregation_options", "breakout_options", "metrics", "segments");
-
-            let updatedTable = await MetabaseApi.table_update(slimTable);
-            _.each(updatedTable, (value, key) => { if (key.charAt(0) !== "$") { updatedTable[key] = value } });
-
-            MetabaseAnalytics.trackEvent("Data Model", "Update Table");
+  return async function(dispatch, getState) {
+    try {
+      // make sure we don't send all the computed metadata
+      let slimTable = { ...table };
+      slimTable = _.omit(
+        slimTable,
+        "fields",
+        "fields_lookup",
+        "aggregation_options",
+        "breakout_options",
+        "metrics",
+        "segments",
+      );
+
+      let updatedTable = await MetabaseApi.table_update(slimTable);
+      _.each(updatedTable, (value, key) => {
+        if (key.charAt(0) !== "$") {
+          updatedTable[key] = value;
+        }
+      });
 
-            // TODO: we are not actually using this because the way the react components works actually mutates the original object :(
-            return updatedTable;
+      MetabaseAnalytics.trackEvent("Data Model", "Update Table");
 
-        } catch (error) {
-            console.log("error updating table", error);
-            //MetabaseAnalytics.trackEvent("Databases", database.id ? "Update Failed" : "Create Failed", database.engine);
-        }
-    };
+      // TODO: we are not actually using this because the way the react components works actually mutates the original object :(
+      return updatedTable;
+    } catch (error) {
+      console.log("error updating table", error);
+      //MetabaseAnalytics.trackEvent("Databases", database.id ? "Update Failed" : "Create Failed", database.engine);
+    }
+  };
 });
 
 // updateField
 export const UPDATE_FIELD = "metabase/admin/datamodel/UPDATE_FIELD";
 export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
-    return async function(dispatch, getState) {
-        const editingDatabase = getEditingDatabase(getState());
-
-        try {
-            // make sure we don't send all the computed metadata
-            let slimField = { ...field };
-            slimField = _.omit(slimField, "operators_lookup", "operators", "values");
-
-            // update the field
-            let updatedField = await MetabaseApi.field_update(slimField);
-
-            // refresh idfields
-            let table = _.findWhere(editingDatabase.tables, {id: updatedField.table_id});
-            dispatch(fetchDatabaseIdfields(table.db_id));
-
-            MetabaseAnalytics.trackEvent("Data Model", "Update Field");
-
-            // TODO: we are not actually using this because the way the react components works actually mutates the original object :(
-            return updatedField;
-
-        } catch (error) {
-            console.log("error updating field", error);
-            //MetabaseAnalytics.trackEvent("Databases", database.id ? "Update Failed" : "Create Failed", database.engine);
-        }
-    };
+  return async function(dispatch, getState) {
+    const editingDatabase = getEditingDatabase(getState());
+
+    try {
+      // make sure we don't send all the computed metadata
+      let slimField = { ...field };
+      slimField = _.omit(slimField, "operators_lookup", "operators", "values");
+
+      // update the field
+      let updatedField = await MetabaseApi.field_update(slimField);
+
+      // refresh idfields
+      let table = _.findWhere(editingDatabase.tables, {
+        id: updatedField.table_id,
+      });
+      dispatch(fetchDatabaseIdfields(table.db_id));
+
+      MetabaseAnalytics.trackEvent("Data Model", "Update Field");
+
+      // TODO: we are not actually using this because the way the react components works actually mutates the original object :(
+      return updatedField;
+    } catch (error) {
+      console.log("error updating field", error);
+      //MetabaseAnalytics.trackEvent("Databases", database.id ? "Update Failed" : "Create Failed", database.engine);
+    }
+  };
 });
 
 // retireSegment
 export const RETIRE_SEGMENT = "metabase/admin/datamodel/RETIRE_SEGMENT";
-export const onRetireSegment = createThunkAction(RETIRE_SEGMENT, function(segment) {
-    return async function(dispatch, getState) {
-        const editingDatabase = getEditingDatabase(getState());
+export const onRetireSegment = createThunkAction(RETIRE_SEGMENT, function(
+  segment,
+) {
+  return async function(dispatch, getState) {
+    const editingDatabase = getEditingDatabase(getState());
 
-        await SegmentApi.delete(segment);
-        MetabaseAnalytics.trackEvent("Data Model", "Retire Segment");
+    await SegmentApi.delete(segment);
+    MetabaseAnalytics.trackEvent("Data Model", "Retire Segment");
 
-        return await loadDatabaseMetadata(editingDatabase.id);
-    };
+    return await loadDatabaseMetadata(editingDatabase.id);
+  };
 });
 
 // retireMetric
 export const RETIRE_METRIC = "metabase/admin/datamodel/RETIRE_METRIC";
-export const onRetireMetric = createThunkAction(RETIRE_METRIC, function(metric) {
-    return async function(dispatch, getState) {
-        const editingDatabase = getEditingDatabase(getState());
+export const onRetireMetric = createThunkAction(RETIRE_METRIC, function(
+  metric,
+) {
+  return async function(dispatch, getState) {
+    const editingDatabase = getEditingDatabase(getState());
 
-        await MetricApi.delete(metric);
-        MetabaseAnalytics.trackEvent("Data Model", "Retire Metric");
+    await MetricApi.delete(metric);
+    MetabaseAnalytics.trackEvent("Data Model", "Retire Metric");
 
-        return await loadDatabaseMetadata(editingDatabase.id);
-    };
+    return await loadDatabaseMetadata(editingDatabase.id);
+  };
 });
 
-
 // SEGMENTS
 
 export const GET_SEGMENT = "metabase/admin/datamodel/GET_SEGMENT";
@@ -178,7 +215,7 @@ 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 getSegment = createAction(GET_SEGMENT, SegmentApi.get);
 export const createSegment = createAction(CREATE_SEGMENT, SegmentApi.create);
 export const updateSegment = createAction(UPDATE_SEGMENT, SegmentApi.update);
 export const deleteSegment = createAction(DELETE_SEGMENT, SegmentApi.delete);
@@ -190,102 +227,192 @@ 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 getMetric = createAction(GET_METRIC, MetricApi.get);
 export const createMetric = createAction(CREATE_METRIC, MetricApi.create);
 export const updateMetric = createAction(UPDATE_METRIC, MetricApi.update);
 export const deleteMetric = createAction(DELETE_METRIC, MetricApi.delete);
 
 // SEGMENT DETAIL
 
-export const LOAD_TABLE_METADATA = "metabase/admin/datamodel/LOAD_TABLE_METADATA";
-export const UPDATE_PREVIEW_SUMMARY = "metabase/admin/datamodel/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) => {
+export const loadTableMetadata = createAction(
+  LOAD_TABLE_METADATA,
+  loadTableAndForeignKeys,
+);
+export const updatePreviewSummary = createAction(
+  UPDATE_PREVIEW_SUMMARY,
+  async query => {
     let result = await MetabaseApi.dataset(query);
     return result.data.rows[0][0];
-});
+  },
+);
 
 // REVISION HISTORY
 
 export const FETCH_REVISIONS = "metabase/admin/datamodel/FETCH_REVISIONS";
 
-export const fetchRevisions = createThunkAction(FETCH_REVISIONS, ({ entity, id }) =>
-    async (dispatch, getState) => {
-        let action;
-        switch (entity) {
-            case "segment": action = getSegment({ segmentId: id }); break;
-            case "metric": action = getMetric({ metricId: id }); break;
-        }
-        let [object, revisions] = await Promise.all([
-            dispatch(action),
-            RevisionsApi.get({ entity, id })
-        ]);
-        await dispatch(loadTableMetadata(object.payload.definition.source_table));
-        return { object: object.payload, revisions };
+export const fetchRevisions = createThunkAction(
+  FETCH_REVISIONS,
+  ({ entity, id }) => async (dispatch, getState) => {
+    let action;
+    switch (entity) {
+      case "segment":
+        action = getSegment({ segmentId: id });
+        break;
+      case "metric":
+        action = getMetric({ metricId: id });
+        break;
     }
+    let [object, revisions] = await Promise.all([
+      dispatch(action),
+      RevisionsApi.get({ entity, id }),
+    ]);
+    await dispatch(loadTableMetadata(object.payload.definition.source_table));
+    return { object: object.payload, revisions };
+  },
 );
 
-
 // reducers
 
-const databases = handleActions({
-    [INITIALIZE_METADATA]: { next: (state, { payload }) => payload.databases }
-}, []);
+const databases = handleActions(
+  {
+    [INITIALIZE_METADATA]: { next: (state, { payload }) => payload.databases },
+  },
+  [],
+);
 
-const idfields = handleActions({
-    [FETCH_IDFIELDS]: { next: (state, { payload }) => payload ? payload : state }
-}, []);
+const idfields = handleActions(
+  {
+    [FETCH_IDFIELDS]: {
+      next: (state, { payload }) => (payload ? payload : state),
+    },
+  },
+  [],
+);
 
-const editingDatabase = handleActions({
+const editingDatabase = handleActions(
+  {
     [INITIALIZE_METADATA]: { next: (state, { payload }) => payload.database },
-    [SELECT_DATABASE]: { next: (state, { payload }) => payload ? payload : state },
+    [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 }
-}, null);
-
-const segments = handleActions({
-    [GET_SEGMENT]:    { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [CREATE_SEGMENT]: { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [UPDATE_SEGMENT]: { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [DELETE_SEGMENT]: { next: (state, { payload }) => { state = { ...state }; delete state[payload.id]; return state; }}
-}, {});
-
-const metrics = handleActions({
-    [GET_METRIC]:    { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [CREATE_METRIC]: { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [UPDATE_METRIC]: { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [DELETE_METRIC]: { next: (state, { payload }) => { state = { ...state }; delete state[payload.id]; return state; }}
-}, {});
-
-const tableMetadata = handleActions({
+    [RETIRE_METRIC]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
+
+const editingTable = handleActions(
+  {
+    [INITIALIZE_METADATA]: {
+      next: (state, { payload }) => payload.tableId || null,
+    },
+    [SELECT_TABLE]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
+
+const segments = handleActions(
+  {
+    [GET_SEGMENT]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [CREATE_SEGMENT]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [UPDATE_SEGMENT]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [DELETE_SEGMENT]: {
+      next: (state, { payload }) => {
+        state = { ...state };
+        delete state[payload.id];
+        return state;
+      },
+    },
+  },
+  {},
+);
+
+const metrics = handleActions(
+  {
+    [GET_METRIC]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [CREATE_METRIC]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [UPDATE_METRIC]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [DELETE_METRIC]: {
+      next: (state, { payload }) => {
+        state = { ...state };
+        delete state[payload.id];
+        return state;
+      },
+    },
+  },
+  {},
+);
+
+const tableMetadata = handleActions(
+  {
     [LOAD_TABLE_METADATA]: {
-        next: (state, { payload }) => (payload && payload.table) ? payload.table : null,
-        throw: (state, action) => null
-    }
-}, null);
+      next: (state, { payload }) =>
+        payload && payload.table ? payload.table : null,
+      throw: (state, action) => null,
+    },
+  },
+  null,
+);
 
-const previewSummary = handleActions({
-    [UPDATE_PREVIEW_SUMMARY]: { next: (state, { payload }) => payload }
-}, null);
+const previewSummary = handleActions(
+  {
+    [UPDATE_PREVIEW_SUMMARY]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
 
-const revisionObject = handleActions({
-    [FETCH_REVISIONS]: { next: (state, { payload: revisionObject }) => revisionObject }
-}, null);
+const revisionObject = handleActions(
+  {
+    [FETCH_REVISIONS]: {
+      next: (state, { payload: revisionObject }) => revisionObject,
+    },
+  },
+  null,
+);
 
 export default combineReducers({
-    databases,
-    idfields,
-    editingDatabase,
-    editingTable,
-    segments,
-    metrics,
-    tableMetadata,
-    previewSummary,
-    revisionObject
+  databases,
+  idfields,
+  editingDatabase,
+  editingTable,
+  segments,
+  metrics,
+  tableMetadata,
+  previewSummary,
+  revisionObject,
 });
diff --git a/frontend/src/metabase/admin/datamodel/field.js b/frontend/src/metabase/admin/datamodel/field.js
index 55f6efdb3ce2fdaead12cc17798db3109d849fb6..b55794d9c6f8ceb2c85d01ec1bec8e1a008d2468 100644
--- a/frontend/src/metabase/admin/datamodel/field.js
+++ b/frontend/src/metabase/admin/datamodel/field.js
@@ -4,28 +4,41 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 import { MetabaseApi } from "metabase/services";
 
 export const RESCAN_FIELD_VALUES = "metabase/admin/fields/RESCAN_FIELD_VALUES";
-export const DISCARD_FIELD_VALUES = "metabase/admin/fields/DISCARD_FIELD_VALUES";
+export const DISCARD_FIELD_VALUES =
+  "metabase/admin/fields/DISCARD_FIELD_VALUES";
 
-export const rescanFieldValues = createThunkAction(RESCAN_FIELD_VALUES, function(fieldId) {
+export const rescanFieldValues = createThunkAction(
+  RESCAN_FIELD_VALUES,
+  function(fieldId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.field_rescan_values({fieldId});
-            MetabaseAnalytics.trackEvent("Data Model", "Manual Re-scan Field Values");
-            return call;
-        } catch(error) {
-            console.log('error manually re-scanning field values', error);
-        }
+      try {
+        let call = await MetabaseApi.field_rescan_values({ fieldId });
+        MetabaseAnalytics.trackEvent(
+          "Data Model",
+          "Manual Re-scan Field Values",
+        );
+        return call;
+      } catch (error) {
+        console.log("error manually re-scanning field values", error);
+      }
     };
-});
+  },
+);
 
-export const discardFieldValues = createThunkAction(DISCARD_FIELD_VALUES, function(fieldId) {
+export const discardFieldValues = createThunkAction(
+  DISCARD_FIELD_VALUES,
+  function(fieldId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.field_discard_values({fieldId});
-            MetabaseAnalytics.trackEvent("Data Model", "Manual Discard Field Values");
-            return call;
-        } catch(error) {
-            console.log('error discarding field values', error);
-        }
+      try {
+        let call = await MetabaseApi.field_discard_values({ fieldId });
+        MetabaseAnalytics.trackEvent(
+          "Data Model",
+          "Manual Discard Field Values",
+        );
+        return call;
+      } catch (error) {
+        console.log("error discarding field values", error);
+      }
     };
-});
+  },
+);
diff --git a/frontend/src/metabase/admin/datamodel/selectors.js b/frontend/src/metabase/admin/datamodel/selectors.js
index 5e782bc986437b37540130591b33abfc89363087..b75e08fa0322606a8c1a40c7d57e68390a924f4b 100644
--- a/frontend/src/metabase/admin/datamodel/selectors.js
+++ b/frontend/src/metabase/admin/datamodel/selectors.js
@@ -1,96 +1,104 @@
-
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 import { computeMetadataStrength } from "metabase/lib/schema_metadata";
 
+const segmentsSelector = (state, props) => state.admin.datamodel.segments;
+const metricsSelector = (state, props) => state.admin.datamodel.metrics;
 
-const segmentsSelector         = (state, props) => state.admin.datamodel.segments;
-const metricsSelector          = (state, props) => state.admin.datamodel.metrics;
-
-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 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);
+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);
 
-const userSelector             = (state, props) => state.currentUser;
+const userSelector = (state, props) => state.currentUser;
 
 export const segmentEditSelectors = createSelector(
-    segmentsSelector,
-    idSelector,
-    tableIdSelector,
-    tableMetadataSelector,
-    (segments, id, tableId, tableMetadata) => ({
-        segment: id == null ?
-            { id: null, table_id: tableId, definition: { filter: [] } } :
-            segments[id],
-        tableMetadata
-    })
+  segmentsSelector,
+  idSelector,
+  tableIdSelector,
+  tableMetadataSelector,
+  (segments, id, tableId, tableMetadata) => ({
+    segment:
+      id == null
+        ? { id: null, table_id: tableId, definition: { filter: [] } }
+        : segments[id],
+    tableMetadata,
+  }),
 );
 
 export const segmentFormSelectors = createSelector(
-    segmentEditSelectors,
-    previewSummarySelector,
-    ({ segment, tableMetadata }, previewSummary) => ({
-        initialValues: segment,
-        tableMetadata,
-        previewSummary
-    })
+  segmentEditSelectors,
+  previewSummarySelector,
+  ({ segment, tableMetadata }, previewSummary) => ({
+    initialValues: segment,
+    tableMetadata,
+    previewSummary,
+  }),
 );
 
 export const metricEditSelectors = createSelector(
-    metricsSelector,
-    idSelector,
-    tableIdSelector,
-    tableMetadataSelector,
-    (metrics, id, tableId, tableMetadata) => ({
-        metric: id == null ?
-            { id: null, table_id: tableId, definition: { aggregation: [null] } } :
-            metrics[id],
-        tableMetadata
-    })
+  metricsSelector,
+  idSelector,
+  tableIdSelector,
+  tableMetadataSelector,
+  (metrics, id, tableId, tableMetadata) => ({
+    metric:
+      id == null
+        ? { id: null, table_id: tableId, definition: { aggregation: [null] } }
+        : metrics[id],
+    tableMetadata,
+  }),
 );
 
 export const metricFormSelectors = createSelector(
-    metricEditSelectors,
-    previewSummarySelector,
-    ({ metric, tableMetadata }, previewSummary) => ({
-        initialValues: metric,
-        tableMetadata,
-        previewSummary
-    })
+  metricEditSelectors,
+  previewSummarySelector,
+  ({ metric, tableMetadata }, previewSummary) => ({
+    initialValues: metric,
+    tableMetadata,
+    previewSummary,
+  }),
 );
 
 export const revisionHistorySelectors = createSelector(
-    revisionObjectSelector,
-    tableMetadataSelector,
-    userSelector,
-    (revisionObject, tableMetadata, user) => ({
-        ...revisionObject,
-        tableMetadata,
-        user
-    })
+  revisionObjectSelector,
+  tableMetadataSelector,
+  userSelector,
+  (revisionObject, tableMetadata, user) => ({
+    ...revisionObject,
+    tableMetadata,
+    user,
+  }),
 );
 
-
-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 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.admin.datamodel.editingDatabase,
-    (database) => {
-        if (!database || !database.tables) {
-            return null;
-        }
+  state => state.admin.datamodel.editingDatabase,
+  database => {
+    if (!database || !database.tables) {
+      return null;
+    }
 
-        database.tables =  database.tables.map((table) => {
-            table.metadataStrength = computeMetadataStrength(table);
-            return table;
-        });
+    database.tables = database.tables.map(table => {
+      table.metadataStrength = computeMetadataStrength(table);
+      return table;
+    });
 
-        return database;
-    }
+    return database;
+  },
 );
diff --git a/frontend/src/metabase/admin/datamodel/table.js b/frontend/src/metabase/admin/datamodel/table.js
index 60848985fd75ed7b22e19176caf02b7b91b6df0f..30ac9094bea0e9dd2db9919eaf345436d5435ac0 100644
--- a/frontend/src/metabase/admin/datamodel/table.js
+++ b/frontend/src/metabase/admin/datamodel/table.js
@@ -4,28 +4,41 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 import { MetabaseApi } from "metabase/services";
 
 export const RESCAN_TABLE_VALUES = "metabase/admin/tables/RESCAN_TABLE_VALUES";
-export const DISCARD_TABLE_VALUES = "metabase/admin/tables/DISCARD_TABLE_VALUES";
+export const DISCARD_TABLE_VALUES =
+  "metabase/admin/tables/DISCARD_TABLE_VALUES";
 
-export const rescanTableFieldValues = createThunkAction(RESCAN_TABLE_VALUES, function(tableId) {
+export const rescanTableFieldValues = createThunkAction(
+  RESCAN_TABLE_VALUES,
+  function(tableId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.table_rescan_values({tableId});
-            MetabaseAnalytics.trackEvent("Data Model", "Manual Re-scan Field Values for Table");
-            return call;
-        } catch(error) {
-            console.log('error manually re-scanning field values', error);
-        }
+      try {
+        let call = await MetabaseApi.table_rescan_values({ tableId });
+        MetabaseAnalytics.trackEvent(
+          "Data Model",
+          "Manual Re-scan Field Values for Table",
+        );
+        return call;
+      } catch (error) {
+        console.log("error manually re-scanning field values", error);
+      }
     };
-});
+  },
+);
 
-export const discardTableFieldValues = createThunkAction(DISCARD_TABLE_VALUES, function(tableId) {
+export const discardTableFieldValues = createThunkAction(
+  DISCARD_TABLE_VALUES,
+  function(tableId) {
     return async function(dispatch, getState) {
-        try {
-            let call = await MetabaseApi.table_discard_values({tableId});
-            MetabaseAnalytics.trackEvent("Data Model", "Manual Discard Field Values for Table");
-            return call;
-        } catch(error) {
-            console.log('error discarding field values', error);
-        }
+      try {
+        let call = await MetabaseApi.table_discard_values({ tableId });
+        MetabaseAnalytics.trackEvent(
+          "Data Model",
+          "Manual Discard Field Values for Table",
+        );
+        return call;
+      } catch (error) {
+        console.log("error discarding field values", error);
+      }
     };
-});
+  },
+);
diff --git a/frontend/src/metabase/admin/people/components/AddRow.jsx b/frontend/src/metabase/admin/people/components/AddRow.jsx
index e1220225cc944d6373c30dc8dd10c95eda9146da..7ab400af14bd7f130f3f2d853db5fa48bb68a53c 100644
--- a/frontend/src/metabase/admin/people/components/AddRow.jsx
+++ b/frontend/src/metabase/admin/people/components/AddRow.jsx
@@ -1,25 +1,39 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
-const AddRow = ({ value, isValid, placeholder, onKeyDown, onChange, onDone, onCancel, children }) =>
-    <div className="my2 pl1 p1 bordered border-brand rounded relative flex align-center">
-        {children}
-        <input
-            className="input--borderless h3 ml1 flex-full"
-            type="text"
-            value={value}
-            placeholder={placeholder}
-            autoFocus
-            onKeyDown={onKeyDown}
-            onChange={onChange}
-        />
-        <span className="link no-decoration cursor-pointer" onClick={onCancel}>
-            {t`Cancel`}
-        </span>
-        <button className={cx("Button ml2", {"Button--primary": !!isValid})} disabled={!isValid} onClick={onDone}>
-            {t`Add`}
-        </button>
-    </div>
+const AddRow = ({
+  value,
+  isValid,
+  placeholder,
+  onKeyDown,
+  onChange,
+  onDone,
+  onCancel,
+  children,
+}) => (
+  <div className="my2 pl1 p1 bordered border-brand rounded relative flex align-center">
+    {children}
+    <input
+      className="input--borderless h3 ml1 flex-full"
+      type="text"
+      value={value}
+      placeholder={placeholder}
+      autoFocus
+      onKeyDown={onKeyDown}
+      onChange={onChange}
+    />
+    <span className="link no-decoration cursor-pointer" onClick={onCancel}>
+      {t`Cancel`}
+    </span>
+    <button
+      className={cx("Button ml2", { "Button--primary": !!isValid })}
+      disabled={!isValid}
+      onClick={onDone}
+    >
+      {t`Add`}
+    </button>
+  </div>
+);
 
 export default AddRow;
diff --git a/frontend/src/metabase/admin/people/components/EditUserForm.jsx b/frontend/src/metabase/admin/people/components/EditUserForm.jsx
index 03922577a6805e68c57101522ee6d0593f61a5ce..5f90ba07ff0c37038c8ed657ed702ccbebe2099f 100644
--- a/frontend/src/metabase/admin/people/components/EditUserForm.jsx
+++ b/frontend/src/metabase/admin/people/components/EditUserForm.jsx
@@ -7,7 +7,7 @@ import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import GroupSelect from "../components/GroupSelect.jsx";
 import GroupSummary from "../components/GroupSummary.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import MetabaseUtils from "metabase/lib/utils";
 import SelectButton from "metabase/components/SelectButton.jsx";
 import Toggle from "metabase/components/Toggle.jsx";
@@ -20,174 +20,224 @@ import _ from "underscore";
 import { isAdminGroup, canEditMembership } from "metabase/lib/groups";
 
 export default class EditUserForm extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-
-        const user = props.user
-
-        this.state = {
-            formError: null,
-            valid: false,
-            selectedGroups: {},
-            firstName: user ? user.first_name : null,
-            lastName: user ? user.last_name : null,
-            email: user? user.email : null
-        }
-    }
-
-    static propTypes = {
-        buttonText: PropTypes.string,
-        submitFn: PropTypes.func.isRequired,
-        user: PropTypes.object,
-        groups: PropTypes.array
+  constructor(props, context) {
+    super(props, context);
+
+    const user = props.user;
+
+    this.state = {
+      formError: null,
+      valid: false,
+      selectedGroups: {},
+      firstName: user ? user.first_name : null,
+      lastName: user ? user.last_name : null,
+      email: user ? user.email : null,
     };
+  }
+
+  static propTypes = {
+    buttonText: PropTypes.string,
+    submitFn: PropTypes.func.isRequired,
+    user: PropTypes.object,
+    groups: PropTypes.array,
+  };
+
+  validateForm() {
+    let { valid } = this.state;
+    let isValid = true;
+
+    ["firstName", "lastName", "email"].forEach(fieldName => {
+      if (MetabaseUtils.isEmpty(this.state[fieldName])) isValid = false;
+    });
+
+    if (isValid !== valid) {
+      this.setState({
+        valid: isValid,
+      });
+    }
+  }
 
-    validateForm() {
-        let { valid } = this.state;
-        let isValid = true;
+  onChange = e => {
+    this.validateForm();
+  };
 
-        ["firstName", "lastName", "email"].forEach((fieldName) => {
-            if (MetabaseUtils.isEmpty(this.state[fieldName])) isValid = false;
-        });
+  formSubmitted(e) {
+    e.preventDefault();
 
-        if(isValid !== valid) {
-            this.setState({
-                'valid': isValid
-            });
-        }
-    }
+    this.setState({
+      formError: null,
+    });
 
-    onChange = (e) => {
-        this.validateForm();
-    }
+    let formErrors = { data: { errors: {} } };
 
-    formSubmitted(e) {
-        e.preventDefault();
-
-        this.setState({
-            formError: null
-        });
-
-        let formErrors = {data:{errors:{}}};
-
-        // validate email address
-        let email = ReactDOM.findDOMNode(this.refs.email).value ? ReactDOM.findDOMNode(this.refs.email).value.trim() : null;
-        if (!MetabaseUtils.validEmail(email)) {
-            formErrors.data.errors.email = t`Not a valid formatted email address`;
-        }
-
-        if (_.keys(formErrors.data.errors).length > 0) {
-            this.setState({
-                formError: formErrors
-            });
-            return;
-        }
-
-        this.props.submitFn({
-            ...(this.props.user || {}),
-            first_name: ReactDOM.findDOMNode(this.refs.firstName).value,
-            last_name: ReactDOM.findDOMNode(this.refs.lastName).value,
-            email: email,
-            groups: this.props.groups && this.state.selectedGroups ?
-                Object.entries(this.state.selectedGroups).filter(([key, value]) => value).map(([key, value]) => parseInt(key, 10)) :
-                null
-        });
+    // validate email address
+    let email = ReactDOM.findDOMNode(this.refs.email).value
+      ? ReactDOM.findDOMNode(this.refs.email).value.trim()
+      : null;
+    if (!MetabaseUtils.validEmail(email)) {
+      formErrors.data.errors.email = t`Not a valid formatted email address`;
     }
 
-    cancel() {
-        this.props.submitFn(null);
+    if (_.keys(formErrors.data.errors).length > 0) {
+      this.setState({
+        formError: formErrors,
+      });
+      return;
     }
 
-    render() {
-        const { buttonText, groups } = this.props;
-        const { formError, valid, selectedGroups, firstName, lastName, email } = this.state;
-
-        const adminGroup = _.find(groups, isAdminGroup);
-
-        return (
-            <form onSubmit={this.formSubmitted.bind(this)} noValidate>
-                <div className="px4 pb2">
-                    <FormField fieldName="first_name" formError={formError}>
-                        <FormLabel title={t`First name`} fieldName="first_name" formError={formError} offset={false}></FormLabel>
-                        <input
-                            ref="firstName"
-                            className="Form-input full"
-                            name="firstName"
-                            placeholder="Johnny"
-                            value={firstName}
-                            onChange={(e) => { this.setState({ firstName: e.target.value }, () => this.onChange(e)) }}
-                        />
-                    </FormField>
-
-                    <FormField fieldName="last_name" formError={formError}>
-                        <FormLabel title={t`Last name`} fieldName="last_name" formError={formError} offset={false}></FormLabel>
-                        <input
-                            ref="lastName"
-                            className="Form-input full"
-                            name="lastName"
-                            placeholder="Appleseed"
-                            required
-                            value={lastName}
-                            onChange={(e) => { this.setState({ lastName: e.target.value }, () => this.onChange(e)) }}
-                        />
-                    </FormField>
-
-                    <FormField fieldName="email" formError={formError}>
-                        <FormLabel title={t`Email address`} fieldName="email" formError={formError} offset={false}></FormLabel>
-                        <input
-                            ref="email"
-                            className="Form-input full"
-                            name="email"
-                            placeholder="youlooknicetoday@email.com"
-                            required
-                            value={email}
-                            onChange={(e) => { this.setState({ email: e.target.value }, () => this.onChange(e)) }}
-                        />
-                    </FormField>
-
-                    { groups && groups.filter(g => canEditMembership(g) && !isAdminGroup(g)).length > 0 ?
-                        <FormField>
-                            <FormLabel title={t`Permission Groups`} offset={false}></FormLabel>
-                            <PopoverWithTrigger
-                                sizeToFit
-                                triggerElement={
-                                    <SelectButton>
-                                        <GroupSummary groups={groups} selectedGroups={selectedGroups}/>
-                                    </SelectButton>
-                                }
-                            >
-                                <GroupSelect
-                                    groups={groups}
-                                    selectedGroups={selectedGroups}
-                                    onGroupChange={(group, selected) => {
-                                        this.setState({ selectedGroups: { ...selectedGroups, [group.id]: selected }})
-                                    }}
-                                />
-                            </PopoverWithTrigger>
-                        </FormField>
-                    : adminGroup ?
-                        <div className="flex align-center">
-                            <Toggle
-                                value={selectedGroups[adminGroup.id]}
-                                onChange={(isAdmin) => {
-                                    this.setState({ selectedGroups: isAdmin ? { [adminGroup.id]: true } : {} })
-                                }}
-                            />
-                            <span className="ml2">{t`Make this user an admin`}</span>
-                        </div>
-                    : null }
-                </div>
-
-                <ModalFooter>
-                    <Button type="button" onClick={this.cancel.bind(this)}>
-                        {t`Cancel`}
-                    </Button>
-                    <Button primary disabled={!valid}>
-                        { buttonText ? buttonText : t`Save changes` }
-                    </Button>
-                </ModalFooter>
-            </form>
-        );
-    }
+    this.props.submitFn({
+      ...(this.props.user || {}),
+      first_name: ReactDOM.findDOMNode(this.refs.firstName).value,
+      last_name: ReactDOM.findDOMNode(this.refs.lastName).value,
+      email: email,
+      groups:
+        this.props.groups && this.state.selectedGroups
+          ? Object.entries(this.state.selectedGroups)
+              .filter(([key, value]) => value)
+              .map(([key, value]) => parseInt(key, 10))
+          : null,
+    });
+  }
+
+  cancel() {
+    this.props.submitFn(null);
+  }
+
+  render() {
+    const { buttonText, groups } = this.props;
+    const {
+      formError,
+      valid,
+      selectedGroups,
+      firstName,
+      lastName,
+      email,
+    } = this.state;
+
+    const adminGroup = _.find(groups, isAdminGroup);
+
+    return (
+      <form onSubmit={this.formSubmitted.bind(this)} noValidate>
+        <div className="px4 pb2">
+          <FormField fieldName="first_name" formError={formError}>
+            <FormLabel
+              title={t`First name`}
+              fieldName="first_name"
+              formError={formError}
+              offset={false}
+            />
+            <input
+              ref="firstName"
+              className="Form-input full"
+              name="firstName"
+              placeholder="Johnny"
+              value={firstName}
+              onChange={e => {
+                this.setState({ firstName: e.target.value }, () =>
+                  this.onChange(e),
+                );
+              }}
+            />
+          </FormField>
+
+          <FormField fieldName="last_name" formError={formError}>
+            <FormLabel
+              title={t`Last name`}
+              fieldName="last_name"
+              formError={formError}
+              offset={false}
+            />
+            <input
+              ref="lastName"
+              className="Form-input full"
+              name="lastName"
+              placeholder="Appleseed"
+              required
+              value={lastName}
+              onChange={e => {
+                this.setState({ lastName: e.target.value }, () =>
+                  this.onChange(e),
+                );
+              }}
+            />
+          </FormField>
+
+          <FormField fieldName="email" formError={formError}>
+            <FormLabel
+              title={t`Email address`}
+              fieldName="email"
+              formError={formError}
+              offset={false}
+            />
+            <input
+              ref="email"
+              className="Form-input full"
+              name="email"
+              placeholder="youlooknicetoday@email.com"
+              required
+              value={email}
+              onChange={e => {
+                this.setState({ email: e.target.value }, () =>
+                  this.onChange(e),
+                );
+              }}
+            />
+          </FormField>
+
+          {groups &&
+          groups.filter(g => canEditMembership(g) && !isAdminGroup(g)).length >
+            0 ? (
+            <FormField>
+              <FormLabel title={t`Permission Groups`} offset={false} />
+              <PopoverWithTrigger
+                sizeToFit
+                triggerElement={
+                  <SelectButton>
+                    <GroupSummary
+                      groups={groups}
+                      selectedGroups={selectedGroups}
+                    />
+                  </SelectButton>
+                }
+              >
+                <GroupSelect
+                  groups={groups}
+                  selectedGroups={selectedGroups}
+                  onGroupChange={(group, selected) => {
+                    this.setState({
+                      selectedGroups: {
+                        ...selectedGroups,
+                        [group.id]: selected,
+                      },
+                    });
+                  }}
+                />
+              </PopoverWithTrigger>
+            </FormField>
+          ) : adminGroup ? (
+            <div className="flex align-center">
+              <Toggle
+                value={selectedGroups[adminGroup.id]}
+                onChange={isAdmin => {
+                  this.setState({
+                    selectedGroups: isAdmin ? { [adminGroup.id]: true } : {},
+                  });
+                }}
+              />
+              <span className="ml2">{t`Make this user an admin`}</span>
+            </div>
+          ) : null}
+        </div>
+
+        <ModalFooter>
+          <Button type="button" onClick={this.cancel.bind(this)}>
+            {t`Cancel`}
+          </Button>
+          <Button primary disabled={!valid}>
+            {buttonText ? buttonText : t`Save changes`}
+          </Button>
+        </ModalFooter>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/components/GroupDetail.jsx b/frontend/src/metabase/admin/people/components/GroupDetail.jsx
index 213f813c12fb836c2aa633a74b28cb77859acd5f..af59810d6a39886b1b56079f1a7682f569ae592d 100644
--- a/frontend/src/metabase/admin/people/components/GroupDetail.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupDetail.jsx
@@ -3,10 +3,14 @@ import React, { Component } from "react";
 import _ from "underscore";
 import cx from "classnames";
 
-import { isAdminGroup, isDefaultGroup, canEditMembership } from "metabase/lib/groups";
+import {
+  isAdminGroup,
+  isDefaultGroup,
+  canEditMembership,
+} from "metabase/lib/groups";
 
 import { PermissionsApi } from "metabase/services";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 import Popover from "metabase/components/Popover.jsx";
 import UserAvatar from "metabase/components/UserAvatar.jsx";
@@ -21,264 +25,330 @@ import Typeahead from "metabase/hoc/Typeahead.jsx";
 import AddRow from "./AddRow.jsx";
 
 const GroupDescription = ({ group }) =>
-    isDefaultGroup(group) ?
-        <div className="px2 text-measure">
-            <p>
-                {t`All users belong to the {group.name} group and can't be removed from it. Setting permissions for this group is a great way to
+  isDefaultGroup(group) ? (
+    <div className="px2 text-measure">
+      <p>
+        {t`All users belong to the {group.name} group and can't be removed from it. Setting permissions for this group is a great way to
                 make sure you know what new Metabase users will be able to see.`}
-            </p>
-        </div>
-    : isAdminGroup(group) ?
-        <div className="px2 text-measure">
-            <p>
-                {t`This is a special group whose members can see everything in the Metabase instance, and who can access and make changes to the
+      </p>
+    </div>
+  ) : isAdminGroup(group) ? (
+    <div className="px2 text-measure">
+      <p>
+        {t`This is a special group whose members can see everything in the Metabase instance, and who can access and make changes to the
                 settings in the Admin Panel, including changing permissions! So, add people to this group with care.`}
-            </p>
-            <p>
-                {t`To make sure you don't get locked out of Metabase, there always has to be at least one user in this group.`}
-            </p>
-        </div>
-    :
-        null
+      </p>
+      <p>
+        {t`To make sure you don't get locked out of Metabase, there always has to be at least one user in this group.`}
+      </p>
+    </div>
+  ) : null;
 
 // ------------------------------------------------------------ Add User Row / Autocomplete ------------------------------------------------------------
 
-const AddMemberAutocompleteSuggestion = ({ user, color, selected, onClick }) =>
-    <div className={cx("px2 py1 cursor-pointer", {"bg-brand": selected})} onClick={onClick} >
-        <span className="inline-block text-white mr2">
-            <UserAvatar background={color} user={user} />
-        </span>
-        <span className={cx("h3", {"text-white": selected})}>
-            {user.common_name}
-        </span>
-    </div>
+const AddMemberAutocompleteSuggestion = ({
+  user,
+  color,
+  selected,
+  onClick,
+}) => (
+  <div
+    className={cx("px2 py1 cursor-pointer", { "bg-brand": selected })}
+    onClick={onClick}
+  >
+    <span className="inline-block text-white mr2">
+      <UserAvatar background={color} user={user} />
+    </span>
+    <span className={cx("h3", { "text-white": selected })}>
+      {user.common_name}
+    </span>
+  </div>
+);
 
-const COLORS = ['bg-error', 'bg-purple', 'bg-brand', 'bg-gold', 'bg-green'];
+const COLORS = ["bg-error", "bg-purple", "bg-brand", "bg-gold", "bg-green"];
 
 const AddMemberTypeahead = Typeahead({
-    optionFilter: (text, user) => (user.common_name || "").toLowerCase().includes(text.toLowerCase()),
-    optionIsEqual: (userA, userB) => userA.id === userB.id
-})(({
-    suggestions,
-    selectedSuggestion,
-    onSuggestionAccepted
-}) =>
-    <Popover className="bordered" hasArrow={false} targetOffsetY={2} targetOffsetX={0} horizontalAttachments={["left"]}>
-        {suggestions && suggestions.map((user, index) =>
-            <AddMemberAutocompleteSuggestion
-                key={index}
-                user={user}
-                color={COLORS[(index % COLORS.length)]}
-                selected={selectedSuggestion && user.id === selectedSuggestion.id}
-                onClick={onSuggestionAccepted.bind(null, user)}
+  optionFilter: (text, user) =>
+    (user.common_name || "").toLowerCase().includes(text.toLowerCase()),
+  optionIsEqual: (userA, userB) => userA.id === userB.id,
+})(({ suggestions, selectedSuggestion, onSuggestionAccepted }) => (
+  <Popover
+    className="bordered"
+    hasArrow={false}
+    targetOffsetY={2}
+    targetOffsetX={0}
+    horizontalAttachments={["left"]}
+  >
+    {suggestions &&
+      suggestions.map((user, index) => (
+        <AddMemberAutocompleteSuggestion
+          key={index}
+          user={user}
+          color={COLORS[index % COLORS.length]}
+          selected={selectedSuggestion && user.id === selectedSuggestion.id}
+          onClick={onSuggestionAccepted.bind(null, user)}
+        />
+      ))}
+  </Popover>
+));
+
+const AddUserRow = ({
+  users,
+  text,
+  selectedUsers,
+  onCancel,
+  onDone,
+  onTextChange,
+  onSuggestionAccepted,
+  onRemoveUserFromSelection,
+}) => (
+  <tr>
+    <td colSpan="3" style={{ padding: 0 }}>
+      <AddRow
+        value={text}
+        isValid={selectedUsers.length}
+        placeholder="Julie McMemberson"
+        onChange={e => onTextChange(e.target.value)}
+        onDone={onDone}
+        onCancel={onCancel}
+      >
+        {selectedUsers.map(user => (
+          <div className="bg-slate-light p1 px2 mr1 rounded flex align-center">
+            {user.common_name}
+            <Icon
+              className="pl1 cursor-pointer text-slate text-grey-4-hover"
+              name="close"
+              onClick={() => onRemoveUserFromSelection(user)}
             />
-         )}
-    </Popover>
+          </div>
+        ))}
+        <div className="absolute bottom left">
+          <AddMemberTypeahead
+            value={text}
+            options={Object.values(users)}
+            onSuggestionAccepted={onSuggestionAccepted}
+          />
+        </div>
+      </AddRow>
+    </td>
+  </tr>
 );
 
-const AddUserRow = ({ users, text, selectedUsers, onCancel, onDone, onTextChange, onSuggestionAccepted, onRemoveUserFromSelection }) =>
-    <tr>
-        <td colSpan="3" style={{ padding: 0 }}>
-            <AddRow
-                value={text}
-                isValid={selectedUsers.length}
-                placeholder="Julie McMemberson"
-                onChange={(e) => onTextChange(e.target.value)}
-                onDone={onDone}
-                onCancel={onCancel}
-            >
-                { selectedUsers.map(user =>
-                    <div className="bg-slate-light p1 px2 mr1 rounded flex align-center">
-                        {user.common_name}
-                        <Icon className="pl1 cursor-pointer text-slate text-grey-4-hover" name="close" onClick={() => onRemoveUserFromSelection(user)} />
-                    </div>
-                )}
-                <div className="absolute bottom left">
-                     <AddMemberTypeahead
-                        value={text}
-                        options={Object.values(users)}
-                        onSuggestionAccepted={onSuggestionAccepted}
-                    />
-                </div>
-            </AddRow>
-        </td>
-    </tr>
-
-
 // ------------------------------------------------------------ Users Table ------------------------------------------------------------
 
-const UserRow = ({ user, showRemoveButton, onRemoveUserClicked }) =>
-    <tr>
-        <td>{user.first_name + " " + user.last_name}</td>
-        <td>{user.email}</td>
-        {showRemoveButton ? (
-             <td className="text-right cursor-pointer" onClick={onRemoveUserClicked.bind(null, user)}>
-                 <Icon name="close" className="text-grey-1" size={16} />
-             </td>
-        ) : null}
-    </tr>
+const UserRow = ({ user, showRemoveButton, onRemoveUserClicked }) => (
+  <tr>
+    <td>{user.first_name + " " + user.last_name}</td>
+    <td>{user.email}</td>
+    {showRemoveButton ? (
+      <td
+        className="text-right cursor-pointer"
+        onClick={onRemoveUserClicked.bind(null, user)}
+      >
+        <Icon name="close" className="text-grey-1" size={16} />
+      </td>
+    ) : null}
+  </tr>
+);
 
 const MembersTable = ({
-    group, members, users, showAddUser, text, selectedUsers,
-    onAddUserCancel, onAddUserDone, onAddUserTextChange,
-    onUserSuggestionAccepted, onRemoveUserClicked, onRemoveUserFromSelection
+  group,
+  members,
+  users,
+  showAddUser,
+  text,
+  selectedUsers,
+  onAddUserCancel,
+  onAddUserDone,
+  onAddUserTextChange,
+  onUserSuggestionAccepted,
+  onRemoveUserClicked,
+  onRemoveUserFromSelection,
 }) => {
-    // you can't remove people from Default and you can't remove the last user from Admin
-    const showRemoveMemeberButton = !isDefaultGroup(group) && (!isAdminGroup(group) || members.length > 1);
-
-    return (
-        <div>
-            <AdminContentTable columnTitles={[t`Members`, t`Email`]}>
-                { showAddUser && (
-                    <AddUserRow
-                        users={users}
-                        text={text}
-                        selectedUsers={selectedUsers}
-                        onCancel={onAddUserCancel}
-                        onDone={onAddUserDone}
-                        onTextChange={onAddUserTextChange}
-                        onSuggestionAccepted={onUserSuggestionAccepted}
-                        onRemoveUserFromSelection={onRemoveUserFromSelection}
-                     />
-                 )}
-                { members && members.map((user, index) =>
-                    <UserRow key={index} user={user} showRemoveButton={showRemoveMemeberButton} onRemoveUserClicked={onRemoveUserClicked} />
-                )}
-            </AdminContentTable>
-            { members.length === 0 && (
-                <div className="mt4 pt4 flex layout-centered">
-                    <AdminEmptyText message={t`A group is only as good as its members.`} />
-                </div>
-            )}
+  // you can't remove people from Default and you can't remove the last user from Admin
+  const showRemoveMemeberButton =
+    !isDefaultGroup(group) && (!isAdminGroup(group) || members.length > 1);
+
+  return (
+    <div>
+      <AdminContentTable columnTitles={[t`Members`, t`Email`]}>
+        {showAddUser && (
+          <AddUserRow
+            users={users}
+            text={text}
+            selectedUsers={selectedUsers}
+            onCancel={onAddUserCancel}
+            onDone={onAddUserDone}
+            onTextChange={onAddUserTextChange}
+            onSuggestionAccepted={onUserSuggestionAccepted}
+            onRemoveUserFromSelection={onRemoveUserFromSelection}
+          />
+        )}
+        {members &&
+          members.map((user, index) => (
+            <UserRow
+              key={index}
+              user={user}
+              showRemoveButton={showRemoveMemeberButton}
+              onRemoveUserClicked={onRemoveUserClicked}
+            />
+          ))}
+      </AdminContentTable>
+      {members.length === 0 && (
+        <div className="mt4 pt4 flex layout-centered">
+          <AdminEmptyText
+            message={t`A group is only as good as its members.`}
+          />
         </div>
-    );
-}
+      )}
+    </div>
+  );
+};
 
 // ------------------------------------------------------------ Logic ------------------------------------------------------------
 
-
-
 export default class GroupDetail extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            addUserVisible: false,
-            text: "",
-            selectedUsers: [],
-            members: null,
-            alertMessage: null
-        };
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      addUserVisible: false,
+      text: "",
+      selectedUsers: [],
+      members: null,
+      alertMessage: null,
+    };
+  }
+
+  alert(alertMessage) {
+    this.setState({ alertMessage });
+  }
+
+  onAddUsersClicked() {
+    this.setState({
+      addUserVisible: true,
+    });
+  }
+
+  onAddUserCanceled() {
+    this.setState({
+      addUserVisible: false,
+      text: "",
+      selectedUsers: [],
+    });
+  }
+
+  async onAddUserDone() {
+    this.setState({
+      addUserVisible: false,
+      text: "",
+      selectedUsers: [],
+    });
+    try {
+      await Promise.all(
+        this.state.selectedUsers.map(async user => {
+          let members = await PermissionsApi.createMembership({
+            group_id: this.props.group.id,
+            user_id: user.id,
+          });
+          this.setState({ members });
+        }),
+      );
+    } catch (error) {
+      this.alert(error && typeof error.data ? error.data : error);
     }
-
-    alert(alertMessage) {
-        this.setState({ alertMessage });
+  }
+
+  onAddUserTextChange(newText) {
+    this.setState({
+      text: newText,
+    });
+  }
+
+  onUserSuggestionAccepted(user) {
+    this.setState({
+      selectedUsers: this.state.selectedUsers.concat(user),
+      text: "",
+    });
+  }
+
+  onRemoveUserFromSelection(user) {
+    this.setState({
+      selectedUsers: this.state.selectedUsers.filter(u => u.id !== user.id),
+    });
+  }
+
+  async onRemoveUserClicked(membership) {
+    try {
+      await PermissionsApi.deleteMembership({ id: membership.membership_id });
+      const newMembers = _.reject(
+        this.getMembers(),
+        m => m.user_id === membership.user_id,
+      );
+      this.setState({ members: newMembers });
+    } catch (error) {
+      console.error("Error deleting PermissionsMembership:", error);
+      this.alert(error && typeof error.data ? error.data : error);
     }
+  }
 
-    onAddUsersClicked() {
-        this.setState({
-            addUserVisible: true
-        });
-    }
-
-    onAddUserCanceled() {
-        this.setState({
-            addUserVisible: false,
-            text: "",
-            selectedUsers: []
-        });
-    }
+  // TODO - bad!
+  // TODO - this totally breaks if you edit members and then switch groups !
+  getMembers() {
+    return (
+      this.state.members || (this.props.group && this.props.group.members) || []
+    );
+  }
 
-    async onAddUserDone() {
-        this.setState({
-            addUserVisible: false,
-            text: "",
-            selectedUsers: []
-        });
-        try {
-            await Promise.all(this.state.selectedUsers.map(async (user) => {
-                let members = await PermissionsApi.createMembership({group_id: this.props.group.id, user_id: user.id});
-                this.setState({ members });
-            }));
-        } catch (error) {
-            this.alert(error && typeof error.data ? error.data : error);
-        }
-    }
+  render() {
+    // users = array of all users for purposes of adding new users to group
+    // [group.]members = array of users currently in the group
+    let { group, users } = this.props;
+    const { text, selectedUsers, addUserVisible, alertMessage } = this.state;
+    const members = this.getMembers();
 
-    onAddUserTextChange(newText) {
-        this.setState({
-            text: newText,
-        });
-    }
+    group = group || {};
+    users = users || {};
 
-    onUserSuggestionAccepted(user) {
-        this.setState({
-            selectedUsers: this.state.selectedUsers.concat(user),
-            text: "",
-        });
+    let usedUsers = {};
+    for (const user of members) {
+      usedUsers[user.user_id] = true;
     }
-
-    onRemoveUserFromSelection(user) {
-        this.setState({
-            selectedUsers: this.state.selectedUsers.filter(u => u.id !== user.id),
-        });
+    for (const user of selectedUsers) {
+      usedUsers[user.id] = true;
     }
+    const filteredUsers = Object.values(users).filter(
+      user => !usedUsers[user.id],
+    );
 
-    async onRemoveUserClicked(membership) {
-        try {
-            await PermissionsApi.deleteMembership({ id: membership.membership_id })
-            const newMembers = _.reject(this.getMembers(), (m) => m.user_id === membership.user_id);
-            this.setState({ members: newMembers });
-        } catch (error) {
-            console.error("Error deleting PermissionsMembership:", error);
-            this.alert(error && typeof error.data ? error.data : error);
+    return (
+      <AdminPaneLayout
+        title={group.name}
+        buttonText="Add members"
+        buttonAction={
+          canEditMembership(group) ? this.onAddUsersClicked.bind(this) : null
         }
-    }
-
-    // TODO - bad!
-    // TODO - this totally breaks if you edit members and then switch groups !
-    getMembers() {
-        return this.state.members|| (this.props.group && this.props.group.members) || [];
-    }
-
-    render() {
-        // users = array of all users for purposes of adding new users to group
-        // [group.]members = array of users currently in the group
-        let { group, users } = this.props;
-        const { text, selectedUsers, addUserVisible, alertMessage } = this.state;
-        const members = this.getMembers();
-
-        group = group || {};
-        users = users || {};
-
-        let usedUsers = {};
-        for (const user of members) { usedUsers[user.user_id] = true; }
-        for (const user of selectedUsers) { usedUsers[user.id] = true; }
-        const filteredUsers = Object.values(users).filter(user => !usedUsers[user.id])
-
-        return (
-            <AdminPaneLayout
-                title={group.name}
-                buttonText="Add members"
-                buttonAction={canEditMembership(group) ? this.onAddUsersClicked.bind(this) : null}
-                buttonDisabled={addUserVisible}
-            >
-                <GroupDescription group={group} />
-                <MembersTable
-                    group={group}
-                    members={members}
-                    users={filteredUsers}
-                    showAddUser={addUserVisible}
-                    text={text || ""}
-                    selectedUsers={selectedUsers}
-                    onAddUserCancel={this.onAddUserCanceled.bind(this)}
-                    onAddUserDone={this.onAddUserDone.bind(this)}
-                    onAddUserTextChange={this.onAddUserTextChange.bind(this)}
-                    onUserSuggestionAccepted={this.onUserSuggestionAccepted.bind(this)}
-                    onRemoveUserFromSelection={this.onRemoveUserFromSelection.bind(this)}
-                    onRemoveUserClicked={this.onRemoveUserClicked.bind(this)}
-                />
-                <Alert message={alertMessage} onClose={() => this.setState({ alertMessage: null })} />
-            </AdminPaneLayout>
-        );
-    }
+        buttonDisabled={addUserVisible}
+      >
+        <GroupDescription group={group} />
+        <MembersTable
+          group={group}
+          members={members}
+          users={filteredUsers}
+          showAddUser={addUserVisible}
+          text={text || ""}
+          selectedUsers={selectedUsers}
+          onAddUserCancel={this.onAddUserCanceled.bind(this)}
+          onAddUserDone={this.onAddUserDone.bind(this)}
+          onAddUserTextChange={this.onAddUserTextChange.bind(this)}
+          onUserSuggestionAccepted={this.onUserSuggestionAccepted.bind(this)}
+          onRemoveUserFromSelection={this.onRemoveUserFromSelection.bind(this)}
+          onRemoveUserClicked={this.onRemoveUserClicked.bind(this)}
+        />
+        <Alert
+          message={alertMessage}
+          onClose={() => this.setState({ alertMessage: null })}
+        />
+      </AdminPaneLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/components/GroupSelect.jsx b/frontend/src/metabase/admin/people/components/GroupSelect.jsx
index 9a0c030761091529ec6f0790a68d21cffe611084..7c2a9aa6e54ee5aa523c16de5b82a931befc3359 100644
--- a/frontend/src/metabase/admin/people/components/GroupSelect.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupSelect.jsx
@@ -2,41 +2,59 @@ import React from "react";
 
 import CheckBox from "metabase/components/CheckBox.jsx";
 
-import { isDefaultGroup, isAdminGroup, canEditMembership, getGroupColor } from "metabase/lib/groups";
+import {
+  isDefaultGroup,
+  isAdminGroup,
+  canEditMembership,
+  getGroupColor,
+} from "metabase/lib/groups";
 import cx from "classnames";
 import _ from "underscore";
 
 export const GroupOption = ({ group, selectedGroups = {}, onGroupChange }) => {
-    const disabled = !canEditMembership(group);
-    const selected = isDefaultGroup(group) || selectedGroups[group.id];
-    return (
-        <div className={cx("GroupOption flex align-center p1 px2", { "cursor-pointer": !disabled })} onClick={() => !disabled && onGroupChange(group, !selected) }>
-            <span className={cx("pr1", getGroupColor(group), { disabled })}>
-                <CheckBox
-                    checked={selected}
-                    size={18}
-                />
-            </span>
-            {group.name}
-        </div>
-    )
-}
+  const disabled = !canEditMembership(group);
+  const selected = isDefaultGroup(group) || selectedGroups[group.id];
+  return (
+    <div
+      className={cx("GroupOption flex align-center p1 px2", {
+        "cursor-pointer": !disabled,
+      })}
+      onClick={() => !disabled && onGroupChange(group, !selected)}
+    >
+      <span className={cx("pr1", getGroupColor(group), { disabled })}>
+        <CheckBox checked={selected} size={18} />
+      </span>
+      {group.name}
+    </div>
+  );
+};
 
 export const GroupSelect = ({ groups, selectedGroups, onGroupChange }) => {
-    const other = groups.filter(g => !isAdminGroup(g) && !isDefaultGroup(g));
-    return (
-        <div className="GroupSelect py1">
-            <GroupOption group={_.find(groups, isAdminGroup)} selectedGroups={selectedGroups} onGroupChange={onGroupChange} />
-            <GroupOption group={_.find(groups, isDefaultGroup)} selectedGroups={selectedGroups} onGroupChange={onGroupChange} />
-            { other.length > 0 &&
-                <div key="divider" className="border-bottom pb1 mb1" />
-            }
-            { other.map(group =>
-                <GroupOption group={group} selectedGroups={selectedGroups} onGroupChange={onGroupChange} />
-            )}
-        </div>
-    )
-}
-
+  const other = groups.filter(g => !isAdminGroup(g) && !isDefaultGroup(g));
+  return (
+    <div className="GroupSelect py1">
+      <GroupOption
+        group={_.find(groups, isAdminGroup)}
+        selectedGroups={selectedGroups}
+        onGroupChange={onGroupChange}
+      />
+      <GroupOption
+        group={_.find(groups, isDefaultGroup)}
+        selectedGroups={selectedGroups}
+        onGroupChange={onGroupChange}
+      />
+      {other.length > 0 && (
+        <div key="divider" className="border-bottom pb1 mb1" />
+      )}
+      {other.map(group => (
+        <GroupOption
+          group={group}
+          selectedGroups={selectedGroups}
+          onGroupChange={onGroupChange}
+        />
+      ))}
+    </div>
+  );
+};
 
 export default GroupSelect;
diff --git a/frontend/src/metabase/admin/people/components/GroupSummary.jsx b/frontend/src/metabase/admin/people/components/GroupSummary.jsx
index a57e22718dd374dabc3229029f88d5a89f5c7272..1665ac6c6b92646574bc81cdb67ba18bc96a6e0c 100644
--- a/frontend/src/metabase/admin/people/components/GroupSummary.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupSummary.jsx
@@ -1,28 +1,40 @@
 import React from "react";
 
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { inflect } from "metabase/lib/formatting";
 import { isAdminGroup, isDefaultGroup } from "metabase/lib/groups";
 
 const GroupSummary = ({ groups, selectedGroups }) => {
-    let adminGroup = _.find(groups, isAdminGroup);
-    let otherGroups = groups.filter(g => selectedGroups[g.id] && !isAdminGroup(g) && !isDefaultGroup(g));
-    if (selectedGroups[adminGroup.id]) {
-        return (
-            <span>
-                <span className="text-purple">{t`Admin`}</span>
-                { otherGroups.length > 0 && " and " }
-                { otherGroups.length > 0 && <span className="text-brand">{otherGroups.length + " other " + inflect("group", otherGroups.length)}</span> }
-            </span>
-        );
-    } else if (otherGroups.length === 1) {
-        return <span className="text-brand">{otherGroups[0].name}</span>;
-    } else if (otherGroups.length > 1) {
-        return <span className="text-brand">{otherGroups.length + " " + inflect("group", otherGroups.length)}</span>;
-    } else {
-        return <span>{t`Default`}</span>;
-    }
-}
+  let adminGroup = _.find(groups, isAdminGroup);
+  let otherGroups = groups.filter(
+    g => selectedGroups[g.id] && !isAdminGroup(g) && !isDefaultGroup(g),
+  );
+  if (selectedGroups[adminGroup.id]) {
+    return (
+      <span>
+        <span className="text-purple">{t`Admin`}</span>
+        {otherGroups.length > 0 && " and "}
+        {otherGroups.length > 0 && (
+          <span className="text-brand">
+            {otherGroups.length +
+              " other " +
+              inflect("group", otherGroups.length)}
+          </span>
+        )}
+      </span>
+    );
+  } else if (otherGroups.length === 1) {
+    return <span className="text-brand">{otherGroups[0].name}</span>;
+  } else if (otherGroups.length > 1) {
+    return (
+      <span className="text-brand">
+        {otherGroups.length + " " + inflect("group", otherGroups.length)}
+      </span>
+    );
+  } else {
+    return <span>{t`Default`}</span>;
+  }
+};
 
 export default GroupSummary;
diff --git a/frontend/src/metabase/admin/people/components/GroupsListing.jsx b/frontend/src/metabase/admin/people/components/GroupsListing.jsx
index 740f15da107ff4260c710c809dc21b1e5ad23226..785bda2aba2d9a248237c608844a307794e21320 100644
--- a/frontend/src/metabase/admin/people/components/GroupsListing.jsx
+++ b/frontend/src/metabase/admin/people/components/GroupsListing.jsx
@@ -9,7 +9,7 @@ import { isDefaultGroup, isAdminGroup } from "metabase/lib/groups";
 import { KEYCODE_ENTER } from "metabase/lib/keyboard";
 
 import { PermissionsApi } from "metabase/services";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 import Input from "metabase/components/Input.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
@@ -26,308 +26,396 @@ import AddRow from "./AddRow.jsx";
 // ------------------------------------------------------------ Add Group ------------------------------------------------------------
 
 function AddGroupRow({ text, onCancelClicked, onCreateClicked, onTextChange }) {
-    const textIsValid = text && text.length;
-    return (
-        <tr>
-            <td colSpan="3" style={{ padding: 0 }}>
-                <AddRow
-                    value={text}
-                    isValid={textIsValid}
-                    placeholder={t`Something like "Marketing"`}
-                    onChange={(e) => onTextChange(e.target.value)}
-                    onKeyDown={(e) => {
-                        if (e.keyCode === KEYCODE_ENTER) {
-                            onCreateClicked();
-                        }
-                    }}
-                    onDone={onCreateClicked}
-                    onCancel={onCancelClicked}
-                />
-            </td>
-        </tr>
-    );
+  const textIsValid = text && text.length;
+  return (
+    <tr>
+      <td colSpan="3" style={{ padding: 0 }}>
+        <AddRow
+          value={text}
+          isValid={textIsValid}
+          placeholder={t`Something like "Marketing"`}
+          onChange={e => onTextChange(e.target.value)}
+          onKeyDown={e => {
+            if (e.keyCode === KEYCODE_ENTER) {
+              onCreateClicked();
+            }
+          }}
+          onDone={onCreateClicked}
+          onCancel={onCancelClicked}
+        />
+      </td>
+    </tr>
+  );
 }
 
-
 // ------------------------------------------------------------ Groups Table: editing ------------------------------------------------------------
 
-function DeleteGroupModal({ group, onConfirm = () => {} , onClose = () => {} }) {
-    return (
-        <ModalContent title={t`Remove this group?`} onClose={onClose}>
-            <p className="px4 pb4">
-                {t`Are you sure? All members of this group will lose any permissions settings the have based on this group.
+function DeleteGroupModal({ group, onConfirm = () => {}, onClose = () => {} }) {
+  return (
+    <ModalContent title={t`Remove this group?`} onClose={onClose}>
+      <p className="px4 pb4">
+        {t`Are you sure? All members of this group will lose any permissions settings the have based on this group.
                 This can't be undone.`}
-            </p>
-            <div className="Form-actions">
-                <button className="Button Button--danger" onClick={() => { onClose(); onConfirm(group); }}>
-                    {t`Yes`}
-                </button>
-                <button className="Button ml1" onClick={onClose}>
-                    {t`No`}
-                </button>
-            </div>
-        </ModalContent>
-    );
+      </p>
+      <div className="Form-actions">
+        <button
+          className="Button Button--danger"
+          onClick={() => {
+            onClose();
+            onConfirm(group);
+          }}
+        >
+          {t`Yes`}
+        </button>
+        <button className="Button ml1" onClick={onClose}>
+          {t`No`}
+        </button>
+      </div>
+    </ModalContent>
+  );
 }
 
 function ActionsPopover({ group, onEditGroupClicked, onDeleteGroupClicked }) {
-    return (
-        <PopoverWithTrigger className="block" triggerElement={<Icon className="text-grey-1" name="ellipsis" />}>
-            <ul className="UserActionsSelect">
-                <li className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer" onClick={onEditGroupClicked.bind(null, group)}>
-                    {t`Edit Name`}
-                </li>
-                <li className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer text-error">
-                    <ModalWithTrigger triggerElement={t`Remove Group`}>
-                        <DeleteGroupModal group={group} onConfirm={onDeleteGroupClicked} />
-                    </ModalWithTrigger>
-                </li>
-            </ul>
-        </PopoverWithTrigger>
-    )
+  return (
+    <PopoverWithTrigger
+      className="block"
+      triggerElement={<Icon className="text-grey-1" name="ellipsis" />}
+    >
+      <ul className="UserActionsSelect">
+        <li
+          className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer"
+          onClick={onEditGroupClicked.bind(null, group)}
+        >
+          {t`Edit Name`}
+        </li>
+        <li className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer text-error">
+          <ModalWithTrigger triggerElement={t`Remove Group`}>
+            <DeleteGroupModal group={group} onConfirm={onDeleteGroupClicked} />
+          </ModalWithTrigger>
+        </li>
+      </ul>
+    </PopoverWithTrigger>
+  );
 }
 
-function EditingGroupRow({ group, textHasChanged, onTextChange, onCancelClicked, onDoneClicked }) {
-    const textIsValid = group.name && group.name.length;
-    return (
-        <tr className="bordered border-brand rounded">
-            <td>
-                <Input className="AdminInput h3" type="text" autoFocus={true} value={group.name}
-                       onChange={(e) => onTextChange(e.target.value)}
-                />
-            </td>
-            <td />
-            <td className="text-right">
-                <span className="link no-decoration cursor-pointer" onClick={onCancelClicked}>
-                    Cancel
-                </span>
-                <button className={cx("Button ml2", {"Button--primary": textIsValid && textHasChanged})} disabled={!textIsValid || !textHasChanged} onClick={onDoneClicked}>
-                    {t`Done`}
-                </button>
-            </td>
-        </tr>
-    )
+function EditingGroupRow({
+  group,
+  textHasChanged,
+  onTextChange,
+  onCancelClicked,
+  onDoneClicked,
+}) {
+  const textIsValid = group.name && group.name.length;
+  return (
+    <tr className="bordered border-brand rounded">
+      <td>
+        <Input
+          className="AdminInput h3"
+          type="text"
+          autoFocus={true}
+          value={group.name}
+          onChange={e => onTextChange(e.target.value)}
+        />
+      </td>
+      <td />
+      <td className="text-right">
+        <span
+          className="link no-decoration cursor-pointer"
+          onClick={onCancelClicked}
+        >
+          Cancel
+        </span>
+        <button
+          className={cx("Button ml2", {
+            "Button--primary": textIsValid && textHasChanged,
+          })}
+          disabled={!textIsValid || !textHasChanged}
+          onClick={onDoneClicked}
+        >
+          {t`Done`}
+        </button>
+      </td>
+    </tr>
+  );
 }
 
-
 // ------------------------------------------------------------ Groups Table: not editing ------------------------------------------------------------
 
-const COLORS = ['bg-error', 'bg-purple', 'bg-brand', 'bg-gold', 'bg-green'];
-
-function GroupRow({ group, groupBeingEdited, index, showGroupDetail, showAddGroupRow, onEditGroupClicked, onDeleteGroupClicked,
-                    onEditGroupTextChange, onEditGroupCancelClicked, onEditGroupDoneClicked }) {
-    const color  = COLORS[(index % COLORS.length)];
-    const showActionsButton = !isDefaultGroup(group) && !isAdminGroup(group);
-    const editing = groupBeingEdited && groupBeingEdited.id === group.id;
-
-    return editing ? (
-        <EditingGroupRow group={groupBeingEdited} textHasChanged={group.name !== groupBeingEdited.name} onTextChange={onEditGroupTextChange}
-                         onCancelClicked={onEditGroupCancelClicked} onDoneClicked={onEditGroupDoneClicked}
-        />
-    ) : (
-        <tr>
-            <td>
-                <Link to={"/admin/people/groups/" + group.id} className="link no-decoration">
-                    <span className="text-white inline-block">
-                        <UserAvatar background={color} user={{first_name: group.name}} />
-                    </span>
-                    <span className="ml2 text-bold">
-                        {group.name}
-                    </span>
-                </Link>
-            </td>
-            <td>
-                {group.members || 0}
-            </td>
-            <td className="text-right">
-                {showActionsButton ? (
-                     <ActionsPopover group={group} onEditGroupClicked={onEditGroupClicked} onDeleteGroupClicked={onDeleteGroupClicked} />
-                 ) : null}
-            </td>
-        </tr>
-    );
+const COLORS = ["bg-error", "bg-purple", "bg-brand", "bg-gold", "bg-green"];
+
+function GroupRow({
+  group,
+  groupBeingEdited,
+  index,
+  showGroupDetail,
+  showAddGroupRow,
+  onEditGroupClicked,
+  onDeleteGroupClicked,
+  onEditGroupTextChange,
+  onEditGroupCancelClicked,
+  onEditGroupDoneClicked,
+}) {
+  const color = COLORS[index % COLORS.length];
+  const showActionsButton = !isDefaultGroup(group) && !isAdminGroup(group);
+  const editing = groupBeingEdited && groupBeingEdited.id === group.id;
+
+  return editing ? (
+    <EditingGroupRow
+      group={groupBeingEdited}
+      textHasChanged={group.name !== groupBeingEdited.name}
+      onTextChange={onEditGroupTextChange}
+      onCancelClicked={onEditGroupCancelClicked}
+      onDoneClicked={onEditGroupDoneClicked}
+    />
+  ) : (
+    <tr>
+      <td>
+        <Link
+          to={"/admin/people/groups/" + group.id}
+          className="link no-decoration"
+        >
+          <span className="text-white inline-block">
+            <UserAvatar background={color} user={{ first_name: group.name }} />
+          </span>
+          <span className="ml2 text-bold">{group.name}</span>
+        </Link>
+      </td>
+      <td>{group.members || 0}</td>
+      <td className="text-right">
+        {showActionsButton ? (
+          <ActionsPopover
+            group={group}
+            onEditGroupClicked={onEditGroupClicked}
+            onDeleteGroupClicked={onDeleteGroupClicked}
+          />
+        ) : null}
+      </td>
+    </tr>
+  );
 }
 
-function GroupsTable({ groups, text, groupBeingEdited, showAddGroupRow, onAddGroupCanceled, onAddGroupCreateButtonClicked, onAddGroupTextChanged,
-                       onEditGroupClicked, onDeleteGroupClicked, onEditGroupTextChange, onEditGroupCancelClicked, onEditGroupDoneClicked }) {
-
-    return (
-        <AdminContentTable columnTitles={[t`Group name`, t`Members`]}>
-            {showAddGroupRow ? (
-                 <AddGroupRow text={text} onCancelClicked={onAddGroupCanceled} onCreateClicked={onAddGroupCreateButtonClicked} onTextChange={onAddGroupTextChanged} />
-             ) : null}
-            {groups && groups.map((group, index) =>
-                <GroupRow key={group.id} group={group} index={index} groupBeingEdited={groupBeingEdited}
-                          onEditGroupClicked={onEditGroupClicked}
-                          onDeleteGroupClicked={onDeleteGroupClicked}
-                          onEditGroupTextChange={onEditGroupTextChange}
-                          onEditGroupCancelClicked={onEditGroupCancelClicked}
-                          onEditGroupDoneClicked={onEditGroupDoneClicked}
-                />
-             )}
-        </AdminContentTable>
-    );
+function GroupsTable({
+  groups,
+  text,
+  groupBeingEdited,
+  showAddGroupRow,
+  onAddGroupCanceled,
+  onAddGroupCreateButtonClicked,
+  onAddGroupTextChanged,
+  onEditGroupClicked,
+  onDeleteGroupClicked,
+  onEditGroupTextChange,
+  onEditGroupCancelClicked,
+  onEditGroupDoneClicked,
+}) {
+  return (
+    <AdminContentTable columnTitles={[t`Group name`, t`Members`]}>
+      {showAddGroupRow ? (
+        <AddGroupRow
+          text={text}
+          onCancelClicked={onAddGroupCanceled}
+          onCreateClicked={onAddGroupCreateButtonClicked}
+          onTextChange={onAddGroupTextChanged}
+        />
+      ) : null}
+      {groups &&
+        groups.map((group, index) => (
+          <GroupRow
+            key={group.id}
+            group={group}
+            index={index}
+            groupBeingEdited={groupBeingEdited}
+            onEditGroupClicked={onEditGroupClicked}
+            onDeleteGroupClicked={onDeleteGroupClicked}
+            onEditGroupTextChange={onEditGroupTextChange}
+            onEditGroupCancelClicked={onEditGroupCancelClicked}
+            onEditGroupDoneClicked={onEditGroupDoneClicked}
+          />
+        ))}
+    </AdminContentTable>
+  );
 }
 
-
 // ------------------------------------------------------------ Logic ------------------------------------------------------------
 
 function sortGroups(groups) {
-    return _.sortBy(groups, (group) => group.name && group.name.toLowerCase());
+  return _.sortBy(groups, group => group.name && group.name.toLowerCase());
 }
 
 export default class GroupsListing extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            text: "",
-            showAddGroupRow: false,
-            groups: null,
-            groupBeingEdited: null,
-            alertMessage: null,
-        };
-    }
-
-    alert(alertMessage) {
-        this.setState({ alertMessage });
-    }
-
-    onAddGroupCanceled() {
-        this.setState({
-            showAddGroupRow: false
-        });
-    }
-
-    // TODO: move this to Redux
-    onAddGroupCreateButtonClicked() {
-        MetabaseAnalytics.trackEvent("People Groups", "Group Added");
-        PermissionsApi.createGroup({name: this.state.text}).then((newGroup) => {
-            const groups = this.state.groups || this.props.groups || [];
-            const newGroups = sortGroups(_.union(groups, [newGroup]));
-
-            this.setState({
-                groups: newGroups,
-                showAddGroupRow: false,
-                text: ""
-            });
-        }, (error) => {
-            console.error('Error creating group:', error);
-            if (error.data && typeof error.data === "string") this.alert(error.data);
-        });
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      text: "",
+      showAddGroupRow: false,
+      groups: null,
+      groupBeingEdited: null,
+      alertMessage: null,
+    };
+  }
+
+  alert(alertMessage) {
+    this.setState({ alertMessage });
+  }
+
+  onAddGroupCanceled() {
+    this.setState({
+      showAddGroupRow: false,
+    });
+  }
+
+  // TODO: move this to Redux
+  onAddGroupCreateButtonClicked() {
+    MetabaseAnalytics.trackEvent("People Groups", "Group Added");
+    PermissionsApi.createGroup({ name: this.state.text }).then(
+      newGroup => {
+        const groups = this.state.groups || this.props.groups || [];
+        const newGroups = sortGroups(_.union(groups, [newGroup]));
 
-    onAddGroupTextChanged(newText) {
         this.setState({
-            text: newText
+          groups: newGroups,
+          showAddGroupRow: false,
+          text: "",
         });
+      },
+      error => {
+        console.error("Error creating group:", error);
+        if (error.data && typeof error.data === "string")
+          this.alert(error.data);
+      },
+    );
+  }
+
+  onAddGroupTextChanged(newText) {
+    this.setState({
+      text: newText,
+    });
+  }
+
+  onCreateAGroupButtonClicked() {
+    this.setState({
+      text: "",
+      showAddGroupRow: true,
+      groupBeingEdited: null,
+    });
+  }
+
+  onEditGroupClicked(group) {
+    this.setState({
+      groupBeingEdited: _.clone(group),
+      text: "",
+      showAddGroupRow: false,
+    });
+  }
+
+  onEditGroupTextChange(newText) {
+    let groupBeingEdited = this.state.groupBeingEdited;
+    groupBeingEdited.name = newText;
+
+    this.setState({
+      groupBeingEdited: groupBeingEdited,
+    });
+  }
+
+  onEditGroupCancelClicked() {
+    this.setState({
+      groupBeingEdited: null,
+    });
+  }
+
+  // TODO: move this to Redux
+  onEditGroupDoneClicked() {
+    const groups = this.state.groups || this.props.groups || [];
+    const originalGroup = _.findWhere(groups, {
+      id: this.state.groupBeingEdited.id,
+    });
+    const group = this.state.groupBeingEdited;
+
+    // if name hasn't changed there is nothing to do
+    if (originalGroup.name === group.name) {
+      this.setState({
+        groupBeingEdited: null,
+      });
+      return;
     }
 
-    onCreateAGroupButtonClicked() {
-        this.setState({
-            text: "",
-            showAddGroupRow: true,
-            groupBeingEdited: null
-        });
-    }
+    // ok, fire off API call to change the group
+    MetabaseAnalytics.trackEvent("People Groups", "Group Updated");
+    PermissionsApi.updateGroup({ id: group.id, name: group.name }).then(
+      newGroup => {
+        // now replace the original group with the new group and update state
+        let newGroups = _.reject(groups, g => g.id === group.id);
+        newGroups = sortGroups(_.union(newGroups, [newGroup]));
 
-    onEditGroupClicked(group) {
         this.setState({
-            groupBeingEdited: _.clone(group),
-            text: "",
-            showAddGroupRow: false
+          groups: newGroups,
+          groupBeingEdited: null,
         });
-    }
-
-    onEditGroupTextChange(newText) {
-        let groupBeingEdited = this.state.groupBeingEdited;
-        groupBeingEdited.name = newText;
-
+      },
+      error => {
+        console.error("Error updating group name:", error);
+        if (error.data && typeof error.data === "string")
+          this.alert(error.data);
+      },
+    );
+  }
+
+  // TODO: move this to Redux
+  async onDeleteGroupClicked(group) {
+    const groups = this.state.groups || this.props.groups || [];
+    MetabaseAnalytics.trackEvent("People Groups", "Group Deleted");
+    PermissionsApi.deleteGroup({ id: group.id }).then(
+      () => {
+        const newGroups = sortGroups(_.reject(groups, g => g.id === group.id));
         this.setState({
-            groupBeingEdited: groupBeingEdited
+          groups: newGroups,
         });
-    }
+      },
+      error => {
+        console.error("Error deleting group: ", error);
+        if (error.data && typeof error.data === "string")
+          this.alert(error.data);
+      },
+    );
+  }
 
-    onEditGroupCancelClicked() {
-        this.setState({
-            groupBeingEdited: null
-        });
-    }
+  render() {
+    const { alertMessage } = this.state;
+    let { groups } = this.props;
+    groups = this.state.groups || groups || [];
 
-    // TODO: move this to Redux
-    onEditGroupDoneClicked() {
-        const groups = this.state.groups || this.props.groups || [];
-        const originalGroup = _.findWhere(groups, {id: this.state.groupBeingEdited.id});
-        const group = this.state.groupBeingEdited;
-
-        // if name hasn't changed there is nothing to do
-        if (originalGroup.name === group.name) {
-            this.setState({
-                groupBeingEdited: null
-            });
-            return;
+    return (
+      <AdminPaneLayout
+        title={t`Groups`}
+        buttonText={t`Create a group`}
+        buttonAction={
+          this.state.showAddGroupRow
+            ? null
+            : this.onCreateAGroupButtonClicked.bind(this)
         }
-
-        // ok, fire off API call to change the group
-        MetabaseAnalytics.trackEvent("People Groups", "Group Updated");
-        PermissionsApi.updateGroup({id: group.id, name: group.name}).then((newGroup) => {
-            // now replace the original group with the new group and update state
-            let newGroups = _.reject(groups, (g) => g.id === group.id);
-            newGroups = sortGroups(_.union(newGroups, [newGroup]));
-
-            this.setState({
-                groups: newGroups,
-                groupBeingEdited: null
-            });
-
-        }, (error) => {
-            console.error("Error updating group name:", error);
-            if (error.data && typeof error.data === "string") this.alert(error.data);
-        });
-    }
-
-    // TODO: move this to Redux
-    async onDeleteGroupClicked(group) {
-        const groups = this.state.groups || this.props.groups || [];
-        MetabaseAnalytics.trackEvent("People Groups", "Group Deleted");
-        PermissionsApi.deleteGroup({id: group.id}).then(() => {
-            const newGroups = sortGroups(_.reject(groups, (g) => g.id === group.id));
-            this.setState({
-                groups: newGroups
-            });
-        }, (error) => {
-            console.error("Error deleting group: ", error);
-            if (error.data && typeof error.data === "string") this.alert(error.data);
-        });
-    }
-
-    render() {
-        const { alertMessage } = this.state;
-        let { groups } = this.props;
-        groups = this.state.groups || groups || [];
-
-        return (
-            <AdminPaneLayout
-                title={t`Groups`}
-                buttonText={t`Create a group`}
-                buttonAction={this.state.showAddGroupRow ? null : this.onCreateAGroupButtonClicked.bind(this)}
-                description={t`You can use groups to control your users' access to your data. Put users in groups and then go to the Permissions section to control each group's access. The Administrators and All Users groups are special default groups that can't be removed.`}
-            >
-                <GroupsTable
-                    groups={groups}
-                    text={this.state.text}
-                    showAddGroupRow={this.state.showAddGroupRow}
-                    groupBeingEdited={this.state.groupBeingEdited}
-                    onAddGroupCanceled={this.onAddGroupCanceled.bind(this)}
-                    onAddGroupCreateButtonClicked={this.onAddGroupCreateButtonClicked.bind(this)}
-                    onAddGroupTextChanged={this.onAddGroupTextChanged.bind(this)}
-                    onEditGroupClicked={this.onEditGroupClicked.bind(this)}
-                    onEditGroupTextChange={this.onEditGroupTextChange.bind(this)}
-                    onEditGroupCancelClicked={this.onEditGroupCancelClicked.bind(this)}
-                    onEditGroupDoneClicked={this.onEditGroupDoneClicked.bind(this)}
-                    onDeleteGroupClicked={this.onDeleteGroupClicked.bind(this)}
-                />
-                <Alert message={alertMessage} onClose={() => this.setState({ alertMessage: null })} />
-            </AdminPaneLayout>
-        );
-    }
+        description={t`You can use groups to control your users' access to your data. Put users in groups and then go to the Permissions section to control each group's access. The Administrators and All Users groups are special default groups that can't be removed.`}
+      >
+        <GroupsTable
+          groups={groups}
+          text={this.state.text}
+          showAddGroupRow={this.state.showAddGroupRow}
+          groupBeingEdited={this.state.groupBeingEdited}
+          onAddGroupCanceled={this.onAddGroupCanceled.bind(this)}
+          onAddGroupCreateButtonClicked={this.onAddGroupCreateButtonClicked.bind(
+            this,
+          )}
+          onAddGroupTextChanged={this.onAddGroupTextChanged.bind(this)}
+          onEditGroupClicked={this.onEditGroupClicked.bind(this)}
+          onEditGroupTextChange={this.onEditGroupTextChange.bind(this)}
+          onEditGroupCancelClicked={this.onEditGroupCancelClicked.bind(this)}
+          onEditGroupDoneClicked={this.onEditGroupDoneClicked.bind(this)}
+          onDeleteGroupClicked={this.onDeleteGroupClicked.bind(this)}
+        />
+        <Alert
+          message={alertMessage}
+          onClose={() => this.setState({ alertMessage: null })}
+        />
+      </AdminPaneLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx b/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx
index 64c6c7d3efa471cfe7c3e680eda5fc3c942c3965..59d09e138fd6100e09337fd31c698aa4f82d3cff 100644
--- a/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx
+++ b/frontend/src/metabase/admin/people/components/UserActionsSelect.jsx
@@ -5,68 +5,99 @@ import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon.jsx";
 import MetabaseSettings from "metabase/lib/settings";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
-import { t } from 'c-3po';
-import { MODAL_EDIT_DETAILS,
-         MODAL_INVITE_RESENT,
-         MODAL_REMOVE_USER,
-         MODAL_RESET_PASSWORD } from "../containers/PeopleListingApp.jsx";
+import { t } from "c-3po";
+import {
+  MODAL_EDIT_DETAILS,
+  MODAL_INVITE_RESENT,
+  MODAL_REMOVE_USER,
+  MODAL_RESET_PASSWORD,
+} from "../containers/PeopleListingApp.jsx";
 
 export default class UserActionsSelect extends Component {
+  static propTypes = {
+    user: PropTypes.object.isRequired,
+    isActiveUser: PropTypes.bool.isRequired,
+    showModal: PropTypes.func.isRequired,
+    resendInvite: PropTypes.func.isRequired,
+  };
 
-    static propTypes = {
-        user: PropTypes.object.isRequired,
-        isActiveUser: PropTypes.bool.isRequired,
-        showModal: PropTypes.func.isRequired,
-        resendInvite: PropTypes.func.isRequired,
-    };
+  onEditDetails() {
+    this.props.showModal({
+      type: MODAL_EDIT_DETAILS,
+      details: { user: this.props.user },
+    });
+    this.refs.popover.toggle();
+  }
 
-    onEditDetails() {
-        this.props.showModal({type: MODAL_EDIT_DETAILS, details: {user: this.props.user}});
-        this.refs.popover.toggle();
-    }
+  onResendInvite() {
+    this.props.resendInvite(this.props.user);
+    this.props.showModal({
+      type: MODAL_INVITE_RESENT,
+      details: { user: this.props.user },
+    });
+    this.refs.popover.toggle();
+  }
 
-    onResendInvite() {
-        this.props.resendInvite(this.props.user);
-        this.props.showModal({type: MODAL_INVITE_RESENT, details: {user: this.props.user}});
-        this.refs.popover.toggle();
+  onResetPassword() {
+    if (window.OSX) {
+      window.OSX.resetPassword();
+      return;
     }
 
-    onResetPassword() {
-        if (window.OSX) {
-            window.OSX.resetPassword();
-            return;
-        }
+    this.props.showModal({
+      type: MODAL_RESET_PASSWORD,
+      details: { user: this.props.user },
+    });
+    this.refs.popover.toggle();
+  }
 
-        this.props.showModal({type: MODAL_RESET_PASSWORD, details: {user: this.props.user}});
-        this.refs.popover.toggle();
-    }
+  onRemoveUser() {
+    this.props.showModal({
+      type: MODAL_REMOVE_USER,
+      details: { user: this.props.user },
+    });
+    this.refs.popover.toggle();
+  }
 
-    onRemoveUser() {
-        this.props.showModal({type: MODAL_REMOVE_USER, details: {user: this.props.user}});
-        this.refs.popover.toggle();
-    }
+  render() {
+    let { isActiveUser, user } = this.props;
 
-    render() {
-        let { isActiveUser, user } = this.props;
-
-        return (
-            <PopoverWithTrigger ref="popover"
-                                className="block"
-                                triggerElement={<span className="text-grey-1"><Icon name={'ellipsis'}></Icon></span>}>
-                <ul className="UserActionsSelect">
-                    <li className="py1 px2 bg-brand-hover text-white-hover cursor-pointer" onClick={this.onEditDetails.bind(this)}>{t`Edit Details`}</li>
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        className="block"
+        triggerElement={
+          <span className="text-grey-1">
+            <Icon name={"ellipsis"} />
+          </span>
+        }
+      >
+        <ul className="UserActionsSelect">
+          <li
+            className="py1 px2 bg-brand-hover text-white-hover cursor-pointer"
+            onClick={this.onEditDetails.bind(this)}
+          >{t`Edit Details`}</li>
 
-                    { (user.last_login === null && MetabaseSettings.isEmailConfigured()) ?
-                        <li className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer" onClick={this.onResendInvite.bind(this)}>{t`Re-send Invite`}</li>
-                    :
-                        <li className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer" onClick={this.onResetPassword.bind(this)}>{t`Reset Password`}</li>
-                    }
+          {user.last_login === null && MetabaseSettings.isEmailConfigured() ? (
+            <li
+              className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer"
+              onClick={this.onResendInvite.bind(this)}
+            >{t`Re-send Invite`}</li>
+          ) : (
+            <li
+              className="pt1 pb2 px2 bg-brand-hover text-white-hover cursor-pointer"
+              onClick={this.onResetPassword.bind(this)}
+            >{t`Reset Password`}</li>
+          )}
 
-                    { !isActiveUser &&
-                        <li className="p2 border-top bg-error-hover text-error text-white-hover cursor-pointer"  onClick={this.onRemoveUser.bind(this)}>{t`Remove`}</li>
-                    }
-                </ul>
-            </PopoverWithTrigger>
-        );
-    }
+          {!isActiveUser && (
+            <li
+              className="p2 border-top bg-error-hover text-error text-white-hover cursor-pointer"
+              onClick={this.onRemoveUser.bind(this)}
+            >{t`Remove`}</li>
+          )}
+        </ul>
+      </PopoverWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx b/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx
index 40c8e115b4442c3c59e1e1bd5ae87b3cd6ea76c0..880a55b0dd444257c744b4cbfb3ea75f2a6c2e06 100644
--- a/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx
+++ b/frontend/src/metabase/admin/people/components/UserGroupSelect.jsx
@@ -11,76 +11,79 @@ import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 import GroupSelect from "./GroupSelect.jsx";
 import GroupSummary from "./GroupSummary.jsx";
 
-const GroupOption = ({ name, color, selected, disabled, onChange }) =>
-    <div className={cx("flex align-center p1 px2", { "cursor-pointer": !disabled })} onClick={() => !disabled && onChange(!selected) }>
-        <span className={cx("pr1", color, { disabled })}>
-            <CheckBox
-                checked={selected}
-                size={18}
-            />
-        </span>
-        {name}
-    </div>
+const GroupOption = ({ name, color, selected, disabled, onChange }) => (
+  <div
+    className={cx("flex align-center p1 px2", { "cursor-pointer": !disabled })}
+    onClick={() => !disabled && onChange(!selected)}
+  >
+    <span className={cx("pr1", color, { disabled })}>
+      <CheckBox checked={selected} size={18} />
+    </span>
+    {name}
+  </div>
+);
 
 GroupOption.propTypes = {
-    name: PropTypes.string,
-    color: PropTypes.string,
-    selected: PropTypes.bool,
-    disabled: PropTypes.bool,
-    onChange: PropTypes.func,
-}
+  name: PropTypes.string,
+  color: PropTypes.string,
+  selected: PropTypes.bool,
+  disabled: PropTypes.bool,
+  onChange: PropTypes.func,
+};
 
 export default class UserGroupSelect extends Component {
-    static propTypes = {
-        user: PropTypes.object.isRequired,
-        groups: PropTypes.array,
-        createMembership: PropTypes.func.isRequired,
-        deleteMembership: PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    user: PropTypes.object.isRequired,
+    groups: PropTypes.array,
+    createMembership: PropTypes.func.isRequired,
+    deleteMembership: PropTypes.func.isRequired,
+  };
 
-    static defaultProps = {
-        isInitiallyOpen: false
-    };
+  static defaultProps = {
+    isInitiallyOpen: false,
+  };
 
-    toggle () {
-        this.refs.popover.toggle();
-    }
+  toggle() {
+    this.refs.popover.toggle();
+  }
 
-    render() {
-        let { user, groups, createMembership, deleteMembership } = this.props;
+  render() {
+    let { user, groups, createMembership, deleteMembership } = this.props;
 
-        if (!groups || groups.length === 0 || !user.memberships) {
-            return <LoadingSpinner />;
-        }
+    if (!groups || groups.length === 0 || !user.memberships) {
+      return <LoadingSpinner />;
+    }
 
-        const changeMembership = (group, member) => {
-            if (member) {
-                createMembership({ groupId: group.id, userId: user.id })
-            } else {
-                deleteMembership({ membershipId: user.memberships[group.id].membership_id })
-            }
-        }
+    const changeMembership = (group, member) => {
+      if (member) {
+        createMembership({ groupId: group.id, userId: user.id });
+      } else {
+        deleteMembership({
+          membershipId: user.memberships[group.id].membership_id,
+        });
+      }
+    };
 
-        return (
-            <PopoverWithTrigger
-                ref="popover"
-                triggerElement={
-                    <div className="flex align-center">
-                        <span className="mr1 text-grey-4">
-                            <GroupSummary groups={groups} selectedGroups={user.memberships} />
-                        </span>
-                        <Icon className="text-grey-2" name="chevrondown"  size={10}/>
-                    </div>
-                }
-                triggerClasses="AdminSelectBorderless py1"
-                sizeToFit
-            >
-                <GroupSelect
-                    groups={groups}
-                    selectedGroups={user.memberships}
-                    onGroupChange={changeMembership}
-                />
-            </PopoverWithTrigger>
-        );
-    }
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        triggerElement={
+          <div className="flex align-center">
+            <span className="mr1 text-grey-4">
+              <GroupSummary groups={groups} selectedGroups={user.memberships} />
+            </span>
+            <Icon className="text-grey-2" name="chevrondown" size={10} />
+          </div>
+        }
+        triggerClasses="AdminSelectBorderless py1"
+        sizeToFit
+      >
+        <GroupSelect
+          groups={groups}
+          selectedGroups={user.memberships}
+          onGroupChange={changeMembership}
+        />
+      </PopoverWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx
index c08dd3678f592853a466233639d2a949813b3af2..31cd184a93cc6ea73d466eecb0fb74bf3fc10270 100644
--- a/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/AdminPeopleApp.jsx
@@ -2,28 +2,31 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
-import { LeftNavPane, LeftNavPaneItem } from "metabase/components/LeftNavPane.jsx";
+import {
+  LeftNavPane,
+  LeftNavPaneItem,
+} from "metabase/components/LeftNavPane.jsx";
 
 import AdminLayout from "metabase/components/AdminLayout.jsx";
 
 export default class AdminPeopleApp extends Component {
-    static propTypes = {
-        children: PropTypes.any
-    };
+  static propTypes = {
+    children: PropTypes.any,
+  };
 
-    render() {
-        const { children } = this.props;
-        return (
-            <AdminLayout
-                sidebar={
-                    <LeftNavPane>
-                        <LeftNavPaneItem name="People" path="/admin/people" index />
-                        <LeftNavPaneItem name="Groups" path="/admin/people/groups" />
-                    </LeftNavPane>
-                }
-            >
-                {children}
-            </AdminLayout>
-        );
-    }
+  render() {
+    const { children } = this.props;
+    return (
+      <AdminLayout
+        sidebar={
+          <LeftNavPane>
+            <LeftNavPaneItem name="People" path="/admin/people" index />
+            <LeftNavPaneItem name="Groups" path="/admin/people/groups" />
+          </LeftNavPane>
+        }
+      >
+        {children}
+      </AdminLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx b/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx
index 8b05df4eb4d9a47ff3e26c479c4f7e96793ecb6c..38b79e4b9c8ea3ed66e71db476bbc95956a95614 100644
--- a/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/GroupDetailApp.jsx
@@ -7,34 +7,34 @@ import { loadGroups, loadGroupDetails, fetchUsers } from "../people";
 import GroupDetail from "../components/GroupDetail.jsx";
 
 function mapStateToProps(state, props) {
-    return {
-        group: getGroup(state, props),
-        groups: getGroups(state, props),
-        users: getUsers(state, props)
-    };
+  return {
+    group: getGroup(state, props),
+    groups: getGroups(state, props),
+    users: getUsers(state, props),
+  };
 }
 
 const mapDispatchToProps = {
-    loadGroups,
-    loadGroupDetails,
-    fetchUsers
+  loadGroups,
+  loadGroupDetails,
+  fetchUsers,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class GroupDetailApp extends Component {
-    async componentWillMount() {
-        this.props.loadGroups();
-        this.props.fetchUsers();
-        this.props.loadGroupDetails(this.props.params.groupId);
-    }
+  async componentWillMount() {
+    this.props.loadGroups();
+    this.props.fetchUsers();
+    this.props.loadGroupDetails(this.props.params.groupId);
+  }
 
-    async componentWillReceiveProps(nextProps) {
-        if (nextProps.params.groupId !== this.props.params.groupId) {
-            this.props.loadGroupDetails(nextProps.params.groupId);
-        }
+  async componentWillReceiveProps(nextProps) {
+    if (nextProps.params.groupId !== this.props.params.groupId) {
+      this.props.loadGroupDetails(nextProps.params.groupId);
     }
+  }
 
-    render() {
-        return <GroupDetail {...this.props} />;
-    }
+  render() {
+    return <GroupDetail {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/admin/people/containers/GroupsListingApp.jsx b/frontend/src/metabase/admin/people/containers/GroupsListingApp.jsx
index 1d433d3c845fa11b3569d37cf4c75adf561cf5b3..2d53860d347a3f0a74c0b9402713238874186faa 100644
--- a/frontend/src/metabase/admin/people/containers/GroupsListingApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/GroupsListingApp.jsx
@@ -7,24 +7,22 @@ import { loadGroups } from "../people";
 import GroupsListing from "../components/GroupsListing.jsx";
 
 const mapStateToProps = function(state, props) {
-    return {
-        groups: getGroups(state, props)
-    };
-}
+  return {
+    groups: getGroups(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    loadGroups
+  loadGroups,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class GroupsListingApp extends Component {
-    async componentWillMount() {
-        await this.props.loadGroups();
-    }
+  async componentWillMount() {
+    await this.props.loadGroups();
+  }
 
-    render() {
-        return (
-            <GroupsListing {...this.props} />
-        );
-    }
+  render() {
+    return <GroupsListing {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
index 86be5d7c5cda9ce1b72c5b6e91d81118bb6e0775..a022aaca4e46d52e327ce2d86117c390489806ab 100644
--- a/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
+++ b/frontend/src/metabase/admin/people/containers/PeopleListingApp.jsx
@@ -15,404 +15,474 @@ import UserAvatar from "metabase/components/UserAvatar.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import Button from "metabase/components/Button.jsx";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import EditUserForm from "../components/EditUserForm.jsx";
 import UserActionsSelect from "../components/UserActionsSelect.jsx";
 import UserGroupSelect from "../components/UserGroupSelect.jsx";
 
-export const MODAL_ADD_PERSON = 'MODAL_ADD_PERSON';
-export const MODAL_EDIT_DETAILS = 'MODAL_EDIT_DETAILS';
-export const MODAL_INVITE_RESENT = 'MODAL_INVITE_RESENT';
-export const MODAL_REMOVE_USER = 'MODAL_REMOVE_USER';
-export const MODAL_RESET_PASSWORD = 'MODAL_RESET_PASSWORD';
-export const MODAL_RESET_PASSWORD_MANUAL = 'MODAL_RESET_PASSWORD_MANUAL';
-export const MODAL_RESET_PASSWORD_EMAIL = 'MODAL_RESET_PASSWORD_EMAIL';
-export const MODAL_USER_ADDED_WITH_INVITE = 'MODAL_USER_ADDED_WITH_INVITE';
-export const MODAL_USER_ADDED_WITH_PASSWORD = 'MODAL_USER_ADDED_WITH_PASSWORD';
+export const MODAL_ADD_PERSON = "MODAL_ADD_PERSON";
+export const MODAL_EDIT_DETAILS = "MODAL_EDIT_DETAILS";
+export const MODAL_INVITE_RESENT = "MODAL_INVITE_RESENT";
+export const MODAL_REMOVE_USER = "MODAL_REMOVE_USER";
+export const MODAL_RESET_PASSWORD = "MODAL_RESET_PASSWORD";
+export const MODAL_RESET_PASSWORD_MANUAL = "MODAL_RESET_PASSWORD_MANUAL";
+export const MODAL_RESET_PASSWORD_EMAIL = "MODAL_RESET_PASSWORD_EMAIL";
+export const MODAL_USER_ADDED_WITH_INVITE = "MODAL_USER_ADDED_WITH_INVITE";
+export const MODAL_USER_ADDED_WITH_PASSWORD = "MODAL_USER_ADDED_WITH_PASSWORD";
 
 import { getUsers, getModal, getGroups } from "../selectors";
 import {
-    createUser,
-    deleteUser,
-    fetchUsers,
-    resetPasswordManually,
-    resetPasswordViaEmail,
-    showModal,
-    updateUser,
-    resendInvite,
-    loadGroups,
-    loadMemberships,
-    createMembership,
-    deleteMembership,
+  createUser,
+  deleteUser,
+  fetchUsers,
+  resetPasswordManually,
+  resetPasswordViaEmail,
+  showModal,
+  updateUser,
+  resendInvite,
+  loadGroups,
+  loadMemberships,
+  createMembership,
+  deleteMembership,
 } from "../people";
 
 const mapStateToProps = (state, props) => {
-    return {
-        users: getUsers(state, props),
-        modal: getModal(state, props),
-        user: state.currentUser,
-        groups: getGroups(state, props)
-    }
-}
+  return {
+    users: getUsers(state, props),
+    modal: getModal(state, props),
+    user: state.currentUser,
+    groups: getGroups(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    createUser,
-    deleteUser,
-    fetchUsers,
-    resetPasswordManually,
-    resetPasswordViaEmail,
-    showModal,
-    updateUser,
-    resendInvite,
-    loadGroups,
-    loadMemberships,
-    createMembership,
-    deleteMembership
+  createUser,
+  deleteUser,
+  fetchUsers,
+  resetPasswordManually,
+  resetPasswordViaEmail,
+  showModal,
+  updateUser,
+  resendInvite,
+  loadGroups,
+  loadMemberships,
+  createMembership,
+  deleteMembership,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class PeopleListingApp extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = { error: null };
-    }
-
-    static propTypes = {
-        user: PropTypes.object.isRequired,
-        users: PropTypes.object,
-        groups: PropTypes.array,
-        modal: PropTypes.object,
-        createUser: PropTypes.func.isRequired,
-        deleteUser: PropTypes.func.isRequired,
-        fetchUsers: PropTypes.func.isRequired,
-        resetPasswordManually: PropTypes.func.isRequired,
-        resetPasswordViaEmail: PropTypes.func.isRequired,
-        showModal: PropTypes.func.isRequired,
-        updateUser: PropTypes.func.isRequired,
-        resendInvite: PropTypes.func.isRequired,
-        loadGroups: PropTypes.func.isRequired,
-        loadMemberships: PropTypes.func.isRequired,
-        createMembership: PropTypes.func.isRequired,
-        deleteMembership: PropTypes.func.isRequired,
-    };
-
-    async componentDidMount() {
-        try {
-            await Promise.all([
-                this.props.fetchUsers(),
-                this.props.loadGroups(),
-                this.props.loadMemberships()
-            ]);
-        } catch (error) {
-            this.setState({ error });
-        }
-    }
-
-    async onAddPerson(user) {
-        // close the modal no matter what
-        this.props.showModal(null);
-
-        if (user) {
-            let modal = MODAL_USER_ADDED_WITH_INVITE;
-
-            // we assume invite style creation and tweak as needed if email not available
-            if (!MetabaseSettings.isEmailConfigured()) {
-                modal = MODAL_USER_ADDED_WITH_PASSWORD;
-                user.password = MetabaseUtils.generatePassword();
-            }
-
-            // create the user
-            this.props.createUser(user);
-
-            // carry on
-            this.props.showModal({
-                type: modal,
-                details: {
-                    user: user
-                }
-            });
-        }
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = { error: null };
+  }
+
+  static propTypes = {
+    user: PropTypes.object.isRequired,
+    users: PropTypes.object,
+    groups: PropTypes.array,
+    modal: PropTypes.object,
+    createUser: PropTypes.func.isRequired,
+    deleteUser: PropTypes.func.isRequired,
+    fetchUsers: PropTypes.func.isRequired,
+    resetPasswordManually: PropTypes.func.isRequired,
+    resetPasswordViaEmail: PropTypes.func.isRequired,
+    showModal: PropTypes.func.isRequired,
+    updateUser: PropTypes.func.isRequired,
+    resendInvite: PropTypes.func.isRequired,
+    loadGroups: PropTypes.func.isRequired,
+    loadMemberships: PropTypes.func.isRequired,
+    createMembership: PropTypes.func.isRequired,
+    deleteMembership: PropTypes.func.isRequired,
+  };
+
+  async componentDidMount() {
+    try {
+      await Promise.all([
+        this.props.fetchUsers(),
+        this.props.loadGroups(),
+        this.props.loadMemberships(),
+      ]);
+    } catch (error) {
+      this.setState({ error });
     }
-
-    onEditDetails(user) {
-        // close the modal no matter what
-        this.props.showModal(null);
-
-        if (user) {
-            this.props.updateUser(user);
-        }
+  }
+
+  async onAddPerson(user) {
+    // close the modal no matter what
+    this.props.showModal(null);
+
+    if (user) {
+      let modal = MODAL_USER_ADDED_WITH_INVITE;
+
+      // we assume invite style creation and tweak as needed if email not available
+      if (!MetabaseSettings.isEmailConfigured()) {
+        modal = MODAL_USER_ADDED_WITH_PASSWORD;
+        user.password = MetabaseUtils.generatePassword();
+      }
+
+      // create the user
+      this.props.createUser(user);
+
+      // carry on
+      this.props.showModal({
+        type: modal,
+        details: {
+          user: user,
+        },
+      });
     }
+  }
 
-    onPasswordResetConfirm(user) {
-        if (MetabaseSettings.isEmailConfigured()) {
-            // trigger password reset email
-            this.props.resetPasswordViaEmail(user);
-
-            // show confirmation modal
-            this.props.showModal({
-                type: MODAL_RESET_PASSWORD_EMAIL,
-                details: {user: user}
-            });
-
-        } else {
-            // generate a password
-            const password = MetabaseUtils.generatePassword(14, MetabaseSettings.get('password_complexity'));
-
-            // trigger the reset
-            this.props.resetPasswordManually(user, password);
-
-            // show confirmation modal
-            this.props.showModal({
-                type: MODAL_RESET_PASSWORD_MANUAL,
-                details: {password: password, user: user}
-            });
-        }
-    }
+  onEditDetails(user) {
+    // close the modal no matter what
+    this.props.showModal(null);
 
-    onRemoveUserConfirm(user) {
-        this.props.showModal(null);
-        this.props.deleteUser(user);
+    if (user) {
+      this.props.updateUser(user);
     }
-
-    onCloseModal = () => {
-        this.props.showModal(null);
+  }
+
+  onPasswordResetConfirm(user) {
+    if (MetabaseSettings.isEmailConfigured()) {
+      // trigger password reset email
+      this.props.resetPasswordViaEmail(user);
+
+      // show confirmation modal
+      this.props.showModal({
+        type: MODAL_RESET_PASSWORD_EMAIL,
+        details: { user: user },
+      });
+    } else {
+      // generate a password
+      const password = MetabaseUtils.generatePassword(
+        14,
+        MetabaseSettings.get("password_complexity"),
+      );
+
+      // trigger the reset
+      this.props.resetPasswordManually(user, password);
+
+      // show confirmation modal
+      this.props.showModal({
+        type: MODAL_RESET_PASSWORD_MANUAL,
+        details: { password: password, user: user },
+      });
     }
-
-    renderAddPersonModal(modalDetails) {
-        return (
-            <Modal title={t`Who do you want to add?`} onClose={this.onCloseModal}>
-                <EditUserForm
-                    buttonText={t`Add`}
-                    submitFn={this.onAddPerson.bind(this)}
-                    groups={this.props.groups}
-                />
-            </Modal>
-        );
-    }
-
-    renderEditDetailsModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal full form title={t`Edit ${user.first_name}'s details`} onClose={this.onCloseModal}>
-                <EditUserForm
-                    user={user}
-                    submitFn={this.onEditDetails.bind(this)}
-                />
-            </Modal>
-        );
-    }
-
-    renderUserAddedWithPasswordModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal small
-                title={t`${user.first_name} has been added`}
-                footer={[
-                    <Button onClick={() => this.props.showModal({type: MODAL_ADD_PERSON})}>{t`Add another person`}</Button>,
-                    <Button primary onClick={this.onCloseModal}>{t`Done`}</Button>
-                ]}
-                onClose={this.onCloseModal}
-            >
-                <div className="px4 pb4">
-                    <div className="pb4">{jt`We couldn’t send them an email invitation,
-                    so make sure to tell them to log in using ${<span className="text-bold">{user.email}</span>}
+  }
+
+  onRemoveUserConfirm(user) {
+    this.props.showModal(null);
+    this.props.deleteUser(user);
+  }
+
+  onCloseModal = () => {
+    this.props.showModal(null);
+  };
+
+  renderAddPersonModal(modalDetails) {
+    return (
+      <Modal title={t`Who do you want to add?`} onClose={this.onCloseModal}>
+        <EditUserForm
+          buttonText={t`Add`}
+          submitFn={this.onAddPerson.bind(this)}
+          groups={this.props.groups}
+        />
+      </Modal>
+    );
+  }
+
+  renderEditDetailsModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        full
+        form
+        title={t`Edit ${user.first_name}'s details`}
+        onClose={this.onCloseModal}
+      >
+        <EditUserForm user={user} submitFn={this.onEditDetails.bind(this)} />
+      </Modal>
+    );
+  }
+
+  renderUserAddedWithPasswordModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        small
+        title={t`${user.first_name} has been added`}
+        footer={[
+          <Button
+            onClick={() => this.props.showModal({ type: MODAL_ADD_PERSON })}
+          >{t`Add another person`}</Button>,
+          <Button primary onClick={this.onCloseModal}>{t`Done`}</Button>,
+        ]}
+        onClose={this.onCloseModal}
+      >
+        <div className="px4 pb4">
+          <div className="pb4">{jt`We couldn’t send them an email invitation,
+                    so make sure to tell them to log in using ${(
+                      <span className="text-bold">{user.email}</span>
+                    )}
                     and this password we’ve generated for them:`}</div>
 
-                    <PasswordReveal password={user.password} />
-
-                    <div style={{paddingLeft: "5em", paddingRight: "5em"}} className="pt4 text-centered">{jt`If you want to be able to send email invites, just go to the ${<Link to="/admin/settings/email" className="link text-bold">Email Settings</Link>} page.`}</div>
-                </div>
-            </Modal>
-        );
-    }
-
-    renderUserAddedWithInviteModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal small
-                title={t`${user.first_name} has been added`}
-                footer={[
-                    <Button onClick={() => this.props.showModal({type: MODAL_ADD_PERSON})}>{t`Add another person`}</Button>,
-                    <Button primary onClick={this.onCloseModal}>{t`Done`}</Button>
-                ]}
-                onClose={this.onCloseModal}
-            >
-                <div style={{paddingLeft: "5em", paddingRight: "5em"}} className="pb4">{jt`We’ve sent an invite to ${<span className="text-bold">{user.email}</span>} with instructions to set their password.`}</div>
-            </Modal>
-        );
-    }
-
-    renderInviteResentModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal small form
-                title={t`We've re-sent ${user.first_name}'s invite`}
-                footer={[
-                    <Button primary onClick={this.onCloseModal}>{t`Okay`}</Button>
-                ]}
-                onClose={this.onCloseModal}
-            >
-                <p className="text-paragraph pb2">{t`Any previous email invites they have will no longer work.`}</p>
-            </Modal>
-        );
-    }
-
-    renderRemoveUserModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal small
-                title={t`Remove ${user.common_name}?`}
-                footer={[
-                    <Button onClick={this.onCloseModal}>{t`Cancel`}</Button>,
-                    <Button className="Button--danger" onClick={() => this.onRemoveUserConfirm(user)}>{t`Remove`}</Button>
-                ]}
-                onClose={this.onCloseModal}
-            >
-                <div className="px4 pb4">
-                    {t`${user.first_name} won't be able to log in anymore. This can't be undone.`}
-                </div>
-            </Modal>
-        );
-    }
-
-    renderResetPasswordModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal small
-                title={t`Reset ${user.first_name}'s password?`}
-                footer={[
-                    <Button onClick={this.onCloseModal}>{t`Cancel`}</Button>,
-                    <Button warning onClick={() => this.onPasswordResetConfirm(user)}>{t`Reset`}</Button>
-                ]}
-                onClose={this.onCloseModal}
-            >
-                <div className="px4 pb4">
-                    {t`Are you sure you want to do this?`}
-                </div>
-            </Modal>
-        );
-    }
-
-    renderPasswordResetManuallyModal(modalDetails) {
-        let { user, password } = modalDetails;
-
-        return (
-            <Modal small
-                title={t`${user.first_name}'s password has been reset`}
-                footer={<button className="Button Button--primary mr2" onClick={this.onCloseModal}>{t`Done`}</button>}
-                onClose={this.onCloseModal}
-            >
-                <div className="px4 pb4">
-                    <span className="pb3 block">{t`Here’s a temporary password they can use to log in and then change their password.`}</span>
-
-                    <PasswordReveal password={password} />
-                </div>
-            </Modal>
-        );
+          <PasswordReveal password={user.password} />
+
+          <div
+            style={{ paddingLeft: "5em", paddingRight: "5em" }}
+            className="pt4 text-centered"
+          >{jt`If you want to be able to send email invites, just go to the ${(
+            <Link to="/admin/settings/email" className="link text-bold">
+              Email Settings
+            </Link>
+          )} page.`}</div>
+        </div>
+      </Modal>
+    );
+  }
+
+  renderUserAddedWithInviteModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        small
+        title={t`${user.first_name} has been added`}
+        footer={[
+          <Button
+            onClick={() => this.props.showModal({ type: MODAL_ADD_PERSON })}
+          >{t`Add another person`}</Button>,
+          <Button primary onClick={this.onCloseModal}>{t`Done`}</Button>,
+        ]}
+        onClose={this.onCloseModal}
+      >
+        <div
+          style={{ paddingLeft: "5em", paddingRight: "5em" }}
+          className="pb4"
+        >{jt`We’ve sent an invite to ${(
+          <span className="text-bold">{user.email}</span>
+        )} with instructions to set their password.`}</div>
+      </Modal>
+    );
+  }
+
+  renderInviteResentModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        small
+        form
+        title={t`We've re-sent ${user.first_name}'s invite`}
+        footer={[
+          <Button primary onClick={this.onCloseModal}>{t`Okay`}</Button>,
+        ]}
+        onClose={this.onCloseModal}
+      >
+        <p className="text-paragraph pb2">{t`Any previous email invites they have will no longer work.`}</p>
+      </Modal>
+    );
+  }
+
+  renderRemoveUserModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        small
+        title={t`Remove ${user.common_name}?`}
+        footer={[
+          <Button onClick={this.onCloseModal}>{t`Cancel`}</Button>,
+          <Button
+            className="Button--danger"
+            onClick={() => this.onRemoveUserConfirm(user)}
+          >{t`Remove`}</Button>,
+        ]}
+        onClose={this.onCloseModal}
+      >
+        <div className="px4 pb4">
+          {t`${
+            user.first_name
+          } won't be able to log in anymore. This can't be undone.`}
+        </div>
+      </Modal>
+    );
+  }
+
+  renderResetPasswordModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        small
+        title={t`Reset ${user.first_name}'s password?`}
+        footer={[
+          <Button onClick={this.onCloseModal}>{t`Cancel`}</Button>,
+          <Button
+            warning
+            onClick={() => this.onPasswordResetConfirm(user)}
+          >{t`Reset`}</Button>,
+        ]}
+        onClose={this.onCloseModal}
+      >
+        <div className="px4 pb4">{t`Are you sure you want to do this?`}</div>
+      </Modal>
+    );
+  }
+
+  renderPasswordResetManuallyModal(modalDetails) {
+    let { user, password } = modalDetails;
+
+    return (
+      <Modal
+        small
+        title={t`${user.first_name}'s password has been reset`}
+        footer={
+          <button
+            className="Button Button--primary mr2"
+            onClick={this.onCloseModal}
+          >{t`Done`}</button>
+        }
+        onClose={this.onCloseModal}
+      >
+        <div className="px4 pb4">
+          <span className="pb3 block">{t`Here’s a temporary password they can use to log in and then change their password.`}</span>
+
+          <PasswordReveal password={password} />
+        </div>
+      </Modal>
+    );
+  }
+
+  renderPasswordResetViaEmailModal(modalDetails) {
+    let { user } = modalDetails;
+
+    return (
+      <Modal
+        small
+        title={t`${user.first_name}'s password has been reset`}
+        footer={<Button primary onClick={this.onCloseModal}>{t`Done`}</Button>}
+        onClose={this.onCloseModal}
+      >
+        <div className="px4 pb4">{t`We've sent them an email with instructions for creating a new password.`}</div>
+      </Modal>
+    );
+  }
+
+  renderModal(modalType, modalDetails) {
+    switch (modalType) {
+      case MODAL_ADD_PERSON:
+        return this.renderAddPersonModal(modalDetails);
+      case MODAL_EDIT_DETAILS:
+        return this.renderEditDetailsModal(modalDetails);
+      case MODAL_USER_ADDED_WITH_PASSWORD:
+        return this.renderUserAddedWithPasswordModal(modalDetails);
+      case MODAL_USER_ADDED_WITH_INVITE:
+        return this.renderUserAddedWithInviteModal(modalDetails);
+      case MODAL_INVITE_RESENT:
+        return this.renderInviteResentModal(modalDetails);
+      case MODAL_REMOVE_USER:
+        return this.renderRemoveUserModal(modalDetails);
+      case MODAL_RESET_PASSWORD:
+        return this.renderResetPasswordModal(modalDetails);
+      case MODAL_RESET_PASSWORD_MANUAL:
+        return this.renderPasswordResetManuallyModal(modalDetails);
+      case MODAL_RESET_PASSWORD_EMAIL:
+        return this.renderPasswordResetViaEmailModal(modalDetails);
     }
 
-    renderPasswordResetViaEmailModal(modalDetails) {
-        let { user } = modalDetails;
-
-        return (
-            <Modal
-                small
-                title={t`${user.first_name}'s password has been reset`}
-                footer={<Button primary onClick={this.onCloseModal}>{t`Done`}</Button>}
-                onClose={this.onCloseModal}
-            >
-                <div className="px4 pb4">{t`We've sent them an email with instructions for creating a new password.`}</div>
-            </Modal>
-        );
-    }
+    return null;
+  }
 
-    renderModal(modalType, modalDetails) {
-
-        switch(modalType) {
-            case MODAL_ADD_PERSON:               return this.renderAddPersonModal(modalDetails);
-            case MODAL_EDIT_DETAILS:             return this.renderEditDetailsModal(modalDetails);
-            case MODAL_USER_ADDED_WITH_PASSWORD: return this.renderUserAddedWithPasswordModal(modalDetails);
-            case MODAL_USER_ADDED_WITH_INVITE:   return this.renderUserAddedWithInviteModal(modalDetails);
-            case MODAL_INVITE_RESENT:            return this.renderInviteResentModal(modalDetails);
-            case MODAL_REMOVE_USER:              return this.renderRemoveUserModal(modalDetails);
-            case MODAL_RESET_PASSWORD:           return this.renderResetPasswordModal(modalDetails);
-            case MODAL_RESET_PASSWORD_MANUAL:    return this.renderPasswordResetManuallyModal(modalDetails);
-            case MODAL_RESET_PASSWORD_EMAIL:     return this.renderPasswordResetViaEmailModal(modalDetails);
-        }
+  render() {
+    let { modal, users, groups } = this.props;
+    let { error } = this.state;
 
-        return null;
-    }
+    users = _.values(users).sort((a, b) => b.date_joined - a.date_joined);
 
-    render() {
-        let { modal, users, groups } = this.props;
-        let { error } = this.state;
-
-        users = _.values(users).sort((a, b) => (b.date_joined - a.date_joined));
-
-        return (
-            <LoadingAndErrorWrapper loading={!users} error={error}>
-            {() =>
-                <AdminPaneLayout
-                    title={t`People`}
-                    buttonText={t`Add someone`}
-                    buttonAction={() => this.props.showModal({type: MODAL_ADD_PERSON})}
-                >
-                    <section className="pb4">
-                        <table className="ContentTable">
-                            <thead>
-                                <tr>
-                                    <th>{t`Name`}</th>
-                                    <th></th>
-                                    <th>{t`Email`}</th>
-                                    <th>{t`Groups`}</th>
-                                    <th>{t`Last Login`}</th>
-                                    <th></th>
-                                </tr>
-                            </thead>
-                            <tbody>
-                                { users.map(user =>
-                                <tr key={user.id}>
-                                    <td><span className="text-white inline-block"><UserAvatar background={(user.is_superuser) ? "bg-purple" : "bg-brand"} user={user} /></span> <span className="ml2 text-bold">{user.common_name}</span></td>
-                                    <td>
-                                      {user.google_auth ?
-                                        <Tooltip tooltip={t`Signed up via Google`}>
-                                            <Icon name='google' />
-                                        </Tooltip> : null}
-                                      {user.ldap_auth ?
-                                        <Tooltip tooltip={t`Signed up via LDAP`}>
-                                            <Icon name='ldap' />
-                                        </Tooltip> : null }
-                                    </td>
-                                    <td>{user.email}</td>
-                                    <td>
-                                        <UserGroupSelect
-                                            user={user}
-                                            groups={groups}
-                                            createMembership={this.props.createMembership}
-                                            deleteMembership={this.props.deleteMembership}
-                                        />
-                                    </td>
-                                    <td>{ user.last_login ? user.last_login.fromNow() : t`Never` }</td>
-                                    <td className="text-right">
-                                        <UserActionsSelect user={user} showModal={this.props.showModal} resendInvite={this.props.resendInvite} isActiveUser={this.props.user.id === user.id} />
-                                    </td>
-                                </tr>
-                                )}
-                            </tbody>
-                        </table>
-                    </section>
-                    { modal ? this.renderModal(modal.type, modal.details) : null }
-                </AdminPaneLayout>
+    return (
+      <LoadingAndErrorWrapper loading={!users} error={error}>
+        {() => (
+          <AdminPaneLayout
+            title={t`People`}
+            buttonText={t`Add someone`}
+            buttonAction={() =>
+              this.props.showModal({ type: MODAL_ADD_PERSON })
             }
-            </LoadingAndErrorWrapper>
-        );
-    }
+          >
+            <section className="pb4">
+              <table className="ContentTable">
+                <thead>
+                  <tr>
+                    <th>{t`Name`}</th>
+                    <th />
+                    <th>{t`Email`}</th>
+                    <th>{t`Groups`}</th>
+                    <th>{t`Last Login`}</th>
+                    <th />
+                  </tr>
+                </thead>
+                <tbody>
+                  {users.map(user => (
+                    <tr key={user.id}>
+                      <td>
+                        <span className="text-white inline-block">
+                          <UserAvatar
+                            background={
+                              user.is_superuser ? "bg-purple" : "bg-brand"
+                            }
+                            user={user}
+                          />
+                        </span>{" "}
+                        <span className="ml2 text-bold">
+                          {user.common_name}
+                        </span>
+                      </td>
+                      <td>
+                        {user.google_auth ? (
+                          <Tooltip tooltip={t`Signed up via Google`}>
+                            <Icon name="google" />
+                          </Tooltip>
+                        ) : null}
+                        {user.ldap_auth ? (
+                          <Tooltip tooltip={t`Signed up via LDAP`}>
+                            <Icon name="ldap" />
+                          </Tooltip>
+                        ) : null}
+                      </td>
+                      <td>{user.email}</td>
+                      <td>
+                        <UserGroupSelect
+                          user={user}
+                          groups={groups}
+                          createMembership={this.props.createMembership}
+                          deleteMembership={this.props.deleteMembership}
+                        />
+                      </td>
+                      <td>
+                        {user.last_login ? user.last_login.fromNow() : t`Never`}
+                      </td>
+                      <td className="text-right">
+                        <UserActionsSelect
+                          user={user}
+                          showModal={this.props.showModal}
+                          resendInvite={this.props.resendInvite}
+                          isActiveUser={this.props.user.id === user.id}
+                        />
+                      </td>
+                    </tr>
+                  ))}
+                </tbody>
+              </table>
+            </section>
+            {modal ? this.renderModal(modal.type, modal.details) : null}
+          </AdminPaneLayout>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/people/people.js b/frontend/src/metabase/admin/people/people.js
index 680630d5804804416e0f1354a5eb28b6ba054513..e8b31f0580ba6941910a8437cfe4104f620ad5ad 100644
--- a/frontend/src/metabase/admin/people/people.js
+++ b/frontend/src/metabase/admin/people/people.js
@@ -1,5 +1,9 @@
-
-import { createAction, createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
+import {
+  createAction,
+  createThunkAction,
+  handleActions,
+  combineReducers,
+} from "metabase/lib/redux";
 import { normalize, schema } from "normalizr";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
@@ -10,177 +14,235 @@ import moment from "moment";
 import _ from "underscore";
 import { assoc, dissoc } from "icepick";
 
-const user = new schema.Entity('user');
+const user = new schema.Entity("user");
 
 // action constants
-export const CREATE_USER = 'metabase/admin/people/CREATE_USER';
-export const DELETE_USER = 'metabase/admin/people/DELETE_USER';
-export const FETCH_USERS = 'metabase/admin/people/FETCH_USERS';
-export const RESEND_INVITE = 'metabase/admin/people/RESEND_INVITE';
-export const RESET_PASSWORD_EMAIL = 'metabase/admin/people/RESET_PASSWORD_EMAIL';
-export const RESET_PASSWORD_MANUAL = 'metabase/admin/people/RESET_PASSWORD_MANUAL';
-export const SHOW_MODAL = 'metabase/admin/people/SHOW_MODAL';
-export const UPDATE_USER = 'metabase/admin/people/UPDATE_USER';
-export const LOAD_GROUPS = 'metabase/admin/people/LOAD_GROUPS';
-export const LOAD_MEMBERSHIPS = 'metabase/admin/people/LOAD_MEMBERSHIPS';
-export const LOAD_GROUP_DETAILS = 'metabase/admin/people/LOAD_GROUP_DETAILS';
-
-export const CREATE_MEMBERSHIP = 'metabase/admin/people/CREATE_MEMBERSHIP';
-export const DELETE_MEMBERSHIP = 'metabase/admin/people/DELETE_MEMBERSHIP';
-
+export const CREATE_USER = "metabase/admin/people/CREATE_USER";
+export const DELETE_USER = "metabase/admin/people/DELETE_USER";
+export const FETCH_USERS = "metabase/admin/people/FETCH_USERS";
+export const RESEND_INVITE = "metabase/admin/people/RESEND_INVITE";
+export const RESET_PASSWORD_EMAIL =
+  "metabase/admin/people/RESET_PASSWORD_EMAIL";
+export const RESET_PASSWORD_MANUAL =
+  "metabase/admin/people/RESET_PASSWORD_MANUAL";
+export const SHOW_MODAL = "metabase/admin/people/SHOW_MODAL";
+export const UPDATE_USER = "metabase/admin/people/UPDATE_USER";
+export const LOAD_GROUPS = "metabase/admin/people/LOAD_GROUPS";
+export const LOAD_MEMBERSHIPS = "metabase/admin/people/LOAD_MEMBERSHIPS";
+export const LOAD_GROUP_DETAILS = "metabase/admin/people/LOAD_GROUP_DETAILS";
+
+export const CREATE_MEMBERSHIP = "metabase/admin/people/CREATE_MEMBERSHIP";
+export const DELETE_MEMBERSHIP = "metabase/admin/people/DELETE_MEMBERSHIP";
 
 // action creators
 
 export const showModal = createAction(SHOW_MODAL);
 
 export const loadGroups = createAction(LOAD_GROUPS, () =>
-    PermissionsApi.groups()
+  PermissionsApi.groups(),
 );
 
-export const loadGroupDetails = createAction(LOAD_GROUP_DETAILS, (id) =>
-    PermissionsApi.groupDetails({ id: id })
+export const loadGroupDetails = createAction(LOAD_GROUP_DETAILS, id =>
+  PermissionsApi.groupDetails({ id: id }),
 );
 
 export const loadMemberships = createAction(LOAD_MEMBERSHIPS, async () =>
-    // flatten the map of user id => memberships
-    _.chain(await PermissionsApi.memberships())
-        .values().flatten()
-        .map(m => ([m.membership_id, m]))
-        .object().value()
+  // flatten the map of user id => memberships
+  _.chain(await PermissionsApi.memberships())
+    .values()
+    .flatten()
+    .map(m => [m.membership_id, m])
+    .object()
+    .value(),
 );
-export const createMembership = createAction(CREATE_MEMBERSHIP, async ({ userId, groupId }) => {
+export const createMembership = createAction(
+  CREATE_MEMBERSHIP,
+  async ({ userId, groupId }) => {
     // pull the membership_id from the list of all memberships of the group
-    let groupMemberships = await PermissionsApi.createMembership({ user_id: userId, group_id: groupId });
+    let groupMemberships = await PermissionsApi.createMembership({
+      user_id: userId,
+      group_id: groupId,
+    });
     MetabaseAnalytics.trackEvent("People Groups", "Membership Added");
     return {
-        user_id: userId,
-        group_id: groupId,
-        membership_id: _.findWhere(groupMemberships, { user_id: userId }).membership_id
-    }
-});
-export const deleteMembership = createAction(DELETE_MEMBERSHIP, async ({ membershipId }) => {
+      user_id: userId,
+      group_id: groupId,
+      membership_id: _.findWhere(groupMemberships, { user_id: userId })
+        .membership_id,
+    };
+  },
+);
+export const deleteMembership = createAction(
+  DELETE_MEMBERSHIP,
+  async ({ membershipId }) => {
     await PermissionsApi.deleteMembership({ id: membershipId });
     MetabaseAnalytics.trackEvent("People Groups", "Membership Deleted");
     return membershipId;
-});
+  },
+);
 
 export const createUser = createThunkAction(CREATE_USER, function(user) {
-    return async function(dispatch, getState) {
-        // apply any user defaults here
-        user.is_superuser = false;
-
-        let newUser = await UserApi.create(user);
-        newUser.date_joined = (newUser.date_joined) ? moment(newUser.date_joined) : null;
-        newUser.last_login = (newUser.last_login) ? moment(newUser.last_login) : null;
-
-        if (user.groups) {
-            await Promise.all(user.groups.map(groupId =>
-                dispatch(createMembership({ userId: newUser.id, groupId: groupId }))
-            ));
-        }
+  return async function(dispatch, getState) {
+    // apply any user defaults here
+    user.is_superuser = false;
+
+    let newUser = await UserApi.create(user);
+    newUser.date_joined = newUser.date_joined
+      ? moment(newUser.date_joined)
+      : null;
+    newUser.last_login = newUser.last_login ? moment(newUser.last_login) : null;
+
+    if (user.groups) {
+      await Promise.all(
+        user.groups.map(groupId =>
+          dispatch(createMembership({ userId: newUser.id, groupId: groupId })),
+        ),
+      );
+    }
 
-        MetabaseAnalytics.trackEvent("People Admin", "User Added", (user.password !== null) ? "password" : "email");
+    MetabaseAnalytics.trackEvent(
+      "People Admin",
+      "User Added",
+      user.password !== null ? "password" : "email",
+    );
 
-        return newUser;
-    };
+    return newUser;
+  };
 });
 
 export const deleteUser = createThunkAction(DELETE_USER, function(user) {
-    return async function(dispatch, getState) {
-        await UserApi.delete({
-            userId: user.id
-        });
-
-        MetabaseAnalytics.trackEvent("People Admin", "User Removed");
-        return user;
-    };
+  return async function(dispatch, getState) {
+    await UserApi.delete({
+      userId: user.id,
+    });
+
+    MetabaseAnalytics.trackEvent("People Admin", "User Removed");
+    return user;
+  };
 });
 
 export const fetchUsers = createThunkAction(FETCH_USERS, function() {
-    return async function(dispatch, getState) {
-        let users = await UserApi.list();
+  return async function(dispatch, getState) {
+    let users = await UserApi.list();
 
-        for (var u of users) {
-            u.date_joined = (u.date_joined) ? moment(u.date_joined) : null;
-            u.last_login = (u.last_login) ? moment(u.last_login) : null;
-        }
+    for (var u of users) {
+      u.date_joined = u.date_joined ? moment(u.date_joined) : null;
+      u.last_login = u.last_login ? moment(u.last_login) : null;
+    }
 
-        return normalize(users, [user]);
-    };
+    return normalize(users, [user]);
+  };
 });
 
 export const resendInvite = createThunkAction(RESEND_INVITE, function(user) {
-    return async function(dispatch, getState) {
-        MetabaseAnalytics.trackEvent("People Admin", "Resent Invite");
-        return await UserApi.send_invite({id: user.id});
-    };
+  return async function(dispatch, getState) {
+    MetabaseAnalytics.trackEvent("People Admin", "Resent Invite");
+    return await UserApi.send_invite({ id: user.id });
+  };
 });
 
-export const resetPasswordManually = createThunkAction(RESET_PASSWORD_MANUAL, function(user, password) {
+export const resetPasswordManually = createThunkAction(
+  RESET_PASSWORD_MANUAL,
+  function(user, password) {
     return async function(dispatch, getState) {
-        MetabaseAnalytics.trackEvent("People Admin", "Manual Password Reset");
-        return await UserApi.update_password({id: user.id, password: password});
+      MetabaseAnalytics.trackEvent("People Admin", "Manual Password Reset");
+      return await UserApi.update_password({ id: user.id, password: password });
     };
-});
+  },
+);
 
-export const resetPasswordViaEmail = createThunkAction(RESET_PASSWORD_EMAIL, function(user) {
+export const resetPasswordViaEmail = createThunkAction(
+  RESET_PASSWORD_EMAIL,
+  function(user) {
     return async function(dispatch, getState) {
-        MetabaseAnalytics.trackEvent("People Admin", "Trigger User Password Reset");
-        return await SessionApi.forgot_password({email: user.email});
+      MetabaseAnalytics.trackEvent(
+        "People Admin",
+        "Trigger User Password Reset",
+      );
+      return await SessionApi.forgot_password({ email: user.email });
     };
-});
+  },
+);
 
 export const updateUser = createThunkAction(UPDATE_USER, function(user) {
-    return async function(dispatch, getState) {
-        let updatedUser = await UserApi.update(user);
+  return async function(dispatch, getState) {
+    let updatedUser = await UserApi.update(user);
 
-        updatedUser.date_joined = (updatedUser.date_joined) ? moment(updatedUser.date_joined) : null;
-        updatedUser.last_login = (updatedUser.last_login) ? moment(updatedUser.last_login) : null;
+    updatedUser.date_joined = updatedUser.date_joined
+      ? moment(updatedUser.date_joined)
+      : null;
+    updatedUser.last_login = updatedUser.last_login
+      ? moment(updatedUser.last_login)
+      : null;
 
-        MetabaseAnalytics.trackEvent("People Admin", "Update Updated");
+    MetabaseAnalytics.trackEvent("People Admin", "Update Updated");
 
-        return updatedUser;
-    };
+    return updatedUser;
+  };
 });
 
-const modal = handleActions({
-    [SHOW_MODAL]: { next: (state, { payload }) => payload }
-}, null);
-
+const modal = handleActions(
+  {
+    [SHOW_MODAL]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
 
-const users = handleActions({
-    [FETCH_USERS]: { next: (state, { payload }) => ({ ...payload.entities.user }) },
-    [CREATE_USER]: { next: (state, { payload: user }) => ({ ...state, [user.id]: user }) },
-    [DELETE_USER]: { next: (state, { payload: user }) => _.omit(state, user.id) },
-    [UPDATE_USER]: { next: (state, { payload: user }) => ({ ...state, [user.id]: user }) },
-}, null);
+const users = handleActions(
+  {
+    [FETCH_USERS]: {
+      next: (state, { payload }) => ({ ...payload.entities.user }),
+    },
+    [CREATE_USER]: {
+      next: (state, { payload: user }) => ({ ...state, [user.id]: user }),
+    },
+    [DELETE_USER]: {
+      next: (state, { payload: user }) => _.omit(state, user.id),
+    },
+    [UPDATE_USER]: {
+      next: (state, { payload: user }) => ({ ...state, [user.id]: user }),
+    },
+  },
+  null,
+);
 
-const groups = handleActions({
-    [LOAD_GROUPS]: { next: (state, { payload }) =>
-        payload && payload.filter(group => group.name !== "MetaBot")
+const groups = handleActions(
+  {
+    [LOAD_GROUPS]: {
+      next: (state, { payload }) =>
+        payload && payload.filter(group => group.name !== "MetaBot"),
     },
-}, null);
+  },
+  null,
+);
 
-const memberships = handleActions({
-    [LOAD_MEMBERSHIPS]: { next: (state, { payload: memberships }) =>
-        memberships
+const memberships = handleActions(
+  {
+    [LOAD_MEMBERSHIPS]: {
+      next: (state, { payload: memberships }) => memberships,
     },
-    [CREATE_MEMBERSHIP]: { next: (state, { payload: membership }) =>
-        assoc(state, membership.membership_id, membership)
+    [CREATE_MEMBERSHIP]: {
+      next: (state, { payload: membership }) =>
+        assoc(state, membership.membership_id, membership),
     },
-    [DELETE_MEMBERSHIP]: { next: (state, { payload: membershipId }) =>
-        dissoc(state, membershipId)
+    [DELETE_MEMBERSHIP]: {
+      next: (state, { payload: membershipId }) => dissoc(state, membershipId),
     },
-}, {});
+  },
+  {},
+);
 
-const group = handleActions({
+const group = handleActions(
+  {
     [LOAD_GROUP_DETAILS]: { next: (state, { payload }) => payload },
-}, null);
+  },
+  null,
+);
 
 export default combineReducers({
-    modal,
-    users,
-    groups,
-    group,
-    memberships
+  modal,
+  users,
+  groups,
+  group,
+  memberships,
 });
diff --git a/frontend/src/metabase/admin/people/selectors.js b/frontend/src/metabase/admin/people/selectors.js
index 05398a59dbee38f1ffbd211bbd450abac0c96df6..358d1aad1698b253aa0501ff8810c7ed09756248 100644
--- a/frontend/src/metabase/admin/people/selectors.js
+++ b/frontend/src/metabase/admin/people/selectors.js
@@ -1,22 +1,25 @@
-
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 import _ from "underscore";
 
-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 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.admin.people.users,
-    (state) => state.admin.people.memberships,
-    (users, memberships) =>
-        users && _.mapObject(users, user => ({
-            ...user,
-            memberships: memberships && _.chain(memberships)
-                .values()
-                .filter(m => m.user_id === user.id)
-                .map(m => [m.group_id, m]).object()
-                .value()
-        }))
-)
+export const getUsers = createSelector(
+  state => state.admin.people.users,
+  state => state.admin.people.memberships,
+  (users, memberships) =>
+    users &&
+    _.mapObject(users, user => ({
+      ...user,
+      memberships:
+        memberships &&
+        _.chain(memberships)
+          .values()
+          .filter(m => m.user_id === user.id)
+          .map(m => [m.group_id, m])
+          .object()
+          .value(),
+    })),
+);
diff --git a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.css b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.css
index 9c9a0ba944ae7f4af244b343d853ae1e36d6627e..01c5667a0b84a515bf782072721eda8b3b587024 100644
--- a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.css
+++ b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.css
@@ -1,4 +1,4 @@
 /* disable focus rings on react-virtualized's Grids */
 :local(.fixedHeaderGrid) [tabindex] {
-    outline: none !important;
+  outline: none !important;
 }
diff --git a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx
index 205a6e09307b50a56cac6eca799970919bbe509a..f76d3b23e860347c2210f30a2d9da977c049708a 100644
--- a/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx
+++ b/frontend/src/metabase/admin/permissions/components/FixedHeaderGrid.jsx
@@ -2,98 +2,156 @@
 
 import React from "react";
 
-import { Grid, ScrollSync } from 'react-virtualized'
-import 'react-virtualized/styles.css';
+import { Grid, ScrollSync } from "react-virtualized";
+import "react-virtualized/styles.css";
 import S from "./FixedHeaderGrid.css";
 
 import cx from "classnames";
 
 const FixedHeaderGrid = ({
-    className,
-    rowCount,
-    columnCount,
-    renderCell,
-    columnWidth,
-    rowHeight,
-    renderColumnHeader,
-    columnHeaderHeight,
-    rowHeaderWidth,
-    renderRowHeader,
-    renderCorner,
-    width,
-    height,
-    paddingBottom = 0,
-    paddingRight = 0
-}) =>
-    <div className={cx(className, S.fixedHeaderGrid, "relative")}>
-        <ScrollSync>
-            {({ clientHeight, clientWidth, onScroll, scrollHeight, scrollLeft, scrollTop, scrollWidth }) =>
-                <div>
-                    {/* CORNER */}
-                    <div className="scroll-hide-all" style={{ position: "absolute", top: 0, left: 0, width: rowHeaderWidth, height: columnHeaderHeight, overflow: "hidden" }}>
-                        {renderCorner()}
-                    </div>
-                    {/* COLUMN HEADERS */}
-                    <div className="scroll-hide-all" style={{ position: "absolute", top: 0, left: rowHeaderWidth, height: columnHeaderHeight, overflow: "hidden" }}>
-                        <Grid
-                            width={width - rowHeaderWidth}
-                            height={columnHeaderHeight}
-                            cellRenderer={({ key, style, columnIndex, rowIndex }) =>
-                                <div key={key} style={style}>
-                                    {/*  HACK: pad the right with a phantom cell */}
-                                    {columnIndex >= columnCount ? null : renderColumnHeader({ columnIndex })}
-                                </div>
-                            }
-                            columnCount={columnCount + 1}
-                            columnWidth={({ index }) => index >= columnCount ? paddingRight : columnWidth}
-                            rowCount={1}
-                            rowHeight={columnHeaderHeight}
-                            onScroll={({ scrollLeft }) => onScroll({ scrollLeft })}
-                            scrollLeft={scrollLeft}
-                        />
-                    </div>
-                    {/* ROW HEADERS */}
-                    <div className="scroll-hide-all" style={{ position: "absolute", top: columnHeaderHeight, left: 0, width: rowHeaderWidth, overflow: "hidden" }}>
-                        <Grid
-                            width={rowHeaderWidth}
-                            height={height - columnHeaderHeight}
-                            cellRenderer={({ key, style, columnIndex, rowIndex }) =>
-                                <div key={key} style={style}>
-                                    {/*  HACK: pad the bottom with a phantom cell */}
-                                    {rowIndex >= rowCount ? null : renderRowHeader({ rowIndex })}
-                                </div>
-                            }
-                            columnCount={1}
-                            columnWidth={rowHeaderWidth}
-                            rowCount={rowCount + 1}
-                            rowHeight={({ index }) => index >= rowCount ? paddingBottom : rowHeight}
-                            onScroll={({ scrollTop }) => onScroll({ scrollTop })}
-                            scrollTop={scrollTop}
-                        />
-                    </div>
-                    {/* CELLS */}
-                    <div style={{ position: "absolute", top: columnHeaderHeight, left: rowHeaderWidth, overflow: "hidden" }}>
-                        <Grid
-                            width={width - rowHeaderWidth}
-                            height={height - columnHeaderHeight}
-                            cellRenderer={({ key, style, columnIndex, rowIndex }) =>
-                                <div key={key} style={style}>
-                                    {/*  HACK: pad the bottom/right with a phantom cell */}
-                                    {rowIndex >= rowCount || columnIndex >= columnCount ? null : renderCell({ columnIndex, rowIndex })}
-                                </div>
-                            }
-                            columnCount={columnCount + 1}
-                            columnWidth={({ index }) => index >= columnCount ? paddingRight : columnWidth}
-                            rowCount={rowCount + 1}
-                            rowHeight={({ index }) => index >= rowCount ? paddingBottom : rowHeight}
-                            onScroll={({ scrollTop, scrollLeft }) => onScroll({ scrollTop, scrollLeft })}
-                            scrollTop={scrollTop}
-                            scrollLeft={scrollLeft}
-                        />
-                    </div>
+  className,
+  rowCount,
+  columnCount,
+  renderCell,
+  columnWidth,
+  rowHeight,
+  renderColumnHeader,
+  columnHeaderHeight,
+  rowHeaderWidth,
+  renderRowHeader,
+  renderCorner,
+  width,
+  height,
+  paddingBottom = 0,
+  paddingRight = 0,
+}) => (
+  <div className={cx(className, S.fixedHeaderGrid, "relative")}>
+    <ScrollSync>
+      {({
+        clientHeight,
+        clientWidth,
+        onScroll,
+        scrollHeight,
+        scrollLeft,
+        scrollTop,
+        scrollWidth,
+      }) => (
+        <div>
+          {/* CORNER */}
+          <div
+            className="scroll-hide-all"
+            style={{
+              position: "absolute",
+              top: 0,
+              left: 0,
+              width: rowHeaderWidth,
+              height: columnHeaderHeight,
+              overflow: "hidden",
+            }}
+          >
+            {renderCorner()}
+          </div>
+          {/* COLUMN HEADERS */}
+          <div
+            className="scroll-hide-all"
+            style={{
+              position: "absolute",
+              top: 0,
+              left: rowHeaderWidth,
+              height: columnHeaderHeight,
+              overflow: "hidden",
+            }}
+          >
+            <Grid
+              width={width - rowHeaderWidth}
+              height={columnHeaderHeight}
+              cellRenderer={({ key, style, columnIndex, rowIndex }) => (
+                <div key={key} style={style}>
+                  {/*  HACK: pad the right with a phantom cell */}
+                  {columnIndex >= columnCount
+                    ? null
+                    : renderColumnHeader({ columnIndex })}
                 </div>
-            }
-        </ScrollSync>
-    </div>
+              )}
+              columnCount={columnCount + 1}
+              columnWidth={({ index }) =>
+                index >= columnCount ? paddingRight : columnWidth
+              }
+              rowCount={1}
+              rowHeight={columnHeaderHeight}
+              onScroll={({ scrollLeft }) => onScroll({ scrollLeft })}
+              scrollLeft={scrollLeft}
+            />
+          </div>
+          {/* ROW HEADERS */}
+          <div
+            className="scroll-hide-all"
+            style={{
+              position: "absolute",
+              top: columnHeaderHeight,
+              left: 0,
+              width: rowHeaderWidth,
+              overflow: "hidden",
+            }}
+          >
+            <Grid
+              width={rowHeaderWidth}
+              height={height - columnHeaderHeight}
+              cellRenderer={({ key, style, columnIndex, rowIndex }) => (
+                <div key={key} style={style}>
+                  {/*  HACK: pad the bottom with a phantom cell */}
+                  {rowIndex >= rowCount ? null : renderRowHeader({ rowIndex })}
+                </div>
+              )}
+              columnCount={1}
+              columnWidth={rowHeaderWidth}
+              rowCount={rowCount + 1}
+              rowHeight={({ index }) =>
+                index >= rowCount ? paddingBottom : rowHeight
+              }
+              onScroll={({ scrollTop }) => onScroll({ scrollTop })}
+              scrollTop={scrollTop}
+            />
+          </div>
+          {/* CELLS */}
+          <div
+            style={{
+              position: "absolute",
+              top: columnHeaderHeight,
+              left: rowHeaderWidth,
+              overflow: "hidden",
+            }}
+          >
+            <Grid
+              width={width - rowHeaderWidth}
+              height={height - columnHeaderHeight}
+              cellRenderer={({ key, style, columnIndex, rowIndex }) => (
+                <div key={key} style={style}>
+                  {/*  HACK: pad the bottom/right with a phantom cell */}
+                  {rowIndex >= rowCount || columnIndex >= columnCount
+                    ? null
+                    : renderCell({ columnIndex, rowIndex })}
+                </div>
+              )}
+              columnCount={columnCount + 1}
+              columnWidth={({ index }) =>
+                index >= columnCount ? paddingRight : columnWidth
+              }
+              rowCount={rowCount + 1}
+              rowHeight={({ index }) =>
+                index >= rowCount ? paddingBottom : rowHeight
+              }
+              onScroll={({ scrollTop, scrollLeft }) =>
+                onScroll({ scrollTop, scrollLeft })
+              }
+              scrollTop={scrollTop}
+              scrollLeft={scrollLeft}
+            />
+          </div>
+        </div>
+      )}
+    </ScrollSync>
+  </div>
+);
 
 export default FixedHeaderGrid;
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx
index 30c145f8f6943c10a5074fa6ff96faa7d15ed1cf..65e37b6ef3f276134c1618fd07f1b72edaa1eb4f 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsConfirm.jsx
@@ -1,70 +1,91 @@
 import React from "react";
 
 import { inflect } from "metabase/lib/formatting";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Tooltip from "metabase/components/Tooltip";
 
-const GroupName = ({ group }) =>
-    <span className="text-brand">{group.name}</span>
+const GroupName = ({ group }) => (
+  <span className="text-brand">{group.name}</span>
+);
 
-const DatabaseName = ({ database }) =>
-    <span className="text-brand">{database.name}</span>
+const DatabaseName = ({ database }) => (
+  <span className="text-brand">{database.name}</span>
+);
 
 const TableAccessChange = ({ tables, verb, color }) => {
-    const tableNames = Object.values(tables).map(t => t.name);
-    return (
+  const tableNames = Object.values(tables).map(t => t.name);
+  return (
+    <span>
+      {verb}
+      <Tooltip
+        tooltip={
+          <div className="p1">{tableNames.map(name => <div>{name}</div>)}</div>
+        }
+      >
         <span>
-            {verb}
-            <Tooltip tooltip={<div className="p1">{tableNames.map(name => <div>{name}</div>)}</div>}>
-                <span>
-                    <span className={color}>{" " + tableNames.length + " " + inflect("table", tableNames.length)}</span>
-                </span>
-            </Tooltip>
+          <span className={color}>
+            {" " +
+              tableNames.length +
+              " " +
+              inflect("table", tableNames.length)}
+          </span>
         </span>
-    )
-}
+      </Tooltip>
+    </span>
+  );
+};
 
-
-const PermissionsConfirm = ({ diff }) =>
-    <div>
-        {Object.values(diff.groups).map(group =>
-            Object.values(group.databases).map(database =>
-                <div>
-                    { (database.grantedTables || database.revokedTables) &&
-                        <div>
-                            <GroupName group={group} />
-                            {t` will be `}
-                            {database.grantedTables && <TableAccessChange verb={t`given access to`} color="text-success" tables={database.grantedTables} /> }
-                            {database.grantedTables && database.revokedTables && t` and `}}
-                            {database.revokedTables && <TableAccessChange verb={t`denied access to`} color="text-warning" tables={database.revokedTables} /> }
-                            {" in "}
-                            <DatabaseName database={database} />
-                            {"."}
-                        </div>
-                    }
-                    { database.native &&
-                        <div>
-                            <GroupName group={group} />
-                            { database.native === "none" ?
-                                t` will no longer able to `
-                            :
-                                t` will now be able to `
-                            }
-                            { database.native === "read" ?
-                                <span className="text-gold">read</span>
-                            : database.native === "write" ?
-                                <span className="text-success">write</span>
-                            :
-                                <span>read or write</span>
-                            }
-                            {t` native queries for `}
-                            <DatabaseName database={database} />
-                            {"."}
-                        </div>
-                    }
-                </div>
-            )
-        )}
-    </div>
+const PermissionsConfirm = ({ diff }) => (
+  <div>
+    {Object.values(diff.groups).map(group =>
+      Object.values(group.databases).map(database => (
+        <div>
+          {(database.grantedTables || database.revokedTables) && (
+            <div>
+              <GroupName group={group} />
+              {t` will be `}
+              {database.grantedTables && (
+                <TableAccessChange
+                  verb={t`given access to`}
+                  color="text-success"
+                  tables={database.grantedTables}
+                />
+              )}
+              {database.grantedTables && database.revokedTables && t` and `}}
+              {database.revokedTables && (
+                <TableAccessChange
+                  verb={t`denied access to`}
+                  color="text-warning"
+                  tables={database.revokedTables}
+                />
+              )}
+              {" in "}
+              <DatabaseName database={database} />
+              {"."}
+            </div>
+          )}
+          {database.native && (
+            <div>
+              <GroupName group={group} />
+              {database.native === "none"
+                ? t` will no longer able to `
+                : t` will now be able to `}
+              {database.native === "read" ? (
+                <span className="text-gold">read</span>
+              ) : database.native === "write" ? (
+                <span className="text-success">write</span>
+              ) : (
+                <span>read or write</span>
+              )}
+              {t` native queries for `}
+              <DatabaseName database={database} />
+              {"."}
+            </div>
+          )}
+        </div>
+      )),
+    )}
+  </div>
+);
 
 export default PermissionsConfirm;
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx
index 0bf1fb7e2a41d789bebdda2b59958e4c241853b5..8d810fa98f12153b6dc473bf616d82e420b14f45 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsEditor.jsx
@@ -8,87 +8,115 @@ import PermissionsConfirm from "../components/PermissionsConfirm.jsx";
 import EditBar from "metabase/components/EditBar.jsx";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import Button from "metabase/components/Button";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 import _ from "underscore";
 
-const PermissionsEditor = ({ title = t`Permissions`, modal, admin, grid, onUpdatePermission, onSave, onCancel, confirmCancel, isDirty, saveError, diff, location }) => {
-    const saveButton =
-        <Confirm
-            title={t`Save permissions?`}
-            action={onSave}
-            content={<PermissionsConfirm diff={diff} />}
-            triggerClasses={cx({ disabled: !isDirty })}
-            key="save"
-        >
-            <Button primary small={!modal}>{t`Save Changes`}</Button>
-        </Confirm>;
+const PermissionsEditor = ({
+  title = t`Permissions`,
+  modal,
+  admin,
+  grid,
+  onUpdatePermission,
+  onSave,
+  onCancel,
+  confirmCancel,
+  isDirty,
+  saveError,
+  diff,
+  location,
+}) => {
+  const saveButton = (
+    <Confirm
+      title={t`Save permissions?`}
+      action={onSave}
+      content={<PermissionsConfirm diff={diff} />}
+      triggerClasses={cx({ disabled: !isDirty })}
+      key="save"
+    >
+      <Button primary small={!modal}>{t`Save Changes`}</Button>
+    </Confirm>
+  );
 
-    const cancelButton = confirmCancel ?
-        <Confirm
-            title={t`Discard changes?`}
-            action={onCancel}
-            content={t`No changes to permissions will be made.`}
-            key="discard"
-        >
-            <Button small={!modal}>{t`Cancel`}</Button>
-        </Confirm>
-    :
-        <Button small={!modal} onClick={onCancel} key="cancel">{t`Cancel`}</Button>;
+  const cancelButton = confirmCancel ? (
+    <Confirm
+      title={t`Discard changes?`}
+      action={onCancel}
+      content={t`No changes to permissions will be made.`}
+      key="discard"
+    >
+      <Button small={!modal}>{t`Cancel`}</Button>
+    </Confirm>
+  ) : (
+    <Button small={!modal} onClick={onCancel} key="cancel">{t`Cancel`}</Button>
+  );
 
-    return (
-        <LoadingAndErrorWrapper loading={!grid} className="flex-full flex flex-column">
-        { () => // eslint-disable-line react/display-name
-        modal ?
-            <Modal inline title={title} footer={[cancelButton, saveButton]} onClose={onCancel}>
-                <PermissionsGrid
-                    className="flex-full"
-                    grid={grid}
-                    onUpdatePermission={onUpdatePermission}
-                    {...getEntityAndGroupIdFromLocation(location)}
-                />
-            </Modal>
-        :
-            <div className="flex-full flex flex-column">
-                { isDirty &&
-                    <EditBar
-                        admin={admin}
-                        title={t`You've made changes to permissions.`}
-                        buttons={[cancelButton, saveButton]}
-                    />
-                }
-                <div className="wrapper pt2">
-                    { grid && grid.crumbs ?
-                        <Breadcrumbs className="py1" crumbs={grid.crumbs} />
-                    :
-                        <h2>{title}</h2>
-                    }
-                </div>
-                <PermissionsGrid
-                    className="flex-full"
-                    grid={grid}
-                    onUpdatePermission={onUpdatePermission}
-                    {...getEntityAndGroupIdFromLocation(location)}
-                />
+  return (
+    <LoadingAndErrorWrapper
+      loading={!grid}
+      className="flex-full flex flex-column"
+    >
+      {() =>
+        // eslint-disable-line react/display-name
+        modal ? (
+          <Modal
+            inline
+            title={title}
+            footer={[cancelButton, saveButton]}
+            onClose={onCancel}
+          >
+            <PermissionsGrid
+              className="flex-full"
+              grid={grid}
+              onUpdatePermission={onUpdatePermission}
+              {...getEntityAndGroupIdFromLocation(location)}
+            />
+          </Modal>
+        ) : (
+          <div className="flex-full flex flex-column">
+            {isDirty && (
+              <EditBar
+                admin={admin}
+                title={t`You've made changes to permissions.`}
+                buttons={[cancelButton, saveButton]}
+              />
+            )}
+            <div className="wrapper pt2">
+              {grid && grid.crumbs ? (
+                <Breadcrumbs className="py1" crumbs={grid.crumbs} />
+              ) : (
+                <h2>{title}</h2>
+              )}
             </div>
-        }
-        </LoadingAndErrorWrapper>
-    )
-}
+            <PermissionsGrid
+              className="flex-full"
+              grid={grid}
+              onUpdatePermission={onUpdatePermission}
+              {...getEntityAndGroupIdFromLocation(location)}
+            />
+          </div>
+        )
+      }
+    </LoadingAndErrorWrapper>
+  );
+};
 
 PermissionsEditor.defaultProps = {
-    admin: true
-}
+  admin: true,
+};
 
-function getEntityAndGroupIdFromLocation({ query = {}} = {}) {
-    query = _.mapObject(query, (value) => isNaN(value) ? value : parseFloat(value));
-    const entityId = _.omit(query, "groupId");
-    const groupId = query.groupId;
-    return {
-        groupId: groupId || null,
-        entityId: Object.keys(entityId).length > 0 ? entityId : null
-    };
+function getEntityAndGroupIdFromLocation({ query = {} } = {}) {
+  query = _.mapObject(
+    query,
+    value => (isNaN(value) ? value : parseFloat(value)),
+  );
+  const entityId = _.omit(query, "groupId");
+  const groupId = query.groupId;
+  return {
+    groupId: groupId || null,
+    entityId: Object.keys(entityId).length > 0 ? entityId : null,
+  };
 }
 
 export default PermissionsEditor;
diff --git a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx
index a2e242a2db27a695861ddef7c703c355d94defe3..2f22653f941f4ab01973dfaa9b4167329aaf0cea 100644
--- a/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx
+++ b/frontend/src/metabase/admin/permissions/components/PermissionsGrid.jsx
@@ -11,7 +11,7 @@ import ConfirmContent from "metabase/components/ConfirmContent.jsx";
 import Modal from "metabase/components/Modal.jsx";
 
 import FixedHeaderGrid from "./FixedHeaderGrid.jsx";
-import { AutoSizer } from 'react-virtualized'
+import { AutoSizer } from "react-virtualized";
 
 import { capitalize, pluralize } from "metabase/lib/formatting";
 import cx from "classnames";
@@ -20,17 +20,22 @@ const LIGHT_BORDER = "rgb(225, 226, 227)";
 const DARK_BORDER = "rgb(161, 163, 169)";
 const BORDER_RADIUS = 4;
 
-const getBorderStyles = ({ isFirstColumn, isLastColumn, isFirstRow, isLastRow }) => ({
-    overflow: "hidden",
-    border: "1px solid " + LIGHT_BORDER,
-    borderTopWidth: isFirstRow ? 1 : 0,
-    borderRightWidth: isLastColumn ? 1 : 0,
-    borderLeftColor: isFirstColumn ? LIGHT_BORDER : DARK_BORDER,
-    borderTopRightRadius: isLastColumn && isFirstRow ? BORDER_RADIUS : 0,
-    borderTopLeftRadius: isFirstColumn && isFirstRow ? BORDER_RADIUS : 0,
-    borderBottomRightRadius: isLastColumn && isLastRow ? BORDER_RADIUS : 0,
-    borderBottomLeftRadius: isFirstColumn && isLastRow ? BORDER_RADIUS : 0,
-})
+const getBorderStyles = ({
+  isFirstColumn,
+  isLastColumn,
+  isFirstRow,
+  isLastRow,
+}) => ({
+  overflow: "hidden",
+  border: "1px solid " + LIGHT_BORDER,
+  borderTopWidth: isFirstRow ? 1 : 0,
+  borderRightWidth: isLastColumn ? 1 : 0,
+  borderLeftColor: isFirstColumn ? LIGHT_BORDER : DARK_BORDER,
+  borderTopRightRadius: isLastColumn && isFirstRow ? BORDER_RADIUS : 0,
+  borderTopLeftRadius: isFirstColumn && isFirstRow ? BORDER_RADIUS : 0,
+  borderBottomRightRadius: isLastColumn && isLastRow ? BORDER_RADIUS : 0,
+  borderBottomLeftRadius: isFirstColumn && isLastRow ? BORDER_RADIUS : 0,
+});
 
 const CELL_HEIGHT = 100;
 const CELL_WIDTH = 246;
@@ -38,276 +43,363 @@ const HEADER_HEIGHT = 65;
 const HEADER_WIDTH = 240;
 
 const DEFAULT_OPTION = {
-    icon: "unknown",
-    iconColor: "#9BA5B1",
-    bgColor: "#DFE8EA"
+  icon: "unknown",
+  iconColor: "#9BA5B1",
+  bgColor: "#DFE8EA",
 };
 
-const GroupColumnHeader = ({ group, permissions, isLastColumn, isFirstColumn }) =>
-    <div className="absolute bottom left right">
-        <h4 className="text-centered full my1 flex layout-centered">
-            { group.name }
-            { group.tooltip &&
-                <Tooltip tooltip={group.tooltip} maxWidth="24em">
-                    <Icon className="ml1" name="question" />
-                </Tooltip>
-            }
-        </h4>
-        <div className="flex" style={getBorderStyles({ isLastColumn, isFirstColumn, isFirstRow: true, isLastRow: false })}>
-            { permissions.map((permission, index) =>
-                <div key={permission.id} className="flex-full border-column-divider" style={{
-                    borderColor: LIGHT_BORDER,
-                }}>
-                    { permission.header &&
-                        <h5 className="my1 text-centered text-grey-3 text-uppercase text-light">{permission.header}</h5>
-                    }
-                </div>
-            )}
+const GroupColumnHeader = ({
+  group,
+  permissions,
+  isLastColumn,
+  isFirstColumn,
+}) => (
+  <div className="absolute bottom left right">
+    <h4 className="text-centered full my1 flex layout-centered">
+      {group.name}
+      {group.tooltip && (
+        <Tooltip tooltip={group.tooltip} maxWidth="24em">
+          <Icon className="ml1" name="question" />
+        </Tooltip>
+      )}
+    </h4>
+    <div
+      className="flex"
+      style={getBorderStyles({
+        isLastColumn,
+        isFirstColumn,
+        isFirstRow: true,
+        isLastRow: false,
+      })}
+    >
+      {permissions.map((permission, index) => (
+        <div
+          key={permission.id}
+          className="flex-full border-column-divider"
+          style={{
+            borderColor: LIGHT_BORDER,
+          }}
+        >
+          {permission.header && (
+            <h5 className="my1 text-centered text-grey-3 text-uppercase text-light">
+              {permission.header}
+            </h5>
+          )}
         </div>
+      ))}
     </div>
+  </div>
+);
 
-const PermissionsCell = ({ group, permissions, entity, onUpdatePermission, isLastRow, isLastColumn, isFirstColumn, isFaded }) =>
-    <div className="flex" style={getBorderStyles({ isLastRow, isLastColumn, isFirstColumn, isFirstRow: false })}>
-        { permissions.map(permission =>
-            <GroupPermissionCell
-                key={permission.id}
-                permission={permission}
-                group={group}
-                entity={entity}
-                onUpdatePermission={onUpdatePermission}
-                isEditable={group.editable}
-                isFaded={isFaded}
-            />
-        )}
-    </div>
+const PermissionsCell = ({
+  group,
+  permissions,
+  entity,
+  onUpdatePermission,
+  isLastRow,
+  isLastColumn,
+  isFirstColumn,
+  isFaded,
+}) => (
+  <div
+    className="flex"
+    style={getBorderStyles({
+      isLastRow,
+      isLastColumn,
+      isFirstColumn,
+      isFirstRow: false,
+    })}
+  >
+    {permissions.map(permission => (
+      <GroupPermissionCell
+        key={permission.id}
+        permission={permission}
+        group={group}
+        entity={entity}
+        onUpdatePermission={onUpdatePermission}
+        isEditable={group.editable}
+        isFaded={isFaded}
+      />
+    ))}
+  </div>
+);
 
 class GroupPermissionCell extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            confirmations: null,
-            confirmAction: null,
-            hovered: false
-        }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      confirmations: null,
+      confirmAction: null,
+      hovered: false,
+    };
+  }
+  hoverEnter() {
+    // only change the hover state if the group is not the admin
+    // this helps indicate to users that the admin group is different
+    if (this.props.isEditable) {
+      return this.setState({ hovered: true });
     }
-    hoverEnter () {
-        // only change the hover state if the group is not the admin
-        // this helps indicate to users that the admin group is different
-        if (this.props.isEditable) {
-            return this.setState({ hovered: true });
-        }
-        return false
-    }
-    hoverExit () {
-        if (this.props.isEditable) {
-            return this.setState({ hovered: false });
-        }
-        return false
+    return false;
+  }
+  hoverExit() {
+    if (this.props.isEditable) {
+      return this.setState({ hovered: false });
     }
-    render() {
-        const { permission, group, entity, onUpdatePermission, isFaded } = this.props;
-        const { confirmations } = this.state;
+    return false;
+  }
+  render() {
+    const {
+      permission,
+      group,
+      entity,
+      onUpdatePermission,
+      isFaded,
+    } = this.props;
+    const { confirmations } = this.state;
 
-        const value = permission.getter(group.id, entity.id);
-        const options = permission.options(group.id, entity.id);
-        const warning = permission.warning && permission.warning(group.id, entity.id);
+    const value = permission.getter(group.id, entity.id);
+    const options = permission.options(group.id, entity.id);
+    const warning =
+      permission.warning && permission.warning(group.id, entity.id);
 
-        let isEditable = this.props.isEditable && options.filter(option => option.value !== value).length > 0;
-        const option = _.findWhere(options, { value }) || DEFAULT_OPTION;
+    let isEditable =
+      this.props.isEditable &&
+      options.filter(option => option.value !== value).length > 0;
+    const option = _.findWhere(options, { value }) || DEFAULT_OPTION;
 
-        return (
-                <PopoverWithTrigger
-                    ref="popover"
-                    disabled={!isEditable}
-                    triggerClasses="cursor-pointer flex flex-full layout-centered border-column-divider"
-                    triggerElement={
-                        <Tooltip tooltip={option.tooltip}>
-                            <div
-                                className={cx('flex-full flex layout-centered relative', {
-                                    'cursor-pointer' : isEditable,
-                                    faded: isFaded
-                                })}
-                                style={{
-                                    borderColor: LIGHT_BORDER,
-                                    height: CELL_HEIGHT - 1,
-                                    backgroundColor: this.state.hovered ? option.iconColor : option.bgColor,
-                                }}
-                                onMouseEnter={() => this.hoverEnter()}
-                                onMouseLeave={() => this.hoverExit()}
-                            >
-                                <Icon
-                                    name={option.icon}
-                                    size={28}
-                                    style={{ color: this.state.hovered ? '#fff' : option.iconColor }}
-                                />
-                                { confirmations && confirmations.length > 0 &&
-                                    <Modal>
-                                        <ConfirmContent
-                                            {...confirmations[0]}
-                                            onAction={() =>
-                                                // if it's the last one call confirmAction, otherwise remove the confirmation that was just confirmed
-                                                confirmations.length === 1 ?
-                                                    this.setState({ confirmations: null, confirmAction: null }, this.state.confirmAction)
-                                                :
-                                                    this.setState({ confirmations: confirmations.slice(1) })
-                                            }
-                                            onCancel={() => this.setState({ confirmations: null, confirmAction: null })}
-                                        />
-                                    </Modal>
-                                }
-                                { warning &&
-                                    <div className="absolute top right p1">
-                                        <Tooltip tooltip={warning} maxWidth="24em">
-                                            <Icon name="warning2" className="text-slate" />
-                                        </Tooltip>
-                                    </div>
-                                }
-                            </div>
-                        </Tooltip>
-                   }
-                >
-                    <AccessOptionList
-                        value={value}
-                        options={options}
-                        permission={permission}
-                        onChange={(value) => {
-                            const confirmAction = () => {
-                                onUpdatePermission({
-                                    groupId: group.id,
-                                    entityId: entity.id,
-                                    value: value,
-                                    updater: permission.updater,
-                                    postAction: permission.postAction
-                                })
-                            }
-                            let confirmations = (permission.confirm && permission.confirm(group.id, entity.id, value) || []).filter(c => c);
-                            if (confirmations.length > 0) {
-                                this.setState({ confirmations, confirmAction });
-                            } else {
-                                confirmAction();
-                            }
-                            this.refs.popover.close();
-                        }}
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        disabled={!isEditable}
+        triggerClasses="cursor-pointer flex flex-full layout-centered border-column-divider"
+        triggerElement={
+          <Tooltip tooltip={option.tooltip}>
+            <div
+              className={cx("flex-full flex layout-centered relative", {
+                "cursor-pointer": isEditable,
+                faded: isFaded,
+              })}
+              style={{
+                borderColor: LIGHT_BORDER,
+                height: CELL_HEIGHT - 1,
+                backgroundColor: this.state.hovered
+                  ? option.iconColor
+                  : option.bgColor,
+              }}
+              onMouseEnter={() => this.hoverEnter()}
+              onMouseLeave={() => this.hoverExit()}
+            >
+              <Icon
+                name={option.icon}
+                size={28}
+                style={{
+                  color: this.state.hovered ? "#fff" : option.iconColor,
+                }}
+              />
+              {confirmations &&
+                confirmations.length > 0 && (
+                  <Modal>
+                    <ConfirmContent
+                      {...confirmations[0]}
+                      onAction={() =>
+                        // if it's the last one call confirmAction, otherwise remove the confirmation that was just confirmed
+                        confirmations.length === 1
+                          ? this.setState(
+                              { confirmations: null, confirmAction: null },
+                              this.state.confirmAction,
+                            )
+                          : this.setState({
+                              confirmations: confirmations.slice(1),
+                            })
+                      }
+                      onCancel={() =>
+                        this.setState({
+                          confirmations: null,
+                          confirmAction: null,
+                        })
+                      }
                     />
-                </PopoverWithTrigger>
-        );
-    }
+                  </Modal>
+                )}
+              {warning && (
+                <div className="absolute top right p1">
+                  <Tooltip tooltip={warning} maxWidth="24em">
+                    <Icon name="warning2" className="text-slate" />
+                  </Tooltip>
+                </div>
+              )}
+            </div>
+          </Tooltip>
+        }
+      >
+        <AccessOptionList
+          value={value}
+          options={options}
+          permission={permission}
+          onChange={value => {
+            const confirmAction = () => {
+              onUpdatePermission({
+                groupId: group.id,
+                entityId: entity.id,
+                value: value,
+                updater: permission.updater,
+                postAction: permission.postAction,
+              });
+            };
+            let confirmations = (
+              (permission.confirm &&
+                permission.confirm(group.id, entity.id, value)) ||
+              []
+            ).filter(c => c);
+            if (confirmations.length > 0) {
+              this.setState({ confirmations, confirmAction });
+            } else {
+              confirmAction();
+            }
+            this.refs.popover.close();
+          }}
+        />
+      </PopoverWithTrigger>
+    );
+  }
 }
 
-const AccessOption = ({ value, option, onChange }) =>
-    <div
-        className={cx("flex py2 px2 align-center bg-brand-hover text-white-hover cursor-pointer", {
-            "bg-brand text-white": value === option
-        })}
-        onClick={() => onChange(option.value)}
-    >
-        <Icon name={option.icon} className="mr1" style={{ color: option.iconColor }} size={18} />
-        {option.title}
-    </div>
+const AccessOption = ({ value, option, onChange }) => (
+  <div
+    className={cx(
+      "flex py2 px2 align-center bg-brand-hover text-white-hover cursor-pointer",
+      {
+        "bg-brand text-white": value === option,
+      },
+    )}
+    onClick={() => onChange(option.value)}
+  >
+    <Icon
+      name={option.icon}
+      className="mr1"
+      style={{ color: option.iconColor }}
+      size={18}
+    />
+    {option.title}
+  </div>
+);
 
-const AccessOptionList = ({ value, options, onChange }) =>
-    <ul className="py1">
-        { options.map(option => {
-            if( value !== option.value ) {
-                return (
-                    <li key={option.value}>
-                        <AccessOption value={value} option={option} onChange={onChange} />
-                    </li>
-               )
-            }
-        }
-        )}
-    </ul>
+const AccessOptionList = ({ value, options, onChange }) => (
+  <ul className="py1">
+    {options.map(option => {
+      if (value !== option.value) {
+        return (
+          <li key={option.value}>
+            <AccessOption value={value} option={option} onChange={onChange} />
+          </li>
+        );
+      }
+    })}
+  </ul>
+);
 
-const EntityRowHeader = ({ entity, icon }) =>
-    <div
-        className="flex flex-column justify-center px1 pl4 ml2"
-        style={{
-            height: CELL_HEIGHT
-        }}
-    >
-        <div className="relative flex align-center">
-            <Icon name={icon} className="absolute" style={{ left: -28 }} />
-            <h4>{entity.name}</h4>
-        </div>
-        { entity.subtitle &&
-            <span className="mt1 h5 text-monospace text-normal text-grey-2 text-uppercase">{entity.subtitle}</span>
-        }
-        { entity.link &&
-            <Link className="mt1 link" to={entity.link.url}>{entity.link.name}</Link>
-        }
+const EntityRowHeader = ({ entity, icon }) => (
+  <div
+    className="flex flex-column justify-center px1 pl4 ml2"
+    style={{
+      height: CELL_HEIGHT,
+    }}
+  >
+    <div className="relative flex align-center">
+      <Icon name={icon} className="absolute" style={{ left: -28 }} />
+      <h4>{entity.name}</h4>
     </div>
+    {entity.subtitle && (
+      <span className="mt1 h5 text-monospace text-normal text-grey-2 text-uppercase">
+        {entity.subtitle}
+      </span>
+    )}
+    {entity.link && (
+      <Link className="mt1 link" to={entity.link.url}>
+        {entity.link.name}
+      </Link>
+    )}
+  </div>
+);
 
-const CornerHeader = ({ grid }) =>
-    <div className="absolute bottom left right flex flex-column align-center pb1">
-        <div className="flex align-center">
-            <h3 className="ml1">{capitalize(pluralize(grid.type))}</h3>
-        </div>
+const CornerHeader = ({ grid }) => (
+  <div className="absolute bottom left right flex flex-column align-center pb1">
+    <div className="flex align-center">
+      <h3 className="ml1">{capitalize(pluralize(grid.type))}</h3>
     </div>
+  </div>
+);
 
 import _ from "underscore";
 
-const PermissionsGrid = ({ className, grid, onUpdatePermission, entityId, groupId }) => {
-    const permissions = Object.entries(grid.permissions).map(([id, permission]) =>
-        ({ id: id, ...permission })
-    );
-    return (
-        <div className={className}>
-            <AutoSizer>
-                {({ height, width }) =>
-                    <FixedHeaderGrid
-                        height={height}
-                        width={width}
-                        rowCount={grid.entities.length}
-                        columnCount={grid.groups.length}
-                        columnWidth={Math.max(CELL_WIDTH, (width - 20 - HEADER_WIDTH) / grid.groups.length)}
-                        rowHeight={CELL_HEIGHT}
-                        paddingBottom={20}
-                        paddingRight={20}
-                        columnHeaderHeight={HEADER_HEIGHT}
-                        rowHeaderWidth={HEADER_WIDTH}
-                        renderCell={({ columnIndex, rowIndex }) =>
-                            <PermissionsCell
-                                group={grid.groups[columnIndex]}
-                                permissions={permissions}
-                                entity={grid.entities[rowIndex]}
-                                onUpdatePermission={onUpdatePermission}
-                                isFirstRow={rowIndex === 0}
-                                isLastRow={rowIndex === grid.entities.length - 1}
-                                isFirstColumn={columnIndex === 0}
-                                isLastColumn={columnIndex === grid.groups.length - 1}
-                                isFaded={
-                                    (groupId != null && grid.groups[columnIndex].id !== groupId) ||
-                                    (entityId != null && !_.isEqual(entityId, grid.entities[rowIndex].id))
-                                }
-                            />
-                        }
-                        renderColumnHeader={({ columnIndex }) =>
-                            <GroupColumnHeader
-                                group={grid.groups[columnIndex]}
-                                permissions={permissions}
-                                isFirstColumn={columnIndex === 0}
-                                isLastColumn={columnIndex === grid.groups.length - 1}
-                            />
-                        }
-                        renderRowHeader={({ rowIndex }) =>
-                            <EntityRowHeader
-                                icon={grid.icon}
-                                entity={grid.entities[rowIndex]}
-                                isFirstRow={rowIndex === 0}
-                                isLastRow={rowIndex === grid.entities.length - 1}
-                            />
-                        }
-                        renderCorner={() =>
-                            <CornerHeader
-                                grid={grid}
-                            />
-                        }
-                    />
+const PermissionsGrid = ({
+  className,
+  grid,
+  onUpdatePermission,
+  entityId,
+  groupId,
+}) => {
+  const permissions = Object.entries(grid.permissions).map(
+    ([id, permission]) => ({ id: id, ...permission }),
+  );
+  return (
+    <div className={className}>
+      <AutoSizer>
+        {({ height, width }) => (
+          <FixedHeaderGrid
+            height={height}
+            width={width}
+            rowCount={grid.entities.length}
+            columnCount={grid.groups.length}
+            columnWidth={Math.max(
+              CELL_WIDTH,
+              (width - 20 - HEADER_WIDTH) / grid.groups.length,
+            )}
+            rowHeight={CELL_HEIGHT}
+            paddingBottom={20}
+            paddingRight={20}
+            columnHeaderHeight={HEADER_HEIGHT}
+            rowHeaderWidth={HEADER_WIDTH}
+            renderCell={({ columnIndex, rowIndex }) => (
+              <PermissionsCell
+                group={grid.groups[columnIndex]}
+                permissions={permissions}
+                entity={grid.entities[rowIndex]}
+                onUpdatePermission={onUpdatePermission}
+                isFirstRow={rowIndex === 0}
+                isLastRow={rowIndex === grid.entities.length - 1}
+                isFirstColumn={columnIndex === 0}
+                isLastColumn={columnIndex === grid.groups.length - 1}
+                isFaded={
+                  (groupId != null &&
+                    grid.groups[columnIndex].id !== groupId) ||
+                  (entityId != null &&
+                    !_.isEqual(entityId, grid.entities[rowIndex].id))
                 }
-            </AutoSizer>
-        </div>
-    );
-}
+              />
+            )}
+            renderColumnHeader={({ columnIndex }) => (
+              <GroupColumnHeader
+                group={grid.groups[columnIndex]}
+                permissions={permissions}
+                isFirstColumn={columnIndex === 0}
+                isLastColumn={columnIndex === grid.groups.length - 1}
+              />
+            )}
+            renderRowHeader={({ rowIndex }) => (
+              <EntityRowHeader
+                icon={grid.icon}
+                entity={grid.entities[rowIndex]}
+                isFirstRow={rowIndex === 0}
+                isLastRow={rowIndex === grid.entities.length - 1}
+              />
+            )}
+            renderCorner={() => <CornerHeader grid={grid} />}
+          />
+        )}
+      </AutoSizer>
+    </div>
+  );
+};
 
 export default PermissionsGrid;
diff --git a/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx
index 585d002b56e20fe08ef0296bb77d8f18f191b215..242d4b188a6efc8d61b1a18e4f636c9010b07c12 100644
--- a/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/CollectionsPermissionsApp.jsx
@@ -6,41 +6,50 @@ import PermissionsApp from "./PermissionsApp.jsx";
 
 import { CollectionsApi } from "metabase/services";
 
-import { getCollectionsPermissionsGrid, getIsDirty, getSaveError, getDiff } from "../selectors";
-import { updatePermission, savePermissions, loadCollections } from "../permissions";
+import {
+  getCollectionsPermissionsGrid,
+  getIsDirty,
+  getSaveError,
+  getDiff,
+} from "../selectors";
+import {
+  updatePermission,
+  savePermissions,
+  loadCollections,
+} from "../permissions";
 import { goBack, push } from "react-router-redux";
 
 const mapStateToProps = (state, props) => {
-    return {
-        grid: getCollectionsPermissionsGrid(state, props),
-        isDirty: getIsDirty(state, props),
-        saveError: getSaveError(state, props),
-        diff: getDiff(state, props)
-    }
-}
+  return {
+    grid: getCollectionsPermissionsGrid(state, props),
+    isDirty: getIsDirty(state, props),
+    saveError: getSaveError(state, props),
+    diff: getDiff(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    onUpdatePermission: updatePermission,
-    onSave: savePermissions,
-    onCancel: () => window.history.length > 1 ? goBack() : push("/questions")
+  onUpdatePermission: updatePermission,
+  onSave: savePermissions,
+  onCancel: () => (window.history.length > 1 ? goBack() : push("/questions")),
 };
 
 const Editor = connect(mapStateToProps, mapDispatchToProps)(PermissionsEditor);
 
 @connect(null, { loadCollections })
 export default class CollectionsPermissionsApp extends Component {
-    componentWillMount() {
-        this.props.loadCollections();
-    }
-    render() {
-        return (
-            <PermissionsApp
-                {...this.props}
-                load={CollectionsApi.graph}
-                save={CollectionsApi.updateGraph}
-            >
-                <Editor {...this.props} modal confirmCancel={false} />
-            </PermissionsApp>
-        )
-    }
+  componentWillMount() {
+    this.props.loadCollections();
+  }
+  render() {
+    return (
+      <PermissionsApp
+        {...this.props}
+        load={CollectionsApi.graph}
+        save={CollectionsApi.updateGraph}
+      >
+        <Editor {...this.props} modal confirmCancel={false} />
+      </PermissionsApp>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx
index fb73b195f357c53b45b0af552dd8587fa6e5266e..a5de2e3d6f2fcab1e870ce5d7c9c2e6128817754 100644
--- a/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/DataPermissionsApp.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { connect } from "react-redux"
+import { connect } from "react-redux";
 
 import PermissionsApp from "./PermissionsApp.jsx";
 
@@ -8,16 +8,16 @@ import { fetchRealDatabases } from "metabase/redux/metadata";
 
 @connect(null, { fetchRealDatabases })
 export default class DataPermissionsApp extends Component {
-    componentWillMount() {
-        this.props.fetchRealDatabases(true);
-    }
-    render() {
-        return (
-            <PermissionsApp
-                {...this.props}
-                load={PermissionsApi.graph}
-                save={PermissionsApi.updateGraph}
-            />
-        );
-    }
+  componentWillMount() {
+    this.props.fetchRealDatabases(true);
+  }
+  render() {
+    return (
+      <PermissionsApp
+        {...this.props}
+        load={PermissionsApi.graph}
+        save={PermissionsApi.updateGraph}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx
index 6a58c07a5fe1d73effdc9789f4074c11db4a59a6..bb2ba035f94050aa5ebcbac4c50061ed823155ff 100644
--- a/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/DatabasesPermissionsApp.jsx
@@ -2,22 +2,31 @@ import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
 
-import { getDatabasesPermissionsGrid, getIsDirty, getSaveError, getDiff } from "../selectors";
-import { updatePermission, savePermissions, loadPermissions } from "../permissions"
+import {
+  getDatabasesPermissionsGrid,
+  getIsDirty,
+  getSaveError,
+  getDiff,
+} from "../selectors";
+import {
+  updatePermission,
+  savePermissions,
+  loadPermissions,
+} from "../permissions";
 
 const mapStateToProps = (state, props) => {
-    return {
-        grid: getDatabasesPermissionsGrid(state, props),
-        isDirty: getIsDirty(state, props),
-        saveError: getSaveError(state, props),
-        diff: getDiff(state, props)
-    }
-}
+  return {
+    grid: getDatabasesPermissionsGrid(state, props),
+    isDirty: getIsDirty(state, props),
+    saveError: getSaveError(state, props),
+    diff: getDiff(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    onUpdatePermission: updatePermission,
-    onSave: savePermissions,
-    onCancel: loadPermissions,
+  onUpdatePermission: updatePermission,
+  onSave: savePermissions,
+  onCancel: loadPermissions,
 };
 
 export default connect(mapStateToProps, mapDispatchToProps)(PermissionsEditor);
diff --git a/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx
index 55aba2188fd6cda2d01ef55be53708882513c9ec..e903bff4eab5179fb3c3bcff9eff90f544ca177b 100644
--- a/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/PermissionsApp.jsx
@@ -1,72 +1,69 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { withRouter } from "react-router";
-import { connect } from "react-redux"
+import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
 import { initialize } from "../permissions";
 import { getIsDirty } from "../selectors";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ConfirmContent from "metabase/components/ConfirmContent.jsx";
 import Modal from "metabase/components/Modal.jsx";
 
 const mapStateToProps = (state, props) => ({
-    isDirty: getIsDirty(state, props)
+  isDirty: getIsDirty(state, props),
 });
 
 const mapDispatchToProps = {
-    initialize,
-    push
+  initialize,
+  push,
 };
 
 @withRouter
 @connect(mapStateToProps, mapDispatchToProps)
 export default class PermissionsApp extends Component {
-    static propTypes = {
-        load: PropTypes.func.isRequired,
-        save: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    load: PropTypes.func.isRequired,
+    save: PropTypes.func.isRequired,
+  };
 
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            nextLocation: false,
-            confirmed: false
-        }
-    }
-    componentWillMount() {
-        this.props.initialize(this.props.load, this.props.save);
-        this.props.router.setRouteLeaveHook(
-            this.props.route,
-            this.routerWillLeave
-        )
-    }
-    routerWillLeave = (nextLocation) => {
-        if (this.props.isDirty && !this.state.confirmed) {
-            this.setState({ nextLocation: nextLocation, confirmed: false });
-            return false;
-        }
-    }
-    render() {
-        return (
-            <div className="flex-full flex">
-                {this.props.children}
-                <Modal isOpen={this.state.nextLocation}>
-                    <ConfirmContent
-                        title={t`You have unsaved changes`}
-                        message={t`Do you want to leave this page and discard your changes?`}
-                        onClose={() => {
-                            this.setState({ nextLocation: null });
-                        }}
-                        onAction={() => {
-                            const { nextLocation } = this.state;
-                            this.setState({ nextLocation: null, confirmed: true }, () => {
-                                this.props.push(nextLocation.pathname, nextLocation.state);
-                            });
-                        }}
-                    />
-                </Modal>
-            </div>
-        );
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      nextLocation: false,
+      confirmed: false,
+    };
+  }
+  componentWillMount() {
+    this.props.initialize(this.props.load, this.props.save);
+    this.props.router.setRouteLeaveHook(this.props.route, this.routerWillLeave);
+  }
+  routerWillLeave = nextLocation => {
+    if (this.props.isDirty && !this.state.confirmed) {
+      this.setState({ nextLocation: nextLocation, confirmed: false });
+      return false;
     }
+  };
+  render() {
+    return (
+      <div className="flex-full flex">
+        {this.props.children}
+        <Modal isOpen={this.state.nextLocation}>
+          <ConfirmContent
+            title={t`You have unsaved changes`}
+            message={t`Do you want to leave this page and discard your changes?`}
+            onClose={() => {
+              this.setState({ nextLocation: null });
+            }}
+            onAction={() => {
+              const { nextLocation } = this.state;
+              this.setState({ nextLocation: null, confirmed: true }, () => {
+                this.props.push(nextLocation.pathname, nextLocation.state);
+              });
+            }}
+          />
+        </Modal>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx
index 744ed2d9e55e3d6af448a665565c8b15c07af414..170675051a4c0db70823dbf9d309c5a61cbbed13 100644
--- a/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/SchemasPermissionsApp.jsx
@@ -2,22 +2,31 @@ import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
 
-import { getSchemasPermissionsGrid, getIsDirty, getSaveError, getDiff } from "../selectors";
-import { updatePermission, savePermissions, loadPermissions } from "../permissions"
+import {
+  getSchemasPermissionsGrid,
+  getIsDirty,
+  getSaveError,
+  getDiff,
+} from "../selectors";
+import {
+  updatePermission,
+  savePermissions,
+  loadPermissions,
+} from "../permissions";
 
 const mapStateToProps = (state, props) => {
-    return {
-        grid: getSchemasPermissionsGrid(state, props),
-        isDirty: getIsDirty(state, props),
-        saveError: getSaveError(state, props),
-        diff: getDiff(state, props)
-    }
-}
+  return {
+    grid: getSchemasPermissionsGrid(state, props),
+    isDirty: getIsDirty(state, props),
+    saveError: getSaveError(state, props),
+    diff: getDiff(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    onUpdatePermission: updatePermission,
-    onSave: savePermissions,
-    onCancel: loadPermissions,
+  onUpdatePermission: updatePermission,
+  onSave: savePermissions,
+  onCancel: loadPermissions,
 };
 
 export default connect(mapStateToProps, mapDispatchToProps)(PermissionsEditor);
diff --git a/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx b/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx
index 31ad4e730a4f722437a4e0979d83cb16e64499e6..a091682cf86907716ea12571dee656231fd2fbaa 100644
--- a/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx
+++ b/frontend/src/metabase/admin/permissions/containers/TablesPermissionsApp.jsx
@@ -2,22 +2,31 @@ import { connect } from "react-redux";
 
 import PermissionsEditor from "../components/PermissionsEditor.jsx";
 
-import { getTablesPermissionsGrid, getIsDirty, getSaveError, getDiff } from "../selectors";
-import { updatePermission, savePermissions, loadPermissions } from "../permissions"
+import {
+  getTablesPermissionsGrid,
+  getIsDirty,
+  getSaveError,
+  getDiff,
+} from "../selectors";
+import {
+  updatePermission,
+  savePermissions,
+  loadPermissions,
+} from "../permissions";
 
 const mapStateToProps = (state, props) => {
-    return {
-        grid: getTablesPermissionsGrid(state, props),
-        isDirty: getIsDirty(state, props),
-        saveError: getSaveError(state, props),
-        diff: getDiff(state, props)
-    }
-}
+  return {
+    grid: getTablesPermissionsGrid(state, props),
+    isDirty: getIsDirty(state, props),
+    saveError: getSaveError(state, props),
+    diff: getDiff(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    onUpdatePermission: updatePermission,
-    onSave: savePermissions,
-    onCancel: loadPermissions,
+  onUpdatePermission: updatePermission,
+  onSave: savePermissions,
+  onCancel: loadPermissions,
 };
 
 export default connect(mapStateToProps, mapDispatchToProps)(PermissionsEditor);
diff --git a/frontend/src/metabase/admin/permissions/permissions.js b/frontend/src/metabase/admin/permissions/permissions.js
index 20ccb664040e1941ed73775fd1894b11a1bc8d60..5dba04466fef3473bbbac513c1e2ac012541e695 100644
--- a/frontend/src/metabase/admin/permissions/permissions.js
+++ b/frontend/src/metabase/admin/permissions/permissions.js
@@ -1,125 +1,166 @@
-import { createAction, createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
+import {
+  createAction,
+  createThunkAction,
+  handleActions,
+  combineReducers,
+} from "metabase/lib/redux";
 
 import { canEditPermissions } from "metabase/lib/groups";
 import MetabaseAnalytics from "metabase/lib/analytics";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { PermissionsApi, CollectionsApi } from "metabase/services";
 
 const RESET = "metabase/admin/permissions/RESET";
 export const reset = createAction(RESET);
 
 const INITIALIZE = "metabase/admin/permissions/INITIALIZE";
-export const initialize = createThunkAction(INITIALIZE, (load, save) =>
-    async (dispatch, getState) => {
-        dispatch(reset({ load, save }));
-        await Promise.all([
-            dispatch(loadPermissions()),
-            dispatch(loadGroups()),
-        ]);
-    }
+export const initialize = createThunkAction(
+  INITIALIZE,
+  (load, save) => async (dispatch, getState) => {
+    dispatch(reset({ load, save }));
+    await Promise.all([dispatch(loadPermissions()), dispatch(loadGroups())]);
+  },
 );
 
 // TODO: move these to their respective ducks
 const LOAD_COLLECTIONS = "metabase/admin/permissions/LOAD_COLLECTIONS";
-export const loadCollections = createAction(LOAD_COLLECTIONS, () => CollectionsApi.list());
-
+export const loadCollections = createAction(LOAD_COLLECTIONS, () =>
+  CollectionsApi.list(),
+);
 
 const LOAD_GROUPS = "metabase/admin/permissions/LOAD_GROUPS";
-export const loadGroups = createAction(LOAD_GROUPS, () => PermissionsApi.groups());
+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().admin.permissions;
-        return load();
-    }
+export const loadPermissions = createThunkAction(
+  LOAD_PERMISSIONS,
+  () => async (dispatch, getState) => {
+    const { load } = getState().admin.permissions;
+    return load();
+  },
 );
 
 const UPDATE_PERMISSION = "metabase/admin/permissions/UPDATE_PERMISSION";
-export const updatePermission = createThunkAction(UPDATE_PERMISSION, ({ groupId, entityId, value, updater, postAction }) =>
-    async (dispatch, getState) => {
-        if (postAction) {
-            let action = postAction(groupId, entityId, value);
-            if (action) {
-                dispatch(action);
-            }
-        }
-        return updater(groupId, entityId, value);
+export const updatePermission = createThunkAction(
+  UPDATE_PERMISSION,
+  ({ groupId, entityId, value, updater, postAction }) => async (
+    dispatch,
+    getState,
+  ) => {
+    if (postAction) {
+      let action = postAction(groupId, entityId, value);
+      if (action) {
+        dispatch(action);
+      }
     }
+    return updater(groupId, entityId, value);
+  },
 );
 
 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().admin.permissions;
-        let result = await save({
-            revision: revision,
-            groups: permissions
-        });
-        return result;
-    }
-)
+export const savePermissions = createThunkAction(
+  SAVE_PERMISSIONS,
+  () => async (dispatch, getState) => {
+    MetabaseAnalytics.trackEvent("Permissions", "save");
+    const { permissions, revision, save } = getState().admin.permissions;
+    let result = await save({
+      revision: revision,
+      groups: permissions,
+    });
+    return result;
+  },
+);
 
-const save = handleActions({
-    [RESET]: { next: (state, { payload }) => payload.save }
-}, null);
-const load = handleActions({
-    [RESET]: { next: (state, { payload }) => payload.load }
-}, null);
+const save = handleActions(
+  {
+    [RESET]: { next: (state, { payload }) => payload.save },
+  },
+  null,
+);
+const load = handleActions(
+  {
+    [RESET]: { next: (state, { payload }) => payload.load },
+  },
+  null,
+);
 
-const permissions = handleActions({
+const permissions = handleActions(
+  {
     [RESET]: { next: () => null },
     [LOAD_PERMISSIONS]: { next: (state, { payload }) => payload.groups },
     [SAVE_PERMISSIONS]: { next: (state, { payload }) => payload.groups },
-    [UPDATE_PERMISSION]: { next: (state, { payload }) => payload }
-}, null);
+    [UPDATE_PERMISSION]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
 
-const originalPermissions = handleActions({
+const originalPermissions = handleActions(
+  {
     [RESET]: { next: () => null },
     [LOAD_PERMISSIONS]: { next: (state, { payload }) => payload.groups },
     [SAVE_PERMISSIONS]: { next: (state, { payload }) => payload.groups },
-}, null);
+  },
+  null,
+);
 
-const revision = handleActions({
+const revision = handleActions(
+  {
     [RESET]: { next: () => null },
     [LOAD_PERMISSIONS]: { next: (state, { payload }) => payload.revision },
     [SAVE_PERMISSIONS]: { next: (state, { payload }) => payload.revision },
-}, null);
-
-const groups = handleActions({
-    [LOAD_GROUPS]: { next: (state, { payload }) =>
-        payload && payload.map(group => ({
-            ...group,
-            editable: canEditPermissions(group)
-        }))
+  },
+  null,
+);
+
+const groups = handleActions(
+  {
+    [LOAD_GROUPS]: {
+      next: (state, { payload }) =>
+        payload &&
+        payload.map(group => ({
+          ...group,
+          editable: canEditPermissions(group),
+        })),
     },
-}, null);
+  },
+  null,
+);
 
-const collections = handleActions({
+const collections = handleActions(
+  {
     [LOAD_COLLECTIONS]: { next: (state, { payload }) => payload },
-}, null);
+  },
+  null,
+);
 
-const saveError = handleActions({
+const saveError = handleActions(
+  {
     [RESET]: { next: () => null },
     [SAVE_PERMISSIONS]: {
-        next: (state) => null,
-        throw: (state, { payload }) => (payload && typeof payload.data === "string" ? payload.data : payload.data.message) || t`Sorry, an error occurred.`
+      next: state => null,
+      throw: (state, { payload }) =>
+        (payload && typeof payload.data === "string"
+          ? payload.data
+          : payload.data.message) || t`Sorry, an error occurred.`,
     },
     [LOAD_PERMISSIONS]: {
-        next: (state) => null,
-    }
-}, null);
+      next: state => null,
+    },
+  },
+  null,
+);
 
 export default combineReducers({
-    save,
-    load,
+  save,
+  load,
 
-    permissions,
-    originalPermissions,
-    saveError,
-    revision,
-    groups,
+  permissions,
+  originalPermissions,
+  saveError,
+  revision,
+  groups,
 
-    collections
+  collections,
 });
diff --git a/frontend/src/metabase/admin/permissions/routes.jsx b/frontend/src/metabase/admin/permissions/routes.jsx
index 3d15e1d4a92074dd3939510da4f40fd0094a7d2f..18c79f63b1e126adfc741bb644805a71c7fbc138 100644
--- a/frontend/src/metabase/admin/permissions/routes.jsx
+++ b/frontend/src/metabase/admin/permissions/routes.jsx
@@ -1,24 +1,42 @@
-
 import React from "react";
 import { Route } from "metabase/hoc/Title";
-import { IndexRedirect } from 'react-router';
-import { t } from 'c-3po';
+import { IndexRedirect } from "react-router";
+import { t } from "c-3po";
 import DataPermissionsApp from "./containers/DataPermissionsApp.jsx";
 import DatabasesPermissionsApp from "./containers/DatabasesPermissionsApp.jsx";
 import SchemasPermissionsApp from "./containers/SchemasPermissionsApp.jsx";
 import TablesPermissionsApp from "./containers/TablesPermissionsApp.jsx";
 
-const getRoutes = (store) =>
-    <Route title={t`Permissions`} path="permissions" component={DataPermissionsApp}>
-        <IndexRedirect to="databases" />
-        <Route path="databases" component={DatabasesPermissionsApp} />
-        <Route path="databases/:databaseId/schemas" component={SchemasPermissionsApp} />
-        <Route path="databases/:databaseId/schemas/:schemaName/tables" component={TablesPermissionsApp} />
+const getRoutes = store => (
+  <Route
+    title={t`Permissions`}
+    path="permissions"
+    component={DataPermissionsApp}
+  >
+    <IndexRedirect to="databases" />
+    <Route path="databases" component={DatabasesPermissionsApp} />
+    <Route
+      path="databases/:databaseId/schemas"
+      component={SchemasPermissionsApp}
+    />
+    <Route
+      path="databases/:databaseId/schemas/:schemaName/tables"
+      component={TablesPermissionsApp}
+    />
 
-        {/* NOTE: this route is to support null schemas, inject the empty string as the schemaName */}
-        <Route path="databases/:databaseId/tables" component={(props) => // eslint-disable-line react/display-name
-            <TablesPermissionsApp {...props} params={{ ...props.params, schemaName: "" }} />
-        }/>
-    </Route>
+    {/* NOTE: this route is to support null schemas, inject the empty string as the schemaName */}
+    <Route
+      path="databases/:databaseId/tables"
+      component={(
+        props, // eslint-disable-line react/display-name
+      ) => (
+        <TablesPermissionsApp
+          {...props}
+          params={{ ...props.params, schemaName: "" }}
+        />
+      )}
+    />
+  </Route>
+);
 
 export default getRoutes;
diff --git a/frontend/src/metabase/admin/permissions/selectors.js b/frontend/src/metabase/admin/permissions/selectors.js
index 1b2dd05d7e5de286c2e4675666186c3ab7c3762f..b4cf5a762c278ace0b1215ce5201dd92e5d0950d 100644
--- a/frontend/src/metabase/admin/permissions/selectors.js
+++ b/frontend/src/metabase/admin/permissions/selectors.js
@@ -1,27 +1,31 @@
 /* @flow weak */
 
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 
 import { push } from "react-router-redux";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
-import { t } from 'c-3po';
-import { isDefaultGroup, isAdminGroup, isMetaBotGroup } from "metabase/lib/groups";
+import { t } from "c-3po";
+import {
+  isDefaultGroup,
+  isAdminGroup,
+  isMetaBotGroup,
+} from "metabase/lib/groups";
 
 import _ from "underscore";
 import { getIn, assocIn } from "icepick";
 
 import {
-    getNativePermission,
-    getSchemasPermission,
-    getTablesPermission,
-    getFieldsPermission,
-    updateFieldsPermission,
-    updateTablesPermission,
-    updateSchemasPermission,
-    updateNativePermission,
-    diffPermissions,
-    inferAndUpdateEntityPermissions
+  getNativePermission,
+  getSchemasPermission,
+  getTablesPermission,
+  getFieldsPermission,
+  updateFieldsPermission,
+  updateTablesPermission,
+  updateSchemasPermission,
+  updateNativePermission,
+  diffPermissions,
+  inferAndUpdateEntityPermissions,
 } from "metabase/lib/permissions";
 
 import { getMetadata } from "metabase/selectors/metadata";
@@ -31,481 +35,713 @@ import type { DatabaseId } from "metabase/meta/types/Database";
 import type { SchemaName } from "metabase/meta/types/Table";
 import type { Group, GroupsPermissions } from "metabase/meta/types/Permissions";
 
-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 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;
 
 // reorder groups to be in this order
-const SPECIAL_GROUP_FILTERS = [isAdminGroup, isDefaultGroup, isMetaBotGroup].reverse();
+const SPECIAL_GROUP_FILTERS = [
+  isAdminGroup,
+  isDefaultGroup,
+  isMetaBotGroup,
+].reverse();
 
 function getTooltipForGroup(group) {
-    if (isAdminGroup(group)) {
-        return t`Administrators always have the highest level of access to everything in Metabase.`;
-    } else if (isDefaultGroup(group)) {
-        return t`Every Metabase user belongs to the All Users group. If you want to limit or restrict a group's access to something, make sure the All Users group has an equal or lower level of access.`;
-    } else if (isMetaBotGroup(group)) {
-        return t`MetaBot is Metabase's Slack bot. You can choose what it has access to here.`;
-    }
-    return null;
+  if (isAdminGroup(group)) {
+    return t`Administrators always have the highest level of access to everything in Metabase.`;
+  } else if (isDefaultGroup(group)) {
+    return t`Every Metabase user belongs to the All Users group. If you want to limit or restrict a group's access to something, make sure the All Users group has an equal or lower level of access.`;
+  } else if (isMetaBotGroup(group)) {
+    return t`MetaBot is Metabase's Slack bot. You can choose what it has access to here.`;
+  }
+  return null;
 }
 
 export const getGroups = createSelector(
-    (state) => state.admin.permissions.groups,
-    (groups) => {
-        let orderedGroups = groups ? [...groups] : [];
-        for (let groupFilter of SPECIAL_GROUP_FILTERS) {
-            let index = _.findIndex(orderedGroups, groupFilter);
-            if (index >= 0) {
-                orderedGroups.unshift(...orderedGroups.splice(index, 1))
-            }
-        }
-        return orderedGroups.map(group => ({
-            ...group,
-            tooltip: getTooltipForGroup(group)
-        }))
+  state => state.admin.permissions.groups,
+  groups => {
+    let orderedGroups = groups ? [...groups] : [];
+    for (let groupFilter of SPECIAL_GROUP_FILTERS) {
+      let index = _.findIndex(orderedGroups, groupFilter);
+      if (index >= 0) {
+        orderedGroups.unshift(...orderedGroups.splice(index, 1));
+      }
     }
+    return orderedGroups.map(group => ({
+      ...group,
+      tooltip: getTooltipForGroup(group),
+    }));
+  },
 );
 
 export const getIsDirty = createSelector(
-    getPermissions, getOriginalPermissions,
-    (permissions, originalPermissions) =>
-        JSON.stringify(permissions) !== JSON.stringify(originalPermissions)
-)
-
-export const getSaveError = (state) => state.admin.permissions.saveError;
+  getPermissions,
+  getOriginalPermissions,
+  (permissions, originalPermissions) =>
+    JSON.stringify(permissions) !== JSON.stringify(originalPermissions),
+);
 
+export const getSaveError = state => state.admin.permissions.saveError;
 
 // these are all the permission levels ordered by level of access
 const PERM_LEVELS = ["write", "read", "all", "controlled", "none"];
 function hasGreaterPermissions(a, b) {
-    return (PERM_LEVELS.indexOf(a) - PERM_LEVELS.indexOf(b)) < 0
+  return PERM_LEVELS.indexOf(a) - PERM_LEVELS.indexOf(b) < 0;
 }
 
-function getPermissionWarning(getter, entityType, defaultGroup, permissions, groupId, entityId, value) {
-    if (!defaultGroup || groupId === defaultGroup.id) {
-        return null;
-    }
-    let perm = value || getter(permissions, groupId, entityId);
-    let defaultPerm = getter(permissions, defaultGroup.id, entityId);
-    if (perm === "controlled" && defaultPerm === "controlled") {
-        return t`The "${defaultGroup.name}" group may have access to a different set of ${entityType} than this group, which may give this group additional access to some ${entityType}.`;
-    }
-    if (hasGreaterPermissions(defaultPerm, perm)) {
-        return t`The "${defaultGroup.name}" group has a higher level of access than this, which will override this setting. You should limit or revoke the "${defaultGroup.name}" group's access to this item.`;
-    }
+function getPermissionWarning(
+  getter,
+  entityType,
+  defaultGroup,
+  permissions,
+  groupId,
+  entityId,
+  value,
+) {
+  if (!defaultGroup || groupId === defaultGroup.id) {
     return null;
+  }
+  let perm = value || getter(permissions, groupId, entityId);
+  let defaultPerm = getter(permissions, defaultGroup.id, entityId);
+  if (perm === "controlled" && defaultPerm === "controlled") {
+    return t`The "${
+      defaultGroup.name
+    }" group may have access to a different set of ${entityType} than this group, which may give this group additional access to some ${entityType}.`;
+  }
+  if (hasGreaterPermissions(defaultPerm, perm)) {
+    return t`The "${
+      defaultGroup.name
+    }" group has a higher level of access than this, which will override this setting. You should limit or revoke the "${
+      defaultGroup.name
+    }" group's access to this item.`;
+  }
+  return null;
 }
 
-function getPermissionWarningModal(entityType, getter, defaultGroup, permissions, groupId, entityId, value) {
-    let permissionWarning = getPermissionWarning(entityType, getter, defaultGroup, permissions, groupId, entityId, value);
-    if (permissionWarning) {
-        return {
-            title: t`${value === "controlled" ? "Limit" : "Revoke"} access even though "${defaultGroup.name}" has greater access?`,
-            message: permissionWarning,
-            confirmButtonText: (value === "controlled" ? t`Limit access` : t`Revoke access`),
-            cancelButtonText: t`Cancel`
-        };
-    }
+function getPermissionWarningModal(
+  entityType,
+  getter,
+  defaultGroup,
+  permissions,
+  groupId,
+  entityId,
+  value,
+) {
+  let permissionWarning = getPermissionWarning(
+    entityType,
+    getter,
+    defaultGroup,
+    permissions,
+    groupId,
+    entityId,
+    value,
+  );
+  if (permissionWarning) {
+    return {
+      title: t`${
+        value === "controlled" ? "Limit" : "Revoke"
+      } access even though "${defaultGroup.name}" has greater access?`,
+      message: permissionWarning,
+      confirmButtonText:
+        value === "controlled" ? t`Limit access` : t`Revoke access`,
+      cancelButtonText: t`Cancel`,
+    };
+  }
 }
 
 function getControlledDatabaseWarningModal(permissions, groupId, entityId) {
-    if (getSchemasPermission(permissions, groupId, entityId) !== "controlled") {
-        return {
-            title: t`Change access to this database to limited?`,
-            confirmButtonText: t`Change`,
-            cancelButtonText: t`Cancel`
-        };
-    }
+  if (getSchemasPermission(permissions, groupId, entityId) !== "controlled") {
+    return {
+      title: t`Change access to this database to limited?`,
+      confirmButtonText: t`Change`,
+      cancelButtonText: t`Cancel`,
+    };
+  }
 }
 
 function getRawQueryWarningModal(permissions, groupId, entityId, value) {
-    if (value === "write" &&
-        getNativePermission(permissions, groupId, entityId) !== "write" &&
-        getSchemasPermission(permissions, groupId, entityId) !== "all"
-    ) {
-        return {
-            title: t`Allow Raw Query Writing?`,
-            message: t`This will also change this group's data access to Unrestricted for this database.`,
-            confirmButtonText: t`Allow`,
-            cancelButtonText: t`Cancel`
-        };
-    }
+  if (
+    value === "write" &&
+    getNativePermission(permissions, groupId, entityId) !== "write" &&
+    getSchemasPermission(permissions, groupId, entityId) !== "all"
+  ) {
+    return {
+      title: t`Allow Raw Query Writing?`,
+      message: t`This will also change this group's data access to Unrestricted for this database.`,
+      confirmButtonText: t`Allow`,
+      cancelButtonText: t`Cancel`,
+    };
+  }
 }
 
 // If the user is revoking an access to every single table of a database for a specific user group,
 // warn the user that the access to raw queries will be revoked as well.
 // This warning will only be shown if the user is editing the permissions of individual tables.
-function getRevokingAccessToAllTablesWarningModal(database, permissions, groupId, entityId, value) {
-    if (value === "none" &&
-        getSchemasPermission(permissions, groupId, entityId) === "controlled" &&
-        getNativePermission(permissions, groupId, entityId) !== "none"
-    ) {
-        // allTableEntityIds contains tables from all schemas
-        const allTableEntityIds = database.tables.map((table) => ({
-            databaseId: table.db_id,
-            schemaName: table.schema || "",
-            tableId: table.id
-        }));
-
-        // Show the warning only if user tries to revoke access to the very last table of all schemas
-        const afterChangesNoAccessToAnyTable = _.every(allTableEntityIds, (id) =>
-            getFieldsPermission(permissions, groupId, id) === "none" || _.isEqual(id, entityId)
-        );
-        if (afterChangesNoAccessToAnyTable) {
-            return {
-                title: t`Revoke access to all tables?`,
-                message: t`This will also revoke this group's access to raw queries for this database.`,
-                confirmButtonText: t`Revoke access`,
-                cancelButtonText: t`Cancel`
-            };
-        }
+function getRevokingAccessToAllTablesWarningModal(
+  database,
+  permissions,
+  groupId,
+  entityId,
+  value,
+) {
+  if (
+    value === "none" &&
+    getSchemasPermission(permissions, groupId, entityId) === "controlled" &&
+    getNativePermission(permissions, groupId, entityId) !== "none"
+  ) {
+    // allTableEntityIds contains tables from all schemas
+    const allTableEntityIds = database.tables.map(table => ({
+      databaseId: table.db_id,
+      schemaName: table.schema || "",
+      tableId: table.id,
+    }));
+
+    // Show the warning only if user tries to revoke access to the very last table of all schemas
+    const afterChangesNoAccessToAnyTable = _.every(
+      allTableEntityIds,
+      id =>
+        getFieldsPermission(permissions, groupId, id) === "none" ||
+        _.isEqual(id, entityId),
+    );
+    if (afterChangesNoAccessToAnyTable) {
+      return {
+        title: t`Revoke access to all tables?`,
+        message: t`This will also revoke this group's access to raw queries for this database.`,
+        confirmButtonText: t`Revoke access`,
+        cancelButtonText: t`Cancel`,
+      };
     }
+  }
 }
 
 const OPTION_GREEN = {
-    icon: "check",
-    iconColor: "#9CC177",
-    bgColor: "#F6F9F2"
+  icon: "check",
+  iconColor: "#9CC177",
+  bgColor: "#F6F9F2",
 };
 const OPTION_YELLOW = {
-    icon: "eye",
-    iconColor: "#F9D45C",
-    bgColor: "#FEFAEE"
+  icon: "eye",
+  iconColor: "#F9D45C",
+  bgColor: "#FEFAEE",
 };
 const OPTION_RED = {
-    icon: "close",
-    iconColor: "#EEA5A5",
-    bgColor: "#FDF3F3"
+  icon: "close",
+  iconColor: "#EEA5A5",
+  bgColor: "#FDF3F3",
 };
 
-
 const OPTION_ALL = {
-    ...OPTION_GREEN,
-    value: "all",
-    title: t`Grant unrestricted access`,
-    tooltip: t`Unrestricted access`,
+  ...OPTION_GREEN,
+  value: "all",
+  title: t`Grant unrestricted access`,
+  tooltip: t`Unrestricted access`,
 };
 
 const OPTION_CONTROLLED = {
-    ...OPTION_YELLOW,
-    value: "controlled",
-    title: t`Limit access`,
-    tooltip: t`Limited access`,
-    icon: "permissionsLimited",
+  ...OPTION_YELLOW,
+  value: "controlled",
+  title: t`Limit access`,
+  tooltip: t`Limited access`,
+  icon: "permissionsLimited",
 };
 
 const OPTION_NONE = {
-    ...OPTION_RED,
-    value: "none",
-    title: t`Revoke access`,
-    tooltip: t`No access`,
+  ...OPTION_RED,
+  value: "none",
+  title: t`Revoke access`,
+  tooltip: t`No access`,
 };
 
 const OPTION_NATIVE_WRITE = {
-    ...OPTION_GREEN,
-    value: "write",
-    title: t`Write raw queries`,
-    tooltip: t`Can write raw queries`,
-    icon: "sql",
+  ...OPTION_GREEN,
+  value: "write",
+  title: t`Write raw queries`,
+  tooltip: t`Can write raw queries`,
+  icon: "sql",
 };
 
 const OPTION_NATIVE_READ = {
-    ...OPTION_YELLOW,
-    value: "read",
-    title: t`View raw queries`,
-    tooltip: t`Can view raw queries`,
+  ...OPTION_YELLOW,
+  value: "read",
+  title: t`View raw queries`,
+  tooltip: t`Can view raw queries`,
 };
 
 const OPTION_COLLECTION_WRITE = {
-    ...OPTION_GREEN,
-    value: "write",
-    title: t`Curate collection`,
-    tooltip: t`Can add and remove questions from this collection`,
+  ...OPTION_GREEN,
+  value: "write",
+  title: t`Curate collection`,
+  tooltip: t`Can add and remove questions from this collection`,
 };
 
 const OPTION_COLLECTION_READ = {
-    ...OPTION_YELLOW,
-    value: "read",
-    title: t`View collection`,
-    tooltip: t`Can view questions in this collection`,
+  ...OPTION_YELLOW,
+  value: "read",
+  title: t`View collection`,
+  tooltip: t`Can view questions in this collection`,
 };
 
 export const getTablesPermissionsGrid = createSelector(
-    getMetadata, getGroups, getPermissions, getDatabaseId, getSchemaName,
-    (metadata: Metadata, groups: Array<Group>, permissions: GroupsPermissions, databaseId: DatabaseId, schemaName: SchemaName) => {
-        const database = metadata.databases[databaseId];
-
-        if (!groups || !permissions || !database) {
-            return null;
-        }
-
-        const tables = database.tablesInSchema(schemaName || null);
-        const defaultGroup = _.find(groups, isDefaultGroup);
-
-        return {
-            type: "table",
-            icon: "table",
-            crumbs: database.schemaNames().length > 1 ? [
-                [t`Databases`, "/admin/permissions/databases"],
-                [database.name, "/admin/permissions/databases/"+database.id+"/schemas"],
-                [schemaName]
-            ] : [
-                [t`Databases`, "/admin/permissions/databases"],
-                [database.name],
-            ],
-            groups,
-            permissions: {
-                "fields": {
-                    header: t`Data Access`,
-                    options(groupId, entityId) {
-                        return [OPTION_ALL, OPTION_NONE]
-                    },
-                    getter(groupId, entityId) {
-                        return getFieldsPermission(permissions, groupId, entityId);
-                    },
-                    updater(groupId, entityId, value) {
-                        MetabaseAnalytics.trackEvent("Permissions", "fields", value);
-                        let updatedPermissions = updateFieldsPermission(permissions, groupId, entityId, value, metadata);
-                        return inferAndUpdateEntityPermissions(updatedPermissions, groupId, entityId, metadata);
-                    },
-                    confirm(groupId, entityId, value) {
-                        return [
-                            getPermissionWarningModal(getFieldsPermission, "fields", defaultGroup, permissions, groupId, entityId, value),
-                            getControlledDatabaseWarningModal(permissions, groupId, entityId),
-                            getRevokingAccessToAllTablesWarningModal(database, permissions, groupId, entityId, value)
-                        ];
-                    },
-                    warning(groupId, entityId) {
-                        return getPermissionWarning(getFieldsPermission, "fields", defaultGroup, permissions, groupId, entityId);
-                    }
-                }
-            },
-            entities: tables.map(table => ({
-                id: {
-                    databaseId: databaseId,
-                    schemaName: schemaName,
-                    tableId: table.id
-                },
-                name: table.display_name,
-                subtitle: table.name
-            }))
-        };
+  getMetadata,
+  getGroups,
+  getPermissions,
+  getDatabaseId,
+  getSchemaName,
+  (
+    metadata: Metadata,
+    groups: Array<Group>,
+    permissions: GroupsPermissions,
+    databaseId: DatabaseId,
+    schemaName: SchemaName,
+  ) => {
+    const database = metadata.databases[databaseId];
+
+    if (!groups || !permissions || !database) {
+      return null;
     }
+
+    const tables = database.tablesInSchema(schemaName || null);
+    const defaultGroup = _.find(groups, isDefaultGroup);
+
+    return {
+      type: "table",
+      icon: "table",
+      crumbs:
+        database.schemaNames().length > 1
+          ? [
+              [t`Databases`, "/admin/permissions/databases"],
+              [
+                database.name,
+                "/admin/permissions/databases/" + database.id + "/schemas",
+              ],
+              [schemaName],
+            ]
+          : [[t`Databases`, "/admin/permissions/databases"], [database.name]],
+      groups,
+      permissions: {
+        fields: {
+          header: t`Data Access`,
+          options(groupId, entityId) {
+            return [OPTION_ALL, OPTION_NONE];
+          },
+          getter(groupId, entityId) {
+            return getFieldsPermission(permissions, groupId, entityId);
+          },
+          updater(groupId, entityId, value) {
+            MetabaseAnalytics.trackEvent("Permissions", "fields", value);
+            let updatedPermissions = updateFieldsPermission(
+              permissions,
+              groupId,
+              entityId,
+              value,
+              metadata,
+            );
+            return inferAndUpdateEntityPermissions(
+              updatedPermissions,
+              groupId,
+              entityId,
+              metadata,
+            );
+          },
+          confirm(groupId, entityId, value) {
+            return [
+              getPermissionWarningModal(
+                getFieldsPermission,
+                "fields",
+                defaultGroup,
+                permissions,
+                groupId,
+                entityId,
+                value,
+              ),
+              getControlledDatabaseWarningModal(permissions, groupId, entityId),
+              getRevokingAccessToAllTablesWarningModal(
+                database,
+                permissions,
+                groupId,
+                entityId,
+                value,
+              ),
+            ];
+          },
+          warning(groupId, entityId) {
+            return getPermissionWarning(
+              getFieldsPermission,
+              "fields",
+              defaultGroup,
+              permissions,
+              groupId,
+              entityId,
+            );
+          },
+        },
+      },
+      entities: tables.map(table => ({
+        id: {
+          databaseId: databaseId,
+          schemaName: schemaName,
+          tableId: table.id,
+        },
+        name: table.display_name,
+        subtitle: table.name,
+      })),
+    };
+  },
 );
 
 export const getSchemasPermissionsGrid = createSelector(
-    getMetadata, getGroups, getPermissions, getDatabaseId,
-    (metadata: Metadata, groups: Array<Group>, permissions: GroupsPermissions, databaseId: DatabaseId) => {
-        const database = metadata.databases[databaseId];
-
-        if (!groups || !permissions || !database) {
-            return null;
-        }
-
-        const schemaNames = database.schemaNames();
-        const defaultGroup = _.find(groups, isDefaultGroup);
-
-        return {
-            type: "schema",
-            icon: "folder",
-            crumbs: [
-                [t`Databases`, "/admin/permissions/databases"],
-                [database.name],
-            ],
-            groups,
-            permissions: {
-                "tables": {
-                    header: t`Data Access`,
-                    options(groupId, entityId) {
-                        return [OPTION_ALL, OPTION_CONTROLLED, OPTION_NONE]
-                    },
-                    getter(groupId, entityId) {
-                        return getTablesPermission(permissions, groupId, entityId);
-                    },
-                    updater(groupId, entityId, value) {
-                        MetabaseAnalytics.trackEvent("Permissions", "tables", value);
-                        let updatedPermissions = updateTablesPermission(permissions, groupId, entityId, value, metadata);
-                        return inferAndUpdateEntityPermissions(updatedPermissions, groupId, entityId, metadata);
-                    },
-                    postAction(groupId, { databaseId, schemaName }, value) {
-                        if (value === "controlled") {
-                            return push(`/admin/permissions/databases/${databaseId}/schemas/${encodeURIComponent(schemaName)}/tables`);
-                        }
-                    },
-                    confirm(groupId, entityId, value) {
-                        return [
-                            getPermissionWarningModal(getTablesPermission, "tables", defaultGroup, permissions, groupId, entityId, value),
-                            getControlledDatabaseWarningModal(permissions, groupId, entityId)
-                        ];
-                    },
-                    warning(groupId, entityId) {
-                        return getPermissionWarning(getTablesPermission, "tables", defaultGroup, permissions, groupId, entityId);
-                    }
-                }
-            },
-            entities: schemaNames.map(schemaName => ({
-                id: {
-                    databaseId,
-                    schemaName
-                },
-                name: schemaName,
-                link: { name: t`View tables`, url: `/admin/permissions/databases/${databaseId}/schemas/${encodeURIComponent(schemaName)}/tables`}
-            }))
-        }
+  getMetadata,
+  getGroups,
+  getPermissions,
+  getDatabaseId,
+  (
+    metadata: Metadata,
+    groups: Array<Group>,
+    permissions: GroupsPermissions,
+    databaseId: DatabaseId,
+  ) => {
+    const database = metadata.databases[databaseId];
+
+    if (!groups || !permissions || !database) {
+      return null;
     }
+
+    const schemaNames = database.schemaNames();
+    const defaultGroup = _.find(groups, isDefaultGroup);
+
+    return {
+      type: "schema",
+      icon: "folder",
+      crumbs: [[t`Databases`, "/admin/permissions/databases"], [database.name]],
+      groups,
+      permissions: {
+        tables: {
+          header: t`Data Access`,
+          options(groupId, entityId) {
+            return [OPTION_ALL, OPTION_CONTROLLED, OPTION_NONE];
+          },
+          getter(groupId, entityId) {
+            return getTablesPermission(permissions, groupId, entityId);
+          },
+          updater(groupId, entityId, value) {
+            MetabaseAnalytics.trackEvent("Permissions", "tables", value);
+            let updatedPermissions = updateTablesPermission(
+              permissions,
+              groupId,
+              entityId,
+              value,
+              metadata,
+            );
+            return inferAndUpdateEntityPermissions(
+              updatedPermissions,
+              groupId,
+              entityId,
+              metadata,
+            );
+          },
+          postAction(groupId, { databaseId, schemaName }, value) {
+            if (value === "controlled") {
+              return push(
+                `/admin/permissions/databases/${databaseId}/schemas/${encodeURIComponent(
+                  schemaName,
+                )}/tables`,
+              );
+            }
+          },
+          confirm(groupId, entityId, value) {
+            return [
+              getPermissionWarningModal(
+                getTablesPermission,
+                "tables",
+                defaultGroup,
+                permissions,
+                groupId,
+                entityId,
+                value,
+              ),
+              getControlledDatabaseWarningModal(permissions, groupId, entityId),
+            ];
+          },
+          warning(groupId, entityId) {
+            return getPermissionWarning(
+              getTablesPermission,
+              "tables",
+              defaultGroup,
+              permissions,
+              groupId,
+              entityId,
+            );
+          },
+        },
+      },
+      entities: schemaNames.map(schemaName => ({
+        id: {
+          databaseId,
+          schemaName,
+        },
+        name: schemaName,
+        link: {
+          name: t`View tables`,
+          url: `/admin/permissions/databases/${databaseId}/schemas/${encodeURIComponent(
+            schemaName,
+          )}/tables`,
+        },
+      })),
+    };
+  },
 );
 
 export const getDatabasesPermissionsGrid = createSelector(
-    getMetadata, getGroups, getPermissions,
-    (metadata: Metadata, groups: Array<Group>, permissions: GroupsPermissions) => {
-        if (!groups || !permissions || !metadata) {
-            return null;
-        }
-
-        const databases = Object.values(metadata.databases);
-        const defaultGroup = _.find(groups, isDefaultGroup);
+  getMetadata,
+  getGroups,
+  getPermissions,
+  (
+    metadata: Metadata,
+    groups: Array<Group>,
+    permissions: GroupsPermissions,
+  ) => {
+    if (!groups || !permissions || !metadata) {
+      return null;
+    }
 
+    const databases = Object.values(metadata.databases);
+    const defaultGroup = _.find(groups, isDefaultGroup);
+
+    return {
+      type: "database",
+      icon: "database",
+      groups,
+      permissions: {
+        schemas: {
+          header: t`Data Access`,
+          options(groupId, entityId) {
+            return [OPTION_ALL, OPTION_CONTROLLED, OPTION_NONE];
+          },
+          getter(groupId, entityId) {
+            return getSchemasPermission(permissions, groupId, entityId);
+          },
+          updater(groupId, entityId, value) {
+            MetabaseAnalytics.trackEvent("Permissions", "schemas", value);
+            return updateSchemasPermission(
+              permissions,
+              groupId,
+              entityId,
+              value,
+              metadata,
+            );
+          },
+          postAction(groupId, { databaseId }, value) {
+            if (value === "controlled") {
+              let database = metadata.databases[databaseId];
+              let schemas = database ? database.schemaNames() : [];
+              if (
+                schemas.length === 0 ||
+                (schemas.length === 1 && schemas[0] === "")
+              ) {
+                return push(
+                  `/admin/permissions/databases/${databaseId}/tables`,
+                );
+              } else if (schemas.length === 1) {
+                return push(
+                  `/admin/permissions/databases/${databaseId}/schemas/${
+                    schemas[0]
+                  }/tables`,
+                );
+              } else {
+                return push(
+                  `/admin/permissions/databases/${databaseId}/schemas`,
+                );
+              }
+            }
+          },
+          confirm(groupId, entityId, value) {
+            return [
+              getPermissionWarningModal(
+                getSchemasPermission,
+                "schemas",
+                defaultGroup,
+                permissions,
+                groupId,
+                entityId,
+                value,
+              ),
+            ];
+          },
+          warning(groupId, entityId) {
+            return getPermissionWarning(
+              getSchemasPermission,
+              "schemas",
+              defaultGroup,
+              permissions,
+              groupId,
+              entityId,
+            );
+          },
+        },
+        native: {
+          header: t`SQL Queries`,
+          options(groupId, entityId) {
+            if (
+              getSchemasPermission(permissions, groupId, entityId) === "none"
+            ) {
+              return [OPTION_NONE];
+            } else {
+              return [OPTION_NATIVE_WRITE, OPTION_NATIVE_READ, OPTION_NONE];
+            }
+          },
+          getter(groupId, entityId) {
+            return getNativePermission(permissions, groupId, entityId);
+          },
+          updater(groupId, entityId, value) {
+            MetabaseAnalytics.trackEvent("Permissions", "native", value);
+            return updateNativePermission(
+              permissions,
+              groupId,
+              entityId,
+              value,
+              metadata,
+            );
+          },
+          confirm(groupId, entityId, value) {
+            return [
+              getPermissionWarningModal(
+                getNativePermission,
+                null,
+                defaultGroup,
+                permissions,
+                groupId,
+                entityId,
+                value,
+              ),
+              getRawQueryWarningModal(permissions, groupId, entityId, value),
+            ];
+          },
+          warning(groupId, entityId) {
+            return getPermissionWarning(
+              getNativePermission,
+              null,
+              defaultGroup,
+              permissions,
+              groupId,
+              entityId,
+            );
+          },
+        },
+      },
+      entities: databases.map(database => {
+        let schemas = database.schemaNames();
         return {
-            type: "database",
-            icon: "database",
-            groups,
-            permissions: {
-                "schemas": {
-                    header: t`Data Access`,
-                    options(groupId, entityId) {
-                        return [OPTION_ALL, OPTION_CONTROLLED, OPTION_NONE]
-                    },
-                    getter(groupId, entityId) {
-                        return getSchemasPermission(permissions, groupId, entityId);
-                    },
-                    updater(groupId, entityId, value) {
-                        MetabaseAnalytics.trackEvent("Permissions", "schemas", value);
-                        return updateSchemasPermission(permissions, groupId, entityId, value, metadata)
-                    },
-                    postAction(groupId, { databaseId }, value) {
-                        if (value === "controlled") {
-                            let database = metadata.databases[databaseId];
-                            let schemas = database ? database.schemaNames() : [];
-                            if (schemas.length === 0 || (schemas.length === 1 && schemas[0] === "")) {
-                                return push(`/admin/permissions/databases/${databaseId}/tables`);
-                            } else if (schemas.length === 1) {
-                                return push(`/admin/permissions/databases/${databaseId}/schemas/${schemas[0]}/tables`);
-                            } else {
-                                return push(`/admin/permissions/databases/${databaseId}/schemas`);
-                            }
-                        }
-                    },
-                    confirm(groupId, entityId, value) {
-                        return [
-                            getPermissionWarningModal(getSchemasPermission, "schemas", defaultGroup, permissions, groupId, entityId, value),
-                        ];
-                    },
-                    warning(groupId, entityId) {
-                        return getPermissionWarning(getSchemasPermission, "schemas", defaultGroup, permissions, groupId, entityId);
-                    }
-                },
-                "native": {
-                    header: t`SQL Queries`,
-                    options(groupId, entityId) {
-                        if (getSchemasPermission(permissions, groupId, entityId) === "none") {
-                            return [OPTION_NONE];
-                        } else {
-                            return [OPTION_NATIVE_WRITE, OPTION_NATIVE_READ, OPTION_NONE];
-                        }
-                    },
-                    getter(groupId, entityId) {
-                        return getNativePermission(permissions, groupId, entityId);
-                    },
-                    updater(groupId, entityId, value) {
-                        MetabaseAnalytics.trackEvent("Permissions", "native", value);
-                        return updateNativePermission(permissions, groupId, entityId, value, metadata);
-                    },
-                    confirm(groupId, entityId, value) {
-                        return [
-                            getPermissionWarningModal(getNativePermission, null, defaultGroup, permissions, groupId, entityId, value),
-                            getRawQueryWarningModal(permissions, groupId, entityId, value)
-                        ];
-                    },
-                    warning(groupId, entityId) {
-                        return getPermissionWarning(getNativePermission, null, defaultGroup, permissions, groupId, entityId);
-                    }
-                },
-            },
-            entities: databases.map(database => {
-                let schemas = database.schemaNames();
-                return {
-                    id: {
-                        databaseId: database.id
-                    },
-                    name: database.name,
-                    link:
-                        schemas.length === 0 || (schemas.length === 1 && schemas[0] === "") ?
-                            { name: t`View tables`, url: `/admin/permissions/databases/${database.id}/tables` }
-                        : schemas.length === 1 ?
-                            { name: t`View tables`, url: `/admin/permissions/databases/${database.id}/schemas/${schemas[0]}/tables` }
-                        :
-                            { name: t`View schemas`, url: `/admin/permissions/databases/${database.id}/schemas`}
+          id: {
+            databaseId: database.id,
+          },
+          name: database.name,
+          link:
+            schemas.length === 0 || (schemas.length === 1 && schemas[0] === "")
+              ? {
+                  name: t`View tables`,
+                  url: `/admin/permissions/databases/${database.id}/tables`,
                 }
-            })
-        }
-    }
+              : schemas.length === 1
+                ? {
+                    name: t`View tables`,
+                    url: `/admin/permissions/databases/${database.id}/schemas/${
+                      schemas[0]
+                    }/tables`,
+                  }
+                : {
+                    name: t`View schemas`,
+                    url: `/admin/permissions/databases/${database.id}/schemas`,
+                  },
+        };
+      }),
+    };
+  },
 );
 
-const getCollections = (state) => state.admin.permissions.collections;
+const getCollections = state => state.admin.permissions.collections;
 const getCollectionPermission = (permissions, groupId, { collectionId }) =>
-    getIn(permissions, [groupId, collectionId]);
+  getIn(permissions, [groupId, collectionId]);
 
 export const getCollectionsPermissionsGrid = createSelector(
-    getCollections, getGroups, getPermissions,
-    (collections, groups: Array<Group>, permissions: GroupsPermissions) => {
-        if (!groups || !permissions || !collections) {
-            return null;
-        }
-
-        const defaultGroup = _.find(groups, isDefaultGroup);
+  getCollections,
+  getGroups,
+  getPermissions,
+  (collections, groups: Array<Group>, permissions: GroupsPermissions) => {
+    if (!groups || !permissions || !collections) {
+      return null;
+    }
 
+    const defaultGroup = _.find(groups, isDefaultGroup);
+
+    return {
+      type: "collection",
+      icon: "collection",
+      groups,
+      permissions: {
+        access: {
+          options(groupId, entityId) {
+            return [
+              OPTION_COLLECTION_WRITE,
+              OPTION_COLLECTION_READ,
+              OPTION_NONE,
+            ];
+          },
+          getter(groupId, entityId) {
+            return getCollectionPermission(permissions, groupId, entityId);
+          },
+          updater(groupId, { collectionId }, value) {
+            return assocIn(permissions, [groupId, collectionId], value);
+          },
+          confirm(groupId, entityId, value) {
+            return [
+              getPermissionWarningModal(
+                getCollectionPermission,
+                null,
+                defaultGroup,
+                permissions,
+                groupId,
+                entityId,
+                value,
+              ),
+            ];
+          },
+          warning(groupId, entityId) {
+            return getPermissionWarning(
+              getCollectionPermission,
+              null,
+              defaultGroup,
+              permissions,
+              groupId,
+              entityId,
+            );
+          },
+        },
+      },
+      entities: collections.map(collection => {
         return {
-            type: "collection",
-            icon: "collection",
-            groups,
-            permissions: {
-                "access": {
-                    options(groupId, entityId) {
-                        return [OPTION_COLLECTION_WRITE, OPTION_COLLECTION_READ, OPTION_NONE];
-                    },
-                    getter(groupId, entityId) {
-                        return getCollectionPermission(permissions, groupId, entityId);
-                    },
-                    updater(groupId, { collectionId }, value) {
-                        return assocIn(permissions, [groupId, collectionId], value);
-                    },
-                    confirm(groupId, entityId, value) {
-                        return [
-                            getPermissionWarningModal(getCollectionPermission, null, defaultGroup, permissions, groupId, entityId, value)
-                        ];
-                    },
-                    warning(groupId, entityId) {
-                        return getPermissionWarning(getCollectionPermission, null, defaultGroup, permissions, groupId, entityId);
-                    }
-                },
-            },
-            entities: collections.map(collection => {
-                return {
-                    id: {
-                        collectionId: collection.id
-                    },
-                    name: collection.name
-                }
-            })
-        }
-    }
+          id: {
+            collectionId: collection.id,
+          },
+          name: collection.name,
+        };
+      }),
+    };
+  },
 );
 
 export const getDiff = createSelector(
-    getMetadata, getGroups, getPermissions, getOriginalPermissions,
-    (metadata: Metadata, groups: Array<Group>, permissions: GroupsPermissions, originalPermissions: GroupsPermissions) =>
-        diffPermissions(permissions, originalPermissions, groups, metadata)
+  getMetadata,
+  getGroups,
+  getPermissions,
+  getOriginalPermissions,
+  (
+    metadata: Metadata,
+    groups: Array<Group>,
+    permissions: GroupsPermissions,
+    originalPermissions: GroupsPermissions,
+  ) => diffPermissions(permissions, originalPermissions, groups, metadata),
 );
diff --git a/frontend/src/metabase/admin/settings/components/SettingHeader.jsx b/frontend/src/metabase/admin/settings/components/SettingHeader.jsx
index 752c614454b44ebb740cbf29c6431c887d6a1a9f..ac5b0d2b3f9211788faaf77d5924864987760e0a 100644
--- a/frontend/src/metabase/admin/settings/components/SettingHeader.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingHeader.jsx
@@ -1,12 +1,15 @@
 import React from "react";
 
-const SettingHeader = ({ setting }) =>
-    <div>
-        <div className="text-grey-4 text-bold text-uppercase">{setting.display_name}</div>
-        <div className="text-grey-4 my1">
-            {setting.description}
-            {setting.note && <div>{setting.note}</div>}
-        </div>
+const SettingHeader = ({ setting }) => (
+  <div>
+    <div className="text-grey-4 text-bold text-uppercase">
+      {setting.display_name}
     </div>
+    <div className="text-grey-4 my1">
+      {setting.description}
+      {setting.note && <div>{setting.note}</div>}
+    </div>
+  </div>
+);
 
 export default SettingHeader;
diff --git a/frontend/src/metabase/admin/settings/components/SettingsAuthenticationOptions.jsx b/frontend/src/metabase/admin/settings/components/SettingsAuthenticationOptions.jsx
index ff928ce39ac250c71bb3ba95000cb28ddedfe18d..b26eed85ac8032fbaa1d24477b06d92925d95393 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsAuthenticationOptions.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsAuthenticationOptions.jsx
@@ -1,29 +1,37 @@
-import React, { Component } from 'react'
-import { Link } from 'react-router'
-import { t } from 'c-3po'
+import React, { Component } from "react";
+import { Link } from "react-router";
+import { t } from "c-3po";
 
 class SettingsAuthenticationOptions extends Component {
-    render () {
-        return (
-            <ul className="text-measure">
-                <li>
-                    <div className="bordered rounded shadowed bg-white p4">
-                        <h2>{t`Sign in with Google`}</h2>
-                        <p>{t`Allows users with existing Metabase accounts to login with a Google account that matches their email address in addition to their Metabase username and password.`}</p>
-                        <Link className="Button" to="/admin/settings/authentication/google">{t`Configure`}</Link>
-                    </div>
-                </li>
+  render() {
+    return (
+      <ul className="text-measure">
+        <li>
+          <div className="bordered rounded shadowed bg-white p4">
+            <h2>{t`Sign in with Google`}</h2>
+            <p
+            >{t`Allows users with existing Metabase accounts to login with a Google account that matches their email address in addition to their Metabase username and password.`}</p>
+            <Link
+              className="Button"
+              to="/admin/settings/authentication/google"
+            >{t`Configure`}</Link>
+          </div>
+        </li>
 
-                <li className="mt2">
-                    <div className="bordered rounded shadowed bg-white p4">
-                        <h2>{t`LDAP`}</h2>
-                        <p>{t`Allows users within your LDAP directory to log in to Metabase with their LDAP credentials, and allows automatic mapping of LDAP groups to Metabase groups.`}</p>
-                        <Link className="Button" to="/admin/settings/authentication/ldap">{t`Configure`}</Link>
-                    </div>
-                </li>
-            </ul>
-        )
-    }
+        <li className="mt2">
+          <div className="bordered rounded shadowed bg-white p4">
+            <h2>{t`LDAP`}</h2>
+            <p
+            >{t`Allows users within your LDAP directory to log in to Metabase with their LDAP credentials, and allows automatic mapping of LDAP groups to Metabase groups.`}</p>
+            <Link
+              className="Button"
+              to="/admin/settings/authentication/ldap"
+            >{t`Configure`}</Link>
+          </div>
+        </li>
+      </ul>
+    );
+  }
 }
 
-export default SettingsAuthenticationOptions
+export default SettingsAuthenticationOptions;
diff --git a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
index 1e765b756c9018a95520b6d1e0efff5adbc23e46..aa9bfb9c715ce807351c1dc4755e6323192a165a 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsEmailForm.jsx
@@ -2,238 +2,290 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import _ from "underscore";
-import { t } from 'c-3po';
-import MetabaseAnalytics from 'metabase/lib/analytics';
+import { t } from "c-3po";
+import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseUtils from "metabase/lib/utils";
 import SettingsSetting from "./SettingsSetting.jsx";
 
 export default class SettingsEmailForm extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            dirty: false,
-            formData: {},
-            sendingEmail: "default",
-            submitting: "default",
-            valid: false,
-            validationErrors: {}
-        }
-    }
-
-    static propTypes = {
-        elements: PropTypes.array.isRequired,
-        formErrors: PropTypes.object,
-        sendTestEmail: PropTypes.func.isRequired,
-        updateEmailSettings: PropTypes.func.isRequired
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      dirty: false,
+      formData: {},
+      sendingEmail: "default",
+      submitting: "default",
+      valid: false,
+      validationErrors: {},
     };
-
-    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 = {};
-        for (const element of props.elements) {
-            formData[element.key] = element.value;
-        }
-        this.setState({ formData });
-    }
-
-    componentDidMount() {
-        this.validateForm();
-    }
-
-    componentDidUpdate() {
-        this.validateForm();
-    }
-
-    setSubmitting(submitting) {
-        this.setState({submitting});
-    }
-
-    setSendingEmail(sendingEmail) {
-        this.setState({sendingEmail});
-    }
-
-    setFormErrors(formErrors) {
-        this.setState({formErrors});
+  }
+
+  static propTypes = {
+    elements: PropTypes.array.isRequired,
+    formErrors: PropTypes.object,
+    sendTestEmail: PropTypes.func.isRequired,
+    updateEmailSettings: PropTypes.func.isRequired,
+  };
+
+  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 = {};
+    for (const element of props.elements) {
+      formData[element.key] = element.value;
     }
-
-    // return null if element passes validation, otherwise return an error message
-    validateElement([validationType, validationMessage], value, element) {
-        if (MetabaseUtils.isEmpty(value)) return;
-
-        switch (validationType) {
-            case "email":
-                return !MetabaseUtils.validEmail(value) ? (validationMessage || t`That's not a valid email address`) : null;
-            case "integer":
-                return isNaN(parseInt(value)) ? (validationMessage || t`That's not a valid integer`) : null;
-        }
+    this.setState({ formData });
+  }
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  componentDidUpdate() {
+    this.validateForm();
+  }
+
+  setSubmitting(submitting) {
+    this.setState({ submitting });
+  }
+
+  setSendingEmail(sendingEmail) {
+    this.setState({ sendingEmail });
+  }
+
+  setFormErrors(formErrors) {
+    this.setState({ formErrors });
+  }
+
+  // return null if element passes validation, otherwise return an error message
+  validateElement([validationType, validationMessage], value, element) {
+    if (MetabaseUtils.isEmpty(value)) return;
+
+    switch (validationType) {
+      case "email":
+        return !MetabaseUtils.validEmail(value)
+          ? validationMessage || t`That's not a valid email address`
+          : null;
+      case "integer":
+        return isNaN(parseInt(value))
+          ? validationMessage || t`That's not a valid integer`
+          : null;
     }
-
-    validateForm() {
-        let { elements } = this.props;
-        let { formData } = this.state;
-
-        let valid = true,
-            validationErrors = {};
-
-        elements.forEach(function(element) {
-            // test for required elements
-            if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
-                valid = false;
-            }
-
-            if (element.validations) {
-                element.validations.forEach(function(validation) {
-                    validationErrors[element.key] = this.validateElement(validation, formData[element.key], element);
-                    if (validationErrors[element.key]) valid = false;
-                }, this);
-            }
+  }
+
+  validateForm() {
+    let { elements } = this.props;
+    let { formData } = this.state;
+
+    let valid = true,
+      validationErrors = {};
+
+    elements.forEach(function(element) {
+      // test for required elements
+      if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
+        valid = false;
+      }
+
+      if (element.validations) {
+        element.validations.forEach(function(validation) {
+          validationErrors[element.key] = this.validateElement(
+            validation,
+            formData[element.key],
+            element,
+          );
+          if (validationErrors[element.key]) valid = false;
         }, this);
-
-        if (this.state.valid !== valid || !_.isEqual(this.state.validationErrors, validationErrors)) {
-            this.setState({ valid, validationErrors });
-        }
+      }
+    }, this);
+
+    if (
+      this.state.valid !== valid ||
+      !_.isEqual(this.state.validationErrors, validationErrors)
+    ) {
+      this.setState({ valid, validationErrors });
+    }
+  }
+
+  handleChangeEvent(element, value, event) {
+    this.setState({
+      dirty: true,
+      formData: {
+        ...this.state.formData,
+        [element.key]: MetabaseUtils.isEmpty(value) ? null : value,
+      },
+    });
+  }
+
+  handleFormErrors(error) {
+    // parse and format
+    let formErrors = {};
+    if (error.data && error.data.message) {
+      formErrors.message = error.data.message;
+    } else {
+      formErrors.message = t`Looks like we ran into some problems`;
     }
 
-    handleChangeEvent(element, value, event) {
-        this.setState({
-            dirty: true,
-            formData: { ...this.state.formData, [element.key]: (MetabaseUtils.isEmpty(value)) ? null : value }
-        });
+    if (error.data && error.data.errors) {
+      formErrors.elements = error.data.errors;
     }
 
-    handleFormErrors(error) {
-        // parse and format
-        let formErrors = {};
-        if (error.data && error.data.message) {
-            formErrors.message = error.data.message;
-        } else {
-            formErrors.message = t`Looks like we ran into some problems`;
-        }
+    return formErrors;
+  }
 
-        if (error.data && error.data.errors) {
-            formErrors.elements = error.data.errors;
-        }
+  sendTestEmail(e) {
+    e.preventDefault();
 
-        return formErrors;
-    }
+    this.setState({
+      formErrors: null,
+      sendingEmail: "working",
+    });
 
-    sendTestEmail(e) {
-        e.preventDefault();
+    this.props.sendTestEmail().then(
+      () => {
+        this.setState({ sendingEmail: "success" });
+        MetabaseAnalytics.trackEvent("Email Settings", "Test Email", "success");
 
+        // show a confirmation for 3 seconds, then return to normal
+        setTimeout(() => this.setState({ sendingEmail: "default" }), 3000);
+      },
+      error => {
         this.setState({
-            formErrors: null,
-            sendingEmail: "working"
-        });
-
-        this.props.sendTestEmail().then(() => {
-            this.setState({sendingEmail: "success"});
-            MetabaseAnalytics.trackEvent("Email Settings", "Test Email", "success");
-
-            // show a confirmation for 3 seconds, then return to normal
-            setTimeout(() => this.setState({sendingEmail: "default"}), 3000);
-        }, (error) => {
-            this.setState({
-                sendingEmail: "default",
-                formErrors: this.handleFormErrors(error)
-            });
-            MetabaseAnalytics.trackEvent("Email Settings", "Test Email", "error");
+          sendingEmail: "default",
+          formErrors: this.handleFormErrors(error),
         });
-    }
+        MetabaseAnalytics.trackEvent("Email Settings", "Test Email", "error");
+      },
+    );
+  }
 
-    updateEmailSettings(e) {
-        e.preventDefault();
+  updateEmailSettings(e) {
+    e.preventDefault();
 
-        this.setState({
-            formErrors: null,
-            submitting: "working"
-        });
+    this.setState({
+      formErrors: null,
+      submitting: "working",
+    });
 
-        let { formData, valid } = this.state;
+    let { formData, valid } = this.state;
 
-        if (valid) {
-            this.props.updateEmailSettings(formData).then(() => {
-                this.setState({
-                    dirty: false,
-                    submitting: "success"
-                });
+    if (valid) {
+      this.props.updateEmailSettings(formData).then(
+        () => {
+          this.setState({
+            dirty: false,
+            submitting: "success",
+          });
 
-                MetabaseAnalytics.trackEvent("Email Settings", "Update", "success");
+          MetabaseAnalytics.trackEvent("Email Settings", "Update", "success");
 
-                // show a confirmation for 3 seconds, then return to normal
-                setTimeout(() => this.setState({submitting: "default"}), 3000);
-            }, (error) => {
-                this.setState({
-                    submitting: "default",
-                    formErrors: this.handleFormErrors(error)
-                });
+          // show a confirmation for 3 seconds, then return to normal
+          setTimeout(() => this.setState({ submitting: "default" }), 3000);
+        },
+        error => {
+          this.setState({
+            submitting: "default",
+            formErrors: this.handleFormErrors(error),
+          });
 
-                MetabaseAnalytics.trackEvent("Email Settings", "Update", "error");
-            });
-        }
+          MetabaseAnalytics.trackEvent("Email Settings", "Update", "error");
+        },
+      );
     }
+  }
+
+  render() {
+    let { elements } = this.props;
+    let {
+      dirty,
+      formData,
+      formErrors,
+      sendingEmail,
+      submitting,
+      valid,
+      validationErrors,
+    } = this.state;
+
+    let settings = elements.map((element, index) => {
+      // merge together data from a couple places to provide a complete view of the Element state
+      let errorMessage =
+        formErrors && formErrors.elements
+          ? formErrors.elements[element.key]
+          : validationErrors[element.key];
+      let value =
+        formData[element.key] == null
+          ? element.defaultValue
+          : formData[element.key];
+
+      return (
+        <SettingsSetting
+          key={element.key}
+          setting={{ ...element, value }}
+          onChange={this.handleChangeEvent.bind(this, element)}
+          errorMessage={errorMessage}
+        />
+      );
+    });
+
+    let sendTestButtonStates = {
+      default: t`Send test email`,
+      working: t`Sending...`,
+      success: t`Sent!`,
+    };
 
-    render() {
-        let { elements } = this.props;
-        let { dirty, formData, formErrors, sendingEmail, submitting, valid, validationErrors } = this.state;
-
-        let settings = elements.map((element, index) => {
-            // merge together data from a couple places to provide a complete view of the Element state
-            let errorMessage = (formErrors && formErrors.elements) ? formErrors.elements[element.key] : validationErrors[element.key];
-            let value = formData[element.key] == null ? element.defaultValue : formData[element.key];
-
-            return (
-                <SettingsSetting
-                    key={element.key}
-                    setting={{ ...element, value }}
-                    onChange={this.handleChangeEvent.bind(this, element)}
-                    errorMessage={errorMessage}
-                />
-            );
-        });
+    let saveSettingsButtonStates = {
+      default: t`Save changes`,
+      working: t`Saving...`,
+      success: t`Changes saved!`,
+    };
 
-        let sendTestButtonStates = {
-            default: t`Send test email`,
-            working: t`Sending...`,
-            success: t`Sent!`
-        };
-
-        let saveSettingsButtonStates = {
-            default: t`Save changes`,
-            working: t`Saving...`,
-            success: t`Changes saved!`
-        };
-
-        let disabled = (!valid || submitting !== "default" || sendingEmail !== "default"),
-            emailButtonText = sendTestButtonStates[sendingEmail],
-            saveButtonText = saveSettingsButtonStates[submitting];
-
-        return (
-            <form noValidate>
-                <ul>
-                    {settings}
-                    <li className="m2 mb4">
-                        <button className={cx("Button mr2", {"Button--primary": !disabled}, {"Button--success-new": submitting === "success"})} disabled={disabled} onClick={this.updateEmailSettings.bind(this)}>
-                            {saveButtonText}
-                        </button>
-                        { (valid && !dirty && submitting === "default") ?
-                            <button className={cx("Button", {"Button--success-new": sendingEmail === "success"})} disabled={disabled} onClick={this.sendTestEmail.bind(this)}>
-                                {emailButtonText}
-                            </button>
-                        : null }
-                        { formErrors && formErrors.message ? <span className="pl2 text-error text-bold">{formErrors.message}</span> : null}
-                    </li>
-                </ul>
-            </form>
-        );
-    }
+    let disabled =
+        !valid || submitting !== "default" || sendingEmail !== "default",
+      emailButtonText = sendTestButtonStates[sendingEmail],
+      saveButtonText = saveSettingsButtonStates[submitting];
+
+    return (
+      <form noValidate>
+        <ul>
+          {settings}
+          <li className="m2 mb4">
+            <button
+              className={cx(
+                "Button mr2",
+                { "Button--primary": !disabled },
+                { "Button--success-new": submitting === "success" },
+              )}
+              disabled={disabled}
+              onClick={this.updateEmailSettings.bind(this)}
+            >
+              {saveButtonText}
+            </button>
+            {valid && !dirty && submitting === "default" ? (
+              <button
+                className={cx("Button", {
+                  "Button--success-new": sendingEmail === "success",
+                })}
+                disabled={disabled}
+                onClick={this.sendTestEmail.bind(this)}
+              >
+                {emailButtonText}
+              </button>
+            ) : null}
+            {formErrors && formErrors.message ? (
+              <span className="pl2 text-error text-bold">
+                {formErrors.message}
+              </span>
+            ) : null}
+          </li>
+        </ul>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
index 0d6d153cf429b76db2596b4f8700dbc0864009cf..9fec2c97f58b64b13d9076af9b9144dd378bc6b3 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsLdapForm.jsx
@@ -4,255 +4,312 @@ import _ from "underscore";
 import cx from "classnames";
 
 import Collapse from "react-collapse";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs";
 import DisclosureTriangle from "metabase/components/DisclosureTriangle";
 import MetabaseUtils from "metabase/lib/utils";
 import SettingsSetting from "./SettingsSetting";
 
 export default class SettingsLdapForm extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            formData: {},
-            showAttributes: false,
-            submitting: "default",
-            valid: false,
-            validationErrors: {}
-        }
-    }
-
-    static propTypes = {
-        elements: PropTypes.array.isRequired,
-        formErrors: PropTypes.object,
-        updateLdapSettings: PropTypes.func.isRequired
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      formData: {},
+      showAttributes: false,
+      submitting: "default",
+      valid: false,
+      validationErrors: {},
     };
-
-    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 = {};
-        for (const element of props.elements) {
-            formData[element.key] = element.value;
-        }
-        this.setState({ formData });
+  }
+
+  static propTypes = {
+    elements: PropTypes.array.isRequired,
+    formErrors: PropTypes.object,
+    updateLdapSettings: PropTypes.func.isRequired,
+  };
+
+  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 = {};
+    for (const element of props.elements) {
+      formData[element.key] = element.value;
     }
-
-    componentDidMount() {
-        this.validateForm();
-    }
-
-    componentDidUpdate() {
-        this.validateForm();
-    }
-
-    setSubmitting(submitting) {
-        this.setState({submitting});
+    this.setState({ formData });
+  }
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  componentDidUpdate() {
+    this.validateForm();
+  }
+
+  setSubmitting(submitting) {
+    this.setState({ submitting });
+  }
+
+  setFormErrors(formErrors) {
+    this.setState({ formErrors });
+  }
+
+  // return null if element passes validation, otherwise return an error message
+  validateElement([validationType, validationMessage], value, element) {
+    if (MetabaseUtils.isEmpty(value)) return;
+
+    switch (validationType) {
+      case "email":
+        return !MetabaseUtils.validEmail(value)
+          ? validationMessage || t`That's not a valid email address`
+          : null;
+      case "integer":
+        return isNaN(parseInt(value))
+          ? validationMessage || t`That's not a valid integer`
+          : null;
+      case "ldap_filter":
+        return (value.match(/\(/g) || []).length !==
+          (value.match(/\)/g) || []).length
+          ? validationMessage || t`Check your parentheses`
+          : null;
     }
+  }
 
-    setFormErrors(formErrors) {
-        this.setState({formErrors});
-    }
+  validateForm() {
+    let { elements } = this.props;
+    let { formData } = this.state;
 
-    // return null if element passes validation, otherwise return an error message
-    validateElement([validationType, validationMessage], value, element) {
-        if (MetabaseUtils.isEmpty(value)) return;
-
-        switch (validationType) {
-            case "email":
-                return !MetabaseUtils.validEmail(value) ? (validationMessage || t`That's not a valid email address`) : null;
-            case "integer":
-                return isNaN(parseInt(value)) ? (validationMessage || t`That's not a valid integer`) : null;
-            case "ldap_filter":
-                return (value.match(/\(/g) || []).length !== (value.match(/\)/g) || []).length ? (validationMessage || t`Check your parentheses`) : null;
-        }
-    }
+    let valid = true,
+      validationErrors = {};
 
-    validateForm() {
-        let { elements } = this.props;
-        let { formData } = this.state;
-
-        let valid = true,
-            validationErrors = {};
-
-        // Validate form only if LDAP is enabled
-        if (formData['ldap-enabled']) {
-            elements.forEach(function(element) {
-                // test for required elements
-                if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
-                    valid = false;
-                }
-
-                if (element.validations) {
-                    element.validations.forEach(function(validation) {
-                        validationErrors[element.key] = this.validateElement(validation, formData[element.key], element);
-                        if (validationErrors[element.key]) valid = false;
-                    }, this);
-                }
-            }, this);
+    // Validate form only if LDAP is enabled
+    if (formData["ldap-enabled"]) {
+      elements.forEach(function(element) {
+        // test for required elements
+        if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
+          valid = false;
         }
 
-        if (this.state.valid !== valid || !_.isEqual(this.state.validationErrors, validationErrors)) {
-            this.setState({ valid, validationErrors });
+        if (element.validations) {
+          element.validations.forEach(function(validation) {
+            validationErrors[element.key] = this.validateElement(
+              validation,
+              formData[element.key],
+              element,
+            );
+            if (validationErrors[element.key]) valid = false;
+          }, this);
         }
+      }, this);
     }
 
-    handleChangeEvent(element, value, event) {
-        this.setState((previousState) => ({ formData: {
-            ...previousState.formData,
-            [element.key]: (MetabaseUtils.isEmpty(value)) ? null : value
-        } }));
+    if (
+      this.state.valid !== valid ||
+      !_.isEqual(this.state.validationErrors, validationErrors)
+    ) {
+      this.setState({ valid, validationErrors });
     }
-
-    handleFormErrors(error) {
-        // parse and format
-        let formErrors = {};
-        if (error.data && error.data.message) {
-            formErrors.message = error.data.message;
-        } else {
-            formErrors.message = t`Looks like we ran into some problems`;
-        }
-
-        if (error.data && error.data.errors) {
-            formErrors.elements = error.data.errors;
-        }
-
-        return formErrors;
+  }
+
+  handleChangeEvent(element, value, event) {
+    this.setState(previousState => ({
+      formData: {
+        ...previousState.formData,
+        [element.key]: MetabaseUtils.isEmpty(value) ? null : value,
+      },
+    }));
+  }
+
+  handleFormErrors(error) {
+    // parse and format
+    let formErrors = {};
+    if (error.data && error.data.message) {
+      formErrors.message = error.data.message;
+    } else {
+      formErrors.message = t`Looks like we ran into some problems`;
     }
 
-    handleAttributeToggle() {
-        this.setState((previousState) => ({ showAttributes: !previousState['showAttributes'] }));
+    if (error.data && error.data.errors) {
+      formErrors.elements = error.data.errors;
     }
 
-    updateLdapSettings(e) {
-        e.preventDefault();
-
-        let { formData, valid } = this.state;
+    return formErrors;
+  }
 
-        if (valid) {
-            this.setState({
-                formErrors: null,
-                submitting: "working"
-            });
+  handleAttributeToggle() {
+    this.setState(previousState => ({
+      showAttributes: !previousState["showAttributes"],
+    }));
+  }
 
-            this.props.updateLdapSettings(formData).then(() => {
-                this.setState({ submitting: "success" });
+  updateLdapSettings(e) {
+    e.preventDefault();
 
-                // show a confirmation for 3 seconds, then return to normal
-                setTimeout(() => this.setState({ submitting: "default" }), 3000);
-            }, (error) => {
-                this.setState({
-                    submitting: "default",
-                    formErrors: this.handleFormErrors(error)
-                });
-            });
-        }
-    }
+    let { formData, valid } = this.state;
 
-    render() {
-        const { elements } = this.props;
-        const { formData, formErrors, showAttributes, submitting, valid, validationErrors } = this.state;
-
-        let sections = {
-            'ldap-enabled': 'server',
-            'ldap-host': 'server',
-            'ldap-port': 'server',
-            'ldap-security': 'server',
-            'ldap-bind-dn': 'server',
-            'ldap-password': 'server',
-            'ldap-user-base': 'user',
-            'ldap-user-filter': 'user',
-            'ldap-attribute-email': 'attribute',
-            'ldap-attribute-firstname': 'attribute',
-            'ldap-attribute-lastname': 'attribute',
-            'ldap-group-sync': 'group',
-            'ldap-group-base': 'group'
-        }
-
-        const toElement = (element) => {
-            // merge together data from a couple places to provide a complete view of the Element state
-            let errorMessage = (formErrors && formErrors.elements) ? formErrors.elements[element.key] : validationErrors[element.key];
-            let value = formData[element.key] == null ? element.defaultValue : formData[element.key];
-
-            if (element.key === 'ldap-group-sync') {
-                return (
-                    <SettingsSetting
-                        key={element.key}
-                        setting={{ ...element, value }}
-                        onChange={this.handleChangeEvent.bind(this, element)}
-                        mappings={formData['ldap-group-mappings']}
-                        updateMappings={this.handleChangeEvent.bind(this, { key: 'ldap-group-mappings' })}
-                        errorMessage={errorMessage}
-                    />
-                );
-            }
-
-            return (
-                <SettingsSetting
-                    key={element.key}
-                    setting={{ ...element, value }}
-                    onChange={this.handleChangeEvent.bind(this, element)}
-                    errorMessage={errorMessage}
-                />
-            );
-        };
+    if (valid) {
+      this.setState({
+        formErrors: null,
+        submitting: "working",
+      });
 
-        let serverSettings = elements.filter(e => sections[e.key] === 'server').map(toElement);
-        let userSettings = elements.filter(e => sections[e.key] === 'user').map(toElement);
-        let attributeSettings = elements.filter(e => sections[e.key] === 'attribute').map(toElement);
-        let groupSettings = elements.filter(e => sections[e.key] === 'group').map(toElement);
+      this.props.updateLdapSettings(formData).then(
+        () => {
+          this.setState({ submitting: "success" });
 
-        let saveSettingsButtonStates = {
-            default: t`Save changes`,
-            working: t`Saving...`,
-            success: t`Changes saved!`
-        };
-
-        let disabled = (!valid || submitting !== "default");
-        let saveButtonText = saveSettingsButtonStates[submitting];
+          // show a confirmation for 3 seconds, then return to normal
+          setTimeout(() => this.setState({ submitting: "default" }), 3000);
+        },
+        error => {
+          this.setState({
+            submitting: "default",
+            formErrors: this.handleFormErrors(error),
+          });
+        },
+      );
+    }
+  }
+
+  render() {
+    const { elements } = this.props;
+    const {
+      formData,
+      formErrors,
+      showAttributes,
+      submitting,
+      valid,
+      validationErrors,
+    } = this.state;
+
+    let sections = {
+      "ldap-enabled": "server",
+      "ldap-host": "server",
+      "ldap-port": "server",
+      "ldap-security": "server",
+      "ldap-bind-dn": "server",
+      "ldap-password": "server",
+      "ldap-user-base": "user",
+      "ldap-user-filter": "user",
+      "ldap-attribute-email": "attribute",
+      "ldap-attribute-firstname": "attribute",
+      "ldap-attribute-lastname": "attribute",
+      "ldap-group-sync": "group",
+      "ldap-group-base": "group",
+    };
 
+    const toElement = element => {
+      // merge together data from a couple places to provide a complete view of the Element state
+      let errorMessage =
+        formErrors && formErrors.elements
+          ? formErrors.elements[element.key]
+          : validationErrors[element.key];
+      let value =
+        formData[element.key] == null
+          ? element.defaultValue
+          : formData[element.key];
+
+      if (element.key === "ldap-group-sync") {
         return (
-            <form noValidate>
-                <Breadcrumbs
-                    crumbs={[
-                        [t`Authentication`, "/admin/settings/authentication"],
-                        [t`LDAP`]
-                    ]}
-                    className="ml2 mb3"
-                />
-                <h2 className="mx2">{t`Server Settings`}</h2>
-                <ul>{serverSettings}</ul>
-                <h2 className="mx2">{t`User Schema`}</h2>
-                <ul>{userSettings}</ul>
-                <div className="mb4">
-                    <div className="inline-block ml1 cursor-pointer text-brand-hover" onClick={this.handleAttributeToggle.bind(this)}>
-                        <div className="flex align-center">
-                            <DisclosureTriangle open={showAttributes} />
-                            <h3>{t`Attributes`}</h3>
-                        </div>
-                    </div>
-                    <Collapse isOpened={showAttributes} keepCollapsedContent>
-                        <ul>{attributeSettings}</ul>
-                    </Collapse>
-                </div>
-                <h2 className="mx2">{t`Group Schema`}</h2>
-                <ul>{groupSettings}</ul>
-                <div className="m2 mb4">
-                    <button className={cx("Button mr2", {"Button--primary": !disabled}, {"Button--success-new": submitting === "success"})} disabled={disabled} onClick={this.updateLdapSettings.bind(this)}>
-                        {saveButtonText}
-                    </button>
-                    { (formErrors && formErrors.message) ? (
-                        <span className="pl2 text-error text-bold">{formErrors.message}</span>
-                    ) : null }
-                </div>
-            </form>
+          <SettingsSetting
+            key={element.key}
+            setting={{ ...element, value }}
+            onChange={this.handleChangeEvent.bind(this, element)}
+            mappings={formData["ldap-group-mappings"]}
+            updateMappings={this.handleChangeEvent.bind(this, {
+              key: "ldap-group-mappings",
+            })}
+            errorMessage={errorMessage}
+          />
         );
-    }
+      }
+
+      return (
+        <SettingsSetting
+          key={element.key}
+          setting={{ ...element, value }}
+          onChange={this.handleChangeEvent.bind(this, element)}
+          errorMessage={errorMessage}
+        />
+      );
+    };
+
+    let serverSettings = elements
+      .filter(e => sections[e.key] === "server")
+      .map(toElement);
+    let userSettings = elements
+      .filter(e => sections[e.key] === "user")
+      .map(toElement);
+    let attributeSettings = elements
+      .filter(e => sections[e.key] === "attribute")
+      .map(toElement);
+    let groupSettings = elements
+      .filter(e => sections[e.key] === "group")
+      .map(toElement);
+
+    let saveSettingsButtonStates = {
+      default: t`Save changes`,
+      working: t`Saving...`,
+      success: t`Changes saved!`,
+    };
+
+    let disabled = !valid || submitting !== "default";
+    let saveButtonText = saveSettingsButtonStates[submitting];
+
+    return (
+      <form noValidate>
+        <Breadcrumbs
+          crumbs={[
+            [t`Authentication`, "/admin/settings/authentication"],
+            [t`LDAP`],
+          ]}
+          className="ml2 mb3"
+        />
+        <h2 className="mx2">{t`Server Settings`}</h2>
+        <ul>{serverSettings}</ul>
+        <h2 className="mx2">{t`User Schema`}</h2>
+        <ul>{userSettings}</ul>
+        <div className="mb4">
+          <div
+            className="inline-block ml1 cursor-pointer text-brand-hover"
+            onClick={this.handleAttributeToggle.bind(this)}
+          >
+            <div className="flex align-center">
+              <DisclosureTriangle open={showAttributes} />
+              <h3>{t`Attributes`}</h3>
+            </div>
+          </div>
+          <Collapse isOpened={showAttributes} keepCollapsedContent>
+            <ul>{attributeSettings}</ul>
+          </Collapse>
+        </div>
+        <h2 className="mx2">{t`Group Schema`}</h2>
+        <ul>{groupSettings}</ul>
+        <div className="m2 mb4">
+          <button
+            className={cx(
+              "Button mr2",
+              { "Button--primary": !disabled },
+              { "Button--success-new": submitting === "success" },
+            )}
+            disabled={disabled}
+            onClick={this.updateLdapSettings.bind(this)}
+          >
+            {saveButtonText}
+          </button>
+          {formErrors && formErrors.message ? (
+            <span className="pl2 text-error text-bold">
+              {formErrors.message}
+            </span>
+          ) : null}
+        </div>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
index 4f972a0f257d8b29d9701ca66afc595586b9233d..a0b98d381d53eb238373289a25ed15c326ec5e06 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetting.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 import { assocIn } from "icepick";
 
 import SettingHeader from "./SettingHeader.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import SettingInput from "./widgets/SettingInput.jsx";
 import SettingNumber from "./widgets/SettingNumber.jsx";
 import SettingPassword from "./widgets/SettingPassword.jsx";
@@ -12,55 +12,58 @@ import SettingToggle from "./widgets/SettingToggle.jsx";
 import SettingSelect from "./widgets/SettingSelect.jsx";
 
 const SETTING_WIDGET_MAP = {
-    "string":   SettingInput,
-    "number":   SettingNumber,
-    "password": SettingPassword,
-    "select":   SettingSelect,
-    "radio":    SettingRadio,
-    "boolean":  SettingToggle
+  string: SettingInput,
+  number: SettingNumber,
+  password: SettingPassword,
+  select: SettingSelect,
+  radio: SettingRadio,
+  boolean: SettingToggle,
 };
 
-const updatePlaceholderForEnvironmentVars = (props) => {
-    if (props && props.setting && props.setting.is_env_setting){
-        return assocIn(props, ["setting", "placeholder"], t`Using ` + props.setting.env_name)
-    }
-    return props
-}
+const updatePlaceholderForEnvironmentVars = props => {
+  if (props && props.setting && props.setting.is_env_setting) {
+    return assocIn(
+      props,
+      ["setting", "placeholder"],
+      t`Using ` + props.setting.env_name,
+    );
+  }
+  return props;
+};
 
 export default class SettingsSetting extends Component {
+  static propTypes = {
+    setting: PropTypes.object.isRequired,
+    onChange: PropTypes.func.isRequired,
+    onChangeSetting: PropTypes.func,
+    autoFocus: PropTypes.bool,
+    disabled: PropTypes.bool,
+  };
 
-
-    static propTypes = {
-        setting: PropTypes.object.isRequired,
-        onChange: PropTypes.func.isRequired,
-        onChangeSetting: PropTypes.func,
-        autoFocus: PropTypes.bool,
-        disabled: PropTypes.bool,
-    };
-
-    render() {
-        const { setting, errorMessage } = this.props;
-        let Widget = setting.widget || SETTING_WIDGET_MAP[setting.type];
-        if (!Widget) {
-            console.warn("No render method for setting type " + setting.type + ", defaulting to string input.");
-            Widget = SettingInput;
-        }
-        return (
-            <li className="m2 mb4">
-                { !setting.noHeader &&
-                    <SettingHeader setting={setting} />
-                }
-                <div className="flex">
-                    <Widget {...updatePlaceholderForEnvironmentVars(this.props)}
-                    />
-                </div>
-                { errorMessage &&
-                    <div className="text-error text-bold pt1">{errorMessage}</div>
-                }
-                { setting.warning &&
-                    <div className="text-gold text-bold pt1">{setting.warning}</div>
-                }
-            </li>
-        );
+  render() {
+    const { setting, errorMessage } = this.props;
+    let Widget = setting.widget || SETTING_WIDGET_MAP[setting.type];
+    if (!Widget) {
+      console.warn(
+        "No render method for setting type " +
+          setting.type +
+          ", defaulting to string input.",
+      );
+      Widget = SettingInput;
     }
+    return (
+      <li className="m2 mb4">
+        {!setting.noHeader && <SettingHeader setting={setting} />}
+        <div className="flex">
+          <Widget {...updatePlaceholderForEnvironmentVars(this.props)} />
+        </div>
+        {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/components/SettingsSetupList.jsx b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
index f95604fe1b6422b272dc87664bbceabb62cfce20..549e5a6f1c6e58b4440f9afac5cd63bc5b230dcc 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSetupList.jsx
@@ -5,101 +5,123 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 import { SetupApi } from "metabase/services";
 import { t } from "c-3po";
 
-const TaskList = ({tasks}) =>
+const TaskList = ({ tasks }) => (
   <ol>
-    { tasks.map((task, index) => <li className="mb2" key={index}><Task {...task} /></li>)}
+    {tasks.map((task, index) => (
+      <li className="mb2" key={index}>
+        <Task {...task} />
+      </li>
+    ))}
   </ol>
+);
 
-const TaskSectionHeader = ({name}) =>
+const TaskSectionHeader = ({ name }) => (
   <h4 className="text-grey-4 text-bold text-uppercase pb2">{name}</h4>
+);
 
-const TaskSection = ({name, tasks}) =>
+const TaskSection = ({ name, tasks }) => (
   <div className="mb4">
     <TaskSectionHeader name={name} />
     <TaskList tasks={tasks} />
   </div>
+);
 
-const TaskTitle = ({title, titleClassName}) =>
+const TaskTitle = ({ title, titleClassName }) => (
   <h3 className={titleClassName}>{title}</h3>
+);
 
-const TaskDescription = ({description}) => <p className="m0 mt1">{description}</p>
-
-const CompletionBadge = ({completed}) =>
-    <div className="mr2 flex align-center justify-center flex-no-shrink" style={{
-        borderWidth: 1,
-        borderStyle: 'solid',
-        borderColor: completed ? '#9CC177' : '#DCE9EA',
-        backgroundColor: completed ? '#9CC177' : '#fff',
-        width: 32,
-        height: 32,
-        borderRadius: 99
-      }}>
-      { completed && <Icon name="check" color={'#fff'} />}
-    </div>
+const TaskDescription = ({ description }) => (
+  <p className="m0 mt1">{description}</p>
+);
 
+const CompletionBadge = ({ completed }) => (
+  <div
+    className="mr2 flex align-center justify-center flex-no-shrink"
+    style={{
+      borderWidth: 1,
+      borderStyle: "solid",
+      borderColor: completed ? "#9CC177" : "#DCE9EA",
+      backgroundColor: completed ? "#9CC177" : "#fff",
+      width: 32,
+      height: 32,
+      borderRadius: 99,
+    }}
+  >
+    {completed && <Icon name="check" color={"#fff"} />}
+  </div>
+);
 
-export const Task = ({title, description, completed, link}) =>
-  <Link to={link} className="bordered border-brand-hover rounded transition-border flex align-center p2 no-decoration">
+export const Task = ({ title, description, completed, link }) => (
+  <Link
+    to={link}
+    className="bordered border-brand-hover rounded transition-border flex align-center p2 no-decoration"
+  >
     <CompletionBadge completed={completed} />
     <div>
-      <TaskTitle title={title} titleClassName={
-          completed ? 'text-success': 'text-brand'
-        } />
-      { !completed ? <TaskDescription description={description} /> : null }
+      <TaskTitle
+        title={title}
+        titleClassName={completed ? "text-success" : "text-brand"}
+      />
+      {!completed ? <TaskDescription description={description} /> : null}
     </div>
   </Link>
+);
 
 export default class SettingsSetupList extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            tasks: null,
-            error: null
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      tasks: null,
+      error: null,
+    };
+  }
 
-    async componentWillMount() {
-        try {
-            const tasks = await SetupApi.admin_checklist();
-            this.setState({ tasks: tasks });
-        } catch (e) {
-            this.setState({ error: e });
-        }
+  async componentWillMount() {
+    try {
+      const tasks = await SetupApi.admin_checklist();
+      this.setState({ tasks: tasks });
+    } catch (e) {
+      this.setState({ error: e });
     }
+  }
 
-    render() {
-        let tasks, nextTask;
-        if (this.state.tasks) {
-            tasks = this.state.tasks.map(section => ({
-                ...section,
-                tasks: section.tasks.filter(task => {
-                    if (task.is_next_step) {
-                        nextTask = task;
-                    }
-                    return !task.is_next_step;
-                })
-            }))
-        }
+  render() {
+    let tasks, nextTask;
+    if (this.state.tasks) {
+      tasks = this.state.tasks.map(section => ({
+        ...section,
+        tasks: section.tasks.filter(task => {
+          if (task.is_next_step) {
+            nextTask = task;
+          }
+          return !task.is_next_step;
+        }),
+      }));
+    }
 
-        return (
-            <div className="px2">
-              <h2>{t`Getting set up`}</h2>
-              <p className="mt1">{t`A few things you can do to get the most out of Metabase.`}</p>
-              <LoadingAndErrorWrapper loading={!this.state.tasks} error={this.state.error} >
-              { () =>
-                  <div style={{maxWidth: 468}}>
-                      { nextTask &&
-                          <TaskSection name={t`Recommended next step`} tasks={[nextTask]} />
-                      }
-                      {
-                        tasks.map((section, index) =>
-                          <TaskSection {...section} key={index} />
-                        )
-                      }
-                  </div>
-              }
-              </LoadingAndErrorWrapper>
+    return (
+      <div className="px2">
+        <h2>{t`Getting set up`}</h2>
+        <p className="mt1">{t`A few things you can do to get the most out of Metabase.`}</p>
+        <LoadingAndErrorWrapper
+          loading={!this.state.tasks}
+          error={this.state.error}
+        >
+          {() => (
+            <div style={{ maxWidth: 468 }}>
+              {nextTask && (
+                <TaskSection
+                  name={t`Recommended next step`}
+                  tasks={[nextTask]}
+                />
+              )}
+              {tasks.map((section, index) => (
+                <TaskSection {...section} key={index} />
+              ))}
             </div>
-        );
-    }
+          )}
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx
index d75319327b874bb3125e49392d71a86f9eb87ce7..185508697235cbee95b25b05d8ae1ef2c02259c8 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSingleSignOnForm.jsx
@@ -8,147 +8,156 @@ import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
 import Input from "metabase/components/Input.jsx";
 
 export default class SettingsSingleSignOnForm extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.updateClientID    = this.updateClientID.bind(this);
-        this.updateDomain      = this.updateDomain.bind(this);
-        this.onCheckboxClicked = this.onCheckboxClicked.bind(this),
-        this.saveChanges       = this.saveChanges.bind(this),
-        this.clientIDChanged   = this.clientIDChanged.bind(this),
-        this.domainChanged     = this.domainChanged.bind(this)
+  constructor(props, context) {
+    super(props, context);
+    this.updateClientID = this.updateClientID.bind(this);
+    this.updateDomain = this.updateDomain.bind(this);
+    (this.onCheckboxClicked = this.onCheckboxClicked.bind(this)),
+      (this.saveChanges = this.saveChanges.bind(this)),
+      (this.clientIDChanged = this.clientIDChanged.bind(this)),
+      (this.domainChanged = this.domainChanged.bind(this));
+  }
+
+  static propTypes = {
+    elements: PropTypes.array,
+    updateSetting: PropTypes.func.isRequired,
+  };
+
+  componentWillMount() {
+    let { elements } = this.props,
+      clientID = _.findWhere(elements, { key: "google-auth-client-id" }),
+      domain = _.findWhere(elements, {
+        key: "google-auth-auto-create-accounts-domain",
+      });
+
+    this.setState({
+      clientID: clientID,
+      domain: domain,
+      clientIDValue: clientID.value,
+      domainValue: domain.value,
+      recentlySaved: false,
+    });
+  }
+
+  updateClientID(newValue) {
+    if (newValue === this.state.clientIDValue) return;
+
+    this.setState({
+      clientIDValue: newValue && newValue.length ? newValue : null,
+      recentlySaved: false,
+    });
+  }
+
+  updateDomain(newValue) {
+    if (newValue === this.state.domain.value) return;
+
+    this.setState({
+      domainValue: newValue && newValue.length ? newValue : null,
+      recentlySaved: false,
+    });
+  }
+
+  clientIDChanged() {
+    return this.state.clientID.value !== this.state.clientIDValue;
+  }
+
+  domainChanged() {
+    return this.state.domain.value !== this.state.domainValue;
+  }
+
+  saveChanges() {
+    let { clientID, clientIDValue, domain, domainValue } = this.state;
+
+    if (this.clientIDChanged()) {
+      this.props.updateSetting(clientID, clientIDValue);
+      this.setState({
+        clientID: {
+          value: clientIDValue,
+        },
+        recentlySaved: true,
+      });
     }
 
-    static propTypes = {
-        elements: PropTypes.array,
-        updateSetting: PropTypes.func.isRequired
-    };
-
-    componentWillMount() {
-        let { elements } = this.props,
-            clientID     = _.findWhere(elements, {key: 'google-auth-client-id'}),
-            domain       = _.findWhere(elements, {key: 'google-auth-auto-create-accounts-domain'});
-
-        this.setState({
-            clientID:      clientID,
-            domain:        domain,
-            clientIDValue: clientID.value,
-            domainValue:   domain.value,
-            recentlySaved: false
-        });
-    }
-
-    updateClientID(newValue) {
-        if (newValue === this.state.clientIDValue) return;
-
-        this.setState({
-            clientIDValue: newValue && newValue.length ? newValue : null,
-            recentlySaved: false
-        });
-    }
-
-    updateDomain(newValue) {
-        if (newValue === this.state.domain.value) return;
-
-        this.setState({
-            domainValue: newValue && newValue.length ? newValue : null,
-            recentlySaved: false
-        });
-    }
-
-    clientIDChanged() {
-        return this.state.clientID.value !== this.state.clientIDValue;
-    }
-
-    domainChanged() {
-        return this.state.domain.value !== this.state.domainValue;
-    }
-
-    saveChanges() {
-        let { clientID, clientIDValue, domain, domainValue } = this.state;
-
-        if (this.clientIDChanged()) {
-            this.props.updateSetting(clientID, clientIDValue);
-            this.setState({
-                clientID: {
-                    value: clientIDValue
-                },
-                recentlySaved: true
-            });
-        }
-
-        if (this.domainChanged()) {
-            this.props.updateSetting(domain, domainValue);
-            this.setState({
-                domain: {
-                    value: domainValue
-                },
-                recentlySaved: true
-            });
-        }
-    }
-
-    onCheckboxClicked() {
-        // if domain is present, clear it out; otherwise if there's no domain try to set it back to what it was
-        this.setState({
-            domainValue: this.state.domainValue ? null : this.state.domain.value,
-            recentlySaved: false
-        });
-    }
-
-    render() {
-        let hasChanges  = this.domainChanged() || this.clientIDChanged(),
-            hasClientID = this.state.clientIDValue;
-
-        return (
-            <form noValidate>
-                <div
-                    className="px2"
-                    style={{maxWidth: "585px"}}
-                >
-                    <Breadcrumbs
-                        crumbs={[
-                            [t`Authentication`, "/admin/settings/authentication"],
-                            [t`Google Sign-In`]
-                        ]}
-                        className="mb2"
-                    />
-                    <h2>{t`Sign in with Google`}</h2>
-                    <p className="text-grey-4">
-                        {t`Allows users with existing Metabase accounts to login with a Google account that matches their email address in addition to their Metabase username and password.`}
-                    </p>
-                    <p className="text-grey-4">
-                        {jt`To allow users to sign in with Google you'll need to give Metabase a Google Developers console application client ID. It only takes a few steps and instructions on how to create a key can be found ${<a className="link" href="https://developers.google.com/identity/sign-in/web/devconsole-project" target="_blank">here.</a>}`}
-                    </p>
-                    <Input
-                        className="SettingsInput AdminInput bordered rounded h3"
-                        type="text"
-                        value={this.state.clientIDValue}
-                        placeholder={t`Your Google client ID`}
-                        onChange={(event) => this.updateClientID(event.target.value)}
-                    />
-                    <div className="py3">
-                        <div className="flex align-center">
-                            <p className="text-grey-4">{t`Allow users to sign up on their own if their Google account email address is from:`}</p>
-                        </div>
-                        <div className="mt1 bordered rounded inline-block">
-                            <div className="inline-block px2 h2">@</div>
-                            <Input
-                                className="SettingsInput inline-block AdminInput h3 border-left"
-                                type="text"
-                                value={this.state.domainValue}
-                                onChange={(event) => this.updateDomain(event.target.value)}
-                                disabled={!hasClientID}
-                            />
-                        </div>
-                    </div>
-
-                    <button className={cx("Button mr2", {"Button--primary": hasChanges})}
-                            disabled={!hasChanges}
-                            onClick={this.saveChanges}>
-                        {this.state.recentlySaved ? t`Changes saved!` : t`Save Changes`}
-                    </button>
-                </div>
-            </form>
-        );
+    if (this.domainChanged()) {
+      this.props.updateSetting(domain, domainValue);
+      this.setState({
+        domain: {
+          value: domainValue,
+        },
+        recentlySaved: true,
+      });
     }
+  }
+
+  onCheckboxClicked() {
+    // if domain is present, clear it out; otherwise if there's no domain try to set it back to what it was
+    this.setState({
+      domainValue: this.state.domainValue ? null : this.state.domain.value,
+      recentlySaved: false,
+    });
+  }
+
+  render() {
+    let hasChanges = this.domainChanged() || this.clientIDChanged(),
+      hasClientID = this.state.clientIDValue;
+
+    return (
+      <form noValidate>
+        <div className="px2" style={{ maxWidth: "585px" }}>
+          <Breadcrumbs
+            crumbs={[
+              [t`Authentication`, "/admin/settings/authentication"],
+              [t`Google Sign-In`],
+            ]}
+            className="mb2"
+          />
+          <h2>{t`Sign in with Google`}</h2>
+          <p className="text-grey-4">
+            {t`Allows users with existing Metabase accounts to login with a Google account that matches their email address in addition to their Metabase username and password.`}
+          </p>
+          <p className="text-grey-4">
+            {jt`To allow users to sign in with Google you'll need to give Metabase a Google Developers console application client ID. It only takes a few steps and instructions on how to create a key can be found ${(
+              <a
+                className="link"
+                href="https://developers.google.com/identity/sign-in/web/devconsole-project"
+                target="_blank"
+              >
+                here.
+              </a>
+            )}`}
+          </p>
+          <Input
+            className="SettingsInput AdminInput bordered rounded h3"
+            type="text"
+            value={this.state.clientIDValue}
+            placeholder={t`Your Google client ID`}
+            onChange={event => this.updateClientID(event.target.value)}
+          />
+          <div className="py3">
+            <div className="flex align-center">
+              <p className="text-grey-4">{t`Allow users to sign up on their own if their Google account email address is from:`}</p>
+            </div>
+            <div className="mt1 bordered rounded inline-block">
+              <div className="inline-block px2 h2">@</div>
+              <Input
+                className="SettingsInput inline-block AdminInput h3 border-left"
+                type="text"
+                value={this.state.domainValue}
+                onChange={event => this.updateDomain(event.target.value)}
+                disabled={!hasClientID}
+              />
+            </div>
+          </div>
+
+          <button
+            className={cx("Button mr2", { "Button--primary": hasChanges })}
+            disabled={!hasChanges}
+            onClick={this.saveChanges}
+          >
+            {this.state.recentlySaved ? t`Changes saved!` : t`Save Changes`}
+          </button>
+        </div>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
index bbffe44e4906658d6eb5b50e2d6e0ecc315ff315..25c9c4ffac5ad923820fce8fd3b2e680ae1542e5 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsSlackForm.jsx
@@ -13,221 +13,274 @@ import _ from "underscore";
 import { t, jt } from "c-3po";
 
 export default class SettingsSlackForm extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            formData: {},
-            submitting: "default",
-            valid: false,
-            validationErrors: {}
-        }
-    }
-
-    static propTypes = {
-        elements: PropTypes.array,
-        formErrors: PropTypes.object,
-        updateSlackSettings: PropTypes.func.isRequired
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      formData: {},
+      submitting: "default",
+      valid: false,
+      validationErrors: {},
     };
-
-    componentWillMount() {
-        // this gives us an opportunity to load up our formData with any existing values for elements
-        let formData = {};
-        this.props.elements.forEach(function(element) {
-            formData[element.key] = element.value == null ? element.defaultValue : element.value;
-        });
-
-        this.setState({formData});
-    }
-
-    componentDidMount() {
-        this.validateForm();
+  }
+
+  static propTypes = {
+    elements: PropTypes.array,
+    formErrors: PropTypes.object,
+    updateSlackSettings: PropTypes.func.isRequired,
+  };
+
+  componentWillMount() {
+    // this gives us an opportunity to load up our formData with any existing values for elements
+    let formData = {};
+    this.props.elements.forEach(function(element) {
+      formData[element.key] =
+        element.value == null ? element.defaultValue : element.value;
+    });
+
+    this.setState({ formData });
+  }
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  componentDidUpdate() {
+    this.validateForm();
+  }
+
+  setSubmitting(submitting) {
+    this.setState({ submitting });
+  }
+
+  setFormErrors(formErrors) {
+    this.setState({ formErrors });
+  }
+
+  // return null if element passes validation, otherwise return an error message
+  validateElement([validationType, validationMessage], value, element) {
+    if (MetabaseUtils.isEmpty(value)) return;
+
+    switch (validationType) {
+      case "email":
+        return !MetabaseUtils.validEmail(value)
+          ? validationMessage || t`That's not a valid email address`
+          : null;
+      case "integer":
+        return isNaN(parseInt(value))
+          ? validationMessage || t`That's not a valid integer`
+          : null;
     }
-
-    componentDidUpdate() {
-        this.validateForm();
+  }
+
+  validateForm() {
+    let { elements } = this.props;
+    let { formData } = this.state;
+
+    let valid = true,
+      validationErrors = {};
+
+    elements.forEach(function(element) {
+      // test for required elements
+      if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
+        valid = false;
+      }
+
+      if (element.validations) {
+        element.validations.forEach(function(validation) {
+          validationErrors[element.key] = this.validateElement(
+            validation,
+            formData[element.key],
+            element,
+          );
+          if (validationErrors[element.key]) valid = false;
+        }, this);
+      }
+    }, this);
+
+    if (
+      this.state.valid !== valid ||
+      !_.isEqual(this.state.validationErrors, validationErrors)
+    ) {
+      this.setState({ valid, validationErrors });
     }
-
-    setSubmitting(submitting) {
-        this.setState({submitting});
+  }
+
+  handleChangeEvent(element, value, event) {
+    this.setState({
+      formData: {
+        ...this.state.formData,
+        [element.key]: MetabaseUtils.isEmpty(value) ? null : value,
+      },
+    });
+
+    if (element.key === "metabot-enabled") {
+      MetabaseAnalytics.trackEvent("Slack Settings", "Toggle Metabot", value);
     }
-
-    setFormErrors(formErrors) {
-        this.setState({formErrors});
+  }
+
+  handleFormErrors(error) {
+    // parse and format
+    let formErrors = {};
+    if (error.data && error.data.message) {
+      formErrors.message = error.data.message;
+    } else {
+      formErrors.message = t`Looks like we ran into some problems`;
     }
 
-    // return null if element passes validation, otherwise return an error message
-    validateElement([validationType, validationMessage], value, element) {
-        if (MetabaseUtils.isEmpty(value)) return;
-
-        switch (validationType) {
-            case "email":
-                return !MetabaseUtils.validEmail(value) ? (validationMessage || t`That's not a valid email address`) : null;
-            case "integer":
-                return isNaN(parseInt(value)) ? (validationMessage || t`That's not a valid integer`) : null;
-        }
+    if (error.data && error.data.errors) {
+      formErrors.elements = error.data.errors;
     }
 
-    validateForm() {
-        let { elements } = this.props;
-        let { formData } = this.state;
-
-        let valid = true,
-            validationErrors = {};
-
-        elements.forEach(function(element) {
-            // test for required elements
-            if (element.required && MetabaseUtils.isEmpty(formData[element.key])) {
-                valid = false;
-            }
-
-            if (element.validations) {
-                element.validations.forEach(function(validation) {
-                    validationErrors[element.key] = this.validateElement(validation, formData[element.key], element);
-                    if (validationErrors[element.key]) valid = false;
-                }, this);
-            }
-        }, this);
+    return formErrors;
+  }
 
-        if (this.state.valid !== valid || !_.isEqual(this.state.validationErrors, validationErrors)) {
-            this.setState({ valid, validationErrors });
-        }
-    }
+  updateSlackSettings(e) {
+    e.preventDefault();
 
-    handleChangeEvent(element, value, event) {
-        this.setState({
-            formData: { ...this.state.formData, [element.key]: (MetabaseUtils.isEmpty(value)) ? null : value }
-        });
+    this.setState({
+      formErrors: null,
+      submitting: "working",
+    });
 
-        if (element.key === "metabot-enabled") {
-            MetabaseAnalytics.trackEvent("Slack Settings", "Toggle Metabot", value);
-        }
-    }
+    let { formData, valid } = this.state;
 
-    handleFormErrors(error) {
-        // parse and format
-        let formErrors = {};
-        if (error.data && error.data.message) {
-            formErrors.message = error.data.message;
-        } else {
-            formErrors.message = t`Looks like we ran into some problems`;
-        }
+    if (valid) {
+      this.props.updateSlackSettings(formData).then(
+        () => {
+          this.setState({
+            submitting: "success",
+          });
 
-        if (error.data && error.data.errors) {
-            formErrors.elements = error.data.errors;
-        }
+          MetabaseAnalytics.trackEvent("Slack Settings", "Update", "success");
 
-        return formErrors;
-    }
-
-    updateSlackSettings(e) {
-        e.preventDefault();
-
-        this.setState({
-            formErrors: null,
-            submitting: "working"
-        });
-
-        let { formData, valid } = this.state;
-
-        if (valid) {
-            this.props.updateSlackSettings(formData).then(() => {
-                this.setState({
-                    submitting: "success"
-                });
-
-                MetabaseAnalytics.trackEvent("Slack Settings", "Update", "success");
-
-                // show a confirmation for 3 seconds, then return to normal
-                setTimeout(() => this.setState({submitting: "default"}), 3000);
-            }, (error) => {
-                this.setState({
-                    submitting: "default",
-                    formErrors: this.handleFormErrors(error)
-                });
+          // show a confirmation for 3 seconds, then return to normal
+          setTimeout(() => this.setState({ submitting: "default" }), 3000);
+        },
+        error => {
+          this.setState({
+            submitting: "default",
+            formErrors: this.handleFormErrors(error),
+          });
 
-                MetabaseAnalytics.trackEvent("Slack Settings", "Update", "error");
-            });
-        }
+          MetabaseAnalytics.trackEvent("Slack Settings", "Update", "error");
+        },
+      );
     }
-
-    render() {
-        let { elements } = this.props;
-        let { formData, formErrors, submitting, valid, validationErrors } = this.state;
-
-        let settings = elements.map((element, index) => {
-            // merge together data from a couple places to provide a complete view of the Element state
-            let errorMessage = (formErrors && formErrors.elements) ? formErrors.elements[element.key] : validationErrors[element.key];
-            let value = formData[element.key] == null ? element.defaultValue : formData[element.key];
-
-            if (element.key === "slack-token") {
-                return (
-                    <SettingsSetting
-                        key={element.key}
-                        setting={{ ...element, value }}
-                        onChange={(value) => this.handleChangeEvent(element, value)}
-                        errorMessage={errorMessage}
-                        fireOnChange
-                    />
-                );
-            } else if (element.key === "metabot-enabled") {
-                return (
-                    <SettingsSetting
-                        key={element.key}
-                        setting={{ ...element, value }}
-                        onChange={(value) => this.handleChangeEvent(element, value)}
-                        errorMessage={errorMessage}
-                        disabled={!this.state.formData["slack-token"]}
-                    />
-                );
-            }
-        });
-
-        let saveSettingsButtonStates = {
-            default: t`Save changes`,
-            working: t`Saving...`,
-            success: t`Changes saved!`
-        };
-
-        let disabled = (!valid || submitting !== "default"),
-            saveButtonText = saveSettingsButtonStates[submitting];
-
+  }
+
+  render() {
+    let { elements } = this.props;
+    let {
+      formData,
+      formErrors,
+      submitting,
+      valid,
+      validationErrors,
+    } = this.state;
+
+    let settings = elements.map((element, index) => {
+      // merge together data from a couple places to provide a complete view of the Element state
+      let errorMessage =
+        formErrors && formErrors.elements
+          ? formErrors.elements[element.key]
+          : validationErrors[element.key];
+      let value =
+        formData[element.key] == null
+          ? element.defaultValue
+          : formData[element.key];
+
+      if (element.key === "slack-token") {
         return (
-            <form noValidate>
-                <div className="px2" style={{maxWidth: "585px"}}>
-                    <h1>
-                        Metabase
-                        <RetinaImage
-                            className="mx1"
-                            src="app/assets/img/slack_emoji.png"
-                            width={79}
-                            forceOriginalDimensions={false /* broken in React v0.13 */}
-                        />
-                        Slack
-                    </h1>
-                    <h3 className="text-grey-1">{t`Answers sent right to your Slack #channels`}</h3>
-
-                    <div className="pt3">
-                        <a href="https://my.slack.com/services/new/bot" target="_blank" className="Button Button--primary" style={{padding:0}}>
-                            <div className="float-left py2 pl2">{t`Create a Slack Bot User for MetaBot`}</div>
-                            <Icon className="float-right p2 text-white cursor-pointer" style={{opacity:0.6}} name="external" size={18}/>
-                        </a>
-                    </div>
-                    <div className="py2">
-                        {jt`Once you're there, give it a name and click ${<strong>"Add bot integration"</strong>}. Then copy and paste the Bot API Token into the field below. Once you are done, create a "metabase_files" channel in Slack. Metabase needs this to upload graphs.`}
-                    </div>
-                </div>
-                <ul>
-                    {settings}
-                    <li className="m2 mb4">
-                        <button className={cx("Button mr2", {"Button--primary": !disabled}, {"Button--success-new": submitting === "success"})} disabled={disabled} onClick={this.updateSlackSettings.bind(this)}>
-                            {saveButtonText}
-                        </button>
-                        { formErrors && formErrors.message ? <span className="pl2 text-error text-bold">{formErrors.message}</span> : null}
-                    </li>
-                </ul>
-            </form>
+          <SettingsSetting
+            key={element.key}
+            setting={{ ...element, value }}
+            onChange={value => this.handleChangeEvent(element, value)}
+            errorMessage={errorMessage}
+            fireOnChange
+          />
         );
-    }
+      } else if (element.key === "metabot-enabled") {
+        return (
+          <SettingsSetting
+            key={element.key}
+            setting={{ ...element, value }}
+            onChange={value => this.handleChangeEvent(element, value)}
+            errorMessage={errorMessage}
+            disabled={!this.state.formData["slack-token"]}
+          />
+        );
+      }
+    });
+
+    let saveSettingsButtonStates = {
+      default: t`Save changes`,
+      working: t`Saving...`,
+      success: t`Changes saved!`,
+    };
+
+    let disabled = !valid || submitting !== "default",
+      saveButtonText = saveSettingsButtonStates[submitting];
+
+    return (
+      <form noValidate>
+        <div className="px2" style={{ maxWidth: "585px" }}>
+          <h1>
+            Metabase
+            <RetinaImage
+              className="mx1"
+              src="app/assets/img/slack_emoji.png"
+              width={79}
+              forceOriginalDimensions={false /* broken in React v0.13 */}
+            />
+            Slack
+          </h1>
+          <h3 className="text-grey-1">{t`Answers sent right to your Slack #channels`}</h3>
+
+          <div className="pt3">
+            <a
+              href="https://my.slack.com/services/new/bot"
+              target="_blank"
+              className="Button Button--primary"
+              style={{ padding: 0 }}
+            >
+              <div className="float-left py2 pl2">{t`Create a Slack Bot User for MetaBot`}</div>
+              <Icon
+                className="float-right p2 text-white cursor-pointer"
+                style={{ opacity: 0.6 }}
+                name="external"
+                size={18}
+              />
+            </a>
+          </div>
+          <div className="py2">
+            {jt`Once you're there, give it a name and click ${(
+              <strong>"Add bot integration"</strong>
+            )}. Then copy and paste the Bot API Token into the field below. Once you are done, create a "metabase_files" channel in Slack. Metabase needs this to upload graphs.`}
+          </div>
+        </div>
+        <ul>
+          {settings}
+          <li className="m2 mb4">
+            <button
+              className={cx(
+                "Button mr2",
+                { "Button--primary": !disabled },
+                { "Button--success-new": submitting === "success" },
+              )}
+              disabled={disabled}
+              onClick={this.updateSlackSettings.bind(this)}
+            >
+              {saveButtonText}
+            </button>
+            {formErrors && formErrors.message ? (
+              <span className="pl2 text-error text-bold">
+                {formErrors.message}
+              </span>
+            ) : null}
+          </li>
+        </ul>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
index 83178e3ce26f0ab5b40dcd951d22c6dff70e6db2..365be4d82dc2c800b1231f4c1e6e77bcfce9a3e8 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsUpdatesForm.jsx
@@ -1,43 +1,49 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import MetabaseSettings from "metabase/lib/settings";
 import MetabaseUtils from "metabase/lib/utils";
 import SettingsSetting from "./SettingsSetting.jsx";
 
 import _ from "underscore";
 
-
 export default class SettingsUpdatesForm extends Component {
-
-    static propTypes = {
-        elements: PropTypes.array
-    };
-
-    removeVersionPrefixIfNeeded(versionLabel) {
-        return versionLabel.startsWith("v") ? versionLabel.substring(1) : versionLabel;
-    }
-
-    renderVersion(version) {
-        return (
-            <div className="pb3">
-                <h3 className="text-grey-4">{this.removeVersionPrefixIfNeeded(version.version)} {version.patch ? "(patch release)" : null}</h3>
-                <ul style={{listStyleType: "disc", listStylePosition: "inside"}}>
-                    {version.highlights && version.highlights.map(highlight =>
-                        <li style={{lineHeight: "1.5"}} className="pl1">{highlight}</li>
-                    )}
-                </ul>
-            </div>
-        );
-    }
-
-    renderVersionUpdateNotice() {
-        let versionInfo = _.findWhere(this.props.settings, {key: "version-info"}),
-            currentVersion = MetabaseSettings.get("version").tag;
-
-        if (versionInfo) versionInfo = versionInfo.value;
-
-        /*
+  static propTypes = {
+    elements: PropTypes.array,
+  };
+
+  removeVersionPrefixIfNeeded(versionLabel) {
+    return versionLabel.startsWith("v")
+      ? versionLabel.substring(1)
+      : versionLabel;
+  }
+
+  renderVersion(version) {
+    return (
+      <div className="pb3">
+        <h3 className="text-grey-4">
+          {this.removeVersionPrefixIfNeeded(version.version)}{" "}
+          {version.patch ? "(patch release)" : null}
+        </h3>
+        <ul style={{ listStyleType: "disc", listStylePosition: "inside" }}>
+          {version.highlights &&
+            version.highlights.map(highlight => (
+              <li style={{ lineHeight: "1.5" }} className="pl1">
+                {highlight}
+              </li>
+            ))}
+        </ul>
+      </div>
+    );
+  }
+
+  renderVersionUpdateNotice() {
+    let versionInfo = _.findWhere(this.props.settings, { key: "version-info" }),
+      currentVersion = MetabaseSettings.get("version").tag;
+
+    if (versionInfo) versionInfo = versionInfo.value;
+
+    /*
             We expect the versionInfo to take on the JSON structure detailed below.
             The 'older' section should contain only the last 5 previous versions, we don't need to go on forever.
             The highlights for a version should just be text and should be limited to 5 items tops.
@@ -70,56 +76,75 @@ export default class SettingsUpdatesForm extends Component {
 
         */
 
-        if (!versionInfo || MetabaseUtils.compareVersions(currentVersion, versionInfo.latest.version) >= 0) {
-            return (
-                <div className="p2 bg-brand bordered rounded border-brand text-white text-bold">
-                    {jt`You're running Metabase ${this.removeVersionPrefixIfNeeded(currentVersion)} which is the latest and greatest!`}
-                </div>
-            );
-        } else {
-            return (
-                <div>
-                    <div className="p2 bg-green bordered rounded border-green flex flex-row align-center justify-between">
-                        <span className="text-white text-bold">{jt`Metabase ${this.removeVersionPrefixIfNeeded(versionInfo.latest.version)} is available.  You're running ${this.removeVersionPrefixIfNeeded(currentVersion)}`}</span>
-                        <a data-metabase-event={"Updates Settings; Update link clicked; "+versionInfo.latest.version} className="Button Button--white Button--medium borderless" href="http://www.metabase.com/start" target="_blank">{t`Update`}</a>
-                    </div>
-
-                    <div className="text-grey-3">
-                        <h3 className="py3 text-uppercase">{t`What's Changed:`}</h3>
-
-                        {this.renderVersion(versionInfo.latest)}
-
-                        {versionInfo.older && versionInfo.older.map(this.renderVersion.bind(this))}
-                    </div>
-                </div>
-            );
-        }
-    }
-
-    render() {
-        let { elements, updateSetting } = this.props;
-
-        let settings = elements.map((setting, index) =>
-            <SettingsSetting
-                key={setting.key}
-                setting={setting}
-                onChange={(value) => updateSetting(setting, value)}
-                autoFocus={index === 0}
-            />
-        );
-
-        return (
-            <div style={{width: "585px"}}>
-                <ul>
-                    {settings}
-                </ul>
-
-                <div className="px2">
-                    <div className="pt3 border-top">
-                        {this.renderVersionUpdateNotice()}
-                    </div>
-                </div>
-            </div>
-        );
+    if (
+      !versionInfo ||
+      MetabaseUtils.compareVersions(
+        currentVersion,
+        versionInfo.latest.version,
+      ) >= 0
+    ) {
+      return (
+        <div className="p2 bg-brand bordered rounded border-brand text-white text-bold">
+          {jt`You're running Metabase ${this.removeVersionPrefixIfNeeded(
+            currentVersion,
+          )} which is the latest and greatest!`}
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <div className="p2 bg-green bordered rounded border-green flex flex-row align-center justify-between">
+            <span className="text-white text-bold">{jt`Metabase ${this.removeVersionPrefixIfNeeded(
+              versionInfo.latest.version,
+            )} is available.  You're running ${this.removeVersionPrefixIfNeeded(
+              currentVersion,
+            )}`}</span>
+            <a
+              data-metabase-event={
+                "Updates Settings; Update link clicked; " +
+                versionInfo.latest.version
+              }
+              className="Button Button--white Button--medium borderless"
+              href="http://www.metabase.com/start"
+              target="_blank"
+            >{t`Update`}</a>
+          </div>
+
+          <div className="text-grey-3">
+            <h3 className="py3 text-uppercase">{t`What's Changed:`}</h3>
+
+            {this.renderVersion(versionInfo.latest)}
+
+            {versionInfo.older &&
+              versionInfo.older.map(this.renderVersion.bind(this))}
+          </div>
+        </div>
+      );
     }
+  }
+
+  render() {
+    let { elements, updateSetting } = this.props;
+
+    let settings = elements.map((setting, index) => (
+      <SettingsSetting
+        key={setting.key}
+        setting={setting}
+        onChange={value => updateSetting(setting, value)}
+        autoFocus={index === 0}
+      />
+    ));
+
+    return (
+      <div style={{ width: "585px" }}>
+        <ul>{settings}</ul>
+
+        <div className="px2">
+          <div className="pt3 border-top">
+            {this.renderVersionUpdateNotice()}
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx b/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx
index e624aca4878f37647afda7b3b99204b193155647..f426b8de0052ec9160ea87254861a8251230593b 100644
--- a/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx
+++ b/frontend/src/metabase/admin/settings/components/SettingsXrayForm.jsx
@@ -1,74 +1,76 @@
-import React from 'react'
-import SettingsSetting from 'metabase/admin/settings/components/SettingsSetting'
-import cx from 'classnames'
-import { t, jt } from 'c-3po'
+import React from "react";
+import SettingsSetting from "metabase/admin/settings/components/SettingsSetting";
+import cx from "classnames";
+import { t, jt } from "c-3po";
 
-import Icon from 'metabase/components/Icon'
+import Icon from "metabase/components/Icon";
 
-import COSTS from 'metabase/xray/costs'
+import COSTS from "metabase/xray/costs";
 
 const SettingsXrayForm = ({ settings, elements, updateSetting }) => {
-    let maxCost = Object.assign({}, ...elements.filter(e => e.key === 'xray-max-cost',))
-    const enabled = Object.assign({}, ...elements.filter(e => e.key === 'enable-xrays',))
+  let maxCost = Object.assign(
+    {},
+    ...elements.filter(e => e.key === "xray-max-cost"),
+  );
+  const enabled = Object.assign(
+    {},
+    ...elements.filter(e => e.key === "enable-xrays"),
+  );
 
-    // handle the current behavior of the default
-    if(maxCost.value == null) {
-        maxCost.value = 'extended'
-    }
+  // handle the current behavior of the default
+  if (maxCost.value == null) {
+    maxCost.value = "extended";
+  }
 
-    return (
-        <div>
-            <div className="mx2">
-                <h2>{t`X-Rays and Comparisons`}</h2>
-            </div>
+  return (
+    <div>
+      <div className="mx2">
+        <h2>{t`X-Rays and Comparisons`}</h2>
+      </div>
 
-            <ol className="mt4">
-                <SettingsSetting
-                    key={enabled.key}
-                    setting={enabled}
-                    onChange={(value) => updateSetting(enabled, value)}
-                />
-            </ol>
+      <ol className="mt4">
+        <SettingsSetting
+          key={enabled.key}
+          setting={enabled}
+          onChange={value => updateSetting(enabled, value)}
+        />
+      </ol>
 
-            <div className="mx2 text-measure">
-                <h3>{t`Maximum Cost`}</h3>
-                <p className="m0 text-paragraph">
-                    {t`If you're having performance issues related to x-ray usage you can cap how expensive x-rays are allowed to be.`}
-                </p>
-                <p className="text-paragraph">
-                  <em>{jt`${<strong>Note:</strong>} "Extended" is required for viewing time series x-rays.`}</em>
-                </p>
+      <div className="mx2 text-measure">
+        <h3>{t`Maximum Cost`}</h3>
+        <p className="m0 text-paragraph">
+          {t`If you're having performance issues related to x-ray usage you can cap how expensive x-rays are allowed to be.`}
+        </p>
+        <p className="text-paragraph">
+          <em>{jt`${(
+            <strong>Note:</strong>
+          )} "Extended" is required for viewing time series x-rays.`}</em>
+        </p>
 
-                <ol className="mt4">
-                    { Object.keys(COSTS).map(key => {
-                        const cost = COSTS[key]
-                        return (
-                            <li
-                                className={cx(
-                                    'flex align-center mb2 cursor-pointer text-brand-hover',
-                                    { 'text-brand' : maxCost.value === key }
-                                )}
-                                key={key}
-                                onClick={() => updateSetting(maxCost, key)}
-                            >
-                                <Icon
-                                    className="flex-no-shrink"
-                                    name={cost.icon}
-                                    size={24}
-                                />
-                                <div className="ml2">
-                                    <h4>{cost.display_name}</h4>
-                                    <p className="m0 text-paragraph">
-                                        {cost.description}
-                                    </p>
-                                </div>
-                            </li>
-                        )
-                    })}
-                </ol>
-            </div>
-        </div>
-    )
-}
+        <ol className="mt4">
+          {Object.keys(COSTS).map(key => {
+            const cost = COSTS[key];
+            return (
+              <li
+                className={cx(
+                  "flex align-center mb2 cursor-pointer text-brand-hover",
+                  { "text-brand": maxCost.value === key },
+                )}
+                key={key}
+                onClick={() => updateSetting(maxCost, key)}
+              >
+                <Icon className="flex-no-shrink" name={cost.icon} size={24} />
+                <div className="ml2">
+                  <h4>{cost.display_name}</h4>
+                  <p className="m0 text-paragraph">{cost.description}</p>
+                </div>
+              </li>
+            );
+          })}
+        </ol>
+      </div>
+    </div>
+  );
+};
 
-export default SettingsXrayForm
+export default SettingsXrayForm;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
index 74af0f53570b9f5e6feae355a4273baa047ab4ff..ca0170a61ee18ba053df755d57def90e11be9d6c 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/CustomGeoJSONWidget.jsx
@@ -20,289 +20,353 @@ import LeafletChoropleth from "metabase/visualizations/components/LeafletChoropl
 import pure from "recompose/pure";
 
 export default class CustomGeoJSONWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            map: null,
-            originalMap: null,
-            geoJson: null,
-            geoJsonLoading: false,
-            geoJsonError: null,
-        };
-    }
-
-    static propTypes = {
-        setting: PropTypes.object.isRequired,
-        reloadSettings: PropTypes.func.isRequired
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      map: null,
+      originalMap: null,
+      geoJson: null,
+      geoJsonLoading: false,
+      geoJsonError: null,
     };
-    static defaultProps = {};
-
-    _saveMap = async (id, map) => {
-        const { setting } = this.props;
+  }
 
-        const value = {};
-        for (const [existingId, existingMap] of Object.entries(setting.value)) {
-            if (!existingMap.builtin) {
-                value[existingId] = { ...existingMap, id: undefined };
-            }
-        }
-        if (map) {
-            value[id] = { ...map, id: undefined };
-        } else {
-            delete value[id];
-        }
+  static propTypes = {
+    setting: PropTypes.object.isRequired,
+    reloadSettings: PropTypes.func.isRequired,
+  };
+  static defaultProps = {};
 
-        await SettingsApi.put({
-            key: "custom-geojson",
-            value: value
-        });
+  _saveMap = async (id, map) => {
+    const { setting } = this.props;
 
-        await this.props.reloadSettings();
+    const value = {};
+    for (const [existingId, existingMap] of Object.entries(setting.value)) {
+      if (!existingMap.builtin) {
+        value[existingId] = { ...existingMap, id: undefined };
+      }
     }
-
-    _save = async () => {
-        const { map } = this.state;
-        await this._saveMap(map.id, map);
-        this.setState({ map: null, originalMap: null });
+    if (map) {
+      value[id] = { ...map, id: undefined };
+    } else {
+      delete value[id];
     }
 
-    _cancel = async () => {
-        const { map, originalMap } = this.state;
-        await this._saveMap(map.id, originalMap);
-        this.setState({ map: null, originalMap: null });
-    }
+    await SettingsApi.put({
+      key: "custom-geojson",
+      value: value,
+    });
 
-    _delete = async (map) => {
-        await this._saveMap(map.id, null);
+    await this.props.reloadSettings();
+  };
+
+  _save = async () => {
+    const { map } = this.state;
+    await this._saveMap(map.id, map);
+    this.setState({ map: null, originalMap: null });
+  };
+
+  _cancel = async () => {
+    const { map, originalMap } = this.state;
+    await this._saveMap(map.id, originalMap);
+    this.setState({ map: null, originalMap: null });
+  };
+
+  _delete = async map => {
+    await this._saveMap(map.id, null);
+  };
+
+  // This is a bit of a hack, but the /api/geojson endpoint only works if the map is saved in the custom-geojson setting
+  _loadGeoJson = async () => {
+    try {
+      const { map } = this.state;
+      this.setState({
+        geoJson: null,
+        geoJsonLoading: true,
+        geoJsonError: null,
+      });
+      await this._saveMap(map.id, map);
+      let geoJson = await GeoJSONApi.get({ id: map.id });
+      this.setState({
+        geoJson: geoJson,
+        geoJsonLoading: false,
+        geoJsonError: null,
+      });
+    } catch (e) {
+      this.setState({
+        geoJson: null,
+        geoJsonLoading: false,
+        geoJsonError: e,
+      });
+      console.warn("map fetch failed", e);
     }
+  };
+
+  render() {
+    const { setting } = this.props;
 
-    // This is a bit of a hack, but the /api/geojson endpoint only works if the map is saved in the custom-geojson setting
-    _loadGeoJson = async () => {
-        try {
-            const { map } = this.state;
-            this.setState({
+    return (
+      <div className="flex-full">
+        <div className="flex">
+          <SettingHeader setting={setting} />
+          {!this.state.map && (
+            <button
+              className="Button Button--primary flex-align-right"
+              onClick={() =>
+                this.setState({
+                  map: {
+                    id: Utils.uuid(),
+                    name: "",
+                    url: "",
+                    region_key: null,
+                    region_name: null,
+                  },
+                  originalMap: null,
+                  geoJson: null,
+                  geoJsonLoading: false,
+                  geoJsonError: null,
+                })
+              }
+            >
+              {t`Add a map`}
+            </button>
+          )}
+        </div>
+        <ListMaps
+          maps={Object.entries(setting.value).map(([key, value]) => ({
+            ...value,
+            id: key,
+          }))}
+          onEditMap={map =>
+            this.setState(
+              {
+                map: {
+                  ...map,
+                },
+                originalMap: map,
                 geoJson: null,
-                geoJsonLoading: true,
-                geoJsonError: null,
-            });
-            await this._saveMap(map.id, map);
-            let geoJson = await GeoJSONApi.get({ id: map.id });
-            this.setState({
-                geoJson: geoJson,
                 geoJsonLoading: false,
                 geoJsonError: null,
-            });
-        } catch (e) {
-            this.setState({
-                geoJson: null,
-                geoJsonLoading: false,
-                geoJsonError: e,
-            });
-            console.warn("map fetch failed", e)
-        }
-    }
-
-    render() {
-        const { setting } = this.props;
-
-        return (
-            <div className="flex-full">
-                <div className="flex">
-                    <SettingHeader setting={setting} />
-                    { !this.state.map &&
-                        <button
-                            className="Button Button--primary flex-align-right"
-                            onClick={() => this.setState({
-                                map: {
-                                    id: Utils.uuid(),
-                                    name: "",
-                                    url: "",
-                                    region_key: null,
-                                    region_name: null
-                                },
-                                originalMap: null,
-                                geoJson: null,
-                                geoJsonLoading: false,
-                                geoJsonError: null,
-                            })}
-                        >
-                            {t`Add a map`}
-                        </button>
-                    }
-                </div>
-                <ListMaps
-                    maps={Object.entries(setting.value).map(([key, value]) => ({ ...value, id: key }))}
-                    onEditMap={(map) => this.setState({
-                        map: {
-                            ...map
-                        },
-                        originalMap: map,
-                        geoJson: null,
-                        geoJsonLoading: false,
-                        geoJsonError: null,
-                    }, this._loadGeoJson)}
-                    onDeleteMap={this._delete}
-                />
-                { this.state.map ?
-                    <Modal wide>
-                        <div className="p4">
-                            <EditMap
-                                map={this.state.map}
-                                originalMap={this.state.originalMap}
-                                onMapChange={(map) => this.setState({ map })}
-                                geoJson={this.state.geoJson}
-                                geoJsonLoading={this.state.geoJsonLoading}
-                                geoJsonError={this.state.geoJsonError}
-                                onLoadGeoJson={this._loadGeoJson}
-                                onCancel={this._cancel}
-                                onSave={this._save}
-                            />
-                        </div>
-                    </Modal>
-                : null }
+              },
+              this._loadGeoJson,
+            )
+          }
+          onDeleteMap={this._delete}
+        />
+        {this.state.map ? (
+          <Modal wide>
+            <div className="p4">
+              <EditMap
+                map={this.state.map}
+                originalMap={this.state.originalMap}
+                onMapChange={map => this.setState({ map })}
+                geoJson={this.state.geoJson}
+                geoJsonLoading={this.state.geoJsonLoading}
+                geoJsonError={this.state.geoJsonError}
+                onLoadGeoJson={this._loadGeoJson}
+                onCancel={this._cancel}
+                onSave={this._save}
+              />
             </div>
-        );
-    }
+          </Modal>
+        ) : null}
+      </div>
+    );
+  }
 }
 
-const ListMaps = ({ maps, onEditMap, onDeleteMap }) =>
-    <section>
-        <table className="ContentTable">
-            <thead>
-                <tr>
-                    <th>{t`Name`}</th>
-                    <th>{t`URL`}</th>
-                </tr>
-            </thead>
-            <tbody>
-            {maps.filter(map => !map.builtin).map(map =>
-                <tr key={map.id}>
-                    <td className="cursor-pointer" onClick={() => onEditMap(map)}>
-                        {map.name}
-                    </td>
-                    <td className="cursor-pointer" onClick={() => onEditMap(map)}>
-                        <Ellipsified style={{ maxWidth: 600 }}>{map.url}</Ellipsified>
-                    </td>
-                    <td className="Table-actions">
-                        <Confirm action={() => onDeleteMap(map)} title={t`Delete custom map`}>
-                            <button className="Button Button--danger">{t`Remove`}</button>
-                        </Confirm>
-                    </td>
-                </tr>
-            )}
-            </tbody>
-        </table>
-    </section>
+const ListMaps = ({ maps, onEditMap, onDeleteMap }) => (
+  <section>
+    <table className="ContentTable">
+      <thead>
+        <tr>
+          <th>{t`Name`}</th>
+          <th>{t`URL`}</th>
+        </tr>
+      </thead>
+      <tbody>
+        {maps.filter(map => !map.builtin).map(map => (
+          <tr key={map.id}>
+            <td className="cursor-pointer" onClick={() => onEditMap(map)}>
+              {map.name}
+            </td>
+            <td className="cursor-pointer" onClick={() => onEditMap(map)}>
+              <Ellipsified style={{ maxWidth: 600 }}>{map.url}</Ellipsified>
+            </td>
+            <td className="Table-actions">
+              <Confirm
+                action={() => onDeleteMap(map)}
+                title={t`Delete custom map`}
+              >
+                <button className="Button Button--danger">{t`Remove`}</button>
+              </Confirm>
+            </td>
+          </tr>
+        ))}
+      </tbody>
+    </table>
+  </section>
+);
 
 const GeoJsonPropertySelect = ({ value, onChange, geoJson }) => {
-    let options = {};
-    if (geoJson) {
-        for (const feature of geoJson.features) {
-            for (const property in feature.properties) {
-                options[property] = options[property] || [];
-                options[property].push(feature.properties[property]);
-            }
-        }
+  let options = {};
+  if (geoJson) {
+    for (const feature of geoJson.features) {
+      for (const property in feature.properties) {
+        options[property] = options[property] || [];
+        options[property].push(feature.properties[property]);
+      }
     }
+  }
 
-    return (
-        <Select
-            value={value}
-            onChange={(e) => onChange(e.target.value)}
-            placeholder={t`Select…`}
-        >
-            {Object.entries(options).map(([name, values]) =>
-                <Option key={name} value={name}>
-                    <div>
-                        <div>{name}</div>
-                        <div className="mt1 h6" style={{ maxWidth: 250, whiteSpace: "nowrap", overflow: "hidden", textOverflow: "ellipsis" }}>
-                            {t`Sample values:`} {values.join(", ")}
-                        </div>
-                    </div>
-                </Option>
-            )}
-        </Select>
-    )
-}
+  return (
+    <Select
+      value={value}
+      onChange={e => onChange(e.target.value)}
+      placeholder={t`Select…`}
+    >
+      {Object.entries(options).map(([name, values]) => (
+        <Option key={name} value={name}>
+          <div>
+            <div>{name}</div>
+            <div
+              className="mt1 h6"
+              style={{
+                maxWidth: 250,
+                whiteSpace: "nowrap",
+                overflow: "hidden",
+                textOverflow: "ellipsis",
+              }}
+            >
+              {t`Sample values:`} {values.join(", ")}
+            </div>
+          </div>
+        </Option>
+      ))}
+    </Select>
+  );
+};
 
-const SettingContainer = ({ name, description, className="py1", children }) =>
-    <div className={className}>
-        { name && <div className="text-grey-4 text-bold text-uppercase my1">{name}</div>}
-        { description && <div className="text-grey-4 my1">{description}</div>}
-        {children}
-    </div>
+const SettingContainer = ({
+  name,
+  description,
+  className = "py1",
+  children,
+}) => (
+  <div className={className}>
+    {name && (
+      <div className="text-grey-4 text-bold text-uppercase my1">{name}</div>
+    )}
+    {description && <div className="text-grey-4 my1">{description}</div>}
+    {children}
+  </div>
+);
 
-const EditMap = ({ map, onMapChange, originalMap, geoJson, geoJsonLoading, geoJsonError, onLoadGeoJson, onCancel, onSave }) =>
-    <div>
+const EditMap = ({
+  map,
+  onMapChange,
+  originalMap,
+  geoJson,
+  geoJsonLoading,
+  geoJsonError,
+  onLoadGeoJson,
+  onCancel,
+  onSave,
+}) => (
+  <div>
     <div className="flex">
-        <div className="flex-no-shrink">
-            <h2>{ !originalMap ? t`Add a new map` : t`Edit map` }</h2>
-            <SettingContainer description={t`What do you want to call this map?`}>
-                <div className="flex">
-                    <input
-                        type="text"
-                        className="SettingsInput AdminInput bordered rounded h3"
-                        placeholder={t`e.g. United Kingdom, Brazil, Mars`}
-                        value={map.name}
-                        onChange={(e) => onMapChange({ ...map, "name": e.target.value })}
-                    />
-                </div>
-            </SettingContainer>
-            <SettingContainer description={t`URL for the GeoJSON file you want to use`}>
-                <div className="flex">
-                    <input
-                        type="text"
-                        className="SettingsInput AdminInput bordered rounded h3"
-                        placeholder={t`Like https://my-mb-server.com/maps/my-map.json`}
-                        value={map.url}
-                        onChange={(e) => onMapChange({ ...map, "url": e.target.value })}
-                    />
-                    <button className={cx("Button ml1", { "Button--primary" : !geoJson, disabled: !map.url })} onClick={onLoadGeoJson}>{geoJson ? t`Refresh` : t`Load`}</button>
-                </div>
-            </SettingContainer>
-            <div className={cx({ "disabled": !geoJson })}>
-                <SettingContainer description={t`Which property specifies the region’s identifier?`}>
-                    <GeoJsonPropertySelect
-                        value={map.region_key}
-                        onChange={(value) => onMapChange({ ...map, "region_key": value })}
-                        geoJson={geoJson}
-                    />
-                </SettingContainer>
-                <SettingContainer description={t`Which property specifies the region’s display name?`}>
-                    <GeoJsonPropertySelect
-                        value={map.region_name}
-                        onChange={(value) => onMapChange({ ...map, "region_name": value })}
-                        geoJson={geoJson}
-                    />
-                </SettingContainer>
-            </div>
-        </div>
-        <div className="flex-full ml4 relative bordered rounded flex my4">
-        { geoJson ||  geoJsonLoading || geoJsonError ?
-            <LoadingAndErrorWrapper loading={geoJsonLoading} error={geoJsonError}>
-            {() =>
-                <div className="m4 spread relative">
-                    <ChoroplethPreview geoJson={geoJson} />
-                </div>
-            }
-            </LoadingAndErrorWrapper>
-        :
-            <div className="flex-full flex layout-centered text-bold text-grey-1 text-centered">
-                {t`Load a GeoJSON file to see a preview`}
-            </div>
-        }
+      <div className="flex-no-shrink">
+        <h2>{!originalMap ? t`Add a new map` : t`Edit map`}</h2>
+        <SettingContainer description={t`What do you want to call this map?`}>
+          <div className="flex">
+            <input
+              type="text"
+              className="SettingsInput AdminInput bordered rounded h3"
+              placeholder={t`e.g. United Kingdom, Brazil, Mars`}
+              value={map.name}
+              onChange={e => onMapChange({ ...map, name: e.target.value })}
+            />
+          </div>
+        </SettingContainer>
+        <SettingContainer
+          description={t`URL for the GeoJSON file you want to use`}
+        >
+          <div className="flex">
+            <input
+              type="text"
+              className="SettingsInput AdminInput bordered rounded h3"
+              placeholder={t`Like https://my-mb-server.com/maps/my-map.json`}
+              value={map.url}
+              onChange={e => onMapChange({ ...map, url: e.target.value })}
+            />
+            <button
+              className={cx("Button ml1", {
+                "Button--primary": !geoJson,
+                disabled: !map.url,
+              })}
+              onClick={onLoadGeoJson}
+            >
+              {geoJson ? t`Refresh` : t`Load`}
+            </button>
+          </div>
+        </SettingContainer>
+        <div className={cx({ disabled: !geoJson })}>
+          <SettingContainer
+            description={t`Which property specifies the region’s identifier?`}
+          >
+            <GeoJsonPropertySelect
+              value={map.region_key}
+              onChange={value => onMapChange({ ...map, region_key: value })}
+              geoJson={geoJson}
+            />
+          </SettingContainer>
+          <SettingContainer
+            description={t`Which property specifies the region’s display name?`}
+          >
+            <GeoJsonPropertySelect
+              value={map.region_name}
+              onChange={value => onMapChange({ ...map, region_name: value })}
+              geoJson={geoJson}
+            />
+          </SettingContainer>
         </div>
       </div>
-      <div className="py1 flex">
-        <div className="ml-auto">
-          <button className={cx("Button Button")} onClick={onCancel}>{t`Cancel`}</button>
-          <button className={cx("Button Button--primary ml1", { "disabled" : !map.name || !map.url || !map.region_name || !map.region_key })} onClick={onSave}>
-              {originalMap ? t`Save map` : t`Add map`}
-          </button>
-        </div>
+      <div className="flex-full ml4 relative bordered rounded flex my4">
+        {geoJson || geoJsonLoading || geoJsonError ? (
+          <LoadingAndErrorWrapper loading={geoJsonLoading} error={geoJsonError}>
+            {() => (
+              <div className="m4 spread relative">
+                <ChoroplethPreview geoJson={geoJson} />
+              </div>
+            )}
+          </LoadingAndErrorWrapper>
+        ) : (
+          <div className="flex-full flex layout-centered text-bold text-grey-1 text-centered">
+            {t`Load a GeoJSON file to see a preview`}
+          </div>
+        )}
       </div>
     </div>
-
-const ChoroplethPreview = pure(({ geoJson }) =>
-    <LeafletChoropleth geoJson={geoJson} />
+    <div className="py1 flex">
+      <div className="ml-auto">
+        <button
+          className={cx("Button Button")}
+          onClick={onCancel}
+        >{t`Cancel`}</button>
+        <button
+          className={cx("Button Button--primary ml1", {
+            disabled:
+              !map.name || !map.url || !map.region_name || !map.region_key,
+          })}
+          onClick={onSave}
+        >
+          {originalMap ? t`Save map` : t`Add map`}
+        </button>
+      </div>
+    </div>
+  </div>
 );
+
+const ChoroplethPreview = pure(({ geoJson }) => (
+  <LeafletChoropleth geoJson={geoJson} />
+));
diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
index 80bc233fcf9fda3c0beff8874b2b4616d080f949..4d04483001d724d56060ab50366be5e0d5640493 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLegalese.jsx
@@ -1,27 +1,38 @@
-import React from 'react';
-import MetabaseAnalytics from 'metabase/lib/analytics';
-import { t } from 'c-3po';
+import React from "react";
+import MetabaseAnalytics from "metabase/lib/analytics";
+import { t } from "c-3po";
 
-const EmbeddingLegalese = ({ onChange }) =>
-    <div className="bordered rounded text-measure p4">
-        <h3 className="text-brand">{t`Using embedding`}</h3>
-        <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
-            {t`By enabling embedding you're agreeing to the embedding license located at`} <a className="link"  href="http://www.metabase.com/license/embedding" target="_blank">metabase.com/license/embedding</a>.
-        </p>
-        <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
-            {t`In plain english, when you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds. You should however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.`}
-        </p>
-        <div className="flex layout-centered mt4">
-            <button
-                className="Button Button--primary"
-                onClick={() => {
-                    MetabaseAnalytics.trackEvent("Admin Embed Settings", "Embedding Enable Click");
-                    onChange(true);
-                }}
-            >
-                {t`Enable`}
-            </button>
-        </div>
+const EmbeddingLegalese = ({ onChange }) => (
+  <div className="bordered rounded text-measure p4">
+    <h3 className="text-brand">{t`Using embedding`}</h3>
+    <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
+      {t`By enabling embedding you're agreeing to the embedding license located at`}{" "}
+      <a
+        className="link"
+        href="http://www.metabase.com/license/embedding"
+        target="_blank"
+      >
+        metabase.com/license/embedding
+      </a>.
+    </p>
+    <p className="text-grey-4" style={{ lineHeight: 1.48 }}>
+      {t`In plain english, when you embed charts or dashboards from Metabase in your own application, that application isn't subject to the Affero General Public License that covers the rest of Metabase, provided you keep the Metabase logo and the "Powered by Metabase" visible on those embeds. You should however, read the license text linked above as that is the actual license that you will be agreeing to by enabling this feature.`}
+    </p>
+    <div className="flex layout-centered mt4">
+      <button
+        className="Button Button--primary"
+        onClick={() => {
+          MetabaseAnalytics.trackEvent(
+            "Admin Embed Settings",
+            "Embedding Enable Click",
+          );
+          onChange(true);
+        }}
+      >
+        {t`Enable`}
+      </button>
     </div>
+  </div>
+);
 
 export default EmbeddingLegalese;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
index 408e4dbd43e9b0187923abfeb8db8bf4ebca0353..d69767a01267531f8dba1179902f61e665e3e268 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/EmbeddingLevel.jsx
@@ -1,93 +1,96 @@
-import React, { Component } from 'react'
-import ReactRetinaImage from 'react-retina-image'
-import { t } from 'c-3po';
-import SettingsInput from "./SettingInput"
+import React, { Component } from "react";
+import ReactRetinaImage from "react-retina-image";
+import { t } from "c-3po";
+import SettingsInput from "./SettingInput";
 
-const PREMIUM_EMBEDDING_STORE_URL = 'https://store.metabase.com/product/embedding'
-const PREMIUM_EMBEDDING_SETTING_KEY = 'premium-embedding-token'
+const PREMIUM_EMBEDDING_STORE_URL =
+  "https://store.metabase.com/product/embedding";
+const PREMIUM_EMBEDDING_SETTING_KEY = "premium-embedding-token";
 
-const PremiumTokenInput = ({ token, onChangeSetting }) =>
-    <div className="mb3">
-        <h3 className="mb1">
-            { token
-                ? t`Premium embedding enabled`
-                : t`Enter the token you bought from the Metabase Store`
-            }
-        </h3>
-        <SettingsInput
-            onChange={(value) =>
-                onChangeSetting(PREMIUM_EMBEDDING_SETTING_KEY, value)
-            }
-            setting={{ value: token }}
-            autoFocus={!token}
-        />
-    </div>
+const PremiumTokenInput = ({ token, onChangeSetting }) => (
+  <div className="mb3">
+    <h3 className="mb1">
+      {token
+        ? t`Premium embedding enabled`
+        : t`Enter the token you bought from the Metabase Store`}
+    </h3>
+    <SettingsInput
+      onChange={value => onChangeSetting(PREMIUM_EMBEDDING_SETTING_KEY, value)}
+      setting={{ value: token }}
+      autoFocus={!token}
+    />
+  </div>
+);
 
-const PremiumExplanation = ({ showEnterScreen }) =>
-    <div>
-        <h2>Premium embedding</h2>
-        <p className="mt1">{t`Premium embedding lets you disable "Powered by Metabase" on your embeded dashboards and questions.`}</p>
-        <div className="mt2 mb3">
-            <a className="link mx1" href={PREMIUM_EMBEDDING_STORE_URL} target="_blank">
-                {t`Buy a token`}
-            </a>
-            <a className="link mx1" onClick={showEnterScreen}>
-                {t`Enter a token`}
-            </a>
-        </div>
+const PremiumExplanation = ({ showEnterScreen }) => (
+  <div>
+    <h2>Premium embedding</h2>
+    <p className="mt1">{t`Premium embedding lets you disable "Powered by Metabase" on your embeded dashboards and questions.`}</p>
+    <div className="mt2 mb3">
+      <a
+        className="link mx1"
+        href={PREMIUM_EMBEDDING_STORE_URL}
+        target="_blank"
+      >
+        {t`Buy a token`}
+      </a>
+      <a className="link mx1" onClick={showEnterScreen}>
+        {t`Enter a token`}
+      </a>
     </div>
+  </div>
+);
 
 class PremiumEmbedding extends Component {
-    constructor(props) {
-        super(props)
-        this.state = {
-            showEnterScreen: props.token
-        }
-    }
-    render () {
-        const { token, onChangeSetting } = this.props
-        const { showEnterScreen } = this.state
+  constructor(props) {
+    super(props);
+    this.state = {
+      showEnterScreen: props.token,
+    };
+  }
+  render() {
+    const { token, onChangeSetting } = this.props;
+    const { showEnterScreen } = this.state;
 
-        return (
-            <div className="text-centered text-paragraph">
-                { showEnterScreen
-                    ? (
-                        <PremiumTokenInput
-                            onChangeSetting={onChangeSetting}
-                            token={token}
-                        />
-                    )
-                    : (
-                        <PremiumExplanation
-                            showEnterScreen={() =>
-                                this.setState({ showEnterScreen: true })
-                            }
-                        />
-                    )
-                }
-            </div>
-        )
-    }
+    return (
+      <div className="text-centered text-paragraph">
+        {showEnterScreen ? (
+          <PremiumTokenInput onChangeSetting={onChangeSetting} token={token} />
+        ) : (
+          <PremiumExplanation
+            showEnterScreen={() => this.setState({ showEnterScreen: true })}
+          />
+        )}
+      </div>
+    );
+  }
 }
 
 class EmbeddingLevel extends Component {
-    render () {
-        const { onChangeSetting, settingValues } = this.props
+  render() {
+    const { onChangeSetting, settingValues } = this.props;
 
-        const premiumToken = settingValues[PREMIUM_EMBEDDING_SETTING_KEY]
+    const premiumToken = settingValues[PREMIUM_EMBEDDING_SETTING_KEY];
 
-        return (
-            <div className="bordered rounded full text-centered" style={{ maxWidth: 820 }}>
-                <ReactRetinaImage src={`app/assets/img/${ premiumToken ? 'premium_embed_added' : 'premium_embed'}.png`}/>
-                <div className="flex align-center justify-center">
-                    <PremiumEmbedding
-                        token={premiumToken}
-                        onChangeSetting={onChangeSetting}
-                    />
-                </div>
-            </div>
-        )
-    }
+    return (
+      <div
+        className="bordered rounded full text-centered"
+        style={{ maxWidth: 820 }}
+      >
+        <ReactRetinaImage
+          src={`app/assets/img/${
+            premiumToken ? "premium_embed_added" : "premium_embed"
+          }.png`}
+        />
+        <div className="flex align-center justify-center">
+          <PremiumEmbedding
+            token={premiumToken}
+            onChangeSetting={onChangeSetting}
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
-export default EmbeddingLevel
+export default EmbeddingLevel;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
index b00baa9cb2750eb8fa8bf3fa7f4efc08e25eb09e..81b51b7663b0d10d45a301f995993ef2cad84450 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/LdapGroupMappingsWidget.jsx
@@ -2,7 +2,7 @@
 
 import React from "react";
 
-import { ModalFooter } from "metabase/components/ModalContent"
+import { ModalFooter } from "metabase/components/ModalContent";
 import AdminContentTable from "metabase/components/AdminContentTable";
 import Button from "metabase/components/Button";
 import GroupSelect from "metabase/admin/people/components/GroupSelect";
@@ -11,270 +11,315 @@ import Icon from "metabase/components/Icon";
 import LoadingSpinner from "metabase/components/LoadingSpinner";
 import Modal from "metabase/components/Modal";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { PermissionsApi, SettingsApi } from "metabase/services";
 
 import _ from "underscore";
 
-import SettingToggle from './SettingToggle';
+import SettingToggle from "./SettingToggle";
 
 type Props = {
-    setting: any,
-    onChange: (value: any) => void,
-    mappings: { [string]: number[] },
-    updateMappings: (value: { [string]: number[] }) => void
+  setting: any,
+  onChange: (value: any) => void,
+  mappings: { [string]: number[] },
+  updateMappings: (value: { [string]: number[] }) => void,
 };
 
 type State = {
-    showEditModal: boolean,
-    showAddRow: boolean,
-    groups: Object[],
-    mappings: { [string]: number[] }
+  showEditModal: boolean,
+  showAddRow: boolean,
+  groups: Object[],
+  mappings: { [string]: number[] },
 };
 
 export default class LdapGroupMappingsWidget extends React.Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props, context: any) {
-        super(props, context);
-        this.state = {
-            showEditModal: false,
-            showAddRow: false,
-            groups: [],
-            mappings: {}
-        };
-    }
-
-    _showEditModal = (e: Event) => {
-        e.preventDefault();
-        this.setState({ mappings: this.props.mappings || {}, showEditModal: true });
-        PermissionsApi.groups().then((groups) => this.setState({ groups }));
-    }
-
-    _showAddRow = (e: Event) => {
-        e.preventDefault();
-        this.setState({ showAddRow: true });
-    }
-
-    _hideAddRow = () => {
-        this.setState({ showAddRow: false });
-    }
-
-    _addMapping = (dn: string) => {
-        this.setState((prevState: State) => ({ mappings: { ...prevState.mappings, [dn]: [] }, showAddRow: false }));
-    }
-
-    _changeMapping = (dn: string) => (group: { id: number }, selected: boolean) => {
-        if (selected) {
-            this.setState((prevState: State) => ({
-                mappings: {
-                    ...prevState.mappings,
-                    [dn]: [...prevState.mappings[dn], group.id]
-                }
-            }));
-        } else {
-            this.setState((prevState: State) => ({
-                mappings: {
-                    ...prevState.mappings,
-                    [dn]: prevState.mappings[dn].filter(id => id !== group.id)
-                }
-            }));
-        }
-    }
-
-    _deleteMapping = (dn: string) => (e: Event) => {
-        e.preventDefault();
-        this.setState((prevState: State) => ({ mappings: _.omit(prevState.mappings, dn) }));
+  props: Props;
+  state: State;
+
+  constructor(props: Props, context: any) {
+    super(props, context);
+    this.state = {
+      showEditModal: false,
+      showAddRow: false,
+      groups: [],
+      mappings: {},
+    };
+  }
+
+  _showEditModal = (e: Event) => {
+    e.preventDefault();
+    this.setState({ mappings: this.props.mappings || {}, showEditModal: true });
+    PermissionsApi.groups().then(groups => this.setState({ groups }));
+  };
+
+  _showAddRow = (e: Event) => {
+    e.preventDefault();
+    this.setState({ showAddRow: true });
+  };
+
+  _hideAddRow = () => {
+    this.setState({ showAddRow: false });
+  };
+
+  _addMapping = (dn: string) => {
+    this.setState((prevState: State) => ({
+      mappings: { ...prevState.mappings, [dn]: [] },
+      showAddRow: false,
+    }));
+  };
+
+  _changeMapping = (dn: string) => (
+    group: { id: number },
+    selected: boolean,
+  ) => {
+    if (selected) {
+      this.setState((prevState: State) => ({
+        mappings: {
+          ...prevState.mappings,
+          [dn]: [...prevState.mappings[dn], group.id],
+        },
+      }));
+    } else {
+      this.setState((prevState: State) => ({
+        mappings: {
+          ...prevState.mappings,
+          [dn]: prevState.mappings[dn].filter(id => id !== group.id),
+        },
+      }));
     }
-
-    _cancelClick = (e: Event) => {
-        e.preventDefault();
+  };
+
+  _deleteMapping = (dn: string) => (e: Event) => {
+    e.preventDefault();
+    this.setState((prevState: State) => ({
+      mappings: _.omit(prevState.mappings, dn),
+    }));
+  };
+
+  _cancelClick = (e: Event) => {
+    e.preventDefault();
+    this.setState({ showEditModal: false, showAddRow: false });
+  };
+
+  _saveClick = (e: Event) => {
+    e.preventDefault();
+    const { state: { mappings }, props: { updateMappings } } = this;
+    SettingsApi.put({ key: "ldap-group-mappings", value: mappings }).then(
+      () => {
+        updateMappings && updateMappings(mappings);
         this.setState({ showEditModal: false, showAddRow: false });
-    }
-
-    _saveClick = (e: Event) => {
-        e.preventDefault();
-        const { state: { mappings }, props: { updateMappings } } = this;
-        SettingsApi.put({ key: "ldap-group-mappings", value: mappings }).then(() => {
-            updateMappings && updateMappings(mappings);
-            this.setState({ showEditModal: false, showAddRow: false });
-        });
-    }
-
-    render() {
-        const { showEditModal, showAddRow, groups, mappings } = this.state;
-
-        return (
-            <div className="flex align-center">
-                <SettingToggle {...this.props} />
-                <div className="flex align-center pt1">
-                    <Button type="button" className="ml1" medium onClick={this._showEditModal}>{t`Edit Mappings`}</Button>
-                </div>
-                { showEditModal ? (
-                    <Modal wide>
-                        <div>
-                            <div className="pt4 px4">
-                                <h2>{t`Group Mappings`}</h2>
-                            </div>
-                            <div className="px4">
-                                <Button className="float-right" primary onClick={this._showAddRow}>{t`Create a mapping`}</Button>
-                                <p className="text-measure">
-                                    {t`Mappings allow Metabase to automatically add and remove users from groups based on the membership information provided by the
+      },
+    );
+  };
+
+  render() {
+    const { showEditModal, showAddRow, groups, mappings } = this.state;
+
+    return (
+      <div className="flex align-center">
+        <SettingToggle {...this.props} />
+        <div className="flex align-center pt1">
+          <Button
+            type="button"
+            className="ml1"
+            medium
+            onClick={this._showEditModal}
+          >{t`Edit Mappings`}</Button>
+        </div>
+        {showEditModal ? (
+          <Modal wide>
+            <div>
+              <div className="pt4 px4">
+                <h2>{t`Group Mappings`}</h2>
+              </div>
+              <div className="px4">
+                <Button
+                  className="float-right"
+                  primary
+                  onClick={this._showAddRow}
+                >{t`Create a mapping`}</Button>
+                <p className="text-measure">
+                  {t`Mappings allow Metabase to automatically add and remove users from groups based on the membership information provided by the
                                     directory server. Membership to the Admin group can be granted through mappings, but will not be automatically removed as a
                                     failsafe measure.`}
-                                </p>
-                                <AdminContentTable columnTitles={[t`Distinguished Name`, t`Groups`, '']}>
-                                    { showAddRow ? (
-                                        <AddMappingRow mappings={mappings} onCancel={this._hideAddRow} onAdd={this._addMapping} />
-                                    ) : null }
-                                    { ((Object.entries(mappings): any): Array<[string, number[]]>).map(([dn, ids]) =>
-                                        <MappingRow
-                                            key={dn}
-                                            dn={dn}
-                                            groups={groups}
-                                            selectedGroups={ids}
-                                            onChange={this._changeMapping(dn)}
-                                            onDelete={this._deleteMapping(dn)}
-                                        />
-                                    ) }
-                                </AdminContentTable>
-                            </div>
-                            <ModalFooter>
-                                <Button type="button" onClick={this._cancelClick}>{t`Cancel`}</Button>
-                                <Button primary onClick={this._saveClick}>{t`Save`}</Button>
-                            </ModalFooter>
-                        </div>
-                    </Modal>
-                ) : null }
+                </p>
+                <AdminContentTable
+                  columnTitles={[t`Distinguished Name`, t`Groups`, ""]}
+                >
+                  {showAddRow ? (
+                    <AddMappingRow
+                      mappings={mappings}
+                      onCancel={this._hideAddRow}
+                      onAdd={this._addMapping}
+                    />
+                  ) : null}
+                  {((Object.entries(mappings): any): Array<
+                    [string, number[]],
+                  >).map(([dn, ids]) => (
+                    <MappingRow
+                      key={dn}
+                      dn={dn}
+                      groups={groups}
+                      selectedGroups={ids}
+                      onChange={this._changeMapping(dn)}
+                      onDelete={this._deleteMapping(dn)}
+                    />
+                  ))}
+                </AdminContentTable>
+              </div>
+              <ModalFooter>
+                <Button
+                  type="button"
+                  onClick={this._cancelClick}
+                >{t`Cancel`}</Button>
+                <Button primary onClick={this._saveClick}>{t`Save`}</Button>
+              </ModalFooter>
             </div>
-        );
-    }
+          </Modal>
+        ) : null}
+      </div>
+    );
+  }
 }
 
 type AddMappingRowProps = {
-    mappings: { [string]: number[] },
-    onAdd?: (dn: string) => void,
-    onCancel?: () => void
+  mappings: { [string]: number[] },
+  onAdd?: (dn: string) => void,
+  onCancel?: () => void,
 };
 
 type AddMappingRowState = {
-    value: ''
+  value: "",
 };
 
 class AddMappingRow extends React.Component {
-    props: AddMappingRowProps;
-    state: AddMappingRowState;
-
-    constructor(props: AddMappingRowProps, context: any) {
-        super(props, context);
-        this.state = {
-            value: ''
-        };
-    }
-
-    _handleCancelClick = (e: Event) => {
-        e.preventDefault();
-        const { onCancel } = this.props;
-        onCancel && onCancel();
-        this.setState({ value: '' });
-    }
-
-    _handleAddClick = (e: Event) => {
-        e.preventDefault();
-        const { onAdd } = this.props;
-        onAdd && onAdd(this.state.value);
-        this.setState({ value: '' });
-    }
+  props: AddMappingRowProps;
+  state: AddMappingRowState;
 
-    render() {
-        const { value } = this.state;
-
-        const isValid = value && this.props.mappings[value] === undefined;
-
-        return (
-            <tr>
-                <td colSpan="3" style={{ padding: 0 }}>
-                    <div className="my2 pl1 p1 bordered border-brand rounded relative flex align-center">
-                        <input
-                            className="input--borderless h3 ml1 flex-full"
-                            type="text"
-                            value={value}
-                            placeholder="cn=People,ou=Groups,dc=metabase,dc=com"
-                            autoFocus
-                            onChange={(e) => this.setState({ value: e.target.value })}
-                        />
-                        <span className="link no-decoration cursor-pointer" onClick={this._handleCancelClick}>{t`Cancel`}</span>
-                        <Button className="ml2" primary={!!isValid} disabled={!isValid} onClick={this._handleAddClick}>{t`Add`}</Button>
-                    </div>
-                </td>
-            </tr>
-        );
-    }
+  constructor(props: AddMappingRowProps, context: any) {
+    super(props, context);
+    this.state = {
+      value: "",
+    };
+  }
+
+  _handleCancelClick = (e: Event) => {
+    e.preventDefault();
+    const { onCancel } = this.props;
+    onCancel && onCancel();
+    this.setState({ value: "" });
+  };
+
+  _handleAddClick = (e: Event) => {
+    e.preventDefault();
+    const { onAdd } = this.props;
+    onAdd && onAdd(this.state.value);
+    this.setState({ value: "" });
+  };
+
+  render() {
+    const { value } = this.state;
+
+    const isValid = value && this.props.mappings[value] === undefined;
+
+    return (
+      <tr>
+        <td colSpan="3" style={{ padding: 0 }}>
+          <div className="my2 pl1 p1 bordered border-brand rounded relative flex align-center">
+            <input
+              className="input--borderless h3 ml1 flex-full"
+              type="text"
+              value={value}
+              placeholder="cn=People,ou=Groups,dc=metabase,dc=com"
+              autoFocus
+              onChange={e => this.setState({ value: e.target.value })}
+            />
+            <span
+              className="link no-decoration cursor-pointer"
+              onClick={this._handleCancelClick}
+            >{t`Cancel`}</span>
+            <Button
+              className="ml2"
+              primary={!!isValid}
+              disabled={!isValid}
+              onClick={this._handleAddClick}
+            >{t`Add`}</Button>
+          </div>
+        </td>
+      </tr>
+    );
+  }
 }
 
 class MappingGroupSelect extends React.Component {
-    props: {
-        groups: Array<{ id: number }>,
-        selectedGroups: number[],
-        onGroupChange?: (group: { id: number }, selected: boolean) => void
-    };
-
-    render() {
-        const { groups, selectedGroups, onGroupChange } = this.props;
+  props: {
+    groups: Array<{ id: number }>,
+    selectedGroups: number[],
+    onGroupChange?: (group: { id: number }, selected: boolean) => void,
+  };
 
-        if (!groups || groups.length === 0) {
-            return <LoadingSpinner />;
-        }
+  render() {
+    const { groups, selectedGroups, onGroupChange } = this.props;
 
-        const selected = selectedGroups.reduce((g, id) => ({ ...g, [id]: true }), {});
-
-        return (
-            <PopoverWithTrigger
-                ref="popover"
-                triggerElement={
-                    <div className="flex align-center">
-                        <span className="mr1 text-grey-4">
-                            <GroupSummary groups={groups} selectedGroups={selected} />
-                        </span>
-                        <Icon className="text-grey-2" name="chevrondown"  size={10}/>
-                    </div>
-                }
-                triggerClasses="AdminSelectBorderless py1"
-                sizeToFit
-            >
-                <GroupSelect groups={groups} selectedGroups={selected} onGroupChange={onGroupChange} />
-            </PopoverWithTrigger>
-        );
+    if (!groups || groups.length === 0) {
+      return <LoadingSpinner />;
     }
+
+    const selected = selectedGroups.reduce(
+      (g, id) => ({ ...g, [id]: true }),
+      {},
+    );
+
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        triggerElement={
+          <div className="flex align-center">
+            <span className="mr1 text-grey-4">
+              <GroupSummary groups={groups} selectedGroups={selected} />
+            </span>
+            <Icon className="text-grey-2" name="chevrondown" size={10} />
+          </div>
+        }
+        triggerClasses="AdminSelectBorderless py1"
+        sizeToFit
+      >
+        <GroupSelect
+          groups={groups}
+          selectedGroups={selected}
+          onGroupChange={onGroupChange}
+        />
+      </PopoverWithTrigger>
+    );
+  }
 }
 
 class MappingRow extends React.Component {
-    props: {
-        dn: string,
-        groups: Array<{ id: number }>,
-        selectedGroups: number[],
-        onChange?: (group: { id: number }, selected: boolean) => void,
-        onDelete?: (e: Event) => void
-    };
-
-    render() {
-        const { dn, groups, selectedGroups, onChange, onDelete } = this.props;
-
-        return (
-            <tr>
-                <td>{dn}</td>
-                <td>
-                    <MappingGroupSelect
-                        groups={groups}
-                        selectedGroups={selectedGroups}
-                        onGroupChange={onChange}
-                    />
-                </td>
-                <td className="Table-actions">
-                    <Button warning onClick={onDelete}>{t`Remove`}</Button>
-                </td>
-            </tr>
-        );
-    }
+  props: {
+    dn: string,
+    groups: Array<{ id: number }>,
+    selectedGroups: number[],
+    onChange?: (group: { id: number }, selected: boolean) => void,
+    onDelete?: (e: Event) => void,
+  };
+
+  render() {
+    const { dn, groups, selectedGroups, onChange, onDelete } = this.props;
+
+    return (
+      <tr>
+        <td>{dn}</td>
+        <td>
+          <MappingGroupSelect
+            groups={groups}
+            selectedGroups={selectedGroups}
+            onGroupChange={onChange}
+          />
+        </td>
+        <td className="Table-actions">
+          <Button warning onClick={onDelete}>{t`Remove`}</Button>
+        </td>
+      </tr>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
index 70c068b712c71b2a27855ca1686996d41455cf76..7378fa0f291e1cda6b748bd68e78799e155d498d 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/PublicLinksListing.jsx
@@ -7,184 +7,181 @@ import Link from "metabase/components/Link";
 import ExternalLink from "metabase/components/ExternalLink";
 import Confirm from "metabase/components/Confirm";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { CardApi, DashboardApi } from "metabase/services";
 import * as Urls from "metabase/lib/urls";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 type PublicLink = {
-    id: string,
-    name: string,
-    public_uuid: string
+  id: string,
+  name: string,
+  public_uuid: string,
 };
 
 type Props = {
-    load:           () => Promise<PublicLink[]>,
-    revoke?:        (link: PublicLink) => Promise<void>,
-    getUrl:         (link: PublicLink) => string,
-    getPublicUrl?:  (link: PublicLink) => string,
-    noLinksMessage: string,
-    type: string
+  load: () => Promise<PublicLink[]>,
+  revoke?: (link: PublicLink) => Promise<void>,
+  getUrl: (link: PublicLink) => string,
+  getPublicUrl?: (link: PublicLink) => string,
+  noLinksMessage: string,
+  type: string,
 };
 
 type State = {
-    list: ?PublicLink[],
-    error: ?any
+  list: ?(PublicLink[]),
+  error: ?any,
 };
 
 export default class PublicLinksListing extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            list: null,
-            error: null
-        };
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      list: null,
+      error: null,
+    };
+  }
+
+  componentWillMount() {
+    this.load();
+  }
+
+  async load() {
+    try {
+      const list = await this.props.load();
+      this.setState({ list });
+    } catch (error) {
+      this.setState({ error });
     }
+  }
 
-    componentWillMount() {
-        this.load();
+  async revoke(link: PublicLink) {
+    if (!this.props.revoke) {
+      return;
     }
-
-    async load() {
-        try {
-            const list = await this.props.load();
-            this.setState({ list });
-        } catch (error) {
-            this.setState({ error });
-        }
+    try {
+      await this.props.revoke(link);
+      this.load();
+    } catch (error) {
+      alert(error);
     }
+  }
 
-    async revoke(link: PublicLink) {
-        if (!this.props.revoke) {
-            return;
-        }
-        try {
-            await this.props.revoke(link);
-            this.load();
-        } catch (error) {
-            alert(error)
-        }
-    }
+  trackEvent(label: string) {
+    MetabaseAnalytics.trackEvent(`Admin ${this.props.type}`, label);
+  }
 
-    trackEvent(label: string) {
-        MetabaseAnalytics.trackEvent(`Admin ${this.props.type}`, label)
-    }
+  render() {
+    const { getUrl, getPublicUrl, revoke, noLinksMessage } = this.props;
+    let { list, error } = this.state;
 
-    render() {
-        const { getUrl, getPublicUrl, revoke, noLinksMessage } = this.props;
-        let { list, error } = this.state;
-
-        if (list && list.length === 0) {
-            error = new Error(noLinksMessage);
-        }
-
-        return (
-            <LoadingAndErrorWrapper loading={!list} error={error}>
-            { () =>
-                <table className="ContentTable">
-                    <thead>
-                        <tr>
-                            <th>{t`Name`}</th>
-                            { getPublicUrl &&
-                                <th>{t`Public Link`}</th>
-                            }
-                            { revoke &&
-                                <th>{t`Revoke Link`}</th>
-                            }
-                        </tr>
-                    </thead>
-                    <tbody>
-                        { list && list.map(link =>
-                            <tr>
-                                <td>
-                                    <Link
-                                        to={getUrl(link)}
-                                        onClick={() =>
-                                            this.trackEvent('Entity Link Clicked')
-                                        }
-                                    >
-                                        {link.name}
-                                    </Link>
-                                </td>
-                                { getPublicUrl &&
-                                    <td>
-                                        <ExternalLink
-                                            href={getPublicUrl(link)}
-                                            onClick={() =>
-                                                this.trackEvent('Public Link Clicked')
-                                            }
-                                        >
-                                            {getPublicUrl(link)}
-                                        </ExternalLink>
-                                    </td>
-                                }
-                                { revoke &&
-                                    <td className="flex layout-centered">
-                                        <Confirm
-                                            title={t`Disable this link?`}
-                                            content={t`They won't work any more, and can't be restored, but you can create new links.`}
-                                            action={() => {
-                                                this.revoke(link)
-                                                this.trackEvent('Revoked link')
-                                            }}
-                                        >
-                                            <Icon
-                                                name="close"
-                                                className="text-grey-2 text-grey-4-hover cursor-pointer"
-                                            />
-                                        </Confirm>
-                                    </td>
-                                }
-                            </tr>
-                        ) }
-                    </tbody>
-                </table>
-            }
-            </LoadingAndErrorWrapper>
-        );
+    if (list && list.length === 0) {
+      error = new Error(noLinksMessage);
     }
+
+    return (
+      <LoadingAndErrorWrapper loading={!list} error={error}>
+        {() => (
+          <table className="ContentTable">
+            <thead>
+              <tr>
+                <th>{t`Name`}</th>
+                {getPublicUrl && <th>{t`Public Link`}</th>}
+                {revoke && <th>{t`Revoke Link`}</th>}
+              </tr>
+            </thead>
+            <tbody>
+              {list &&
+                list.map(link => (
+                  <tr>
+                    <td>
+                      <Link
+                        to={getUrl(link)}
+                        onClick={() => this.trackEvent("Entity Link Clicked")}
+                      >
+                        {link.name}
+                      </Link>
+                    </td>
+                    {getPublicUrl && (
+                      <td>
+                        <ExternalLink
+                          href={getPublicUrl(link)}
+                          onClick={() => this.trackEvent("Public Link Clicked")}
+                        >
+                          {getPublicUrl(link)}
+                        </ExternalLink>
+                      </td>
+                    )}
+                    {revoke && (
+                      <td className="flex layout-centered">
+                        <Confirm
+                          title={t`Disable this link?`}
+                          content={t`They won't work any more, and can't be restored, but you can create new links.`}
+                          action={() => {
+                            this.revoke(link);
+                            this.trackEvent("Revoked link");
+                          }}
+                        >
+                          <Icon
+                            name="close"
+                            className="text-grey-2 text-grey-4-hover cursor-pointer"
+                          />
+                        </Confirm>
+                      </td>
+                    )}
+                  </tr>
+                ))}
+            </tbody>
+          </table>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-export const PublicLinksDashboardListing = () =>
+export const PublicLinksDashboardListing = () => (
+  <PublicLinksListing
+    load={DashboardApi.listPublic}
+    revoke={DashboardApi.deletePublicLink}
+    type={t`Public Dashboard Listing`}
+    getUrl={({ id }) => Urls.dashboard(id)}
+    getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
+    noLinksMessage={t`No dashboards have been publicly shared yet.`}
+  />
+);
+
+export const PublicLinksQuestionListing = () => (
+  <PublicLinksListing
+    load={CardApi.listPublic}
+    revoke={CardApi.deletePublicLink}
+    type={t`Public Card Listing`}
+    getUrl={({ id }) => Urls.question(id)}
+    getPublicUrl={({ public_uuid }) => Urls.publicCard(public_uuid)}
+    noLinksMessage={t`No questions have been publicly shared yet.`}
+  />
+);
+
+export const EmbeddedDashboardListing = () => (
+  <div className="bordered rounded full" style={{ maxWidth: 820 }}>
     <PublicLinksListing
-        load={DashboardApi.listPublic}
-        revoke={DashboardApi.deletePublicLink}
-        type={t`Public Dashboard Listing`}
-        getUrl={({ id }) => Urls.dashboard(id)}
-        getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
-        noLinksMessage={t`No dashboards have been publicly shared yet.`}
-    />;
-
-export const PublicLinksQuestionListing = () =>
+      load={DashboardApi.listEmbeddable}
+      getUrl={({ id }) => Urls.dashboard(id)}
+      type={t`Embedded Dashboard Listing`}
+      noLinksMessage={t`No dashboards have been embedded yet.`}
+    />
+  </div>
+);
+
+export const EmbeddedQuestionListing = () => (
+  <div className="bordered rounded full" style={{ maxWidth: 820 }}>
     <PublicLinksListing
-        load={CardApi.listPublic}
-        revoke={CardApi.deletePublicLink}
-        type={t`Public Card Listing`}
-        getUrl={({ id }) => Urls.question(id)}
-        getPublicUrl={({ public_uuid }) => Urls.publicCard(public_uuid)}
-        noLinksMessage={t`No questions have been publicly shared yet.`}
-    />;
-
-export const EmbeddedDashboardListing = () =>
-    <div className="bordered rounded full" style={{ maxWidth: 820 }}>
-        <PublicLinksListing
-            load={DashboardApi.listEmbeddable}
-            getUrl={({ id }) => Urls.dashboard(id)}
-            type={t`Embedded Dashboard Listing`}
-            noLinksMessage={t`No dashboards have been embedded yet.`}
-        />
-    </div>
-
-export const EmbeddedQuestionListing = () =>
-    <div className="bordered rounded full" style={{ maxWidth: 820 }}>
-        <PublicLinksListing
-            load={CardApi.listEmbeddable}
-            getUrl={({ id }) => Urls.question(id)}
-            type={t`Embedded Card Listing`}
-            noLinksMessage={t`No questions have been embedded yet.`}
-        />
-    </div>
+      load={CardApi.listEmbeddable}
+      getUrl={({ id }) => Urls.question(id)}
+      type={t`Embedded Card Listing`}
+      noLinksMessage={t`No questions have been embedded yet.`}
+    />
+  </div>
+);
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
index 97b3326a8b7300ca3150fdb0168aff7f033d7c28..3f5b84d8391c3888f0bf57f59540b1f20c854eb4 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SecretKeyWidget.jsx
@@ -5,40 +5,48 @@ import React, { Component } from "react";
 import SettingInput from "./SettingInput";
 import Button from "metabase/components/Button";
 import Confirm from "metabase/components/Confirm";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { UtilApi } from "metabase/services";
 
 type Props = {
-    onChange: (value: any) => void,
-    setting: {}
+  onChange: (value: any) => void,
+  setting: {},
 };
 
 export default class SecretKeyWidget extends Component {
-    props: Props;
+  props: Props;
 
-    _generateToken = async () => {
-        const { onChange } = this.props;
-        const result = await UtilApi.random_token();
-        onChange(result.token);
-    }
+  _generateToken = async () => {
+    const { onChange } = this.props;
+    const result = await UtilApi.random_token();
+    onChange(result.token);
+  };
 
-    render() {
-        const { setting } = this.props;
-        return (
-            <div className="p2 flex align-center full bordered rounded" style={{ maxWidth: 820 }}>
-                <SettingInput {...this.props} />
-                { setting.value ?
-                    <Confirm
-                        title={t`Regenerate embedding key?`}
-                        content={t`This will cause existing embeds to stop working until they are updated with the new key.`}
-                        action={this._generateToken}
-                    >
-                        <Button className="ml1" primary medium>{t`Regenerate key`}</Button>
-                    </Confirm>
-                :
-                    <Button className="ml1" primary medium onClick={this._generateToken}>{t`Generate Key`}</Button>
-                }
-            </div>
-        );
-    }
+  render() {
+    const { setting } = this.props;
+    return (
+      <div
+        className="p2 flex align-center full bordered rounded"
+        style={{ maxWidth: 820 }}
+      >
+        <SettingInput {...this.props} />
+        {setting.value ? (
+          <Confirm
+            title={t`Regenerate embedding key?`}
+            content={t`This will cause existing embeds to stop working until they are updated with the new key.`}
+            action={this._generateToken}
+          >
+            <Button className="ml1" primary medium>{t`Regenerate key`}</Button>
+          </Confirm>
+        ) : (
+          <Button
+            className="ml1"
+            primary
+            medium
+            onClick={this._generateToken}
+          >{t`Generate Key`}</Button>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx
index b88888d44b26da696f51b79f0e3e23b7e0f85a1e..e18bb5888a03294ec7afd2a0fb01dfafc57ab1d8 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingInput.jsx
@@ -3,19 +3,28 @@ import React from "react";
 import Input from "metabase/components/Input.jsx";
 import cx from "classnames";
 
-const SettingInput = ({ setting, onChange, disabled, autoFocus, errorMessage, fireOnChange, type = "text" }) =>
-    <Input
-        className={cx(" AdminInput bordered rounded h3", {
-            "SettingsInput": type !== "password",
-            "SettingsPassword": type === "password",
-            "border-error bg-error-input": errorMessage
-        })}
-        type={type}
-        value={setting.value || ""}
-        placeholder={setting.placeholder}
-        onChange={fireOnChange ? (e) => onChange(e.target.value) : null }
-        onBlurChange={!fireOnChange ? (e) => onChange(e.target.value) : null }
-        autoFocus={autoFocus}
-    />
+const SettingInput = ({
+  setting,
+  onChange,
+  disabled,
+  autoFocus,
+  errorMessage,
+  fireOnChange,
+  type = "text",
+}) => (
+  <Input
+    className={cx(" AdminInput bordered rounded h3", {
+      SettingsInput: type !== "password",
+      SettingsPassword: type === "password",
+      "border-error bg-error-input": errorMessage,
+    })}
+    type={type}
+    value={setting.value || ""}
+    placeholder={setting.placeholder}
+    onChange={fireOnChange ? e => onChange(e.target.value) : null}
+    onBlurChange={!fireOnChange ? e => onChange(e.target.value) : null}
+    autoFocus={autoFocus}
+  />
+);
 
 export default SettingInput;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
index 05fd4da0445808f4c7febc128deb46da6df01bd0..f3770a9b6d0b470e70bff1b5c5192d42bd7b0ddc 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingNumber.jsx
@@ -2,7 +2,8 @@ import React from "react";
 
 import SettingInput from "./SettingInput";
 
-const SettingNumber = ({ type = "number", ...props }) =>
-    <SettingInput {...props} type="number" />
+const SettingNumber = ({ type = "number", ...props }) => (
+  <SettingInput {...props} type="number" />
+);
 
 export default SettingNumber;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingPassword.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingPassword.jsx
index 103f1a479394988dbe32b321153a8ab8334b7c6a..3ab5ac103a0a5255eab241d1fe3a67fb84264bd3 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingPassword.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingPassword.jsx
@@ -2,7 +2,6 @@ import React from "react";
 
 import SettingInput from "./SettingInput.jsx";
 
-const SettingPassword = (props) =>
-    <SettingInput {...props} type="password" />;
+const SettingPassword = props => <SettingInput {...props} type="password" />;
 
 export default SettingPassword;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx
index 1f796f5aa8279b3275ea82f9a9f23074b637f1c0..7b67c3172033305e590315cde6c68c5012240da7 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingRadio.jsx
@@ -3,12 +3,16 @@ import React from "react";
 import Radio from "metabase/components/Radio";
 import cx from "classnames";
 
-const SettingRadio = ({ setting, onChange, disabled }) =>
-    <Radio
-        className={cx({ "disabled": disabled })}
-        value={setting.value}
-        onChange={(value) => onChange(value)}
-        options={Object.entries(setting.options).map(([value, name]) => ({ name, value }))}
-    />
+const SettingRadio = ({ setting, onChange, disabled }) => (
+  <Radio
+    className={cx({ disabled: disabled })}
+    value={setting.value}
+    onChange={value => onChange(value)}
+    options={Object.entries(setting.options).map(([value, name]) => ({
+      name,
+      value,
+    }))}
+  />
+);
 
 export default SettingRadio;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx
index 028cff5bda5030e7bc4a0cdc4ac1132fbb2a907a..ec49282e6a9a9af4fec3921badd020a38c8b9f5c 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingSelect.jsx
@@ -3,15 +3,20 @@ import React from "react";
 import Select from "metabase/components/Select.jsx";
 import _ from "underscore";
 
-const SettingSelect = ({ setting, onChange, disabled }) =>
-    <Select
-        className="full-width"
-        placeholder={setting.placeholder}
-        value={_.findWhere(setting.options, { value: setting.value }) || setting.value}
-        options={setting.options}
-        onChange={onChange}
-        optionNameFn={option => typeof option === "object" ? option.name : option }
-        optionValueFn={option => typeof option === "object" ? option.value : option }
-    />
+const SettingSelect = ({ setting, onChange, disabled }) => (
+  <Select
+    className="full-width"
+    placeholder={setting.placeholder}
+    value={
+      _.findWhere(setting.options, { value: setting.value }) || setting.value
+    }
+    options={setting.options}
+    onChange={onChange}
+    optionNameFn={option => (typeof option === "object" ? option.name : option)}
+    optionValueFn={option =>
+      typeof option === "object" ? option.value : option
+    }
+  />
+);
 
 export default SettingSelect;
diff --git a/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx b/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx
index 5818f2ca4edc3daf66ad5d00ee64e279ddde0d51..e2973c6dc029bdb8eb23fc8a36eb7e3f50d35f98 100644
--- a/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx
+++ b/frontend/src/metabase/admin/settings/components/widgets/SettingToggle.jsx
@@ -1,16 +1,16 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Toggle from "metabase/components/Toggle.jsx";
 
 const SettingToggle = ({ setting, onChange, disabled }) => {
-    const value = setting.value == null ? setting.default : setting.value;
-    const on = value === true || value === "true";
-    return (
-        <div className="flex align-center pt1">
-            <Toggle value={on} onChange={!disabled ? () => onChange(!on) : null}/>
-            <span className="text-bold mx1">{on ? t`Enabled` : t`Disabled`}</span>
-        </div>
-    );
-}
+  const value = setting.value == null ? setting.default : setting.value;
+  const on = value === true || value === "true";
+  return (
+    <div className="flex align-center pt1">
+      <Toggle value={on} onChange={!disabled ? () => onChange(!on) : null} />
+      <span className="text-bold mx1">{on ? t`Enabled` : t`Disabled`}</span>
+    </div>
+  );
+};
 
 export default SettingToggle;
diff --git a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
index 754d3250a130f363b5095ab20198b02f6f14cdf3..2daf7d58dabce599a731a18598c52a2056b36c99 100644
--- a/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
+++ b/frontend/src/metabase/admin/settings/containers/SettingsEditorApp.jsx
@@ -5,7 +5,7 @@ import { connect } from "react-redux";
 import title from "metabase/hoc/Title";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import { slugify } from "metabase/lib/formatting";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AdminLayout from "metabase/components/AdminLayout.jsx";
 
 import SettingsSetting from "../components/SettingsSetting.jsx";
@@ -18,240 +18,262 @@ import SettingsSingleSignOnForm from "../components/SettingsSingleSignOnForm.jsx
 import SettingsAuthenticationOptions from "../components/SettingsAuthenticationOptions.jsx";
 import SettingsXrayForm from "../components/SettingsXrayForm.jsx";
 
-import { prepareAnalyticsValue } from 'metabase/admin/settings/utils'
+import { prepareAnalyticsValue } from "metabase/admin/settings/utils";
 
 import _ from "underscore";
-import cx from 'classnames';
+import cx from "classnames";
 
 import {
-    getSettings,
-    getSettingValues,
-    getSections,
-    getActiveSection,
-    getNewVersionAvailable
+  getSettings,
+  getSettingValues,
+  getSections,
+  getActiveSection,
+  getNewVersionAvailable,
 } from "../selectors";
 import * as settingsActions from "../settings";
 
 const mapStateToProps = (state, props) => {
-    return {
-        settings:            getSettings(state, props),
-        settingValues:       getSettingValues(state, props),
-        sections:            getSections(state, props),
-        activeSection:       getActiveSection(state, props),
-        newVersionAvailable: getNewVersionAvailable(state, props)
-    }
-}
+  return {
+    settings: getSettings(state, props),
+    settingValues: getSettingValues(state, props),
+    sections: getSections(state, props),
+    activeSection: getActiveSection(state, props),
+    newVersionAvailable: getNewVersionAvailable(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    ...settingsActions
-}
+  ...settingsActions,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @title(({ activeSection }) => activeSection && activeSection.name)
 export default class SettingsEditorApp extends Component {
-    layout = null // the reference to AdminLayout
+  layout = null; // the reference to AdminLayout
 
-    static propTypes = {
-        sections: PropTypes.array.isRequired,
-        activeSection: PropTypes.object,
-        updateSetting: PropTypes.func.isRequired,
-        updateEmailSettings: PropTypes.func.isRequired,
-        updateSlackSettings: PropTypes.func.isRequired,
-        updateLdapSettings: PropTypes.func.isRequired,
-        sendTestEmail: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    sections: PropTypes.array.isRequired,
+    activeSection: PropTypes.object,
+    updateSetting: PropTypes.func.isRequired,
+    updateEmailSettings: PropTypes.func.isRequired,
+    updateSlackSettings: PropTypes.func.isRequired,
+    updateLdapSettings: PropTypes.func.isRequired,
+    sendTestEmail: PropTypes.func.isRequired,
+  };
 
-    componentWillMount() {
-        this.props.initializeSettings();
-    }
+  componentWillMount() {
+    this.props.initializeSettings();
+  }
 
-    updateSetting = async (setting, newValue) => {
-        const { settingValues, updateSetting } = this.props;
+  updateSetting = async (setting, newValue) => {
+    const { settingValues, updateSetting } = this.props;
 
-        this.layout.setSaving();
+    this.layout.setSaving();
 
-        const oldValue = setting.value;
+    const oldValue = setting.value;
 
-        // TODO: mutation bad!
-        setting.value = newValue;
-        try {
-            await updateSetting(setting);
+    // TODO: mutation bad!
+    setting.value = newValue;
+    try {
+      await updateSetting(setting);
 
-            if (setting.onChanged) {
-                await setting.onChanged(oldValue, newValue, settingValues, this.handleChangeSetting)
-            }
+      if (setting.onChanged) {
+        await setting.onChanged(
+          oldValue,
+          newValue,
+          settingValues,
+          this.handleChangeSetting,
+        );
+      }
 
-            this.layout.setSaved();
+      this.layout.setSaved();
 
-            const value = prepareAnalyticsValue(setting);
+      const value = prepareAnalyticsValue(setting);
 
-            MetabaseAnalytics.trackEvent(
-                "General Settings",
-                setting.display_name || setting.key,
-                value,
-                // pass the actual value if it's a number
-                typeof(value) === 'number' && value
-            );
-        } catch (error) {
-            let message = error && (error.message || (error.data && error.data.message));
-            this.layout.setSaveError(message);
-            MetabaseAnalytics.trackEvent("General Settings", setting.display_name, "error");
-        }
+      MetabaseAnalytics.trackEvent(
+        "General Settings",
+        setting.display_name || setting.key,
+        value,
+        // pass the actual value if it's a number
+        typeof value === "number" && value,
+      );
+    } catch (error) {
+      let message =
+        error && (error.message || (error.data && error.data.message));
+      this.layout.setSaveError(message);
+      MetabaseAnalytics.trackEvent(
+        "General Settings",
+        setting.display_name,
+        "error",
+      );
     }
+  };
 
-    handleChangeSetting = (key, value) => {
-        const { settings, updateSetting } = this.props;
-        const setting = _.findWhere(settings, { key });
-        if (!setting) {
-            throw new Error(t`Unknown setting ${key}`);
-        }
-        return updateSetting({ ...setting, value });
+  handleChangeSetting = (key, value) => {
+    const { settings, updateSetting } = this.props;
+    const setting = _.findWhere(settings, { key });
+    if (!setting) {
+      throw new Error(t`Unknown setting ${key}`);
     }
+    return updateSetting({ ...setting, value });
+  };
 
-    renderSettingsPane() {
-        const { activeSection, settingValues } = this.props;
+  renderSettingsPane() {
+    const { activeSection, settingValues } = this.props;
 
-        if (!activeSection) {
-            return null;
-        }
+    if (!activeSection) {
+      return null;
+    }
 
-        if (activeSection.name === "Email") {
-            return (
-                <SettingsEmailForm
-                    ref="emailForm"
-                    elements={activeSection.settings}
-                    updateEmailSettings={this.props.updateEmailSettings}
-                    sendTestEmail={this.props.sendTestEmail}
-                />
-            );
-        } else if (activeSection.name === "Setup") {
-            return (
-                <SettingsSetupList
-                    ref="settingsForm"
-                />
-            );
-        } else if (activeSection.name === "Slack") {
-            return (
-                <SettingsSlackForm
-                    ref="slackForm"
-                    elements={activeSection.settings}
-                    updateSlackSettings={this.props.updateSlackSettings}
-                />
-            );
-        } else if (activeSection.name === "Updates") {
-            return (
-                <SettingsUpdatesForm
-                    settings={this.props.settings}
-                    elements={activeSection.settings}
-                    updateSetting={this.updateSetting}
-                />
-            );
-        } else if (activeSection.name === "Authentication") {
-            // HACK - the presence of this param is a way for us to tell if
-            // a user is looking at a sub section of the autentication section
-            // since allowing for multi page settings more broadly would require
-            // a fairly significant refactor of how settings does its routing logic
-            if(this.props.params.authType) {
-                if(this.props.params.authType === 'ldap') {
-                    return (
-                        <SettingsLdapForm
-                            elements={_.findWhere(this.props.sections, { slug: 'ldap'}).settings}
-                            updateLdapSettings={this.props.updateLdapSettings}
-                        />
-                    )
-                } else if (this.props.params.authType === 'google') {
-                    return (
-                        <SettingsSingleSignOnForm
-                            elements={ _.findWhere(this.props.sections, { slug: slugify('Single Sign-On')}).settings}
-                            updateSetting={this.updateSetting}
-                        />
-                    )
-                }
-            } else {
-                return (<SettingsAuthenticationOptions />)
-            }
-        } else if (activeSection.name === "X-Rays") {
-            return (
-                <SettingsXrayForm
-                    settings={this.props.settings}
-                    elements={activeSection.settings}
-                    updateSetting={this.updateSetting.bind(this)}
-                />
-            )
-        } else {
-            return (
-                <ul>
-                    {activeSection.settings
-                    .filter((setting) =>
-                        setting.getHidden ? !setting.getHidden(settingValues) : true
-                    )
-                    .map((setting, index) =>
-                        <SettingsSetting
-                            key={setting.key}
-                            setting={setting}
-                            onChange={this.updateSetting.bind(this, setting)}
-                            onChangeSetting={this.handleChangeSetting}
-                            reloadSettings={this.props.reloadSettings}
-                            autoFocus={index === 0}
-                            settingValues={settingValues}
-                        />
-                    )}
-                </ul>
-            );
+    if (activeSection.name === "Email") {
+      return (
+        <SettingsEmailForm
+          ref="emailForm"
+          elements={activeSection.settings}
+          updateEmailSettings={this.props.updateEmailSettings}
+          sendTestEmail={this.props.sendTestEmail}
+        />
+      );
+    } else if (activeSection.name === "Setup") {
+      return <SettingsSetupList ref="settingsForm" />;
+    } else if (activeSection.name === "Slack") {
+      return (
+        <SettingsSlackForm
+          ref="slackForm"
+          elements={activeSection.settings}
+          updateSlackSettings={this.props.updateSlackSettings}
+        />
+      );
+    } else if (activeSection.name === "Updates") {
+      return (
+        <SettingsUpdatesForm
+          settings={this.props.settings}
+          elements={activeSection.settings}
+          updateSetting={this.updateSetting}
+        />
+      );
+    } else if (activeSection.name === "Authentication") {
+      // HACK - the presence of this param is a way for us to tell if
+      // a user is looking at a sub section of the autentication section
+      // since allowing for multi page settings more broadly would require
+      // a fairly significant refactor of how settings does its routing logic
+      if (this.props.params.authType) {
+        if (this.props.params.authType === "ldap") {
+          return (
+            <SettingsLdapForm
+              elements={
+                _.findWhere(this.props.sections, { slug: "ldap" }).settings
+              }
+              updateLdapSettings={this.props.updateLdapSettings}
+            />
+          );
+        } else if (this.props.params.authType === "google") {
+          return (
+            <SettingsSingleSignOnForm
+              elements={
+                _.findWhere(this.props.sections, {
+                  slug: slugify("Single Sign-On"),
+                }).settings
+              }
+              updateSetting={this.updateSetting}
+            />
+          );
         }
+      } else {
+        return <SettingsAuthenticationOptions />;
+      }
+    } else if (activeSection.name === "X-Rays") {
+      return (
+        <SettingsXrayForm
+          settings={this.props.settings}
+          elements={activeSection.settings}
+          updateSetting={this.updateSetting.bind(this)}
+        />
+      );
+    } else {
+      return (
+        <ul>
+          {activeSection.settings
+            .filter(
+              setting =>
+                setting.getHidden ? !setting.getHidden(settingValues) : true,
+            )
+            .map((setting, index) => (
+              <SettingsSetting
+                key={setting.key}
+                setting={setting}
+                onChange={this.updateSetting.bind(this, setting)}
+                onChangeSetting={this.handleChangeSetting}
+                reloadSettings={this.props.reloadSettings}
+                autoFocus={index === 0}
+                settingValues={settingValues}
+              />
+            ))}
+        </ul>
+      );
     }
+  }
 
-    renderSettingsSections() {
-        const { sections, activeSection, newVersionAvailable } = this.props;
-
-        const renderedSections = _.map(sections, (section, idx) => {
+  renderSettingsSections() {
+    const { sections, activeSection, newVersionAvailable } = this.props;
 
-            // HACK - This is used to hide specific items in the sidebar and is currently
-            // only used as a way to fake the multi page auth settings pages without
-            // requiring a larger refactor.
-            if(section.sidebar === false) {
-                return false;
-            }
-            const classes = cx("AdminList-item", "flex", "align-center", "justify-between", "no-decoration", {
-                "selected": activeSection && section.name === activeSection.name // this.state.currentSection === idx
-            });
+    const renderedSections = _.map(sections, (section, idx) => {
+      // HACK - This is used to hide specific items in the sidebar and is currently
+      // only used as a way to fake the multi page auth settings pages without
+      // requiring a larger refactor.
+      if (section.sidebar === false) {
+        return false;
+      }
+      const classes = cx(
+        "AdminList-item",
+        "flex",
+        "align-center",
+        "justify-between",
+        "no-decoration",
+        {
+          selected: activeSection && section.name === activeSection.name, // this.state.currentSection === idx
+        },
+      );
 
-            // if this is the Updates section && there is a new version then lets add a little indicator
-            let newVersionIndicator;
-            if (section.name === "Updates" && newVersionAvailable) {
-                newVersionIndicator = (
-                    <span style={{padding: "4px 8px 4px 8px"}} className="bg-brand rounded text-white text-bold h6">1</span>
-                );
-            }
+      // if this is the Updates section && there is a new version then lets add a little indicator
+      let newVersionIndicator;
+      if (section.name === "Updates" && newVersionAvailable) {
+        newVersionIndicator = (
+          <span
+            style={{ padding: "4px 8px 4px 8px" }}
+            className="bg-brand rounded text-white text-bold h6"
+          >
+            1
+          </span>
+        );
+      }
 
-            return (
-                <li key={section.name}>
-                    <Link to={"/admin/settings/" + section.slug}  className={classes}>
-                        <span>{section.name}</span>
-                        {newVersionIndicator}
-                    </Link>
-                </li>
-            );
-        });
+      return (
+        <li key={section.name}>
+          <Link to={"/admin/settings/" + section.slug} className={classes}>
+            <span>{section.name}</span>
+            {newVersionIndicator}
+          </Link>
+        </li>
+      );
+    });
 
-        return (
-            <div className="MetadataEditor-table-list AdminList flex-no-shrink">
-                <ul className="AdminList-items pt1">
-                    {renderedSections}
-                </ul>
-            </div>
-        );
-    }
+    return (
+      <div className="MetadataEditor-table-list AdminList flex-no-shrink">
+        <ul className="AdminList-items pt1">{renderedSections}</ul>
+      </div>
+    );
+  }
 
-    render() {
-        return (
-            <AdminLayout
-                ref={(layout) => this.layout = layout}
-                title={t`Settings`}
-                sidebar={this.renderSettingsSections()}
-            >
-                {this.renderSettingsPane()}
-            </AdminLayout>
-        )
-    }
+  render() {
+    return (
+      <AdminLayout
+        ref={layout => (this.layout = layout)}
+        title={t`Settings`}
+        sidebar={this.renderSettingsSections()}
+      >
+        {this.renderSettingsPane()}
+      </AdminLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/admin/settings/selectors.js b/frontend/src/metabase/admin/settings/selectors.js
index d8fe2fc41f17067e538ed507edc76f6cf2f6621d..b9a6f0be5e94578342be8281b5f0e1cd5ec77b3b 100644
--- a/frontend/src/metabase/admin/settings/selectors.js
+++ b/frontend/src/metabase/admin/settings/selectors.js
@@ -1,14 +1,14 @@
 import _ from "underscore";
 import { createSelector } from "reselect";
 import MetabaseSettings from "metabase/lib/settings";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { slugify } from "metabase/lib/formatting";
 import CustomGeoJSONWidget from "./components/widgets/CustomGeoJSONWidget.jsx";
 import {
-    PublicLinksDashboardListing,
-    PublicLinksQuestionListing,
-    EmbeddedQuestionListing,
-    EmbeddedDashboardListing
+  PublicLinksDashboardListing,
+  PublicLinksQuestionListing,
+  EmbeddedQuestionListing,
+  EmbeddedDashboardListing,
 } from "./components/widgets/PublicLinksListing.jsx";
 import SecretKeyWidget from "./components/widgets/SecretKeyWidget.jsx";
 import EmbeddingLegalese from "./components/widgets/EmbeddingLegalese";
@@ -18,393 +18,407 @@ import LdapGroupMappingsWidget from "./components/widgets/LdapGroupMappingsWidge
 import { UtilApi } from "metabase/services";
 
 const SECTIONS = [
-    {
-        name: t`Setup`,
-        settings: []
-    },
-    {
-        name: t`General`,
-        settings: [
-            {
-                key: "site-name",
-                display_name: t`Site Name`,
-                type: "string"
-            },
-            {
-                key: "site-url",
-                display_name: t`Site URL`,
-                type: "string"
-            },
-            {
-                key: "admin-email",
-                display_name: t`Email Address for Help Requests`,
-                type: "string"
-            },
-            {
-                key: "report-timezone",
-                display_name: t`Report Timezone`,
-                type: "select",
-                options: [
-                    { name: t`Database Default`, value: "" },
-                    ...MetabaseSettings.get('timezones')
-                ],
-                placeholder: t`Select a timezone`,
-                note: t`Not all databases support timezones, in which case this setting won't take effect.`,
-                allowValueCollection: true
-            },
-            {
-                key: "site-locale",
-                display_name: t`Language`,
-                type: "select",
-                options:  (MetabaseSettings.get("available_locales") || []).map(([value, name]) => ({ name, value })),
-                placeholder: t`Select a language`,
-                getHidden: () => MetabaseSettings.get("available_locales").length < 2
-            },
-            {
-                key: "anon-tracking-enabled",
-                display_name: t`Anonymous Tracking`,
-                type: "boolean"
-            },
-            {
-                key: "humanization-strategy",
-                display_name: t`Friendly Table and Field Names`,
-                type: "select",
-                options: [
-                    { value: "advanced", name: t`Enabled` },
-                    { value: "simple",   name: t`Only replace underscores and dashes with spaces` },
-                    { value: "none",     name: t`Disabled` }
-                ],
-                // this needs to be here because 'advanced' is the default value, so if you select 'advanced' the
-                // widget will always show the placeholder instead of the 'name' defined above :(
-                placeholder: t`Enabled`
-            },
-            {
-                key: "enable-nested-queries",
-                display_name: t`Enable Nested Queries`,
-                type: "boolean"
-            }
-        ]
-    },
-    {
-        name: t`Updates`,
-        settings: [
-            {
-                key: "check-for-updates",
-                display_name: t`Check for updates`,
-                type: "boolean"
-            }
-        ]
-    },
-    {
-        name: t`Email`,
-        settings: [
-            {
-                key: "email-smtp-host",
-                display_name: t`SMTP Host`,
-                placeholder: "smtp.yourservice.com",
-                type: "string",
-                required: true,
-                autoFocus: true
-            },
-            {
-                key: "email-smtp-port",
-                display_name: t`SMTP Port`,
-                placeholder: "587",
-                type: "number",
-                required: true,
-                validations: [["integer", t`That's not a valid port number`]]
-            },
-            {
-                key: "email-smtp-security",
-                display_name: t`SMTP Security`,
-                description: null,
-                type: "radio",
-                options: { none: "None", ssl: "SSL", tls: "TLS", starttls: "STARTTLS" },
-                defaultValue: 'none'
-            },
-            {
-                key: "email-smtp-username",
-                display_name: t`SMTP Username`,
-                description: null,
-                placeholder: "youlooknicetoday",
-                type: "string"
-            },
-            {
-                key: "email-smtp-password",
-                display_name: t`SMTP Password`,
-                description: null,
-                placeholder: "Shh...",
-                type: "password"
-            },
-            {
-                key: "email-from-address",
-                display_name: t`From Address`,
-                placeholder: "metabase@yourcompany.com",
-                type: "string",
-                required: true,
-                validations: [["email", t`That's not a valid email address`]]
-            }
-        ]
-    },
-    {
-        name: "Slack",
-        settings: [
-            {
-                key: "slack-token",
-                display_name: t`Slack API Token`,
-                description: "",
-                placeholder: t`Enter the token you received from Slack`,
-                type: "string",
-                required: false,
-                autoFocus: true
-            },
-            {
-                key: "metabot-enabled",
-                display_name: "MetaBot",
-                type: "boolean",
-                // TODO: why do we have "defaultValue" here in addition to the "default" specified by the backend?
-                defaultValue: false,
-                required: true,
-                autoFocus: false
-            },
-        ]
-    },
-    {
-        name: t`Single Sign-On`,
-        sidebar: false,
-        settings: [
-            {
-                key: "google-auth-client-id"
-            },
-            {
-                key: "google-auth-auto-create-accounts-domain"
-            }
-        ]
-    },
-    {
-        name: t`Authentication`,
-        settings: []
-    },
-    {
-        name: t`LDAP`,
-        sidebar: false,
-        settings: [
-            {
-                key: "ldap-enabled",
-                display_name: t`LDAP Authentication`,
-                description: null,
-                type: "boolean"
-            },
-            {
-                key: "ldap-host",
-                display_name: t`LDAP Host`,
-                placeholder: "ldap.yourdomain.org",
-                type: "string",
-                required: true,
-                autoFocus: true
-            },
-            {
-                key: "ldap-port",
-                display_name: t`LDAP Port`,
-                placeholder: "389",
-                type: "string",
-                validations: [["integer", t`That's not a valid port number`]]
-            },
-            {
-                key: "ldap-security",
-                display_name: t`LDAP Security`,
-                description: null,
-                type: "radio",
-                options: { none: "None", ssl: "SSL", starttls: "StartTLS" },
-                defaultValue: "none"
-            },
-            {
-                key: "ldap-bind-dn",
-                display_name: t`Username or DN`,
-                type: "string"
-            },
-            {
-                key: "ldap-password",
-                display_name: t`Password`,
-                type: "password"
-            },
-            {
-                key: "ldap-user-base",
-                display_name: t`User search base`,
-                type: "string",
-                required: true
-            },
-            {
-                key: "ldap-user-filter",
-                display_name: t`User filter`,
-                type: "string",
-                validations: [["ldap_filter", t`Check your parentheses`]]
-            },
-            {
-                key: "ldap-attribute-email",
-                display_name: t`Email attribute`,
-                type: "string"
-            },
-            {
-                key: "ldap-attribute-firstname",
-                display_name: t`First name attribute`,
-                type: "string"
-            },
-            {
-                key: "ldap-attribute-lastname",
-                display_name: t`Last name attribute`,
-                type: "string"
-            },
-            {
-                key: "ldap-group-sync",
-                display_name: t`Synchronize group memberships`,
-                description: null,
-                widget: LdapGroupMappingsWidget
-            },
-            {
-                key: "ldap-group-base",
-                display_name:t`"Group search base`,
-                type: "string"
-            },
-            {
-                key: "ldap-group-mappings"
-            }
-        ]
-    },
-    {
-        name: t`Maps`,
-        settings: [
-            {
-                key: "map-tile-server-url",
-                display_name: t`Map tile server URL`,
-                note: t`Metabase uses OpenStreetMaps by default.`,
-                type: "string"
-            },
-            {
-                key: "custom-geojson",
-                display_name: t`Custom Maps`,
-                description: t`Add your own GeoJSON files to enable different region map visualizations`,
-                widget: CustomGeoJSONWidget,
-                noHeader: true
-            }
-        ]
-    },
-    {
-        name: t`Public Sharing`,
-        settings: [
-            {
-                key: "enable-public-sharing",
-                display_name: t`Enable Public Sharing`,
-                type: "boolean"
-            },
-            {
-                key: "-public-sharing-dashboards",
-                display_name: t`Shared Dashboards`,
-                widget: PublicLinksDashboardListing,
-                getHidden: (settings) => !settings["enable-public-sharing"]
-            },
-            {
-                key: "-public-sharing-questions",
-                display_name: t`Shared Questions`,
-                widget: PublicLinksQuestionListing,
-                getHidden: (settings) => !settings["enable-public-sharing"]
-            }
-        ]
-    },
-    {
-        name: t`Embedding in other Applications`,
-        settings: [
-            {
-                key: "enable-embedding",
-                description: null,
-                widget: EmbeddingLegalese,
-                getHidden: (settings) => settings["enable-embedding"],
-                onChanged: async (oldValue, newValue, settingsValues, onChangeSetting) => {
-                    // Generate a secret key if none already exists
-                    if (!oldValue && newValue && !settingsValues["embedding-secret-key"]) {
-                        let result = await UtilApi.random_token();
-                        await onChangeSetting("embedding-secret-key", result.token);
-                    }
-                }
-            }, {
-                key: "enable-embedding",
-                display_name: t`Enable Embedding Metabase in other Applications`,
-                type: "boolean",
-                getHidden: (settings) => !settings["enable-embedding"]
-            },
-            {
-                widget: EmbeddingLevel,
-                getHidden: (settings) => !settings["enable-embedding"]
-            },
-            {
-                key: "embedding-secret-key",
-                display_name: t`Embedding secret key`,
-                widget: SecretKeyWidget,
-                getHidden: (settings) => !settings["enable-embedding"]
-            },
-            {
-                key: "-embedded-dashboards",
-                display_name: t`Embedded Dashboards`,
-                widget: EmbeddedDashboardListing,
-                getHidden: (settings) => !settings["enable-embedding"]
-            },
-            {
-                key: "-embedded-questions",
-                display_name: t`Embedded Questions`,
-                widget: EmbeddedQuestionListing,
-                getHidden: (settings) => !settings["enable-embedding"]
-            }
-        ]
-    },
-    {
-        name: t`Caching`,
-        settings: [
-            {
-                key: "enable-query-caching",
-                display_name: t`Enable Caching`,
-                type: "boolean"
-            },
-            {
-                key: "query-caching-min-ttl",
-                display_name: t`Minimum Query Duration`,
-                type: "number",
-                getHidden: (settings) => !settings["enable-query-caching"],
-                allowValueCollection: true
-            },
-            {
-                key: "query-caching-ttl-ratio",
-                display_name: t`Cache Time-To-Live (TTL) multiplier`,
-                type: "number",
-                getHidden: (settings) => !settings["enable-query-caching"],
-                allowValueCollection: true
-            },
-            {
-                key: "query-caching-max-kb",
-                display_name: t`Max Cache Entry Size`,
-                type: "number",
-                getHidden: (settings) => !settings["enable-query-caching"],
-                allowValueCollection: true
-            }
-        ]
-    },
-    {
-        name: t`X-Rays`,
-        settings: [
-            {
-                key: "enable-xrays",
-                display_name: t`Enable X-Rays`,
-                type: "boolean",
-                allowValueCollection: true
-            },
-            {
-                key: "xray-max-cost",
-                type: "string",
-                allowValueCollection: true
-
-            }
-        ]
-    },
-    /*
+  {
+    name: t`Setup`,
+    settings: [],
+  },
+  {
+    name: t`General`,
+    settings: [
+      {
+        key: "site-name",
+        display_name: t`Site Name`,
+        type: "string",
+      },
+      {
+        key: "site-url",
+        display_name: t`Site URL`,
+        type: "string",
+      },
+      {
+        key: "admin-email",
+        display_name: t`Email Address for Help Requests`,
+        type: "string",
+      },
+      {
+        key: "report-timezone",
+        display_name: t`Report Timezone`,
+        type: "select",
+        options: [
+          { name: t`Database Default`, value: "" },
+          ...MetabaseSettings.get("timezones"),
+        ],
+        placeholder: t`Select a timezone`,
+        note: t`Not all databases support timezones, in which case this setting won't take effect.`,
+        allowValueCollection: true,
+      },
+      {
+        key: "site-locale",
+        display_name: t`Language`,
+        type: "select",
+        options: (MetabaseSettings.get("available_locales") || []).map(
+          ([value, name]) => ({ name, value }),
+        ),
+        placeholder: t`Select a language`,
+        getHidden: () => MetabaseSettings.get("available_locales").length < 2,
+      },
+      {
+        key: "anon-tracking-enabled",
+        display_name: t`Anonymous Tracking`,
+        type: "boolean",
+      },
+      {
+        key: "humanization-strategy",
+        display_name: t`Friendly Table and Field Names`,
+        type: "select",
+        options: [
+          { value: "advanced", name: t`Enabled` },
+          {
+            value: "simple",
+            name: t`Only replace underscores and dashes with spaces`,
+          },
+          { value: "none", name: t`Disabled` },
+        ],
+        // this needs to be here because 'advanced' is the default value, so if you select 'advanced' the
+        // widget will always show the placeholder instead of the 'name' defined above :(
+        placeholder: t`Enabled`,
+      },
+      {
+        key: "enable-nested-queries",
+        display_name: t`Enable Nested Queries`,
+        type: "boolean",
+      },
+    ],
+  },
+  {
+    name: t`Updates`,
+    settings: [
+      {
+        key: "check-for-updates",
+        display_name: t`Check for updates`,
+        type: "boolean",
+      },
+    ],
+  },
+  {
+    name: t`Email`,
+    settings: [
+      {
+        key: "email-smtp-host",
+        display_name: t`SMTP Host`,
+        placeholder: "smtp.yourservice.com",
+        type: "string",
+        required: true,
+        autoFocus: true,
+      },
+      {
+        key: "email-smtp-port",
+        display_name: t`SMTP Port`,
+        placeholder: "587",
+        type: "number",
+        required: true,
+        validations: [["integer", t`That's not a valid port number`]],
+      },
+      {
+        key: "email-smtp-security",
+        display_name: t`SMTP Security`,
+        description: null,
+        type: "radio",
+        options: { none: "None", ssl: "SSL", tls: "TLS", starttls: "STARTTLS" },
+        defaultValue: "none",
+      },
+      {
+        key: "email-smtp-username",
+        display_name: t`SMTP Username`,
+        description: null,
+        placeholder: "youlooknicetoday",
+        type: "string",
+      },
+      {
+        key: "email-smtp-password",
+        display_name: t`SMTP Password`,
+        description: null,
+        placeholder: "Shh...",
+        type: "password",
+      },
+      {
+        key: "email-from-address",
+        display_name: t`From Address`,
+        placeholder: "metabase@yourcompany.com",
+        type: "string",
+        required: true,
+        validations: [["email", t`That's not a valid email address`]],
+      },
+    ],
+  },
+  {
+    name: "Slack",
+    settings: [
+      {
+        key: "slack-token",
+        display_name: t`Slack API Token`,
+        description: "",
+        placeholder: t`Enter the token you received from Slack`,
+        type: "string",
+        required: false,
+        autoFocus: true,
+      },
+      {
+        key: "metabot-enabled",
+        display_name: "MetaBot",
+        type: "boolean",
+        // TODO: why do we have "defaultValue" here in addition to the "default" specified by the backend?
+        defaultValue: false,
+        required: true,
+        autoFocus: false,
+      },
+    ],
+  },
+  {
+    name: t`Single Sign-On`,
+    sidebar: false,
+    settings: [
+      {
+        key: "google-auth-client-id",
+      },
+      {
+        key: "google-auth-auto-create-accounts-domain",
+      },
+    ],
+  },
+  {
+    name: t`Authentication`,
+    settings: [],
+  },
+  {
+    name: t`LDAP`,
+    sidebar: false,
+    settings: [
+      {
+        key: "ldap-enabled",
+        display_name: t`LDAP Authentication`,
+        description: null,
+        type: "boolean",
+      },
+      {
+        key: "ldap-host",
+        display_name: t`LDAP Host`,
+        placeholder: "ldap.yourdomain.org",
+        type: "string",
+        required: true,
+        autoFocus: true,
+      },
+      {
+        key: "ldap-port",
+        display_name: t`LDAP Port`,
+        placeholder: "389",
+        type: "string",
+        validations: [["integer", t`That's not a valid port number`]],
+      },
+      {
+        key: "ldap-security",
+        display_name: t`LDAP Security`,
+        description: null,
+        type: "radio",
+        options: { none: "None", ssl: "SSL", starttls: "StartTLS" },
+        defaultValue: "none",
+      },
+      {
+        key: "ldap-bind-dn",
+        display_name: t`Username or DN`,
+        type: "string",
+      },
+      {
+        key: "ldap-password",
+        display_name: t`Password`,
+        type: "password",
+      },
+      {
+        key: "ldap-user-base",
+        display_name: t`User search base`,
+        type: "string",
+        required: true,
+      },
+      {
+        key: "ldap-user-filter",
+        display_name: t`User filter`,
+        type: "string",
+        validations: [["ldap_filter", t`Check your parentheses`]],
+      },
+      {
+        key: "ldap-attribute-email",
+        display_name: t`Email attribute`,
+        type: "string",
+      },
+      {
+        key: "ldap-attribute-firstname",
+        display_name: t`First name attribute`,
+        type: "string",
+      },
+      {
+        key: "ldap-attribute-lastname",
+        display_name: t`Last name attribute`,
+        type: "string",
+      },
+      {
+        key: "ldap-group-sync",
+        display_name: t`Synchronize group memberships`,
+        description: null,
+        widget: LdapGroupMappingsWidget,
+      },
+      {
+        key: "ldap-group-base",
+        display_name: t`"Group search base`,
+        type: "string",
+      },
+      {
+        key: "ldap-group-mappings",
+      },
+    ],
+  },
+  {
+    name: t`Maps`,
+    settings: [
+      {
+        key: "map-tile-server-url",
+        display_name: t`Map tile server URL`,
+        note: t`Metabase uses OpenStreetMaps by default.`,
+        type: "string",
+      },
+      {
+        key: "custom-geojson",
+        display_name: t`Custom Maps`,
+        description: t`Add your own GeoJSON files to enable different region map visualizations`,
+        widget: CustomGeoJSONWidget,
+        noHeader: true,
+      },
+    ],
+  },
+  {
+    name: t`Public Sharing`,
+    settings: [
+      {
+        key: "enable-public-sharing",
+        display_name: t`Enable Public Sharing`,
+        type: "boolean",
+      },
+      {
+        key: "-public-sharing-dashboards",
+        display_name: t`Shared Dashboards`,
+        widget: PublicLinksDashboardListing,
+        getHidden: settings => !settings["enable-public-sharing"],
+      },
+      {
+        key: "-public-sharing-questions",
+        display_name: t`Shared Questions`,
+        widget: PublicLinksQuestionListing,
+        getHidden: settings => !settings["enable-public-sharing"],
+      },
+    ],
+  },
+  {
+    name: t`Embedding in other Applications`,
+    settings: [
+      {
+        key: "enable-embedding",
+        description: null,
+        widget: EmbeddingLegalese,
+        getHidden: settings => settings["enable-embedding"],
+        onChanged: async (
+          oldValue,
+          newValue,
+          settingsValues,
+          onChangeSetting,
+        ) => {
+          // Generate a secret key if none already exists
+          if (
+            !oldValue &&
+            newValue &&
+            !settingsValues["embedding-secret-key"]
+          ) {
+            let result = await UtilApi.random_token();
+            await onChangeSetting("embedding-secret-key", result.token);
+          }
+        },
+      },
+      {
+        key: "enable-embedding",
+        display_name: t`Enable Embedding Metabase in other Applications`,
+        type: "boolean",
+        getHidden: settings => !settings["enable-embedding"],
+      },
+      {
+        widget: EmbeddingLevel,
+        getHidden: settings => !settings["enable-embedding"],
+      },
+      {
+        key: "embedding-secret-key",
+        display_name: t`Embedding secret key`,
+        widget: SecretKeyWidget,
+        getHidden: settings => !settings["enable-embedding"],
+      },
+      {
+        key: "-embedded-dashboards",
+        display_name: t`Embedded Dashboards`,
+        widget: EmbeddedDashboardListing,
+        getHidden: settings => !settings["enable-embedding"],
+      },
+      {
+        key: "-embedded-questions",
+        display_name: t`Embedded Questions`,
+        widget: EmbeddedQuestionListing,
+        getHidden: settings => !settings["enable-embedding"],
+      },
+    ],
+  },
+  {
+    name: t`Caching`,
+    settings: [
+      {
+        key: "enable-query-caching",
+        display_name: t`Enable Caching`,
+        type: "boolean",
+      },
+      {
+        key: "query-caching-min-ttl",
+        display_name: t`Minimum Query Duration`,
+        type: "number",
+        getHidden: settings => !settings["enable-query-caching"],
+        allowValueCollection: true,
+      },
+      {
+        key: "query-caching-ttl-ratio",
+        display_name: t`Cache Time-To-Live (TTL) multiplier`,
+        type: "number",
+        getHidden: settings => !settings["enable-query-caching"],
+        allowValueCollection: true,
+      },
+      {
+        key: "query-caching-max-kb",
+        display_name: t`Max Cache Entry Size`,
+        type: "number",
+        getHidden: settings => !settings["enable-query-caching"],
+        allowValueCollection: true,
+      },
+    ],
+  },
+  {
+    name: t`X-Rays`,
+    settings: [
+      {
+        key: "enable-xrays",
+        display_name: t`Enable X-Rays`,
+        type: "boolean",
+        allowValueCollection: true,
+      },
+      {
+        key: "xray-max-cost",
+        type: "string",
+        allowValueCollection: true,
+      },
+    ],
+  },
+  /*
     {
         name: "Premium Embedding",
         settings: [
@@ -418,76 +432,70 @@ const SECTIONS = [
     */
 ];
 for (const section of SECTIONS) {
-    section.slug = slugify(section.name);
+  section.slug = slugify(section.name);
 }
 
 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
-        )
-)
+  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,
-    (settings) => {
-        const settingValues = {};
-        for (const setting of settings) {
-            settingValues[setting.key] = setting.value;
-        }
-        return settingValues;
-    }
-)
+export const getSettingValues = createSelector(getSettings, settings => {
+  const settingValues = {};
+  for (const setting of settings) {
+    settingValues[setting.key] = setting.value;
+  }
+  return settingValues;
+});
 
-export const getNewVersionAvailable = createSelector(
-    getSettings,
-    (settings) => {
-        return MetabaseSettings.newVersionAvailable(settings);
-    }
-);
+export const getNewVersionAvailable = createSelector(getSettings, settings => {
+  return MetabaseSettings.newVersionAvailable(settings);
+});
 
-export const getSections = createSelector(
-    getSettings,
-    (settings) => {
-        if (!settings || _.isEmpty(settings)) {
-            return [];
-        }
+export const getSections = createSelector(getSettings, settings => {
+  if (!settings || _.isEmpty(settings)) {
+    return [];
+  }
 
-        let settingsByKey = _.groupBy(settings, 'key');
-        return SECTIONS.map(function(section) {
-            let sectionSettings = section.settings.map(function(setting) {
-                const apiSetting = settingsByKey[setting.key] && settingsByKey[setting.key][0];
-                if (apiSetting) {
-                    return {
-                        placeholder: apiSetting.default,
-                        ...apiSetting,
-                        ...setting
-                    };
-                } else {
-                    return setting;
-                }
-            });
-            return {
-                ...section,
-                settings: sectionSettings
-            };
-        });
-    }
-);
+  let settingsByKey = _.groupBy(settings, "key");
+  return SECTIONS.map(function(section) {
+    let sectionSettings = section.settings.map(function(setting) {
+      const apiSetting =
+        settingsByKey[setting.key] && settingsByKey[setting.key][0];
+      if (apiSetting) {
+        return {
+          placeholder: apiSetting.default,
+          ...apiSetting,
+          ...setting,
+        };
+      } else {
+        return setting;
+      }
+    });
+    return {
+      ...section,
+      settings: sectionSettings,
+    };
+  });
+});
 
-export const getActiveSectionName = (state, props) => props.params.section
+export const getActiveSectionName = (state, props) => props.params.section;
 
 export const getActiveSection = createSelector(
-    getActiveSectionName,
-    getSections,
-    (section = "setup", sections) => {
-        if (sections) {
-            return _.findWhere(sections, { slug: section });
-        } else {
-            return null;
-        }
+  getActiveSectionName,
+  getSections,
+  (section = "setup", sections) => {
+    if (sections) {
+      return _.findWhere(sections, { slug: section });
+    } else {
+      return null;
     }
+  },
 );
diff --git a/frontend/src/metabase/admin/settings/settings.js b/frontend/src/metabase/admin/settings/settings.js
index 366c0879685f4e3e2b916171379803757587df8c..cc821ae6838bbc865a5e0a25cd6fc50157cc0bdf 100644
--- a/frontend/src/metabase/admin/settings/settings.js
+++ b/frontend/src/metabase/admin/settings/settings.js
@@ -1,5 +1,8 @@
-
-import { createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
+import {
+  createThunkAction,
+  handleActions,
+  combineReducers,
+} from "metabase/lib/redux";
 
 import { SettingsApi, EmailApi, SlackApi, LdapApi } from "metabase/services";
 
@@ -7,96 +10,120 @@ import { refreshSiteSettings } from "metabase/redux/settings";
 
 // ACITON TYPES AND ACTION CREATORS
 
-export const INITIALIZE_SETTINGS = "metabase/admin/settings/INITIALIZE_SETTINGS";
-export const initializeSettings = createThunkAction(INITIALIZE_SETTINGS, function() {
+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());
-        } catch(error) {
-            console.log("error fetching settings", error);
-            throw error;
-        }
+      try {
+        await dispatch(refreshSiteSettings());
+      } catch (error) {
+        console.log("error fetching settings", error);
+        throw error;
+      }
     };
-});
+  },
+);
 
 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);
-            await dispatch(refreshSiteSettings());
-        } catch(error) {
-            console.log("error updating setting", setting, error);
-            throw error;
-        }
-    };
+export const updateSetting = createThunkAction(UPDATE_SETTING, function(
+  setting,
+) {
+  return async function(dispatch, getState) {
+    try {
+      await SettingsApi.put(setting);
+      await dispatch(refreshSiteSettings());
+    } catch (error) {
+      console.log("error updating setting", setting, error);
+      throw error;
+    }
+  };
 });
 
-export const UPDATE_EMAIL_SETTINGS = "metabase/admin/settings/UPDATE_EMAIL_SETTINGS";
-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 {
-            const result = await EmailApi.updateSettings(settings);
-            await dispatch(refreshSiteSettings());
-            return result;
-        } catch(error) {
-            console.log("error updating email settings", settings, error);
-            throw error;
-        }
+      try {
+        const result = await EmailApi.updateSettings(settings);
+        await dispatch(refreshSiteSettings());
+        return result;
+      } catch (error) {
+        console.log("error updating email settings", settings, error);
+        throw error;
+      }
     };
-});
+  },
+);
 
 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();
-        } catch(error) {
-            console.log("error sending test email", error);
-            throw error;
-        }
-    };
+  return async function(dispatch, getState) {
+    try {
+      await EmailApi.sendTest();
+    } catch (error) {
+      console.log("error sending test email", error);
+      throw error;
+    }
+  };
 });
 
-export const UPDATE_SLACK_SETTINGS = "metabase/admin/settings/UPDATE_SLACK_SETTINGS";
-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);
-            await dispatch(refreshSiteSettings());
-        } catch(error) {
-            console.log("error updating slack settings", settings, error);
-            throw error;
-        }
+      try {
+        await SlackApi.updateSettings(settings);
+        await dispatch(refreshSiteSettings());
+      } catch (error) {
+        console.log("error updating slack settings", settings, error);
+        throw error;
+      }
     };
-}, {});
+  },
+  {},
+);
 
-export const UPDATE_LDAP_SETTINGS = "metabase/admin/settings/UPDATE_LDAP_SETTINGS";
-export const updateLdapSettings = createThunkAction(UPDATE_LDAP_SETTINGS, function(settings) {
+export const UPDATE_LDAP_SETTINGS =
+  "metabase/admin/settings/UPDATE_LDAP_SETTINGS";
+export const updateLdapSettings = createThunkAction(
+  UPDATE_LDAP_SETTINGS,
+  function(settings) {
     return async function(dispatch, getState) {
-        try {
-            await LdapApi.updateSettings(settings);
-            await dispatch(refreshSiteSettings());
-        } catch(error) {
-            console.log("error updating LDAP settings", settings, error);
-            throw error;
-        }
+      try {
+        await LdapApi.updateSettings(settings);
+        await dispatch(refreshSiteSettings());
+      } catch (error) {
+        console.log("error updating LDAP settings", settings, error);
+        throw error;
+      }
     };
-});
+  },
+);
 
 export const RELOAD_SETTINGS = "metabase/admin/settings/RELOAD_SETTINGS";
 export const reloadSettings = createThunkAction(RELOAD_SETTINGS, function() {
-    return async function(dispatch, getState) {
-        await dispatch(refreshSiteSettings());
-    }
+  return async function(dispatch, getState) {
+    await dispatch(refreshSiteSettings());
+  };
 });
 
 // REDUCERS
 
-export const warnings = handleActions({
-    [UPDATE_EMAIL_SETTINGS]: { next: (state, { payload }) => payload["with-corrections"] }
-}, {});
+export const warnings = handleActions(
+  {
+    [UPDATE_EMAIL_SETTINGS]: {
+      next: (state, { payload }) => payload["with-corrections"],
+    },
+  },
+  {},
+);
 
 export default combineReducers({
-    warnings
+  warnings,
 });
diff --git a/frontend/src/metabase/admin/settings/utils.js b/frontend/src/metabase/admin/settings/utils.js
index 7d1979e37c258fd6ca9945b3b619dc5c73c9a530..9316bd34f99ed400b31509c3e0ecb25e56c47cb0 100644
--- a/frontend/src/metabase/admin/settings/utils.js
+++ b/frontend/src/metabase/admin/settings/utils.js
@@ -1,6 +1,6 @@
 // in order to prevent collection of identifying information only fields
 // that are explicitly marked as collectable or booleans should show the true value
-export const prepareAnalyticsValue = (setting) =>
-    (setting.allowValueCollection || setting.type === "boolean")
-        ? setting.value
-        : "success"
+export const prepareAnalyticsValue = setting =>
+  setting.allowValueCollection || setting.type === "boolean"
+    ? setting.value
+    : "success";
diff --git a/frontend/src/metabase/alert/alert.js b/frontend/src/metabase/alert/alert.js
index 5159712eb8862457b81e9eb040c3d52020576310..5d758a8c09a47c1270ba7807ea2050c560d192fa 100644
--- a/frontend/src/metabase/alert/alert.js
+++ b/frontend/src/metabase/alert/alert.js
@@ -1,138 +1,184 @@
 import React from "react";
-import _ from "underscore"
+import _ from "underscore";
 import { handleActions } from "redux-actions";
 import { combineReducers } from "redux";
 import { addUndo, createUndo } from "metabase/redux/undo";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { AlertApi } from "metabase/services";
 import { RestfulRequest } from "metabase/lib/request";
 import Icon from "metabase/components/Icon.jsx";
 
-export const FETCH_ALL_ALERTS = 'metabase/alerts/FETCH_ALL_ALERTS'
+export const FETCH_ALL_ALERTS = "metabase/alerts/FETCH_ALL_ALERTS";
 const fetchAllAlertsRequest = new RestfulRequest({
-    endpoint: AlertApi.list,
-    actionPrefix: FETCH_ALL_ALERTS,
-    storeAsDictionary: true
-})
+  endpoint: AlertApi.list,
+  actionPrefix: FETCH_ALL_ALERTS,
+  storeAsDictionary: true,
+});
 export const fetchAllAlerts = () => {
-    return async (dispatch, getState) => {
-        await dispatch(fetchAllAlertsRequest.trigger())
-        dispatch.action(FETCH_ALL_ALERTS)
-    }
-}
-
-export const FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS = 'metabase/alerts/FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS'
-export const FETCH_ALERTS_FOR_QUESTION = 'metabase/alerts/FETCH_ALERTS_FOR_QUESTION'
+  return async (dispatch, getState) => {
+    await dispatch(fetchAllAlertsRequest.trigger());
+    dispatch.action(FETCH_ALL_ALERTS);
+  };
+};
+
+export const FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS =
+  "metabase/alerts/FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS";
+export const FETCH_ALERTS_FOR_QUESTION =
+  "metabase/alerts/FETCH_ALERTS_FOR_QUESTION";
 const fetchAlertsForQuestionRequest = new RestfulRequest({
-    endpoint: AlertApi.list_for_question,
-    actionPrefix: FETCH_ALERTS_FOR_QUESTION,
-    storeAsDictionary: true
-})
-export const fetchAlertsForQuestion = (questionId) => {
-    return async (dispatch, getState) => {
-        dispatch.action(FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS, questionId)
-        await dispatch(fetchAlertsForQuestionRequest.trigger({ questionId }))
-        dispatch.action(FETCH_ALERTS_FOR_QUESTION)
-    }
-}
-
-export const CREATE_ALERT = 'metabase/alerts/CREATE_ALERT'
+  endpoint: AlertApi.list_for_question,
+  actionPrefix: FETCH_ALERTS_FOR_QUESTION,
+  storeAsDictionary: true,
+});
+export const fetchAlertsForQuestion = questionId => {
+  return async (dispatch, getState) => {
+    dispatch.action(FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS, questionId);
+    await dispatch(fetchAlertsForQuestionRequest.trigger({ questionId }));
+    dispatch.action(FETCH_ALERTS_FOR_QUESTION);
+  };
+};
+
+export const CREATE_ALERT = "metabase/alerts/CREATE_ALERT";
 const createAlertRequest = new RestfulRequest({
-    endpoint: AlertApi.create,
-    actionPrefix: CREATE_ALERT,
-    storeAsDictionary: true
-})
-export const createAlert = (alert) => {
-    return async (dispatch, getState) => {
-        // TODO: How to handle a failed creation and display it to a user?
-        // Maybe RestfulRequest.trigger should throw an exception
-        // that the React component calling createAlert could catch ...?
-        await dispatch(createAlertRequest.trigger(alert))
-
-        dispatch(addUndo(createUndo({
-            type: "create-alert",
-            // eslint-disable-next-line react/display-name
-            message: () => <div className="flex align-center text-bold"><Icon name="alertConfirm" size="19" className="mr2 text-success" />{t`Your alert is all set up.`}</div>,
-            action: null // alert creation is not undoable
-        })));
-
-        dispatch.action(CREATE_ALERT)
-    }
-}
-
-export const UPDATE_ALERT = 'metabase/alerts/UPDATE_ALERT'
+  endpoint: AlertApi.create,
+  actionPrefix: CREATE_ALERT,
+  storeAsDictionary: true,
+});
+export const createAlert = alert => {
+  return async (dispatch, getState) => {
+    // TODO: How to handle a failed creation and display it to a user?
+    // Maybe RestfulRequest.trigger should throw an exception
+    // that the React component calling createAlert could catch ...?
+    await dispatch(createAlertRequest.trigger(alert));
+
+    dispatch(
+      addUndo(
+        createUndo({
+          type: "create-alert",
+          // eslint-disable-next-line react/display-name
+          message: () => (
+            <div className="flex align-center text-bold">
+              <Icon
+                name="alertConfirm"
+                size="19"
+                className="mr2 text-success"
+              />
+              {t`Your alert is all set up.`}
+            </div>
+          ),
+          action: null, // alert creation is not undoable
+        }),
+      ),
+    );
+
+    dispatch.action(CREATE_ALERT);
+  };
+};
+
+export const UPDATE_ALERT = "metabase/alerts/UPDATE_ALERT";
 const updateAlertRequest = new RestfulRequest({
-    endpoint: AlertApi.update,
-    actionPrefix: UPDATE_ALERT,
-    storeAsDictionary: true
-})
-export const updateAlert = (alert) => {
-    return async (dispatch, getState) => {
-        await dispatch(updateAlertRequest.trigger(alert))
-
-        dispatch(addUndo(createUndo({
-            type: "update-alert",
-            // eslint-disable-next-line react/display-name
-            message: () => <div className="flex align-center text-bold"><Icon name="alertConfirm" size="19" className="mr2 text-success" />{t`Your alert was updated.`}</div>,
-            action: null // alert updating is not undoable
-        })));
-
-        dispatch.action(UPDATE_ALERT)
-    }
-}
-
-export const UNSUBSCRIBE_FROM_ALERT = 'metabase/alerts/UNSUBSCRIBE_FROM_ALERT'
-export const UNSUBSCRIBE_FROM_ALERT_CLEANUP = 'metabase/alerts/UNSUBSCRIBE_FROM_ALERT_CLEANUP'
+  endpoint: AlertApi.update,
+  actionPrefix: UPDATE_ALERT,
+  storeAsDictionary: true,
+});
+export const updateAlert = alert => {
+  return async (dispatch, getState) => {
+    await dispatch(updateAlertRequest.trigger(alert));
+
+    dispatch(
+      addUndo(
+        createUndo({
+          type: "update-alert",
+          // eslint-disable-next-line react/display-name
+          message: () => (
+            <div className="flex align-center text-bold">
+              <Icon
+                name="alertConfirm"
+                size="19"
+                className="mr2 text-success"
+              />
+              {t`Your alert was updated.`}
+            </div>
+          ),
+          action: null, // alert updating is not undoable
+        }),
+      ),
+    );
+
+    dispatch.action(UPDATE_ALERT);
+  };
+};
+
+export const UNSUBSCRIBE_FROM_ALERT = "metabase/alerts/UNSUBSCRIBE_FROM_ALERT";
+export const UNSUBSCRIBE_FROM_ALERT_CLEANUP =
+  "metabase/alerts/UNSUBSCRIBE_FROM_ALERT_CLEANUP";
 const unsubscribeFromAlertRequest = new RestfulRequest({
-    endpoint: AlertApi.unsubscribe,
-    actionPrefix: UNSUBSCRIBE_FROM_ALERT,
-    storeAsDictionary: true
-})
-export const unsubscribeFromAlert = (alert) => {
-    return async (dispatch, getState) => {
-        await dispatch(unsubscribeFromAlertRequest.trigger(alert))
-        dispatch.action(UNSUBSCRIBE_FROM_ALERT)
-
-        // This delay lets us to show "You're unsubscribed" text in place of an
-        // alert list item for a while before removing the list item completely
-        setTimeout(() => dispatch.action(UNSUBSCRIBE_FROM_ALERT_CLEANUP, alert.id), 5000)
-    }
-}
-
-export const DELETE_ALERT = 'metabase/alerts/DELETE_ALERT'
+  endpoint: AlertApi.unsubscribe,
+  actionPrefix: UNSUBSCRIBE_FROM_ALERT,
+  storeAsDictionary: true,
+});
+export const unsubscribeFromAlert = alert => {
+  return async (dispatch, getState) => {
+    await dispatch(unsubscribeFromAlertRequest.trigger(alert));
+    dispatch.action(UNSUBSCRIBE_FROM_ALERT);
+
+    // This delay lets us to show "You're unsubscribed" text in place of an
+    // alert list item for a while before removing the list item completely
+    setTimeout(
+      () => dispatch.action(UNSUBSCRIBE_FROM_ALERT_CLEANUP, alert.id),
+      5000,
+    );
+  };
+};
+
+export const DELETE_ALERT = "metabase/alerts/DELETE_ALERT";
 const deleteAlertRequest = new RestfulRequest({
-    endpoint: AlertApi.delete,
-    actionPrefix: DELETE_ALERT,
-    storeAsDictionary: true
-})
-export const deleteAlert = (alertId) => {
-    return async (dispatch, getState) => {
-        await dispatch(deleteAlertRequest.trigger({ id: alertId }))
-
-        dispatch(addUndo(createUndo({
-            type: "delete-alert",
-            // eslint-disable-next-line react/display-name
-            message: () => <div className="flex align-center text-bold"><Icon name="alertConfirm" size="19" className="mr2 text-success" />{t`The alert was successfully deleted.`}</div>,
-            action: null // alert deletion is not undoable
-        })));
-        dispatch.action(DELETE_ALERT, alertId)
-    }
-}
+  endpoint: AlertApi.delete,
+  actionPrefix: DELETE_ALERT,
+  storeAsDictionary: true,
+});
+export const deleteAlert = alertId => {
+  return async (dispatch, getState) => {
+    await dispatch(deleteAlertRequest.trigger({ id: alertId }));
+
+    dispatch(
+      addUndo(
+        createUndo({
+          type: "delete-alert",
+          // eslint-disable-next-line react/display-name
+          message: () => (
+            <div className="flex align-center text-bold">
+              <Icon
+                name="alertConfirm"
+                size="19"
+                className="mr2 text-success"
+              />
+              {t`The alert was successfully deleted.`}
+            </div>
+          ),
+          action: null, // alert deletion is not undoable
+        }),
+      ),
+    );
+    dispatch.action(DELETE_ALERT, alertId);
+  };
+};
 
 // removal from the result dictionary (not supported by RestfulRequest yet)
 const removeAlertReducer = (state, { payload: alertId }) => ({
-    ...state,
-    result: _.omit(state.result || {}, alertId)
-})
+  ...state,
+  result: _.omit(state.result || {}, alertId),
+});
 
 const removeAlertsForQuestionReducer = (state, { payload: questionId }) => {
-    return ({
-        ...state,
-        result: _.omit(state.result || {}, (alert) => alert.card.id === questionId)
-    })
-}
+  return {
+    ...state,
+    result: _.omit(state.result || {}, alert => alert.card.id === questionId),
+  };
+};
 
-const alerts = handleActions({
+const alerts = handleActions(
+  {
     ...fetchAllAlertsRequest.getReducers(),
     [FETCH_ALERTS_FOR_QUESTION_CLEAR_OLD_ALERTS]: removeAlertsForQuestionReducer,
     ...fetchAlertsForQuestionRequest.getReducers(),
@@ -140,8 +186,10 @@ const alerts = handleActions({
     ...updateAlertRequest.getReducers(),
     [DELETE_ALERT]: removeAlertReducer,
     [UNSUBSCRIBE_FROM_ALERT_CLEANUP]: removeAlertReducer,
-}, []);
+  },
+  [],
+);
 
 export default combineReducers({
-    alerts
+  alerts,
 });
diff --git a/frontend/src/metabase/alert/selectors.js b/frontend/src/metabase/alert/selectors.js
index c59f42e77c9fe85419a038efd4717d014da19fb4..18ba2d63354aa62127d20d1819320093042a9f88 100644
--- a/frontend/src/metabase/alert/selectors.js
+++ b/frontend/src/metabase/alert/selectors.js
@@ -1 +1 @@
-export const getAlerts = (state) => state.alert.alerts.result
+export const getAlerts = state => state.alert.alerts.result;
diff --git a/frontend/src/metabase/app-embed.js b/frontend/src/metabase/app-embed.js
index 64a802267ff9a44c6e84dcff52283e793ff60178..c73ea000448125c1e59c99db4979224b46b34ee7 100644
--- a/frontend/src/metabase/app-embed.js
+++ b/frontend/src/metabase/app-embed.js
@@ -3,15 +3,15 @@
  * file 'LICENSE-EMBEDDING.txt', which is part of this source code package.
  */
 
- import { init } from "./app";
+import { init } from "./app";
 
 import { getRoutes } from "./routes-embed.jsx";
-import reducers from './reducers-public';
+import reducers from "./reducers-public";
 
 import { IFRAMED } from "metabase/lib/dom";
 
 init(reducers, getRoutes, () => {
-    if (IFRAMED) {
-        document.body.style.backgroundColor = "transparent";
-    }
-})
+  if (IFRAMED) {
+    document.body.style.backgroundColor = "transparent";
+  }
+});
diff --git a/frontend/src/metabase/app-main.js b/frontend/src/metabase/app-main.js
index 169ad090a05c8749f8c21dff82506369a2b95353..5dba41cdfe6713835855b302c0e3c1155d91342f 100644
--- a/frontend/src/metabase/app-main.js
+++ b/frontend/src/metabase/app-main.js
@@ -1,9 +1,8 @@
-
-import { push } from 'react-router-redux'
+import { push } from "react-router-redux";
 
 import { init } from "metabase/app";
 import { getRoutes } from "metabase/routes.jsx";
-import reducers from 'metabase/reducers-main';
+import reducers from "metabase/reducers-main";
 
 import api from "metabase/lib/api";
 
@@ -12,36 +11,36 @@ 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$/
+  // 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"));
-    });
+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;
-                }
-            }
+  // 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 }));
-    });
-})
+      }
+    }
+    store.dispatch(setErrorPage({ status: 403 }));
+  });
+});
diff --git a/frontend/src/metabase/app-public.js b/frontend/src/metabase/app-public.js
index 434da5c617bdd5b2852ac152b88af5ba88ef4b46..3c2e6afa78fb1804bd0d8e493c2ba06aa0816e9c 100644
--- a/frontend/src/metabase/app-public.js
+++ b/frontend/src/metabase/app-public.js
@@ -1,7 +1,6 @@
 import { init } from "./app";
 
 import { getRoutes } from "./routes-public.jsx";
-import reducers from './reducers-public';
+import reducers from "./reducers-public";
 
-init(reducers, getRoutes, () => {
-})
+init(reducers, getRoutes, () => {});
diff --git a/frontend/src/metabase/app.js b/frontend/src/metabase/app.js
index fc84e2098ff6c05b1dc61c832d916745835b3507..7fc4d2ddcbf3d3316f20d6ee49075dac6bda2eb5 100644
--- a/frontend/src/metabase/app.js
+++ b/frontend/src/metabase/app.js
@@ -1,7 +1,7 @@
 /* @flow weak */
 
-import 'babel-polyfill';
-import 'number-to-locale-string';
+import "babel-polyfill";
+import "number-to-locale-string";
 
 // If enabled this monkeypatches `t` and `jt` to return blacked out
 // strings/elements to assist in finding untranslated strings.
@@ -15,25 +15,27 @@ global.jt = jt;
 // set the locale before loading anything else
 import { setLocalization } from "metabase/lib/i18n";
 if (window.MetabaseLocalization) {
-    setLocalization(window.MetabaseLocalization)
+  setLocalization(window.MetabaseLocalization);
 }
 
-import React from 'react'
-import ReactDOM from 'react-dom'
-import { Provider } from 'react-redux'
+import React from "react";
+import ReactDOM from "react-dom";
+import { Provider } from "react-redux";
 
-import MetabaseAnalytics, { registerAnalyticsClickListener } from "metabase/lib/analytics";
+import MetabaseAnalytics, {
+  registerAnalyticsClickListener,
+} from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
 import api from "metabase/lib/api";
 
-import { getStore } from './store'
+import { getStore } from "./store";
 
 import { refreshSiteSettings } from "metabase/redux/settings";
 
 import { Router, useRouterHistory } from "react-router";
-import { createHistory } from 'history'
-import { syncHistoryWithStore } from 'react-router-redux';
+import { createHistory } from "history";
+import { syncHistoryWithStore } from "react-router-redux";
 
 // remove trailing slash
 const BASENAME = window.MetabaseRoot.replace(/\/+$/, "");
@@ -41,45 +43,46 @@ const BASENAME = window.MetabaseRoot.replace(/\/+$/, "");
 api.basename = BASENAME;
 
 const browserHistory = useRouterHistory(createHistory)({
-    basename: BASENAME
+  basename: BASENAME,
 });
 
 function _init(reducers, getRoutes, callback) {
-    const store = getStore(reducers, browserHistory);
-    const routes = getRoutes(store);
-    const history = syncHistoryWithStore(browserHistory, store);
-
-    ReactDOM.render(
-        <Provider store={store}>
-          <Router history={history}>
-            {routes}
-          </Router>
-        </Provider>
-    , document.getElementById('root'));
-
-    // listen for location changes and use that as a trigger for page view tracking
-    history.listen(location => {
-        MetabaseAnalytics.trackPageView(location.pathname);
-    });
-
-    registerAnalyticsClickListener();
-
-    store.dispatch(refreshSiteSettings());
-
-    // enable / disable GA based on opt-out of anonymous tracking
-    MetabaseSettings.on("anon_tracking_enabled", () => {
-        window['ga-disable-' + MetabaseSettings.get('ga_code')] = MetabaseSettings.isTrackingEnabled() ? null : true;
-    });
-
-    if (callback) {
-        callback(store);
-    }
+  const store = getStore(reducers, browserHistory);
+  const routes = getRoutes(store);
+  const history = syncHistoryWithStore(browserHistory, store);
+
+  ReactDOM.render(
+    <Provider store={store}>
+      <Router history={history}>{routes}</Router>
+    </Provider>,
+    document.getElementById("root"),
+  );
+
+  // listen for location changes and use that as a trigger for page view tracking
+  history.listen(location => {
+    MetabaseAnalytics.trackPageView(location.pathname);
+  });
+
+  registerAnalyticsClickListener();
+
+  store.dispatch(refreshSiteSettings());
+
+  // enable / disable GA based on opt-out of anonymous tracking
+  MetabaseSettings.on("anon_tracking_enabled", () => {
+    window[
+      "ga-disable-" + MetabaseSettings.get("ga_code")
+    ] = MetabaseSettings.isTrackingEnabled() ? null : true;
+  });
+
+  if (callback) {
+    callback(store);
+  }
 }
 
 export function init(...args) {
-    if (document.readyState != 'loading') {
-        _init(...args);
-    } else {
-        document.addEventListener('DOMContentLoaded', () => _init(...args));
-    }
+  if (document.readyState != "loading") {
+    _init(...args);
+  } else {
+    document.addEventListener("DOMContentLoaded", () => _init(...args));
+  }
 }
diff --git a/frontend/src/metabase/auth/auth.js b/frontend/src/metabase/auth/auth.js
index 3f6fff97c3c0af6a6d67f89f960325880b92ed06..7d249fad2d1c0b203cf1bfb3f055b79cc69a86e4 100644
--- a/frontend/src/metabase/auth/auth.js
+++ b/frontend/src/metabase/auth/auth.js
@@ -1,5 +1,8 @@
-
-import { handleActions, combineReducers, createThunkAction } from "metabase/lib/redux";
+import {
+  handleActions,
+  combineReducers,
+  createThunkAction,
+} from "metabase/lib/redux";
 
 import { push } from "react-router-redux";
 
@@ -7,142 +10,164 @@ import MetabaseCookies from "metabase/lib/cookies";
 import MetabaseUtils from "metabase/lib/utils";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { clearGoogleAuthCredentials } from "metabase/lib/auth";
 
 import { refreshCurrentUser } from "metabase/redux/user";
 
 import { SessionApi } from "metabase/services";
 
-
 // login
 export const LOGIN = "metabase/auth/LOGIN";
-export const login = createThunkAction(LOGIN, function(credentials, redirectUrl) {
-    return async function(dispatch, getState) {
-
-        if (!MetabaseSettings.ldapEnabled() && !MetabaseUtils.validEmail(credentials.username)) {
-            return {'data': {'errors': {'email': t`Please enter a valid formatted email address.`}}};
-        }
-
-        try {
-            let newSession = await SessionApi.create(credentials);
-
-            // since we succeeded, lets set the session cookie
-            MetabaseCookies.setSessionCookie(newSession.id);
-
-            MetabaseAnalytics.trackEvent('Auth', 'Login');
-            // TODO: redirect after login (carry user to intended destination)
-            await dispatch(refreshCurrentUser());
-            dispatch(push(redirectUrl || "/"));
-
-        } catch (error) {
-            return error;
-        }
-    };
+export const login = createThunkAction(LOGIN, function(
+  credentials,
+  redirectUrl,
+) {
+  return async function(dispatch, getState) {
+    if (
+      !MetabaseSettings.ldapEnabled() &&
+      !MetabaseUtils.validEmail(credentials.username)
+    ) {
+      return {
+        data: {
+          errors: { email: t`Please enter a valid formatted email address.` },
+        },
+      };
+    }
+
+    try {
+      let newSession = await SessionApi.create(credentials);
+
+      // since we succeeded, lets set the session cookie
+      MetabaseCookies.setSessionCookie(newSession.id);
+
+      MetabaseAnalytics.trackEvent("Auth", "Login");
+      // TODO: redirect after login (carry user to intended destination)
+      await dispatch(refreshCurrentUser());
+      dispatch(push(redirectUrl || "/"));
+    } catch (error) {
+      return error;
+    }
+  };
 });
 
-
 // login Google
 export const LOGIN_GOOGLE = "metabase/auth/LOGIN_GOOGLE";
-export const loginGoogle = createThunkAction(LOGIN_GOOGLE, function(googleUser, redirectUrl) {
-    return async function(dispatch, getState) {
-        try {
-            let newSession = await SessionApi.createWithGoogleAuth({
-                token: googleUser.getAuthResponse().id_token
-            });
-
-            // since we succeeded, lets set the session cookie
-            MetabaseCookies.setSessionCookie(newSession.id);
-
-            MetabaseAnalytics.trackEvent('Auth', 'Google Auth Login');
-
-            // TODO: redirect after login (carry user to intended destination)
-            await dispatch(refreshCurrentUser());
-            dispatch(push(redirectUrl || "/"));
-
-        } catch (error) {
-            clearGoogleAuthCredentials();
-            // If we see a 428 ("Precondition Required") that means we need to show the "No Metabase account exists for this Google Account" page
-            if (error.status === 428) {
-                dispatch(push("/auth/google_no_mb_account"));
-            } else {
-                return error;
-            }
-        }
-    };
+export const loginGoogle = createThunkAction(LOGIN_GOOGLE, function(
+  googleUser,
+  redirectUrl,
+) {
+  return async function(dispatch, getState) {
+    try {
+      let newSession = await SessionApi.createWithGoogleAuth({
+        token: googleUser.getAuthResponse().id_token,
+      });
+
+      // since we succeeded, lets set the session cookie
+      MetabaseCookies.setSessionCookie(newSession.id);
+
+      MetabaseAnalytics.trackEvent("Auth", "Google Auth Login");
+
+      // TODO: redirect after login (carry user to intended destination)
+      await dispatch(refreshCurrentUser());
+      dispatch(push(redirectUrl || "/"));
+    } catch (error) {
+      clearGoogleAuthCredentials();
+      // If we see a 428 ("Precondition Required") that means we need to show the "No Metabase account exists for this Google Account" page
+      if (error.status === 428) {
+        dispatch(push("/auth/google_no_mb_account"));
+      } else {
+        return error;
+      }
+    }
+  };
 });
 
 // logout
 export const LOGOUT = "metabase/auth/LOGOUT";
 export const logout = createThunkAction(LOGOUT, function() {
-    return function(dispatch, getState) {
-        // TODO: as part of a logout we want to clear out any saved state that we have about anything
-
-        let sessionId = MetabaseCookies.setSessionCookie();
-        if (sessionId) {
-            // actively delete the session
-            SessionApi.delete({'session_id': sessionId});
-        }
-        MetabaseAnalytics.trackEvent('Auth', 'Logout');
-
-        dispatch(push("/auth/login"));
-    };
+  return function(dispatch, getState) {
+    // TODO: as part of a logout we want to clear out any saved state that we have about anything
+
+    let sessionId = MetabaseCookies.setSessionCookie();
+    if (sessionId) {
+      // actively delete the session
+      SessionApi.delete({ session_id: sessionId });
+    }
+    MetabaseAnalytics.trackEvent("Auth", "Logout");
+
+    dispatch(push("/auth/login"));
+  };
 });
 
 // passwordReset
-export const PASSWORD_RESET = "metabase/auth/PASSWORD_RESET"
-export const passwordReset = createThunkAction(PASSWORD_RESET, function(token, credentials) {
-    return async function(dispatch, getState) {
-
-        if (credentials.password !== credentials.password2) {
-            return {
-                success: false,
-                error: {'data': {'errors': {'password2': t`Passwords do not match`}}}
-            };
-        }
-
-        try {
-            let result = await SessionApi.reset_password({'token': token, 'password': credentials.password});
-
-            if (result.session_id) {
-                // we should have a valid session that we can use immediately!
-                MetabaseCookies.setSessionCookie(result.session_id);
-            }
-
-            MetabaseAnalytics.trackEvent('Auth', 'Password Reset');
-
-            return {
-                success: true,
-                error: null
-            }
-        } catch (error) {
-            return {
-                success: false,
-                error
-            };
-        }
-    };
+export const PASSWORD_RESET = "metabase/auth/PASSWORD_RESET";
+export const passwordReset = createThunkAction(PASSWORD_RESET, function(
+  token,
+  credentials,
+) {
+  return async function(dispatch, getState) {
+    if (credentials.password !== credentials.password2) {
+      return {
+        success: false,
+        error: { data: { errors: { password2: t`Passwords do not match` } } },
+      };
+    }
+
+    try {
+      let result = await SessionApi.reset_password({
+        token: token,
+        password: credentials.password,
+      });
+
+      if (result.session_id) {
+        // we should have a valid session that we can use immediately!
+        MetabaseCookies.setSessionCookie(result.session_id);
+      }
+
+      MetabaseAnalytics.trackEvent("Auth", "Password Reset");
+
+      return {
+        success: true,
+        error: null,
+      };
+    } catch (error) {
+      return {
+        success: false,
+        error,
+      };
+    }
+  };
 });
 
-
 // reducers
 
-const loginError = handleActions({
-    [LOGIN]: { next: (state, { payload }) => payload ? payload : null },
-    [LOGIN_GOOGLE]: { next: (state, { payload }) => payload ? payload : null }
-}, null);
-
-
-const resetSuccess = handleActions({
-    [PASSWORD_RESET]: { next: (state, { payload }) => payload.success }
-}, false);
-
-const resetError = handleActions({
-    [PASSWORD_RESET]: { next: (state, { payload }) => payload.error }
-}, null);
+const loginError = handleActions(
+  {
+    [LOGIN]: { next: (state, { payload }) => (payload ? payload : null) },
+    [LOGIN_GOOGLE]: {
+      next: (state, { payload }) => (payload ? payload : null),
+    },
+  },
+  null,
+);
+
+const resetSuccess = handleActions(
+  {
+    [PASSWORD_RESET]: { next: (state, { payload }) => payload.success },
+  },
+  false,
+);
+
+const resetError = handleActions(
+  {
+    [PASSWORD_RESET]: { next: (state, { payload }) => payload.error },
+  },
+  null,
+);
 
 export default combineReducers({
-    loginError,
-    resetError,
-    resetSuccess
+  loginError,
+  resetError,
+  resetSuccess,
 });
diff --git a/frontend/src/metabase/auth/components/AuthScene.jsx b/frontend/src/metabase/auth/components/AuthScene.jsx
index 5a00cebce7863e68a0aa865cab3eea906ad2dea8..5e392ff8a62e8f60c605d3be0f12fea2155e222c 100644
--- a/frontend/src/metabase/auth/components/AuthScene.jsx
+++ b/frontend/src/metabase/auth/components/AuthScene.jsx
@@ -2,116 +2,527 @@ import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
 export default class AuthScene extends Component {
-    componentDidMount() {
-        // HACK: React 0.14 doesn't support "fill-rule" attribute. We can remove this when upgrading to React 0.15.
-        ReactDOM.findDOMNode(this.refs.HACK_fill_rule_1).setAttribute("fill-rule", "evenodd");
-        ReactDOM.findDOMNode(this.refs.HACK_fill_rule_2).setAttribute("fill-rule", "evenodd");
-    }
+  componentDidMount() {
+    // HACK: React 0.14 doesn't support "fill-rule" attribute. We can remove this when upgrading to React 0.15.
+    ReactDOM.findDOMNode(this.refs.HACK_fill_rule_1).setAttribute(
+      "fill-rule",
+      "evenodd",
+    );
+    ReactDOM.findDOMNode(this.refs.HACK_fill_rule_2).setAttribute(
+      "fill-rule",
+      "evenodd",
+    );
+  }
 
-    render() {
-        return (
-            <section className="z1 brand-scene absolute bottom left right hide sm-show">
-                <div className="brand-boat-container">
-                    <svg className="brand-boat" width="27px" height="28px" viewBox="0 0 27 28">
-                        <g stroke="none" strokeWidth="1" fill="none" fillRule="evenodd" ref="HACK_fill_rule_1">
-                            <g transform="translate(-52.000000, -49.000000)" fill="#fff">
-                                <path d="M56.9966734,62.0821591 C56.9548869,62.5960122 56.5246217,63 56,63 C55.4477153,63 55,62.5522847 55,62 C55,61.4477153 55.4477153,61 56,61 C56.1542427,61 56.3003292,61.0349209 56.4307846,61.0972878 C56.4365546,60.9708421 56.4672874,60.8469847 56.5249064,60.738086 L60.591292,53.0527085 C60.6128147,53.0120312 60.6340855,52.9741034 60.6550548,52.9389125 C60.2727349,52.7984089 60,52.4310548 60,52 C60,51.4477153 60.4477153,51 61,51 C61.5522847,51 62,51.4477153 62,52 C62,52.4778316 61.6648606,52.8773872 61.2168176,52.9764309 C61.2239494,53.0443316 61.2276783,53.1212219 61.2276783,53.2070193 L61.2276783,64.6460667 C61.2276783,64.7905295 61.2109404,64.9142428 61.1799392,65.0161455 C61.6463447,65.1008929 62,65.5091461 62,66 C62,66.5522847 61.5522847,67 61,67 C60.4477153,67 60,66.5522847 60,66 C60,65.6775356 60.1526298,65.3907197 60.3895873,65.2078547 C60.3353792,65.1698515 60.2797019,65.1246206 60.2229246,65.0720038 L56.9966734,62.0821591 Z M66.1768361,51.0536808 L76.3863147,62.9621534 C76.6248381,62.7575589 76.9348843,62.6339439 77.2738087,62.6339439 C78.0269541,62.6339439 78.6374991,63.2443563 78.6374991,63.9973383 C78.6374991,64.7503202 78.0269541,65.3607327 77.2738087,65.3607327 C76.7179077,65.3607327 76.2396954,65.0281798 76.0273418,64.5512033 L76.0273418,64.5512033 L66.2470617,68.8970508 L66.2470617,68.8970508 C66.3224088,69.0662913 66.3642852,69.2537142 66.3642852,69.4509158 C66.3642852,70.2038977 65.7537402,70.8143102 65.0005948,70.8143102 C64.2474494,70.8143102 63.6369043,70.2038977 63.6369043,69.4509158 C63.6369043,68.6979339 64.2474494,68.0875214 65.0005948,68.0875214 L65.0005948,51.7267888 C64.2474494,51.7267888 63.6369043,51.1163763 63.6369043,50.3633944 C63.6369043,49.6104125 64.2474494,49 65.0005948,49 C65.7537402,49 66.3642852,49.6104125 66.3642852,50.3633944 C66.3642852,50.6152816 66.2959632,50.8512148 66.1768361,51.0536808 L66.1768361,51.0536808 Z M74.9589487,72 C76.0592735,72 76.2934239,72.6072543 75.4783436,73.3596586 L73.4702868,75.2133052 C72.656816,75.9642239 71.1011127,76.5729638 69.999426,76.5729638 L57.9641665,76.5729638 C56.8607339,76.5729638 55.3083859,75.9657095 54.4933056,75.2133052 L52.4852488,73.3596586 C51.6717779,72.6087399 51.9052063,72 53.0046438,72 L74.9589487,72 Z" id="boat" ></path>
-                            </g>
-                        </g>
-                    </svg>
-                </div>
-                <div className="brand-illustration">
-                    <div className="brand-mountain-1">
-                        <svg width="514px" height="175px" viewBox="0 0 514 175" version="1.1">
-                            <path fill="#fff" d="M41.5,45 L78,33 L78,74.5 L2.5,92 L41.5,45 Z" id="Path" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M77.5,32.5 L143,45.5 L76.5,75.5 L77.5,32.5 Z" id="Path-Copy-8" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M1.5,91 L78,73.5 L87,119.5 L41.5,149 L1.5,91 Z" id="Path-Copy" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M77.5000001,74.5 L143,44 L174,46 L86.0000001,119.5 L77.5000001,74.5 Z" id="Path-Copy-7" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M142.5,45 L205,25.5 L229,39.5 L173.5,47.5 L142.5,45 Z" id="Path-Copy-9" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M158.499999,99 L225.499999,80.5 L270.999999,87.5 L226.999999,125 L158.499999,99 Z" id="Path-Copy-3" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M156.499999,101.5 L172.999999,47 L226.499999,39 L226.499999,81 L156.499999,101.5 Z" id="Path-Copy-11" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M243.000001,88.5 L225.5,43.5 L288,39 L288,81.5 L243.000001,88.5 Z" id="Path-Copy-12" stroke="#B6CCE5"  transform="translate(256.750000, 63.750000) scale(-1, 1) translate(-256.750000, -63.750000) "></path>
-                            <path fill="#fff" d="M286.5,44.5 L342.5,64 L319.5,110.5 L270,86.5 L286.5,44.5 Z" id="Path-Copy-4" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M341.5,64 L390.5,111 L396.5,155 L317,109 L341.5,64 Z" id="Path-Copy-10" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M389,111 L505,149 L396,154 L389,111 Z" id="Path-Copy-13" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M321,110 L397,154 L282,149 L321,110 Z" id="Path-Copy-14" stroke="#4A90E2" ></path>
-                            <path fill="#fff" d="M247.5,20.5 L287,44.5 L226.5,40 L247.5,20.5 Z" id="Path-Copy-5" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M221.999999,13 L248.499999,20.5 L227.499999,40 L203.999999,26 L221.999999,13 Z" id="Path-Copy-6" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M228.499999,126.000001 L284.000003,148.499999 L224.500001,171.000001 L158.500001,148.499999 L228.499999,126.000001 Z" id="fg-geometry" stroke="#4A90E2" ></path>
-                            <polygon fill="#fff" id="Path" stroke="#4A90E2"  points="158.5 99 159.5 149 42 149 "></polygon>
-                            <path fill="#fff" d="M271.000001,86.0000002 L321.5,110.5 L283,149.5 L225,125.5 L271.000001,86.0000002 Z" id="Path-Copy-3" stroke="#4A90E2" ></path>
-                            <path fill="#fff" d="M42.2061,154.838108 C45.4069681,154.838108 48.0017844,152.243855 48.0017844,149.043682 C48.0017844,145.843509 45.4069681,143.249256 42.2061,143.249256 C39.0052319,143.249256 36.4104157,145.843509 36.4104157,149.043682 C36.4104157,152.243855 39.0052319,154.838108 42.2061,154.838108 Z M159.483477,154.838108 C162.684345,154.838108 165.279162,152.243855 165.279162,149.043682 C165.279162,145.843509 162.684345,143.249256 159.483477,143.249256 C156.282609,143.249256 153.687793,145.843509 153.687793,149.043682 C153.687793,152.243855 156.282609,154.838108 159.483477,154.838108 Z M282.215617,154.838108 C285.416485,154.838108 288.011301,152.243855 288.011301,149.043682 C288.011301,145.843509 285.416485,143.249256 282.215617,143.249256 C279.014748,143.249256 276.419932,145.843509 276.419932,149.043682 C276.419932,152.243855 279.014748,154.838108 282.215617,154.838108 Z M505.860848,154.156411 C509.061716,154.156411 511.656532,151.562158 511.656532,148.361985 C511.656532,145.161811 509.061716,142.567558 505.860848,142.567558 C502.65998,142.567558 500.065164,145.161811 500.065164,148.361985 C500.065164,151.562158 502.65998,154.156411 505.860848,154.156411 Z M396.083768,159.950837 C399.284636,159.950837 401.879452,157.356584 401.879452,154.156411 C401.879452,150.956238 399.284636,148.361985 396.083768,148.361985 C392.8829,148.361985 390.288084,150.956238 390.288084,154.156411 C390.288084,157.356584 392.8829,159.950837 396.083768,159.950837 Z M319.717104,115.981368 C322.917972,115.981368 325.512788,113.387115 325.512788,110.186942 C325.512788,106.986769 322.917972,104.392516 319.717104,104.392516 C316.516235,104.392516 313.921419,106.986769 313.921419,110.186942 C313.921419,113.387115 316.516235,115.981368 319.717104,115.981368 Z M270.624248,92.8036633 C273.825116,92.8036633 276.419932,90.2094103 276.419932,87.0092371 C276.419932,83.8090639 273.825116,81.214811 270.624248,81.214811 C267.42338,81.214811 264.828563,83.8090639 264.828563,87.0092371 C264.828563,90.2094103 267.42338,92.8036633 270.624248,92.8036633 Z M229.03169,131.660403 C232.232558,131.660403 234.827374,129.06615 234.827374,125.865977 C234.827374,122.665804 232.232558,120.071551 229.03169,120.071551 C225.830822,120.071551 223.236005,122.665804 223.236005,125.865977 C223.236005,129.06615 225.830822,131.660403 229.03169,131.660403 Z M158.119787,104.392516 C161.320655,104.392516 163.915471,101.798263 163.915471,98.5980894 C163.915471,95.3979162 161.320655,92.8036633 158.119787,92.8036633 C154.918919,92.8036633 152.324103,95.3979162 152.324103,98.5980894 C152.324103,101.798263 154.918919,104.392516 158.119787,104.392516 Z M224.940618,173.925629 C228.141486,173.925629 230.736303,171.331376 230.736303,168.131203 C230.736303,164.93103 228.141486,162.336777 224.940618,162.336777 C221.73975,162.336777 219.144934,164.93103 219.144934,168.131203 C219.144934,171.331376 221.73975,173.925629 224.940618,173.925629 Z" id="fg-points" stroke="#4A90E2" ></path>
-                            <path fill="#fff" d="M78.0029739,76.4429306 C79.5092648,76.4429306 80.7303548,75.2221057 80.7303548,73.7161419 C80.7303548,72.210178 79.5092648,70.9893531 78.0029739,70.9893531 C76.4966831,70.9893531 75.275593,72.210178 75.275593,73.7161419 C75.275593,75.2221057 76.4966831,76.4429306 78.0029739,76.4429306 Z M3,94.4779116 C4.50629086,94.4779116 5.72738087,93.2570867 5.72738087,91.7511228 C5.72738087,90.245159 4.50629086,89.024334 3,89.024334 C1.49370914,89.024334 0.27261913,90.245159 0.27261913,91.7511228 C0.27261913,93.2570867 1.49370914,94.4779116 3,94.4779116 Z M42.5470226,48.167594 C44.0533135,48.167594 45.2744035,46.9467691 45.2744035,45.4408052 C45.2744035,43.9348413 44.0533135,42.7140164 42.5470226,42.7140164 C41.0407318,42.7140164 39.8196417,43.9348413 39.8196417,45.4408052 C39.8196417,46.9467691 41.0407318,48.167594 42.5470226,48.167594 Z M78.0029739,35.541099 C79.5092648,35.541099 80.7303548,34.3202741 80.7303548,32.8143102 C80.7303548,31.3083464 79.5092648,30.0875214 78.0029739,30.0875214 C76.4966831,30.0875214 75.275593,31.3083464 75.275593,32.8143102 C75.275593,34.3202741 76.4966831,35.541099 78.0029739,35.541099 Z M142.77827,47.1299513 C144.28456,47.1299513 145.50565,45.9091264 145.50565,44.4031625 C145.50565,42.8971987 144.28456,41.6763737 142.77827,41.6763737 C141.271979,41.6763737 140.050889,42.8971987 140.050889,44.4031625 C140.050889,45.9091264 141.271979,47.1299513 142.77827,47.1299513 Z M86.8669617,122.116643 C88.3732526,122.116643 89.5943426,120.895818 89.5943426,119.389854 C89.5943426,117.88389 88.3732526,116.663065 86.8669617,116.663065 C85.3606709,116.663065 84.1395809,117.88389 84.1395809,119.389854 C84.1395809,120.895818 85.3606709,122.116643 86.8669617,122.116643 Z M246.418743,23.2705495 C247.925033,23.2705495 249.146123,22.0497246 249.146123,20.5437607 C249.146123,19.0377969 247.925033,17.8169719 246.418743,17.8169719 C244.912452,17.8169719 243.691362,19.0377969 243.691362,20.5437607 C243.691362,22.0497246 244.912452,23.2705495 246.418743,23.2705495 Z M173.461304,49.8567401 C174.967595,49.8567401 176.188685,48.6359151 176.188685,47.1299513 C176.188685,45.6239874 174.967595,44.4031625 173.461304,44.4031625 C171.955013,44.4031625 170.733923,45.6239874 170.733923,47.1299513 C170.733923,48.6359151 171.955013,49.8567401 173.461304,49.8567401 Z M204.144339,28.0424299 C205.65063,28.0424299 206.87172,26.8216049 206.87172,25.3156411 C206.87172,23.8096772 205.65063,22.5888523 204.144339,22.5888523 C202.638048,22.5888523 201.416958,23.8096772 201.416958,25.3156411 C201.416958,26.8216049 202.638048,28.0424299 204.144339,28.0424299 Z M225.963386,83.2599026 C227.469677,83.2599026 228.690767,82.0390776 228.690767,80.5331138 C228.690767,79.0271499 227.469677,77.806325 225.963386,77.806325 C224.457095,77.806325 223.236005,79.0271499 223.236005,80.5331138 C223.236005,82.0390776 224.457095,83.2599026 225.963386,83.2599026 Z M225.963386,41.6763737 C227.469677,41.6763737 228.690767,40.4555488 228.690767,38.949585 C228.690767,37.4436211 227.469677,36.2227962 225.963386,36.2227962 C224.457095,36.2227962 223.236005,37.4436211 223.236005,38.949585 C223.236005,40.4555488 224.457095,41.6763737 225.963386,41.6763737 Z M390.288084,113.254579 C391.794374,113.254579 393.015464,112.033754 393.015464,110.52779 C393.015464,109.021826 391.794374,107.801002 390.288084,107.801002 C388.781793,107.801002 387.560703,109.021826 387.560703,110.52779 C387.560703,112.033754 388.781793,113.254579 390.288084,113.254579 Z M342.558918,66.8991699 C344.065209,66.8991699 345.286299,65.678345 345.286299,64.1723811 C345.286299,62.6664173 344.065209,61.4455924 342.558918,61.4455924 C341.052627,61.4455924 339.831537,62.6664173 339.831537,64.1723811 C339.831537,65.678345 341.052627,66.8991699 342.558918,66.8991699 Z M286.64761,47.1299513 C288.153901,47.1299513 289.374991,45.9091264 289.374991,44.4031625 C289.374991,42.8971987 288.153901,41.6763737 286.64761,41.6763737 C285.14132,41.6763737 283.92023,42.8971987 283.92023,44.4031625 C283.92023,45.9091264 285.14132,47.1299513 286.64761,47.1299513 Z M221.872315,16.4535776 C223.378606,16.4535776 224.599696,15.2327526 224.599696,13.7267888 C224.599696,12.2208249 223.378606,11 221.872315,11 C220.366024,11 219.144934,12.2208249 219.144934,13.7267888 C219.144934,15.2327526 220.366024,16.4535776 221.872315,16.4535776 Z" id="bg-points" stroke="#B6CCE6" ></path>
-                        </svg>
-                    </div>
+  render() {
+    return (
+      <section className="z1 brand-scene absolute bottom left right hide sm-show">
+        <div className="brand-boat-container">
+          <svg
+            className="brand-boat"
+            width="27px"
+            height="28px"
+            viewBox="0 0 27 28"
+          >
+            <g
+              stroke="none"
+              strokeWidth="1"
+              fill="none"
+              fillRule="evenodd"
+              ref="HACK_fill_rule_1"
+            >
+              <g transform="translate(-52.000000, -49.000000)" fill="#fff">
+                <path
+                  d="M56.9966734,62.0821591 C56.9548869,62.5960122 56.5246217,63 56,63 C55.4477153,63 55,62.5522847 55,62 C55,61.4477153 55.4477153,61 56,61 C56.1542427,61 56.3003292,61.0349209 56.4307846,61.0972878 C56.4365546,60.9708421 56.4672874,60.8469847 56.5249064,60.738086 L60.591292,53.0527085 C60.6128147,53.0120312 60.6340855,52.9741034 60.6550548,52.9389125 C60.2727349,52.7984089 60,52.4310548 60,52 C60,51.4477153 60.4477153,51 61,51 C61.5522847,51 62,51.4477153 62,52 C62,52.4778316 61.6648606,52.8773872 61.2168176,52.9764309 C61.2239494,53.0443316 61.2276783,53.1212219 61.2276783,53.2070193 L61.2276783,64.6460667 C61.2276783,64.7905295 61.2109404,64.9142428 61.1799392,65.0161455 C61.6463447,65.1008929 62,65.5091461 62,66 C62,66.5522847 61.5522847,67 61,67 C60.4477153,67 60,66.5522847 60,66 C60,65.6775356 60.1526298,65.3907197 60.3895873,65.2078547 C60.3353792,65.1698515 60.2797019,65.1246206 60.2229246,65.0720038 L56.9966734,62.0821591 Z M66.1768361,51.0536808 L76.3863147,62.9621534 C76.6248381,62.7575589 76.9348843,62.6339439 77.2738087,62.6339439 C78.0269541,62.6339439 78.6374991,63.2443563 78.6374991,63.9973383 C78.6374991,64.7503202 78.0269541,65.3607327 77.2738087,65.3607327 C76.7179077,65.3607327 76.2396954,65.0281798 76.0273418,64.5512033 L76.0273418,64.5512033 L66.2470617,68.8970508 L66.2470617,68.8970508 C66.3224088,69.0662913 66.3642852,69.2537142 66.3642852,69.4509158 C66.3642852,70.2038977 65.7537402,70.8143102 65.0005948,70.8143102 C64.2474494,70.8143102 63.6369043,70.2038977 63.6369043,69.4509158 C63.6369043,68.6979339 64.2474494,68.0875214 65.0005948,68.0875214 L65.0005948,51.7267888 C64.2474494,51.7267888 63.6369043,51.1163763 63.6369043,50.3633944 C63.6369043,49.6104125 64.2474494,49 65.0005948,49 C65.7537402,49 66.3642852,49.6104125 66.3642852,50.3633944 C66.3642852,50.6152816 66.2959632,50.8512148 66.1768361,51.0536808 L66.1768361,51.0536808 Z M74.9589487,72 C76.0592735,72 76.2934239,72.6072543 75.4783436,73.3596586 L73.4702868,75.2133052 C72.656816,75.9642239 71.1011127,76.5729638 69.999426,76.5729638 L57.9641665,76.5729638 C56.8607339,76.5729638 55.3083859,75.9657095 54.4933056,75.2133052 L52.4852488,73.3596586 C51.6717779,72.6087399 51.9052063,72 53.0046438,72 L74.9589487,72 Z"
+                  id="boat"
+                />
+              </g>
+            </g>
+          </svg>
+        </div>
+        <div className="brand-illustration">
+          <div className="brand-mountain-1">
+            <svg
+              width="514px"
+              height="175px"
+              viewBox="0 0 514 175"
+              version="1.1"
+            >
+              <path
+                fill="#fff"
+                d="M41.5,45 L78,33 L78,74.5 L2.5,92 L41.5,45 Z"
+                id="Path"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M77.5,32.5 L143,45.5 L76.5,75.5 L77.5,32.5 Z"
+                id="Path-Copy-8"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M1.5,91 L78,73.5 L87,119.5 L41.5,149 L1.5,91 Z"
+                id="Path-Copy"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M77.5000001,74.5 L143,44 L174,46 L86.0000001,119.5 L77.5000001,74.5 Z"
+                id="Path-Copy-7"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M142.5,45 L205,25.5 L229,39.5 L173.5,47.5 L142.5,45 Z"
+                id="Path-Copy-9"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M158.499999,99 L225.499999,80.5 L270.999999,87.5 L226.999999,125 L158.499999,99 Z"
+                id="Path-Copy-3"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M156.499999,101.5 L172.999999,47 L226.499999,39 L226.499999,81 L156.499999,101.5 Z"
+                id="Path-Copy-11"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M243.000001,88.5 L225.5,43.5 L288,39 L288,81.5 L243.000001,88.5 Z"
+                id="Path-Copy-12"
+                stroke="#B6CCE5"
+                transform="translate(256.750000, 63.750000) scale(-1, 1) translate(-256.750000, -63.750000) "
+              />
+              <path
+                fill="#fff"
+                d="M286.5,44.5 L342.5,64 L319.5,110.5 L270,86.5 L286.5,44.5 Z"
+                id="Path-Copy-4"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M341.5,64 L390.5,111 L396.5,155 L317,109 L341.5,64 Z"
+                id="Path-Copy-10"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M389,111 L505,149 L396,154 L389,111 Z"
+                id="Path-Copy-13"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M321,110 L397,154 L282,149 L321,110 Z"
+                id="Path-Copy-14"
+                stroke="#4A90E2"
+              />
+              <path
+                fill="#fff"
+                d="M247.5,20.5 L287,44.5 L226.5,40 L247.5,20.5 Z"
+                id="Path-Copy-5"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M221.999999,13 L248.499999,20.5 L227.499999,40 L203.999999,26 L221.999999,13 Z"
+                id="Path-Copy-6"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M228.499999,126.000001 L284.000003,148.499999 L224.500001,171.000001 L158.500001,148.499999 L228.499999,126.000001 Z"
+                id="fg-geometry"
+                stroke="#4A90E2"
+              />
+              <polygon
+                fill="#fff"
+                id="Path"
+                stroke="#4A90E2"
+                points="158.5 99 159.5 149 42 149 "
+              />
+              <path
+                fill="#fff"
+                d="M271.000001,86.0000002 L321.5,110.5 L283,149.5 L225,125.5 L271.000001,86.0000002 Z"
+                id="Path-Copy-3"
+                stroke="#4A90E2"
+              />
+              <path
+                fill="#fff"
+                d="M42.2061,154.838108 C45.4069681,154.838108 48.0017844,152.243855 48.0017844,149.043682 C48.0017844,145.843509 45.4069681,143.249256 42.2061,143.249256 C39.0052319,143.249256 36.4104157,145.843509 36.4104157,149.043682 C36.4104157,152.243855 39.0052319,154.838108 42.2061,154.838108 Z M159.483477,154.838108 C162.684345,154.838108 165.279162,152.243855 165.279162,149.043682 C165.279162,145.843509 162.684345,143.249256 159.483477,143.249256 C156.282609,143.249256 153.687793,145.843509 153.687793,149.043682 C153.687793,152.243855 156.282609,154.838108 159.483477,154.838108 Z M282.215617,154.838108 C285.416485,154.838108 288.011301,152.243855 288.011301,149.043682 C288.011301,145.843509 285.416485,143.249256 282.215617,143.249256 C279.014748,143.249256 276.419932,145.843509 276.419932,149.043682 C276.419932,152.243855 279.014748,154.838108 282.215617,154.838108 Z M505.860848,154.156411 C509.061716,154.156411 511.656532,151.562158 511.656532,148.361985 C511.656532,145.161811 509.061716,142.567558 505.860848,142.567558 C502.65998,142.567558 500.065164,145.161811 500.065164,148.361985 C500.065164,151.562158 502.65998,154.156411 505.860848,154.156411 Z M396.083768,159.950837 C399.284636,159.950837 401.879452,157.356584 401.879452,154.156411 C401.879452,150.956238 399.284636,148.361985 396.083768,148.361985 C392.8829,148.361985 390.288084,150.956238 390.288084,154.156411 C390.288084,157.356584 392.8829,159.950837 396.083768,159.950837 Z M319.717104,115.981368 C322.917972,115.981368 325.512788,113.387115 325.512788,110.186942 C325.512788,106.986769 322.917972,104.392516 319.717104,104.392516 C316.516235,104.392516 313.921419,106.986769 313.921419,110.186942 C313.921419,113.387115 316.516235,115.981368 319.717104,115.981368 Z M270.624248,92.8036633 C273.825116,92.8036633 276.419932,90.2094103 276.419932,87.0092371 C276.419932,83.8090639 273.825116,81.214811 270.624248,81.214811 C267.42338,81.214811 264.828563,83.8090639 264.828563,87.0092371 C264.828563,90.2094103 267.42338,92.8036633 270.624248,92.8036633 Z M229.03169,131.660403 C232.232558,131.660403 234.827374,129.06615 234.827374,125.865977 C234.827374,122.665804 232.232558,120.071551 229.03169,120.071551 C225.830822,120.071551 223.236005,122.665804 223.236005,125.865977 C223.236005,129.06615 225.830822,131.660403 229.03169,131.660403 Z M158.119787,104.392516 C161.320655,104.392516 163.915471,101.798263 163.915471,98.5980894 C163.915471,95.3979162 161.320655,92.8036633 158.119787,92.8036633 C154.918919,92.8036633 152.324103,95.3979162 152.324103,98.5980894 C152.324103,101.798263 154.918919,104.392516 158.119787,104.392516 Z M224.940618,173.925629 C228.141486,173.925629 230.736303,171.331376 230.736303,168.131203 C230.736303,164.93103 228.141486,162.336777 224.940618,162.336777 C221.73975,162.336777 219.144934,164.93103 219.144934,168.131203 C219.144934,171.331376 221.73975,173.925629 224.940618,173.925629 Z"
+                id="fg-points"
+                stroke="#4A90E2"
+              />
+              <path
+                fill="#fff"
+                d="M78.0029739,76.4429306 C79.5092648,76.4429306 80.7303548,75.2221057 80.7303548,73.7161419 C80.7303548,72.210178 79.5092648,70.9893531 78.0029739,70.9893531 C76.4966831,70.9893531 75.275593,72.210178 75.275593,73.7161419 C75.275593,75.2221057 76.4966831,76.4429306 78.0029739,76.4429306 Z M3,94.4779116 C4.50629086,94.4779116 5.72738087,93.2570867 5.72738087,91.7511228 C5.72738087,90.245159 4.50629086,89.024334 3,89.024334 C1.49370914,89.024334 0.27261913,90.245159 0.27261913,91.7511228 C0.27261913,93.2570867 1.49370914,94.4779116 3,94.4779116 Z M42.5470226,48.167594 C44.0533135,48.167594 45.2744035,46.9467691 45.2744035,45.4408052 C45.2744035,43.9348413 44.0533135,42.7140164 42.5470226,42.7140164 C41.0407318,42.7140164 39.8196417,43.9348413 39.8196417,45.4408052 C39.8196417,46.9467691 41.0407318,48.167594 42.5470226,48.167594 Z M78.0029739,35.541099 C79.5092648,35.541099 80.7303548,34.3202741 80.7303548,32.8143102 C80.7303548,31.3083464 79.5092648,30.0875214 78.0029739,30.0875214 C76.4966831,30.0875214 75.275593,31.3083464 75.275593,32.8143102 C75.275593,34.3202741 76.4966831,35.541099 78.0029739,35.541099 Z M142.77827,47.1299513 C144.28456,47.1299513 145.50565,45.9091264 145.50565,44.4031625 C145.50565,42.8971987 144.28456,41.6763737 142.77827,41.6763737 C141.271979,41.6763737 140.050889,42.8971987 140.050889,44.4031625 C140.050889,45.9091264 141.271979,47.1299513 142.77827,47.1299513 Z M86.8669617,122.116643 C88.3732526,122.116643 89.5943426,120.895818 89.5943426,119.389854 C89.5943426,117.88389 88.3732526,116.663065 86.8669617,116.663065 C85.3606709,116.663065 84.1395809,117.88389 84.1395809,119.389854 C84.1395809,120.895818 85.3606709,122.116643 86.8669617,122.116643 Z M246.418743,23.2705495 C247.925033,23.2705495 249.146123,22.0497246 249.146123,20.5437607 C249.146123,19.0377969 247.925033,17.8169719 246.418743,17.8169719 C244.912452,17.8169719 243.691362,19.0377969 243.691362,20.5437607 C243.691362,22.0497246 244.912452,23.2705495 246.418743,23.2705495 Z M173.461304,49.8567401 C174.967595,49.8567401 176.188685,48.6359151 176.188685,47.1299513 C176.188685,45.6239874 174.967595,44.4031625 173.461304,44.4031625 C171.955013,44.4031625 170.733923,45.6239874 170.733923,47.1299513 C170.733923,48.6359151 171.955013,49.8567401 173.461304,49.8567401 Z M204.144339,28.0424299 C205.65063,28.0424299 206.87172,26.8216049 206.87172,25.3156411 C206.87172,23.8096772 205.65063,22.5888523 204.144339,22.5888523 C202.638048,22.5888523 201.416958,23.8096772 201.416958,25.3156411 C201.416958,26.8216049 202.638048,28.0424299 204.144339,28.0424299 Z M225.963386,83.2599026 C227.469677,83.2599026 228.690767,82.0390776 228.690767,80.5331138 C228.690767,79.0271499 227.469677,77.806325 225.963386,77.806325 C224.457095,77.806325 223.236005,79.0271499 223.236005,80.5331138 C223.236005,82.0390776 224.457095,83.2599026 225.963386,83.2599026 Z M225.963386,41.6763737 C227.469677,41.6763737 228.690767,40.4555488 228.690767,38.949585 C228.690767,37.4436211 227.469677,36.2227962 225.963386,36.2227962 C224.457095,36.2227962 223.236005,37.4436211 223.236005,38.949585 C223.236005,40.4555488 224.457095,41.6763737 225.963386,41.6763737 Z M390.288084,113.254579 C391.794374,113.254579 393.015464,112.033754 393.015464,110.52779 C393.015464,109.021826 391.794374,107.801002 390.288084,107.801002 C388.781793,107.801002 387.560703,109.021826 387.560703,110.52779 C387.560703,112.033754 388.781793,113.254579 390.288084,113.254579 Z M342.558918,66.8991699 C344.065209,66.8991699 345.286299,65.678345 345.286299,64.1723811 C345.286299,62.6664173 344.065209,61.4455924 342.558918,61.4455924 C341.052627,61.4455924 339.831537,62.6664173 339.831537,64.1723811 C339.831537,65.678345 341.052627,66.8991699 342.558918,66.8991699 Z M286.64761,47.1299513 C288.153901,47.1299513 289.374991,45.9091264 289.374991,44.4031625 C289.374991,42.8971987 288.153901,41.6763737 286.64761,41.6763737 C285.14132,41.6763737 283.92023,42.8971987 283.92023,44.4031625 C283.92023,45.9091264 285.14132,47.1299513 286.64761,47.1299513 Z M221.872315,16.4535776 C223.378606,16.4535776 224.599696,15.2327526 224.599696,13.7267888 C224.599696,12.2208249 223.378606,11 221.872315,11 C220.366024,11 219.144934,12.2208249 219.144934,13.7267888 C219.144934,15.2327526 220.366024,16.4535776 221.872315,16.4535776 Z"
+                id="bg-points"
+                stroke="#B6CCE6"
+              />
+            </svg>
+          </div>
 
-                    <div className="brand-bridge">
-                        <svg width="683px" height="154px" viewBox="0 0 683 154" version="1.1">
-                            <g id="Page-1" stroke="none" strokeWidth="1" fill="none" fillRule="evenodd" ref="HACK_fill_rule_2">
-                                <g id="Artboard-1" >
-                                    <path d="M193,69.49194 L193,48.696628 L193,48.696628 C193.437006,48.5974065 193.825695,48.3709078 194.125079,48.0581427 L197.242752,49.9287465 C197.479542,50.0708205 197.786672,49.994038 197.928746,49.7572479 C198.070821,49.5204577 197.994038,49.2133276 197.757248,49.0712535 L194.639252,47.2004558 C194.711048,46.9803849 194.749866,46.7453991 194.749866,46.5013419 C194.749866,45.4300479 194.00192,44.5335399 193,44.3060559 L193,23.4611785 L193,23.4611785 C193.70858,23.3422265 194.332923,22.9747274 194.780416,22.4513446 L197.242752,23.9287465 C197.479542,24.0708205 197.786672,23.994038 197.928746,23.7572479 C198.070821,23.5204577 197.994038,23.2133276 197.757248,23.0712535 L195.294823,21.5937988 C195.427115,21.2552485 195.499732,20.88678 195.499732,20.5013419 C195.499732,18.8437465 194.156706,17.5 192.5,17.5 C190.843294,17.5 189.500268,18.8437465 189.500268,20.5013419 C189.500268,21.1091075 189.680819,21.6746805 189.991183,22.1472957 L165.767183,44.64101 L165.767183,44.64101 C165.406294,44.3944476 164.969976,44.2502684 164.5,44.2502684 C163.257433,44.2502684 162.250134,45.2581083 162.250134,46.5013419 C162.250134,46.9292933 162.369489,47.3293527 162.576711,47.6700143 L138.72545,69.6131742 C138.372873,69.383631 137.952003,69.2502684 137.5,69.2502684 C136.257433,69.2502684 135.250134,70.2581083 135.250134,71.5013419 C135.250134,71.5912406 135.255401,71.6799086 135.265643,71.7670536 L111.39825,80.2911227 C110.99879,79.6645016 110.2979,79.2489265 109.5,79.2489265 C108.429185,79.2489265 107.533094,79.9974077 107.305831,81 L89.6941692,81 C89.4669062,79.9974077 88.5708148,79.2489265 87.5,79.2489265 C86.4291852,79.2489265 85.5330938,79.9974077 85.3058308,81 L60.6941692,81 L60.6941692,81 C60.4669062,79.9974077 59.5708148,79.2489265 58.5,79.2489265 C57.4291852,79.2489265 56.5330938,79.9974077 56.3058308,81 L30.6941692,81 C30.4669062,79.9974077 29.5708148,79.2489265 28.5,79.2489265 C27.4291852,79.2489265 26.5330938,79.9974077 26.3058308,81 L-9,81 L-9,82 L26.3058308,82 C26.5330938,83.0025923 27.4291852,83.7510735 28.5,83.7510735 C29.5708148,83.7510735 30.4669062,83.0025923 30.6941692,82 L30.6941692,82 L56.3058308,82 C56.5330938,83.0025923 57.4291852,83.7510735 58.5,83.7510735 C59.5708148,83.7510735 60.4669062,83.0025923 60.6941692,82 L85.3058308,82 C85.5330938,83.0025923 86.4291852,83.7510735 87.5,83.7510735 C88.5708148,83.7510735 89.4669062,83.0025923 89.6941692,82 L89.6941692,82 L107.305831,82 C107.496622,82.8416934 108.158796,83.5042922 109,83.6952861 L109,83.6952861 L109,91.3060559 C108.159243,91.4969482 107.497326,92.1589443 107.306135,93 L89.6941692,93 L89.6941692,93 C89.4669062,91.9974077 88.5708148,91.2489265 87.5,91.2489265 C86.4291852,91.2489265 85.5330938,91.9974077 85.3058308,93 L60.6941692,93 C60.4669062,91.9974077 59.5708148,91.2489265 58.5,91.2489265 C57.4291852,91.2489265 56.5330938,91.9974077 56.3058308,93 L30.6941692,93 C30.4669062,91.9974077 29.5708148,91.2489265 28.5,91.2489265 C27.4291852,91.2489265 26.5330938,91.9974077 26.3058308,93 L-9,93 L-9,94 L26.3058308,94 C26.5330938,95.0025923 27.4291852,95.7510735 28.5,95.7510735 C29.5708148,95.7510735 30.4669062,95.0025923 30.6941692,94 L56.3058308,94 C56.5330938,95.0025923 57.4291852,95.7510735 58.5,95.7510735 C59.5708148,95.7510735 60.4669062,95.0025923 60.6941692,94 L60.6941692,94 L85.3058308,94 C85.5330938,95.0025923 86.4291852,95.7510735 87.5,95.7510735 C88.5708148,95.7510735 89.4669062,95.0025923 89.6941692,94 L107.305527,94 C107.495918,94.842331 108.158349,95.5055326 109,95.696628 L109,144 L110,144 L110,95.696628 C110.841651,95.5055326 111.504082,94.842331 111.694473,94 L135.305527,94 C135.532294,95.0032662 136.428708,95.7524154 137.5,95.7524154 C138.571292,95.7524154 139.467706,95.0032662 139.694473,94 L162.305527,94 C162.532294,95.0032662 163.428708,95.7524154 164.5,95.7524154 C165.571292,95.7524154 166.467706,95.0032662 166.694473,94 L190.305527,94 C190.495918,94.842331 191.158349,95.5055326 192,95.696628 L192,143 L193,143 L193,95.696628 C193.841651,95.5055326 194.504082,94.842331 194.694473,94 L200,94 L200,93 L194.693865,93 C194.502674,92.1589443 193.840757,91.4969482 193,91.3060559 L193,91.3060559 L193,71.50806 C192.84938,71.5829117 192.679605,71.625 192.5,71.625 C192.320395,71.625 192.15062,71.5829117 192,71.50806 L192,91.3060559 C191.159243,91.4969482 190.497326,92.1589443 190.306135,93 L190.306135,93 L166.693865,93 L166.693865,93 C166.502674,92.1589443 165.840757,91.4969482 165,91.3060559 L165,48.696628 C166.00192,48.4691439 166.749866,47.5726359 166.749866,46.5013419 C166.749866,46.0904765 166.639851,45.7053203 166.447687,45.3737543 L166.447687,45.3737543 L190.671139,22.8805483 L190.671139,22.8805483 C191.052092,23.1741316 191.505536,23.378171 192,23.4611785 L192,44.3060559 L192,44.3060559 C190.99808,44.5335399 190.250134,45.4300479 190.250134,46.5013419 C190.250134,47.5726359 190.99808,48.4691439 192,48.696628 L192,69.49194 C192.15062,69.4170883 192.320395,69.375 192.5,69.375 C192.679605,69.375 192.84938,69.4170883 193,69.49194 L193,69.49194 Z M193.621469,70.5897864 L197.757248,73.0712535 C197.994038,73.2133276 198.070821,73.5204577 197.928746,73.7572479 C197.786672,73.994038 197.479542,74.0708205 197.242752,73.9287465 L193.10705,71.4473254 C193.394921,71.2624741 193.593026,70.9499958 193.621469,70.5897864 L193.621469,70.5897864 Z M110,83.6952861 L110,91.3060559 C110.840757,91.4969482 111.502674,92.1589443 111.693865,93 L111.693865,93 L135.306135,93 C135.497326,92.1589443 136.159243,91.4969482 137,91.3060559 L137,73.696628 L137,73.696628 C136.41374,73.5635187 135.914437,73.2013459 135.601054,72.709126 L111.734205,81.2330006 C111.744547,81.3205598 111.749866,81.4096584 111.749866,81.5 C111.749866,82.571294 111.00192,83.467802 110,83.6952861 L110,83.6952861 Z M164,48.696628 L164,91.3060559 L164,91.3060559 C163.159243,91.4969482 162.497326,92.1589443 162.306135,93 L139.693865,93 C139.502674,92.1589443 138.840757,91.4969482 138,91.3060559 L138,73.696628 L138,73.696628 C139.00192,73.4691439 139.749866,72.5726359 139.749866,71.5013419 C139.749866,71.07267 139.630108,70.671984 139.422242,70.3309493 L163.272842,48.3883968 C162.990559,48.2042543 162.752146,47.9584208 162.576711,47.6700143 L164,48.696628 Z M137.5,94.625 C138.12132,94.625 138.625,94.1213203 138.625,93.5 C138.625,92.8786797 138.12132,92.375 137.5,92.375 C136.87868,92.375 136.375,92.8786797 136.375,93.5 C136.375,94.1213203 136.87868,94.625 137.5,94.625 Z M164.5,94.625 C165.12132,94.625 165.625,94.1213203 165.625,93.5 C165.625,92.8786797 165.12132,92.375 164.5,92.375 C163.87868,92.375 163.375,92.8786797 163.375,93.5 C163.375,94.1213203 163.87868,94.625 164.5,94.625 Z M109.5,94.625 C110.12132,94.625 110.625,94.1213203 110.625,93.5 C110.625,92.8786797 110.12132,92.375 109.5,92.375 C108.87868,92.375 108.375,92.8786797 108.375,93.5 C108.375,94.1213203 108.87868,94.625 109.5,94.625 Z M137.5,72.625 C138.12132,72.625 138.625,72.1213203 138.625,71.5 C138.625,70.8786797 138.12132,70.375 137.5,70.375 C136.87868,70.375 136.375,70.8786797 136.375,71.5 C136.375,72.1213203 136.87868,72.625 137.5,72.625 Z M164.5,47.625 C165.12132,47.625 165.625,47.1213203 165.625,46.5 C165.625,45.8786797 165.12132,45.375 164.5,45.375 C163.87868,45.375 163.375,45.8786797 163.375,46.5 C163.375,47.1213203 163.87868,47.625 164.5,47.625 Z M109.5,82.6236581 C110.12132,82.6236581 110.625,82.1199784 110.625,81.4986581 C110.625,80.8773377 110.12132,80.3736581 109.5,80.3736581 C108.87868,80.3736581 108.375,80.8773377 108.375,81.4986581 C108.375,82.1199784 108.87868,82.6236581 109.5,82.6236581 Z M87.5,82.6236581 C88.1213203,82.6236581 88.625,82.1199784 88.625,81.4986581 C88.625,80.8773377 88.1213203,80.3736581 87.5,80.3736581 C86.8786797,80.3736581 86.375,80.8773377 86.375,81.4986581 C86.375,82.1199784 86.8786797,82.6236581 87.5,82.6236581 Z M87.5,94.6236581 C88.1213203,94.6236581 88.625,94.1199784 88.625,93.4986581 C88.625,92.8773377 88.1213203,92.3736581 87.5,92.3736581 C86.8786797,92.3736581 86.375,92.8773377 86.375,93.4986581 C86.375,94.1199784 86.8786797,94.6236581 87.5,94.6236581 Z M58.5,94.6236581 C59.1213203,94.6236581 59.625,94.1199784 59.625,93.4986581 C59.625,92.8773377 59.1213203,92.3736581 58.5,92.3736581 C57.8786797,92.3736581 57.375,92.8773377 57.375,93.4986581 C57.375,94.1199784 57.8786797,94.6236581 58.5,94.6236581 Z M58.5,82.6236581 C59.1213203,82.6236581 59.625,82.1199784 59.625,81.4986581 C59.625,80.8773377 59.1213203,80.3736581 58.5,80.3736581 C57.8786797,80.3736581 57.375,80.8773377 57.375,81.4986581 C57.375,82.1199784 57.8786797,82.6236581 58.5,82.6236581 Z M28.5,82.6236581 C29.1213203,82.6236581 29.625,82.1199784 29.625,81.4986581 C29.625,80.8773377 29.1213203,80.3736581 28.5,80.3736581 C27.8786797,80.3736581 27.375,80.8773377 27.375,81.4986581 C27.375,82.1199784 27.8786797,82.6236581 28.5,82.6236581 Z M28.5,94.6236581 C29.1213203,94.6236581 29.625,94.1199784 29.625,93.4986581 C29.625,92.8773377 29.1213203,92.3736581 28.5,92.3736581 C27.8786797,92.3736581 27.375,92.8773377 27.375,93.4986581 C27.375,94.1199784 27.8786797,94.6236581 28.5,94.6236581 Z M192.5,47.625 C193.12132,47.625 193.625,47.1213203 193.625,46.5 C193.625,45.8786797 193.12132,45.375 192.5,45.375 C191.87868,45.375 191.375,45.8786797 191.375,46.5 C191.375,47.1213203 191.87868,47.625 192.5,47.625 Z M192.5,22 C193.328427,22 194,21.3284271 194,20.5 C194,19.6715729 193.328427,19 192.5,19 C191.671573,19 191,19.6715729 191,20.5 C191,21.3284271 191.671573,22 192.5,22 Z M192.5,94.625 C193.12132,94.625 193.625,94.1213203 193.625,93.5 C193.625,92.8786797 193.12132,92.375 192.5,92.375 C191.87868,92.375 191.375,92.8786797 191.375,93.5 C191.375,94.1213203 191.87868,94.625 192.5,94.625 Z" id="Rectangle-5" fill="#F55C23" ></path>
-                                    <path d="M362.999924,69.49194 L362.999924,48.696628 L362.999924,48.696628 C363.436931,48.5974065 363.82562,48.3709078 364.125004,48.0581427 L367.242677,49.9287465 C367.479467,50.0708205 367.786597,49.994038 367.928671,49.7572479 C368.070745,49.5204577 367.993962,49.2133276 367.757172,49.0712535 L364.639176,47.2004558 C364.710972,46.9803849 364.74979,46.7453991 364.74979,46.5013419 C364.74979,45.4300479 364.001844,44.5335399 362.999924,44.3060559 L362.999924,23.4611785 L362.999924,23.4611785 C363.708504,23.3422265 364.332848,22.9747274 364.78034,22.4513446 L367.242677,23.9287465 C367.479467,24.0708205 367.786597,23.994038 367.928671,23.7572479 C368.070745,23.5204577 367.993962,23.2133276 367.757172,23.0712535 L365.294748,21.5937988 C365.427039,21.2552485 365.499656,20.88678 365.499656,20.5013419 C365.499656,18.8437465 364.156631,17.5 362.499924,17.5 C360.843218,17.5 359.500193,18.8437465 359.500193,20.5013419 C359.500193,21.1091075 359.680743,21.6746805 359.991107,22.1472957 L335.767107,44.64101 L335.767107,44.64101 C335.406219,44.3944476 334.9699,44.2502684 334.499924,44.2502684 C333.257358,44.2502684 332.250059,45.2581083 332.250059,46.5013419 C332.250059,46.9292933 332.369414,47.3293527 332.576636,47.6700143 L308.725375,69.6131742 C308.372798,69.383631 307.951927,69.2502684 307.499924,69.2502684 C306.257358,69.2502684 305.250059,70.2581083 305.250059,71.5013419 C305.250059,71.5912406 305.255326,71.6799086 305.265568,71.7670536 L281.398174,80.2911227 C280.998715,79.6645016 280.297825,79.2489265 279.499924,79.2489265 C278.42911,79.2489265 277.533018,79.9974077 277.305755,81 L251.694094,81 C251.466831,79.9974077 250.570739,79.2489265 249.499924,79.2489265 C248.42911,79.2489265 247.533018,79.9974077 247.305755,81 L228.694094,81 L228.694094,81 C228.466831,79.9974077 227.570739,79.2489265 226.499924,79.2489265 C225.42911,79.2489265 224.533018,79.9974077 224.305755,81 L196.999924,81 L196.999924,82 L224.305755,82 C224.496546,82.8416934 225.158721,83.5042922 225.999924,83.6952861 L225.999924,91.3047139 C225.158721,91.4957078 224.496546,92.1583066 224.305755,93 L197.999924,93 L197.999924,94 L224.305755,94 C224.533018,95.0025923 225.42911,95.7510735 226.499924,95.7510735 C227.570739,95.7510735 228.466831,95.0025923 228.694094,94 L228.694094,94 L247.305755,94 L247.305755,94 C247.533018,95.0025923 248.42911,95.7510735 249.499924,95.7510735 C250.570739,95.7510735 251.466831,95.0025923 251.694094,94 L277.305452,94 C277.532219,95.0032662 278.428632,95.7524154 279.499924,95.7524154 C280.571217,95.7524154 281.46763,95.0032662 281.694397,94 L305.305452,94 C305.532219,95.0032662 306.428632,95.7524154 307.499924,95.7524154 C308.571217,95.7524154 309.46763,95.0032662 309.694397,94 L309.694397,94 L332.305452,94 C332.532219,95.0032662 333.428632,95.7524154 334.499924,95.7524154 C335.571217,95.7524154 336.46763,95.0032662 336.694397,94 L360.305452,94 C360.495843,94.842331 361.158274,95.5055326 361.999924,95.696628 L361.999924,143 L362.999924,143 L362.999924,95.696628 C363.841575,95.5055326 364.504006,94.842331 364.694397,94 L366.999924,94 L366.999924,93 L364.693789,93 C364.502599,92.1589443 363.840681,91.4969482 362.999924,91.3060559 L362.999924,91.3060559 L362.999924,71.50806 C362.849305,71.5829117 362.67953,71.625 362.499924,71.625 C362.320319,71.625 362.150544,71.5829117 361.999924,71.50806 L361.999924,91.3060559 C361.159168,91.4969482 360.49725,92.1589443 360.30606,93 L360.30606,93 L336.693789,93 L336.693789,93 C336.502599,92.1589443 335.840681,91.4969482 334.999924,91.3060559 L334.999924,48.696628 C336.001844,48.4691439 336.74979,47.5726359 336.74979,46.5013419 C336.74979,46.0904765 336.639775,45.7053203 336.447611,45.3737543 L336.447611,45.3737543 L360.671064,22.8805483 L360.671064,22.8805483 C361.052017,23.1741316 361.505461,23.378171 361.999924,23.4611785 L361.999924,44.3060559 L361.999924,44.3060559 C360.998005,44.5335399 360.250059,45.4300479 360.250059,46.5013419 C360.250059,47.5726359 360.998005,48.4691439 361.999924,48.696628 L361.999924,69.49194 C362.150544,69.4170883 362.320319,69.375 362.499924,69.375 C362.67953,69.375 362.849305,69.4170883 362.999924,69.49194 L362.999924,69.49194 Z M363.621394,70.5897864 L367.757172,73.0712535 C367.993962,73.2133276 368.070745,73.5204577 367.928671,73.7572479 C367.786597,73.994038 367.479467,74.0708205 367.242677,73.9287465 L363.106975,71.4473254 C363.394845,71.2624741 363.59295,70.9499958 363.621394,70.5897864 L363.621394,70.5897864 Z M247.305755,93 L228.694094,93 L228.694094,93 C228.503302,92.1583066 227.841128,91.4957078 226.999924,91.3047139 L226.999924,83.6952861 C227.841128,83.5042922 228.503302,82.8416934 228.694094,82 L247.305755,82 C247.496546,82.8416934 248.158721,83.5042922 248.999924,83.6952861 L248.999924,91.3047139 C248.158721,91.4957078 247.496546,92.1583066 247.305755,93 L247.305755,93 Z M251.694094,93 L277.30606,93 C277.49725,92.1589443 278.159168,91.4969482 278.999924,91.3060559 L278.999924,83.6952861 L278.999924,83.6952861 C278.158721,83.5042922 277.496546,82.8416934 277.305755,82 L251.694094,82 L251.694094,82 C251.503302,82.8416934 250.841128,83.5042922 249.999924,83.6952861 L249.999924,91.3047139 C250.841128,91.4957078 251.503302,92.1583066 251.694094,93 L251.694094,93 Z M279.999924,83.6952861 L279.999924,91.3060559 C280.840681,91.4969482 281.502599,92.1589443 281.693789,93 L281.693789,93 L305.30606,93 C305.49725,92.1589443 306.159168,91.4969482 306.999924,91.3060559 L306.999924,73.696628 L306.999924,73.696628 C306.413665,73.5635187 305.914362,73.2013459 305.600978,72.709126 L281.73413,81.2330006 C281.744471,81.3205598 281.74979,81.4096584 281.74979,81.5 C281.74979,82.571294 281.001844,83.467802 279.999924,83.6952861 L279.999924,83.6952861 Z M333.999924,48.696628 L333.999924,91.3060559 L333.999924,91.3060559 C333.159168,91.4969482 332.49725,92.1589443 332.30606,93 L309.693789,93 C309.502599,92.1589443 308.840681,91.4969482 307.999924,91.3060559 L307.999924,73.696628 L307.999924,73.696628 C309.001844,73.4691439 309.74979,72.5726359 309.74979,71.5013419 C309.74979,71.07267 309.630033,70.671984 309.422166,70.3309493 L333.272767,48.3883968 C332.990484,48.2042543 332.752071,47.9584208 332.576636,47.6700143 L333.999924,48.696628 Z M307.499924,94.625 C308.121245,94.625 308.624924,94.1213203 308.624924,93.5 C308.624924,92.8786797 308.121245,92.375 307.499924,92.375 C306.878604,92.375 306.374924,92.8786797 306.374924,93.5 C306.374924,94.1213203 306.878604,94.625 307.499924,94.625 Z M334.499924,94.625 C335.121245,94.625 335.624924,94.1213203 335.624924,93.5 C335.624924,92.8786797 335.121245,92.375 334.499924,92.375 C333.878604,92.375 333.374924,92.8786797 333.374924,93.5 C333.374924,94.1213203 333.878604,94.625 334.499924,94.625 Z M279.499924,94.625 C280.121245,94.625 280.624924,94.1213203 280.624924,93.5 C280.624924,92.8786797 280.121245,92.375 279.499924,92.375 C278.878604,92.375 278.374924,92.8786797 278.374924,93.5 C278.374924,94.1213203 278.878604,94.625 279.499924,94.625 Z M307.499924,72.625 C308.121245,72.625 308.624924,72.1213203 308.624924,71.5 C308.624924,70.8786797 308.121245,70.375 307.499924,70.375 C306.878604,70.375 306.374924,70.8786797 306.374924,71.5 C306.374924,72.1213203 306.878604,72.625 307.499924,72.625 Z M334.499924,47.625 C335.121245,47.625 335.624924,47.1213203 335.624924,46.5 C335.624924,45.8786797 335.121245,45.375 334.499924,45.375 C333.878604,45.375 333.374924,45.8786797 333.374924,46.5 C333.374924,47.1213203 333.878604,47.625 334.499924,47.625 Z M279.499924,82.6236581 C280.121245,82.6236581 280.624924,82.1199784 280.624924,81.4986581 C280.624924,80.8773377 280.121245,80.3736581 279.499924,80.3736581 C278.878604,80.3736581 278.374924,80.8773377 278.374924,81.4986581 C278.374924,82.1199784 278.878604,82.6236581 279.499924,82.6236581 Z M249.499924,82.6236581 C250.121245,82.6236581 250.624924,82.1199784 250.624924,81.4986581 C250.624924,80.8773377 250.121245,80.3736581 249.499924,80.3736581 C248.878604,80.3736581 248.374924,80.8773377 248.374924,81.4986581 C248.374924,82.1199784 248.878604,82.6236581 249.499924,82.6236581 Z M226.499924,82.6236581 C227.121245,82.6236581 227.624924,82.1199784 227.624924,81.4986581 C227.624924,80.8773377 227.121245,80.3736581 226.499924,80.3736581 C225.878604,80.3736581 225.374924,80.8773377 225.374924,81.4986581 C225.374924,82.1199784 225.878604,82.6236581 226.499924,82.6236581 Z M226.499924,94.6236581 C227.121245,94.6236581 227.624924,94.1199784 227.624924,93.4986581 C227.624924,92.8773377 227.121245,92.3736581 226.499924,92.3736581 C225.878604,92.3736581 225.374924,92.8773377 225.374924,93.4986581 C225.374924,94.1199784 225.878604,94.6236581 226.499924,94.6236581 Z M249.499924,94.6236581 C250.121245,94.6236581 250.624924,94.1199784 250.624924,93.4986581 C250.624924,92.8773377 250.121245,92.3736581 249.499924,92.3736581 C248.878604,92.3736581 248.374924,92.8773377 248.374924,93.4986581 C248.374924,94.1199784 248.878604,94.6236581 249.499924,94.6236581 Z M362.499924,47.625 C363.121245,47.625 363.624924,47.1213203 363.624924,46.5 C363.624924,45.8786797 363.121245,45.375 362.499924,45.375 C361.878604,45.375 361.374924,45.8786797 361.374924,46.5 C361.374924,47.1213203 361.878604,47.625 362.499924,47.625 Z M362.499924,22 C363.328352,22 363.999924,21.3284271 363.999924,20.5 C363.999924,19.6715729 363.328352,19 362.499924,19 C361.671497,19 360.999924,19.6715729 360.999924,20.5 C360.999924,21.3284271 361.671497,22 362.499924,22 Z M362.499924,94.625 C363.121245,94.625 363.624924,94.1213203 363.624924,93.5 C363.624924,92.8786797 363.121245,92.375 362.499924,92.375 C361.878604,92.375 361.374924,92.8786797 361.374924,93.5 C361.374924,94.1213203 361.878604,94.625 362.499924,94.625 Z" id="Rectangle-5-Copy" fill="#F55C23"  transform="translate(282.499962, 80.250000) scale(-1, 1) translate(-282.499962, -80.250000) "></path>
-                                    <path d="M92.2682896,101.529121 L98.4048966,101.529121 L98.9048966,101.529121 L98.9048966,100.529121 L98.4048966,100.529121 L92.2682896,100.529121 L91.7682896,100.529121 L91.7682896,101.529121 L92.2682896,101.529121 Z M30.90222,109.02779 L37.038827,109.02779 L37.538827,109.02779 L37.538827,108.02779 L37.038827,108.02779 L30.90222,108.02779 L30.40222,108.02779 L30.40222,109.02779 L30.90222,109.02779 Z M30.90222,115.02779 L37.038827,115.02779 L37.538827,115.02779 L37.538827,114.02779 L37.038827,114.02779 L30.90222,114.02779 L30.40222,114.02779 L30.40222,115.02779 L30.90222,115.02779 Z M30.90222,123.02779 L37.038827,123.02779 L37.538827,123.02779 L37.538827,122.02779 L37.038827,122.02779 L30.90222,122.02779 L30.40222,122.02779 L30.40222,123.02779 L30.90222,123.02779 Z M30.90222,101.529121 L37.038827,101.529121 L37.538827,101.529121 L37.538827,100.529121 L37.038827,100.529121 L30.90222,100.529121 L30.40222,100.529121 L30.40222,101.529121 L30.90222,101.529121 Z M51.3575766,101.529121 L57.4941835,101.529121 L57.9941835,101.529121 L57.9941835,100.529121 L57.4941835,100.529121 L51.3575766,100.529121 L50.8575766,100.529121 L50.8575766,101.529121 L51.3575766,101.529121 Z M92.2682896,131.523798 L98.4048966,131.523798 L98.9048966,131.523798 L98.9048966,130.523798 L98.4048966,130.523798 L92.2682896,130.523798 L91.7682896,130.523798 L91.7682896,131.523798 L92.2682896,131.523798 Z M92.2682896,124.025129 L98.4048966,124.025129 L98.9048966,124.025129 L98.9048966,123.025129 L98.4048966,123.025129 L92.2682896,123.025129 L91.7682896,123.025129 L91.7682896,124.025129 L92.2682896,124.025129 Z M92.2682896,116.526459 L98.4048966,116.526459 L98.9048966,116.526459 L98.9048966,115.526459 L98.4048966,115.526459 L92.2682896,115.526459 L91.7682896,115.526459 L91.7682896,116.526459 L92.2682896,116.526459 Z M71.8129331,101.529121 L77.94954,101.529121 L78.44954,101.529121 L78.44954,100.529121 L77.94954,100.529121 L71.8129331,100.529121 L71.3129331,100.529121 L71.3129331,101.529121 L71.8129331,101.529121 Z M71.8129331,108.529121 L77.94954,108.529121 L78.44954,108.529121 L78.44954,107.529121 L77.94954,107.529121 L71.8129331,107.529121 L71.3129331,107.529121 L71.3129331,108.529121 L71.8129331,108.529121 Z M91.8129331,108.529121 L97.94954,108.529121 L98.44954,108.529121 L98.44954,107.529121 L97.94954,107.529121 L91.8129331,107.529121 L91.3129331,107.529121 L91.3129331,108.529121 L91.8129331,108.529121 Z" id="bricks" fill="#F55C23" ></path>
-                                    <path d="M87.5316955,137 L87.5316955,126.72949 L76.6335576,111.72949 L59,106 L41.3664424,111.72949 L30.4683045,126.72949 L30.4683045,137 L87.5316955,137 Z" id="Polygon-3" stroke="#F55C23" fill="#FFFFFF" ></path>
-                                    <circle id="Oval-42" stroke="#F65C23" fill="#FFFFFF"  cx="40.75" cy="111.75" r="1.75"></circle>
-                                    <circle id="Oval-42-Copy-2" stroke="#F65C23" fill="#FFFFFF"  cx="76.75" cy="111.75" r="1.75"></circle>
-                                    <circle id="Oval-42-Copy-7" stroke="#F65C23" fill="#FFFFFF"  cx="86.75" cy="126.75" r="1.75"></circle>
-                                    <circle id="Oval-42-Copy" stroke="#F65C23" fill="#FFFFFF"  cx="59" cy="106" r="1.75"></circle>
-                                    <g id="pylon-copy" transform="translate(181.596857, 142.941563)" fill="#FFFFFF">
-                                        <polygon id="Polygon-3" stroke="#F55C23"  transform="translate(16.364285, 4.090183) scale(1, -1) translate(-16.364285, -4.090183) " points="16.3642852 0 31.9276453 2.82624706 25.9829707 7.39921085 6.7455997 7.39921085 0.800925127 2.82624706 "></polygon>
-                                        <ellipse id="Oval-153" stroke="#F65C23"  cx="2.38645826" cy="5.11272896" rx="1.70461304" ry="1.70424299"></ellipse>
-                                        <ellipse id="Oval-153" stroke="#F65C23"  cx="30.3421122" cy="5.11272896" rx="1.70461304" ry="1.70424299"></ellipse>
-                                    </g>
-                                    <ellipse id="Oval-42-Copy-4" stroke="#F65C23" fill="#FFFFFF"  transform="translate(202.726374, 143.734238) scale(-1, 1) translate(-202.726374, -143.734238) " cx="202.726374" cy="143.734238" rx="1.72637406" ry="1.73423766"></ellipse>
-                                    <ellipse id="Oval-42-Copy-3" stroke="#F65C23" fill="#FFFFFF"  transform="translate(192.726374, 143.734238) scale(-1, 1) translate(-192.726374, -143.734238) " cx="192.726374" cy="143.734238" rx="1.72637406" ry="1.73423766"></ellipse>
-                                    <g id="Group" transform="translate(367.000000, 17.000000)">
-                                        <path d="M86,54.50806 L86,74.3060559 C86.8407567,74.4969482 87.5026741,75.1589443 87.6938646,76 L93,76 L93,77 L87.6944729,77 C87.5040817,77.842331 86.8416508,78.5055326 86,78.696628 L86,126 L85,126 L85,78.696628 C84.1583492,78.5055326 83.4959183,77.842331 83.3055271,77 L83.3055271,77 L59.6944729,77 L59.6944729,77 C59.4677057,78.0032662 58.5712925,78.7524154 57.5,78.7524154 C56.4287075,78.7524154 55.5322943,78.0032662 55.3055271,77 L32.6944729,77 C32.4677057,78.0032662 31.5712925,78.7524154 30.5,78.7524154 C29.4287075,78.7524154 28.5322943,78.0032662 28.3055271,77 L4.69447293,77 L4.69447293,77 C4.46770567,78.0032662 3.57129249,78.7524154 2.5,78.7524154 C1.25743338,78.7524154 0.250134119,77.7445755 0.250134119,76.5013419 C0.250134119,75.4300479 0.99808035,74.5335399 2,74.3060559 L2,66.6952861 C0.99808035,66.467802 0.250134119,65.571294 0.250134119,64.5 C0.250134119,63.2567664 1.25743338,62.2489265 2.5,62.2489265 C3.29790038,62.2489265 3.99879007,62.6645016 4.39824959,63.2911227 L4.39824959,63.2911227 L28.2656431,54.7670536 C28.2554011,54.6799086 28.2501341,54.5912406 28.2501341,54.5013419 C28.2501341,53.2581083 29.2574334,52.2502684 30.5,52.2502684 C30.9520026,52.2502684 31.3728733,52.383631 31.7254503,52.6131742 L31.7254503,52.6131742 L55.5767111,30.6700143 L55.5767111,30.6700143 C55.3694895,30.3293527 55.2501341,29.9292933 55.2501341,29.5013419 C55.2501341,28.2581083 56.2574334,27.2502684 57.5,27.2502684 C57.9699755,27.2502684 58.4062942,27.3944476 58.7671826,27.64101 L82.9911826,5.14729569 C82.680819,4.67468048 82.5002682,4.10910746 82.5002682,3.50134191 C82.5002682,1.84374654 83.8432939,0.5 85.5,0.5 C87.1567061,0.5 88.4997318,1.84374654 88.4997318,3.50134191 C88.4997318,3.88678004 88.427115,4.25524852 88.2948233,4.59379876 L90.7572479,6.07125354 C90.994038,6.21332762 91.0708205,6.52045774 90.9287465,6.75724788 C90.7866724,6.99403801 90.4795423,7.07082054 90.2427521,6.92874646 L87.7804156,5.45134457 C87.3329231,5.97472736 86.70858,6.34222649 86,6.46117854 L86,6.46117854 L86,27.3060559 L86,27.3060559 C87.0019197,27.5335399 87.7498659,28.4300479 87.7498659,29.5013419 C87.7498659,29.7453991 87.7110476,29.9803849 87.6392516,30.2004558 L90.7572479,32.0712535 C90.994038,32.2133276 91.0708205,32.5204577 90.9287465,32.7572479 C90.7866724,32.994038 90.4795423,33.0708205 90.2427521,32.9287465 L87.1250792,31.0581427 C86.8256955,31.3709078 86.4370061,31.5974065 86,31.696628 L86,52.49194 C86.3704296,52.6760278 86.625,53.0582848 86.625,53.5 C86.625,53.5302219 86.6238083,53.5601656 86.6214693,53.5897864 L90.7572479,56.0712535 C90.994038,56.2133276 91.0708205,56.5204577 90.9287465,56.7572479 C90.7866724,56.994038 90.4795423,57.0708205 90.2427521,56.9287465 L86.1070504,54.4473254 C86.0726064,54.4694431 86.0368773,54.4897336 86,54.50806 L86,54.50806 Z M85,54.50806 L85,74.3060559 C84.1592433,74.4969482 83.4973259,75.1589443 83.3061354,76 L83.3061354,76 L59.6938646,76 L59.6938646,76 C59.5026741,75.1589443 58.8407567,74.4969482 58,74.3060559 L58,31.696628 C59.0019197,31.4691439 59.7498659,30.5726359 59.7498659,29.5013419 C59.7498659,29.0904765 59.6398508,28.7053203 59.4476867,28.3737543 L59.4476867,28.3737543 L83.6711393,5.8805483 L83.6711393,5.8805483 C84.0520923,6.1741316 84.5055363,6.37817101 85,6.46117854 L85,27.3060559 L85,27.3060559 C83.9980803,27.5335399 83.2501341,28.4300479 83.2501341,29.5013419 C83.2501341,30.5726359 83.9980803,31.4691439 85,31.696628 L85,52.49194 C85.1506199,52.4170883 85.3203948,52.375 85.5,52.375 C85.6796052,52.375 85.8493801,52.4170883 86,52.49194 L85,54.50806 Z M4.73420518,64.2330006 L28.601054,55.709126 C28.9144371,56.2013459 29.4137402,56.5635187 30,56.696628 L30,74.3060559 C29.1592433,74.4969482 28.4973259,75.1589443 28.3061354,76 L4.69386458,76 C4.50267409,75.1589443 3.84075674,74.4969482 3,74.3060559 L3,66.6952861 C4.00191965,66.467802 4.74986588,65.571294 4.74986588,64.5 C4.74986588,64.4096584 4.7445469,64.3205598 4.73420518,64.2330006 L4.73420518,64.2330006 Z M56.2728421,31.3883968 L32.4222415,53.3309493 L32.4222415,53.3309493 C32.6301083,53.671984 32.7498659,54.07267 32.7498659,54.5013419 C32.7498659,55.5726359 32.0019197,56.4691439 31,56.696628 L31,74.3060559 C31.8407567,74.4969482 32.5026741,75.1589443 32.6938646,76 L55.3061354,76 C55.4973259,75.1589443 56.1592433,74.4969482 57,74.3060559 L57,31.696628 C56.7371633,31.6369514 56.4918047,31.5312334 56.2728421,31.3883968 L56.2728421,31.3883968 Z M30.5,77.625 C31.1213203,77.625 31.625,77.1213203 31.625,76.5 C31.625,75.8786797 31.1213203,75.375 30.5,75.375 C29.8786797,75.375 29.375,75.8786797 29.375,76.5 C29.375,77.1213203 29.8786797,77.625 30.5,77.625 Z M57.5,77.625 C58.1213203,77.625 58.625,77.1213203 58.625,76.5 C58.625,75.8786797 58.1213203,75.375 57.5,75.375 C56.8786797,75.375 56.375,75.8786797 56.375,76.5 C56.375,77.1213203 56.8786797,77.625 57.5,77.625 Z M2.5,77.625 C3.12132034,77.625 3.625,77.1213203 3.625,76.5 C3.625,75.8786797 3.12132034,75.375 2.5,75.375 C1.87867966,75.375 1.375,75.8786797 1.375,76.5 C1.375,77.1213203 1.87867966,77.625 2.5,77.625 Z M30.5,55.625 C31.1213203,55.625 31.625,55.1213203 31.625,54.5 C31.625,53.8786797 31.1213203,53.375 30.5,53.375 C29.8786797,53.375 29.375,53.8786797 29.375,54.5 C29.375,55.1213203 29.8786797,55.625 30.5,55.625 Z M57.5,30.625 C58.1213203,30.625 58.625,30.1213203 58.625,29.5 C58.625,28.8786797 58.1213203,28.375 57.5,28.375 C56.8786797,28.375 56.375,28.8786797 56.375,29.5 C56.375,30.1213203 56.8786797,30.625 57.5,30.625 Z M2.5,65.6236581 C3.12132034,65.6236581 3.625,65.1199784 3.625,64.4986581 C3.625,63.8773377 3.12132034,63.3736581 2.5,63.3736581 C1.87867966,63.3736581 1.375,63.8773377 1.375,64.4986581 C1.375,65.1199784 1.87867966,65.6236581 2.5,65.6236581 Z M85.5,30.625 C86.1213203,30.625 86.625,30.1213203 86.625,29.5 C86.625,28.8786797 86.1213203,28.375 85.5,28.375 C84.8786797,28.375 84.375,28.8786797 84.375,29.5 C84.375,30.1213203 84.8786797,30.625 85.5,30.625 Z M85.5,5 C86.3284271,5 87,4.32842712 87,3.5 C87,2.67157288 86.3284271,2 85.5,2 C84.6715729,2 84,2.67157288 84,3.5 C84,4.32842712 84.6715729,5 85.5,5 Z M85.5,77.625 C86.1213203,77.625 86.625,77.1213203 86.625,76.5 C86.625,75.8786797 86.1213203,75.375 85.5,75.375 C84.8786797,75.375 84.375,75.8786797 84.375,76.5 C84.375,77.1213203 84.8786797,77.625 85.5,77.625 Z" id="Rectangle-5-Copy-3" fill="#F55C23" ></path>
-                                        <g id="pylon-copy-2" transform="translate(74.596857, 125.941563)" fill="#FFFFFF" >
-                                            <polygon id="Polygon-3" stroke="#F55C23" transform="translate(16.364285, 4.090183) scale(1, -1) translate(-16.364285, -4.090183) " points="16.3642852 0 31.9276453 2.82624706 25.9829707 7.39921085 6.7455997 7.39921085 0.800925127 2.82624706 "></polygon>
-                                            <ellipse id="Oval-153" stroke="#F65C23" cx="2.38645826" cy="5.11272896" rx="1.70461304" ry="1.70424299"></ellipse>
-                                            <ellipse id="Oval-153" stroke="#F65C23" cx="30.3421122" cy="5.11272896" rx="1.70461304" ry="1.70424299"></ellipse>
-                                        </g>
-                                        <ellipse id="Oval-42-Copy-5" stroke="#F65C23" fill="#FFFFFF"  transform="translate(95.726374, 126.734238) scale(-1, 1) translate(-95.726374, -126.734238) " cx="95.7263741" cy="126.734238" rx="1.72637406" ry="1.73423766"></ellipse>
-                                        <ellipse id="Oval-42-Copy-6" stroke="#F65C23" fill="#FFFFFF"  transform="translate(85.726374, 126.734238) scale(-1, 1) translate(-85.726374, -126.734238) " cx="85.7263741" cy="126.734238" rx="1.72637406" ry="1.73423766"></ellipse>
-                                    </g>
-                                    <path d="M658,69.49194 L658,48.696628 L658,48.696628 C658.437006,48.5974065 658.825695,48.3709078 659.125079,48.0581427 L662.242752,49.9287465 C662.479542,50.0708205 662.786672,49.994038 662.928746,49.7572479 C663.070821,49.5204577 662.994038,49.2133276 662.757248,49.0712535 L659.639252,47.2004558 C659.711048,46.9803849 659.749866,46.7453991 659.749866,46.5013419 C659.749866,45.4300479 659.00192,44.5335399 658,44.3060559 L658,23.4611785 L658,23.4611785 C658.70858,23.3422265 659.332923,22.9747274 659.780416,22.4513446 L662.242752,23.9287465 C662.479542,24.0708205 662.786672,23.994038 662.928746,23.7572479 C663.070821,23.5204577 662.994038,23.2133276 662.757248,23.0712535 L660.294823,21.5937988 C660.427115,21.2552485 660.499732,20.88678 660.499732,20.5013419 C660.499732,18.8437465 659.156706,17.5 657.5,17.5 C655.843294,17.5 654.500268,18.8437465 654.500268,20.5013419 C654.500268,21.1091075 654.680819,21.6746805 654.991183,22.1472957 L630.767183,44.64101 L630.767183,44.64101 C630.406294,44.3944476 629.969976,44.2502684 629.5,44.2502684 C628.257433,44.2502684 627.250134,45.2581083 627.250134,46.5013419 C627.250134,46.9292933 627.369489,47.3293527 627.576711,47.6700143 L603.72545,69.6131742 C603.372873,69.383631 602.952003,69.2502684 602.5,69.2502684 C601.257433,69.2502684 600.250134,70.2581083 600.250134,71.5013419 C600.250134,71.5912406 600.255401,71.6799086 600.265643,71.7670536 L576.39825,80.2911227 C575.99879,79.6645016 575.2979,79.2489265 574.5,79.2489265 C573.429185,79.2489265 572.533094,79.9974077 572.305831,81 L554.694169,81 C554.466906,79.9974077 553.570815,79.2489265 552.5,79.2489265 C551.429185,79.2489265 550.533094,79.9974077 550.305831,81 L525.694169,81 L525.694169,81 C525.466906,79.9974077 524.570815,79.2489265 523.5,79.2489265 C522.429185,79.2489265 521.533094,79.9974077 521.305831,81 L495.694169,81 C495.466906,79.9974077 494.570815,79.2489265 493.5,79.2489265 C492.429185,79.2489265 491.533094,79.9974077 491.305831,81 L456,81 L456,82 L491.305831,82 C491.533094,83.0025923 492.429185,83.7510735 493.5,83.7510735 C494.570815,83.7510735 495.466906,83.0025923 495.694169,82 L495.694169,82 L521.305831,82 C521.533094,83.0025923 522.429185,83.7510735 523.5,83.7510735 C524.570815,83.7510735 525.466906,83.0025923 525.694169,82 L550.305831,82 C550.533094,83.0025923 551.429185,83.7510735 552.5,83.7510735 C553.570815,83.7510735 554.466906,83.0025923 554.694169,82 L554.694169,82 L572.305831,82 C572.496622,82.8416934 573.158796,83.5042922 574,83.6952861 L574,83.6952861 L574,91.3060559 C573.159243,91.4969482 572.497326,92.1589443 572.306135,93 L554.694169,93 L554.694169,93 C554.466906,91.9974077 553.570815,91.2489265 552.5,91.2489265 C551.429185,91.2489265 550.533094,91.9974077 550.305831,93 L525.694169,93 C525.466906,91.9974077 524.570815,91.2489265 523.5,91.2489265 C522.429185,91.2489265 521.533094,91.9974077 521.305831,93 L495.694169,93 C495.466906,91.9974077 494.570815,91.2489265 493.5,91.2489265 C492.429185,91.2489265 491.533094,91.9974077 491.305831,93 L456,93 L456,94 L491.305831,94 C491.533094,95.0025923 492.429185,95.7510735 493.5,95.7510735 C494.570815,95.7510735 495.466906,95.0025923 495.694169,94 L521.305831,94 C521.533094,95.0025923 522.429185,95.7510735 523.5,95.7510735 C524.570815,95.7510735 525.466906,95.0025923 525.694169,94 L525.694169,94 L550.305831,94 C550.533094,95.0025923 551.429185,95.7510735 552.5,95.7510735 C553.570815,95.7510735 554.466906,95.0025923 554.694169,94 L572.305527,94 C572.495918,94.842331 573.158349,95.5055326 574,95.696628 L574,144 L575,144 L575,95.696628 C575.841651,95.5055326 576.504082,94.842331 576.694473,94 L600.305527,94 C600.532294,95.0032662 601.428708,95.7524154 602.5,95.7524154 C603.571292,95.7524154 604.467706,95.0032662 604.694473,94 L627.305527,94 C627.532294,95.0032662 628.428708,95.7524154 629.5,95.7524154 C630.571292,95.7524154 631.467706,95.0032662 631.694473,94 L655.305527,94 C655.495918,94.842331 656.158349,95.5055326 657,95.696628 L657,143 L658,143 L658,95.696628 C658.841651,95.5055326 659.504082,94.842331 659.694473,94 L665,94 L665,93 L659.693865,93 C659.502674,92.1589443 658.840757,91.4969482 658,91.3060559 L658,91.3060559 L658,71.50806 C657.84938,71.5829117 657.679605,71.625 657.5,71.625 C657.320395,71.625 657.15062,71.5829117 657,71.50806 L657,91.3060559 C656.159243,91.4969482 655.497326,92.1589443 655.306135,93 L655.306135,93 L631.693865,93 L631.693865,93 C631.502674,92.1589443 630.840757,91.4969482 630,91.3060559 L630,48.696628 C631.00192,48.4691439 631.749866,47.5726359 631.749866,46.5013419 C631.749866,46.0904765 631.639851,45.7053203 631.447687,45.3737543 L631.447687,45.3737543 L655.671139,22.8805483 L655.671139,22.8805483 C656.052092,23.1741316 656.505536,23.378171 657,23.4611785 L657,44.3060559 L657,44.3060559 C655.99808,44.5335399 655.250134,45.4300479 655.250134,46.5013419 C655.250134,47.5726359 655.99808,48.4691439 657,48.696628 L657,69.49194 C657.15062,69.4170883 657.320395,69.375 657.5,69.375 C657.679605,69.375 657.84938,69.4170883 658,69.49194 L658,69.49194 Z M658.621469,70.5897864 L662.757248,73.0712535 C662.994038,73.2133276 663.070821,73.5204577 662.928746,73.7572479 C662.786672,73.994038 662.479542,74.0708205 662.242752,73.9287465 L658.10705,71.4473254 C658.394921,71.2624741 658.593026,70.9499958 658.621469,70.5897864 L658.621469,70.5897864 Z M575,83.6952861 L575,91.3060559 C575.840757,91.4969482 576.502674,92.1589443 576.693865,93 L576.693865,93 L600.306135,93 C600.497326,92.1589443 601.159243,91.4969482 602,91.3060559 L602,73.696628 L602,73.696628 C601.41374,73.5635187 600.914437,73.2013459 600.601054,72.709126 L576.734205,81.2330006 C576.744547,81.3205598 576.749866,81.4096584 576.749866,81.5 C576.749866,82.571294 576.00192,83.467802 575,83.6952861 L575,83.6952861 Z M629,48.696628 L629,91.3060559 L629,91.3060559 C628.159243,91.4969482 627.497326,92.1589443 627.306135,93 L604.693865,93 C604.502674,92.1589443 603.840757,91.4969482 603,91.3060559 L603,73.696628 L603,73.696628 C604.00192,73.4691439 604.749866,72.5726359 604.749866,71.5013419 C604.749866,71.07267 604.630108,70.671984 604.422242,70.3309493 L628.272842,48.3883968 C627.990559,48.2042543 627.752146,47.9584208 627.576711,47.6700143 L629,48.696628 Z M602.5,94.625 C603.12132,94.625 603.625,94.1213203 603.625,93.5 C603.625,92.8786797 603.12132,92.375 602.5,92.375 C601.87868,92.375 601.375,92.8786797 601.375,93.5 C601.375,94.1213203 601.87868,94.625 602.5,94.625 Z M629.5,94.625 C630.12132,94.625 630.625,94.1213203 630.625,93.5 C630.625,92.8786797 630.12132,92.375 629.5,92.375 C628.87868,92.375 628.375,92.8786797 628.375,93.5 C628.375,94.1213203 628.87868,94.625 629.5,94.625 Z M574.5,94.625 C575.12132,94.625 575.625,94.1213203 575.625,93.5 C575.625,92.8786797 575.12132,92.375 574.5,92.375 C573.87868,92.375 573.375,92.8786797 573.375,93.5 C573.375,94.1213203 573.87868,94.625 574.5,94.625 Z M602.5,72.625 C603.12132,72.625 603.625,72.1213203 603.625,71.5 C603.625,70.8786797 603.12132,70.375 602.5,70.375 C601.87868,70.375 601.375,70.8786797 601.375,71.5 C601.375,72.1213203 601.87868,72.625 602.5,72.625 Z M629.5,47.625 C630.12132,47.625 630.625,47.1213203 630.625,46.5 C630.625,45.8786797 630.12132,45.375 629.5,45.375 C628.87868,45.375 628.375,45.8786797 628.375,46.5 C628.375,47.1213203 628.87868,47.625 629.5,47.625 Z M574.5,82.6236581 C575.12132,82.6236581 575.625,82.1199784 575.625,81.4986581 C575.625,80.8773377 575.12132,80.3736581 574.5,80.3736581 C573.87868,80.3736581 573.375,80.8773377 573.375,81.4986581 C573.375,82.1199784 573.87868,82.6236581 574.5,82.6236581 Z M552.5,82.6236581 C553.12132,82.6236581 553.625,82.1199784 553.625,81.4986581 C553.625,80.8773377 553.12132,80.3736581 552.5,80.3736581 C551.87868,80.3736581 551.375,80.8773377 551.375,81.4986581 C551.375,82.1199784 551.87868,82.6236581 552.5,82.6236581 Z M552.5,94.6236581 C553.12132,94.6236581 553.625,94.1199784 553.625,93.4986581 C553.625,92.8773377 553.12132,92.3736581 552.5,92.3736581 C551.87868,92.3736581 551.375,92.8773377 551.375,93.4986581 C551.375,94.1199784 551.87868,94.6236581 552.5,94.6236581 Z M523.5,94.6236581 C524.12132,94.6236581 524.625,94.1199784 524.625,93.4986581 C524.625,92.8773377 524.12132,92.3736581 523.5,92.3736581 C522.87868,92.3736581 522.375,92.8773377 522.375,93.4986581 C522.375,94.1199784 522.87868,94.6236581 523.5,94.6236581 Z M523.5,82.6236581 C524.12132,82.6236581 524.625,82.1199784 524.625,81.4986581 C524.625,80.8773377 524.12132,80.3736581 523.5,80.3736581 C522.87868,80.3736581 522.375,80.8773377 522.375,81.4986581 C522.375,82.1199784 522.87868,82.6236581 523.5,82.6236581 Z M493.5,82.6236581 C494.12132,82.6236581 494.625,82.1199784 494.625,81.4986581 C494.625,80.8773377 494.12132,80.3736581 493.5,80.3736581 C492.87868,80.3736581 492.375,80.8773377 492.375,81.4986581 C492.375,82.1199784 492.87868,82.6236581 493.5,82.6236581 Z M493.5,94.6236581 C494.12132,94.6236581 494.625,94.1199784 494.625,93.4986581 C494.625,92.8773377 494.12132,92.3736581 493.5,92.3736581 C492.87868,92.3736581 492.375,92.8773377 492.375,93.4986581 C492.375,94.1199784 492.87868,94.6236581 493.5,94.6236581 Z M657.5,47.625 C658.12132,47.625 658.625,47.1213203 658.625,46.5 C658.625,45.8786797 658.12132,45.375 657.5,45.375 C656.87868,45.375 656.375,45.8786797 656.375,46.5 C656.375,47.1213203 656.87868,47.625 657.5,47.625 Z M657.5,22 C658.328427,22 659,21.3284271 659,20.5 C659,19.6715729 658.328427,19 657.5,19 C656.671573,19 656,19.6715729 656,20.5 C656,21.3284271 656.671573,22 657.5,22 Z M657.5,94.625 C658.12132,94.625 658.625,94.1213203 658.625,93.5 C658.625,92.8786797 658.12132,92.375 657.5,92.375 C656.87868,92.375 656.375,92.8786797 656.375,93.5 C656.375,94.1213203 656.87868,94.625 657.5,94.625 Z" id="Rectangle-5-Copy-2" fill="#F55C23"  transform="translate(560.500000, 80.750000) scale(-1, 1) translate(-560.500000, -80.750000) "></path>
-                                    <path d="M563.038827,101.529121 L556.90222,101.529121 L556.40222,101.529121 L556.40222,100.529121 L556.90222,100.529121 L563.038827,100.529121 L563.538827,100.529121 L563.538827,101.529121 L563.038827,101.529121 Z M624.404897,109.02779 L618.26829,109.02779 L617.76829,109.02779 L617.76829,108.02779 L618.26829,108.02779 L624.404897,108.02779 L624.904897,108.02779 L624.904897,109.02779 L624.404897,109.02779 Z M624.404897,115.02779 L618.26829,115.02779 L617.76829,115.02779 L617.76829,114.02779 L618.26829,114.02779 L624.404897,114.02779 L624.904897,114.02779 L624.904897,115.02779 L624.404897,115.02779 Z M624.404897,123.02779 L618.26829,123.02779 L617.76829,123.02779 L617.76829,122.02779 L618.26829,122.02779 L624.404897,122.02779 L624.904897,122.02779 L624.904897,123.02779 L624.404897,123.02779 Z M624.404897,101.529121 L618.26829,101.529121 L617.76829,101.529121 L617.76829,100.529121 L618.26829,100.529121 L624.404897,100.529121 L624.904897,100.529121 L624.904897,101.529121 L624.404897,101.529121 Z M603.94954,101.529121 L597.812933,101.529121 L597.312933,101.529121 L597.312933,100.529121 L597.812933,100.529121 L603.94954,100.529121 L604.44954,100.529121 L604.44954,101.529121 L603.94954,101.529121 Z M563.038827,131.523798 L556.90222,131.523798 L556.40222,131.523798 L556.40222,130.523798 L556.90222,130.523798 L563.038827,130.523798 L563.538827,130.523798 L563.538827,131.523798 L563.038827,131.523798 Z M563.038827,124.025129 L556.90222,124.025129 L556.40222,124.025129 L556.40222,123.025129 L556.90222,123.025129 L563.038827,123.025129 L563.538827,123.025129 L563.538827,124.025129 L563.038827,124.025129 Z M563.038827,116.526459 L556.90222,116.526459 L556.40222,116.526459 L556.40222,115.526459 L556.90222,115.526459 L563.038827,115.526459 L563.538827,115.526459 L563.538827,116.526459 L563.038827,116.526459 Z M583.494184,101.529121 L577.357577,101.529121 L576.857577,101.529121 L576.857577,100.529121 L577.357577,100.529121 L583.494184,100.529121 L583.994184,100.529121 L583.994184,101.529121 L583.494184,101.529121 Z M583.494184,108.529121 L577.357577,108.529121 L576.857577,108.529121 L576.857577,107.529121 L577.357577,107.529121 L583.494184,107.529121 L583.994184,107.529121 L583.994184,108.529121 L583.494184,108.529121 Z M563.494184,108.529121 L557.357577,108.529121 L556.857577,108.529121 L556.857577,107.529121 L557.357577,107.529121 L563.494184,107.529121 L563.994184,107.529121 L563.994184,108.529121 L563.494184,108.529121 Z" id="bricks-copy" fill="#F55C23" ></path>
-                                    <path d="M556.468305,137 L556.468305,126.72949 L567.366442,111.72949 L585,106 L602.633558,111.72949 L613.531695,126.72949 L613.531695,137 L556.468305,137 Z" id="Polygon-3-Copy" stroke="#F55C23" fill="#FFFFFF" ></path>
-                                    <circle id="Oval-42-Copy-8" stroke="#F65C23" fill="#FFFFFF"  cx="566.75" cy="111.75" r="1.75"></circle>
-                                    <circle id="Oval-42-Copy-9" stroke="#F65C23" fill="#FFFFFF"  cx="602.75" cy="111.75" r="1.75"></circle>
-                                    <circle id="Oval-42-Copy-10" stroke="#F65C23" fill="#FFFFFF"  cx="612.75" cy="126.75" r="1.75"></circle>
-                                    <circle id="Oval-42-Copy-11" stroke="#F65C23" fill="#FFFFFF"  cx="585" cy="106" r="1.75"></circle>
-                                </g>
-                            </g>
-                        </svg>
-                    </div>
-                    <div className="brand-mountain-2">
-                        <svg width="514px" height="175px" viewBox="0 0 514 175" version="1.1">
-                            <path fill="#fff" d="M41.5,45 L78,33 L78,74.5 L2.5,92 L41.5,45 Z" id="Path" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M77.5,32.5 L143,45.5 L76.5,75.5 L77.5,32.5 Z" id="Path-Copy-8" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M1.5,91 L78,73.5 L87,119.5 L41.5,149 L1.5,91 Z" id="Path-Copy" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M77.5000001,74.5 L143,44 L174,46 L86.0000001,119.5 L77.5000001,74.5 Z" id="Path-Copy-7" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M142.5,45 L205,25.5 L229,39.5 L173.5,47.5 L142.5,45 Z" id="Path-Copy-9" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M158.499999,99 L225.499999,80.5 L270.999999,87.5 L226.999999,125 L158.499999,99 Z" id="Path-Copy-2" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M156.499999,101.5 L172.999999,47 L226.499999,39 L226.499999,81 L156.499999,101.5 Z" id="Path-Copy-11" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M243.000001,88.5 L225.5,43.5 L288,39 L288,81.5 L243.000001,88.5 Z" id="Path-Copy-12" stroke="#B6CCE5"  transform="translate(256.750000, 63.750000) scale(-1, 1) translate(-256.750000, -63.750000) "></path>
-                            <path fill="#fff" d="M286.5,44.5 L342.5,64 L319.5,110.5 L270,86.5 L286.5,44.5 Z" id="Path-Copy-4" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M341.5,64 L390.5,111 L396.5,155 L317,109 L341.5,64 Z" id="Path-Copy-10" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M389,111 L505,149 L396,154 L389,111 Z" id="Path-Copy-13" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M321,110 L397,154 L282,149 L321,110 Z" id="Path-Copy-14" stroke="#4A90E2" ></path>
-                            <path fill="#fff" d="M247.5,20.5 L287,44.5 L226.5,40 L247.5,20.5 Z" id="Path-Copy-5" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M221.999999,13 L248.499999,20.5 L227.499999,40 L203.999999,26 L221.999999,13 Z" id="Path-Copy-6" stroke="#B6CCE5" ></path>
-                            <path fill="#fff" d="M228.499999,126.000001 L284.000003,148.499999 L224.500001,171.000001 L158.500001,148.499999 L228.499999,126.000001 Z" id="fg-geometry" stroke="#4A90E2" ></path>
-                            <polygon fill="#fff" id="Path" stroke="#4A90E2"  points="158.5 99 159.5 149 42 149 "></polygon>
-                            <path fill="#fff" d="M271.000001,86.0000002 L321.5,110.5 L283,149.5 L225,125.5 L271.000001,86.0000002 Z" id="Path-Copy-3" stroke="#4A90E2" ></path>
-                            <path fill="#fff" d="M42.2061,154.838108 C45.4069681,154.838108 48.0017844,152.243855 48.0017844,149.043682 C48.0017844,145.843509 45.4069681,143.249256 42.2061,143.249256 C39.0052319,143.249256 36.4104157,145.843509 36.4104157,149.043682 C36.4104157,152.243855 39.0052319,154.838108 42.2061,154.838108 Z M159.483477,154.838108 C162.684345,154.838108 165.279162,152.243855 165.279162,149.043682 C165.279162,145.843509 162.684345,143.249256 159.483477,143.249256 C156.282609,143.249256 153.687793,145.843509 153.687793,149.043682 C153.687793,152.243855 156.282609,154.838108 159.483477,154.838108 Z M282.215617,154.838108 C285.416485,154.838108 288.011301,152.243855 288.011301,149.043682 C288.011301,145.843509 285.416485,143.249256 282.215617,143.249256 C279.014748,143.249256 276.419932,145.843509 276.419932,149.043682 C276.419932,152.243855 279.014748,154.838108 282.215617,154.838108 Z M505.860848,154.156411 C509.061716,154.156411 511.656532,151.562158 511.656532,148.361985 C511.656532,145.161811 509.061716,142.567558 505.860848,142.567558 C502.65998,142.567558 500.065164,145.161811 500.065164,148.361985 C500.065164,151.562158 502.65998,154.156411 505.860848,154.156411 Z M396.083768,159.950837 C399.284636,159.950837 401.879452,157.356584 401.879452,154.156411 C401.879452,150.956238 399.284636,148.361985 396.083768,148.361985 C392.8829,148.361985 390.288084,150.956238 390.288084,154.156411 C390.288084,157.356584 392.8829,159.950837 396.083768,159.950837 Z M319.717104,115.981368 C322.917972,115.981368 325.512788,113.387115 325.512788,110.186942 C325.512788,106.986769 322.917972,104.392516 319.717104,104.392516 C316.516235,104.392516 313.921419,106.986769 313.921419,110.186942 C313.921419,113.387115 316.516235,115.981368 319.717104,115.981368 Z M270.624248,92.8036633 C273.825116,92.8036633 276.419932,90.2094103 276.419932,87.0092371 C276.419932,83.8090639 273.825116,81.214811 270.624248,81.214811 C267.42338,81.214811 264.828563,83.8090639 264.828563,87.0092371 C264.828563,90.2094103 267.42338,92.8036633 270.624248,92.8036633 Z M229.03169,131.660403 C232.232558,131.660403 234.827374,129.06615 234.827374,125.865977 C234.827374,122.665804 232.232558,120.071551 229.03169,120.071551 C225.830822,120.071551 223.236005,122.665804 223.236005,125.865977 C223.236005,129.06615 225.830822,131.660403 229.03169,131.660403 Z M158.119787,104.392516 C161.320655,104.392516 163.915471,101.798263 163.915471,98.5980894 C163.915471,95.3979162 161.320655,92.8036633 158.119787,92.8036633 C154.918919,92.8036633 152.324103,95.3979162 152.324103,98.5980894 C152.324103,101.798263 154.918919,104.392516 158.119787,104.392516 Z M224.940618,173.925629 C228.141486,173.925629 230.736303,171.331376 230.736303,168.131203 C230.736303,164.93103 228.141486,162.336777 224.940618,162.336777 C221.73975,162.336777 219.144934,164.93103 219.144934,168.131203 C219.144934,171.331376 221.73975,173.925629 224.940618,173.925629 Z" id="fg-points" stroke="#4A90E2" ></path>
-                            <path fill="#fff" d="M78.0029739,76.4429306 C79.5092648,76.4429306 80.7303548,75.2221057 80.7303548,73.7161419 C80.7303548,72.210178 79.5092648,70.9893531 78.0029739,70.9893531 C76.4966831,70.9893531 75.275593,72.210178 75.275593,73.7161419 C75.275593,75.2221057 76.4966831,76.4429306 78.0029739,76.4429306 Z M3,94.4779116 C4.50629086,94.4779116 5.72738087,93.2570867 5.72738087,91.7511228 C5.72738087,90.245159 4.50629086,89.024334 3,89.024334 C1.49370914,89.024334 0.27261913,90.245159 0.27261913,91.7511228 C0.27261913,93.2570867 1.49370914,94.4779116 3,94.4779116 Z M42.5470226,48.167594 C44.0533135,48.167594 45.2744035,46.9467691 45.2744035,45.4408052 C45.2744035,43.9348413 44.0533135,42.7140164 42.5470226,42.7140164 C41.0407318,42.7140164 39.8196417,43.9348413 39.8196417,45.4408052 C39.8196417,46.9467691 41.0407318,48.167594 42.5470226,48.167594 Z M78.0029739,35.541099 C79.5092648,35.541099 80.7303548,34.3202741 80.7303548,32.8143102 C80.7303548,31.3083464 79.5092648,30.0875214 78.0029739,30.0875214 C76.4966831,30.0875214 75.275593,31.3083464 75.275593,32.8143102 C75.275593,34.3202741 76.4966831,35.541099 78.0029739,35.541099 Z M142.77827,47.1299513 C144.28456,47.1299513 145.50565,45.9091264 145.50565,44.4031625 C145.50565,42.8971987 144.28456,41.6763737 142.77827,41.6763737 C141.271979,41.6763737 140.050889,42.8971987 140.050889,44.4031625 C140.050889,45.9091264 141.271979,47.1299513 142.77827,47.1299513 Z M86.8669617,122.116643 C88.3732526,122.116643 89.5943426,120.895818 89.5943426,119.389854 C89.5943426,117.88389 88.3732526,116.663065 86.8669617,116.663065 C85.3606709,116.663065 84.1395809,117.88389 84.1395809,119.389854 C84.1395809,120.895818 85.3606709,122.116643 86.8669617,122.116643 Z M246.418743,23.2705495 C247.925033,23.2705495 249.146123,22.0497246 249.146123,20.5437607 C249.146123,19.0377969 247.925033,17.8169719 246.418743,17.8169719 C244.912452,17.8169719 243.691362,19.0377969 243.691362,20.5437607 C243.691362,22.0497246 244.912452,23.2705495 246.418743,23.2705495 Z M173.461304,49.8567401 C174.967595,49.8567401 176.188685,48.6359151 176.188685,47.1299513 C176.188685,45.6239874 174.967595,44.4031625 173.461304,44.4031625 C171.955013,44.4031625 170.733923,45.6239874 170.733923,47.1299513 C170.733923,48.6359151 171.955013,49.8567401 173.461304,49.8567401 Z M204.144339,28.0424299 C205.65063,28.0424299 206.87172,26.8216049 206.87172,25.3156411 C206.87172,23.8096772 205.65063,22.5888523 204.144339,22.5888523 C202.638048,22.5888523 201.416958,23.8096772 201.416958,25.3156411 C201.416958,26.8216049 202.638048,28.0424299 204.144339,28.0424299 Z M225.963386,83.2599026 C227.469677,83.2599026 228.690767,82.0390776 228.690767,80.5331138 C228.690767,79.0271499 227.469677,77.806325 225.963386,77.806325 C224.457095,77.806325 223.236005,79.0271499 223.236005,80.5331138 C223.236005,82.0390776 224.457095,83.2599026 225.963386,83.2599026 Z M225.963386,41.6763737 C227.469677,41.6763737 228.690767,40.4555488 228.690767,38.949585 C228.690767,37.4436211 227.469677,36.2227962 225.963386,36.2227962 C224.457095,36.2227962 223.236005,37.4436211 223.236005,38.949585 C223.236005,40.4555488 224.457095,41.6763737 225.963386,41.6763737 Z M390.288084,113.254579 C391.794374,113.254579 393.015464,112.033754 393.015464,110.52779 C393.015464,109.021826 391.794374,107.801002 390.288084,107.801002 C388.781793,107.801002 387.560703,109.021826 387.560703,110.52779 C387.560703,112.033754 388.781793,113.254579 390.288084,113.254579 Z M342.558918,66.8991699 C344.065209,66.8991699 345.286299,65.678345 345.286299,64.1723811 C345.286299,62.6664173 344.065209,61.4455924 342.558918,61.4455924 C341.052627,61.4455924 339.831537,62.6664173 339.831537,64.1723811 C339.831537,65.678345 341.052627,66.8991699 342.558918,66.8991699 Z M286.64761,47.1299513 C288.153901,47.1299513 289.374991,45.9091264 289.374991,44.4031625 C289.374991,42.8971987 288.153901,41.6763737 286.64761,41.6763737 C285.14132,41.6763737 283.92023,42.8971987 283.92023,44.4031625 C283.92023,45.9091264 285.14132,47.1299513 286.64761,47.1299513 Z M221.872315,16.4535776 C223.378606,16.4535776 224.599696,15.2327526 224.599696,13.7267888 C224.599696,12.2208249 223.378606,11 221.872315,11 C220.366024,11 219.144934,12.2208249 219.144934,13.7267888 C219.144934,15.2327526 220.366024,16.4535776 221.872315,16.4535776 Z" id="bg-points" stroke="#B6CCE6" ></path>
-                        </svg>
-                    </div>
-                </div>
-                { /*  09-12-2016 */ }
-                <div id="caspian" className="bg-brand block py3 absolute bottom left right"></div>
-            </section>
-        );
-    }
+          <div className="brand-bridge">
+            <svg
+              width="683px"
+              height="154px"
+              viewBox="0 0 683 154"
+              version="1.1"
+            >
+              <g
+                id="Page-1"
+                stroke="none"
+                strokeWidth="1"
+                fill="none"
+                fillRule="evenodd"
+                ref="HACK_fill_rule_2"
+              >
+                <g id="Artboard-1">
+                  <path
+                    d="M193,69.49194 L193,48.696628 L193,48.696628 C193.437006,48.5974065 193.825695,48.3709078 194.125079,48.0581427 L197.242752,49.9287465 C197.479542,50.0708205 197.786672,49.994038 197.928746,49.7572479 C198.070821,49.5204577 197.994038,49.2133276 197.757248,49.0712535 L194.639252,47.2004558 C194.711048,46.9803849 194.749866,46.7453991 194.749866,46.5013419 C194.749866,45.4300479 194.00192,44.5335399 193,44.3060559 L193,23.4611785 L193,23.4611785 C193.70858,23.3422265 194.332923,22.9747274 194.780416,22.4513446 L197.242752,23.9287465 C197.479542,24.0708205 197.786672,23.994038 197.928746,23.7572479 C198.070821,23.5204577 197.994038,23.2133276 197.757248,23.0712535 L195.294823,21.5937988 C195.427115,21.2552485 195.499732,20.88678 195.499732,20.5013419 C195.499732,18.8437465 194.156706,17.5 192.5,17.5 C190.843294,17.5 189.500268,18.8437465 189.500268,20.5013419 C189.500268,21.1091075 189.680819,21.6746805 189.991183,22.1472957 L165.767183,44.64101 L165.767183,44.64101 C165.406294,44.3944476 164.969976,44.2502684 164.5,44.2502684 C163.257433,44.2502684 162.250134,45.2581083 162.250134,46.5013419 C162.250134,46.9292933 162.369489,47.3293527 162.576711,47.6700143 L138.72545,69.6131742 C138.372873,69.383631 137.952003,69.2502684 137.5,69.2502684 C136.257433,69.2502684 135.250134,70.2581083 135.250134,71.5013419 C135.250134,71.5912406 135.255401,71.6799086 135.265643,71.7670536 L111.39825,80.2911227 C110.99879,79.6645016 110.2979,79.2489265 109.5,79.2489265 C108.429185,79.2489265 107.533094,79.9974077 107.305831,81 L89.6941692,81 C89.4669062,79.9974077 88.5708148,79.2489265 87.5,79.2489265 C86.4291852,79.2489265 85.5330938,79.9974077 85.3058308,81 L60.6941692,81 L60.6941692,81 C60.4669062,79.9974077 59.5708148,79.2489265 58.5,79.2489265 C57.4291852,79.2489265 56.5330938,79.9974077 56.3058308,81 L30.6941692,81 C30.4669062,79.9974077 29.5708148,79.2489265 28.5,79.2489265 C27.4291852,79.2489265 26.5330938,79.9974077 26.3058308,81 L-9,81 L-9,82 L26.3058308,82 C26.5330938,83.0025923 27.4291852,83.7510735 28.5,83.7510735 C29.5708148,83.7510735 30.4669062,83.0025923 30.6941692,82 L30.6941692,82 L56.3058308,82 C56.5330938,83.0025923 57.4291852,83.7510735 58.5,83.7510735 C59.5708148,83.7510735 60.4669062,83.0025923 60.6941692,82 L85.3058308,82 C85.5330938,83.0025923 86.4291852,83.7510735 87.5,83.7510735 C88.5708148,83.7510735 89.4669062,83.0025923 89.6941692,82 L89.6941692,82 L107.305831,82 C107.496622,82.8416934 108.158796,83.5042922 109,83.6952861 L109,83.6952861 L109,91.3060559 C108.159243,91.4969482 107.497326,92.1589443 107.306135,93 L89.6941692,93 L89.6941692,93 C89.4669062,91.9974077 88.5708148,91.2489265 87.5,91.2489265 C86.4291852,91.2489265 85.5330938,91.9974077 85.3058308,93 L60.6941692,93 C60.4669062,91.9974077 59.5708148,91.2489265 58.5,91.2489265 C57.4291852,91.2489265 56.5330938,91.9974077 56.3058308,93 L30.6941692,93 C30.4669062,91.9974077 29.5708148,91.2489265 28.5,91.2489265 C27.4291852,91.2489265 26.5330938,91.9974077 26.3058308,93 L-9,93 L-9,94 L26.3058308,94 C26.5330938,95.0025923 27.4291852,95.7510735 28.5,95.7510735 C29.5708148,95.7510735 30.4669062,95.0025923 30.6941692,94 L56.3058308,94 C56.5330938,95.0025923 57.4291852,95.7510735 58.5,95.7510735 C59.5708148,95.7510735 60.4669062,95.0025923 60.6941692,94 L60.6941692,94 L85.3058308,94 C85.5330938,95.0025923 86.4291852,95.7510735 87.5,95.7510735 C88.5708148,95.7510735 89.4669062,95.0025923 89.6941692,94 L107.305527,94 C107.495918,94.842331 108.158349,95.5055326 109,95.696628 L109,144 L110,144 L110,95.696628 C110.841651,95.5055326 111.504082,94.842331 111.694473,94 L135.305527,94 C135.532294,95.0032662 136.428708,95.7524154 137.5,95.7524154 C138.571292,95.7524154 139.467706,95.0032662 139.694473,94 L162.305527,94 C162.532294,95.0032662 163.428708,95.7524154 164.5,95.7524154 C165.571292,95.7524154 166.467706,95.0032662 166.694473,94 L190.305527,94 C190.495918,94.842331 191.158349,95.5055326 192,95.696628 L192,143 L193,143 L193,95.696628 C193.841651,95.5055326 194.504082,94.842331 194.694473,94 L200,94 L200,93 L194.693865,93 C194.502674,92.1589443 193.840757,91.4969482 193,91.3060559 L193,91.3060559 L193,71.50806 C192.84938,71.5829117 192.679605,71.625 192.5,71.625 C192.320395,71.625 192.15062,71.5829117 192,71.50806 L192,91.3060559 C191.159243,91.4969482 190.497326,92.1589443 190.306135,93 L190.306135,93 L166.693865,93 L166.693865,93 C166.502674,92.1589443 165.840757,91.4969482 165,91.3060559 L165,48.696628 C166.00192,48.4691439 166.749866,47.5726359 166.749866,46.5013419 C166.749866,46.0904765 166.639851,45.7053203 166.447687,45.3737543 L166.447687,45.3737543 L190.671139,22.8805483 L190.671139,22.8805483 C191.052092,23.1741316 191.505536,23.378171 192,23.4611785 L192,44.3060559 L192,44.3060559 C190.99808,44.5335399 190.250134,45.4300479 190.250134,46.5013419 C190.250134,47.5726359 190.99808,48.4691439 192,48.696628 L192,69.49194 C192.15062,69.4170883 192.320395,69.375 192.5,69.375 C192.679605,69.375 192.84938,69.4170883 193,69.49194 L193,69.49194 Z M193.621469,70.5897864 L197.757248,73.0712535 C197.994038,73.2133276 198.070821,73.5204577 197.928746,73.7572479 C197.786672,73.994038 197.479542,74.0708205 197.242752,73.9287465 L193.10705,71.4473254 C193.394921,71.2624741 193.593026,70.9499958 193.621469,70.5897864 L193.621469,70.5897864 Z M110,83.6952861 L110,91.3060559 C110.840757,91.4969482 111.502674,92.1589443 111.693865,93 L111.693865,93 L135.306135,93 C135.497326,92.1589443 136.159243,91.4969482 137,91.3060559 L137,73.696628 L137,73.696628 C136.41374,73.5635187 135.914437,73.2013459 135.601054,72.709126 L111.734205,81.2330006 C111.744547,81.3205598 111.749866,81.4096584 111.749866,81.5 C111.749866,82.571294 111.00192,83.467802 110,83.6952861 L110,83.6952861 Z M164,48.696628 L164,91.3060559 L164,91.3060559 C163.159243,91.4969482 162.497326,92.1589443 162.306135,93 L139.693865,93 C139.502674,92.1589443 138.840757,91.4969482 138,91.3060559 L138,73.696628 L138,73.696628 C139.00192,73.4691439 139.749866,72.5726359 139.749866,71.5013419 C139.749866,71.07267 139.630108,70.671984 139.422242,70.3309493 L163.272842,48.3883968 C162.990559,48.2042543 162.752146,47.9584208 162.576711,47.6700143 L164,48.696628 Z M137.5,94.625 C138.12132,94.625 138.625,94.1213203 138.625,93.5 C138.625,92.8786797 138.12132,92.375 137.5,92.375 C136.87868,92.375 136.375,92.8786797 136.375,93.5 C136.375,94.1213203 136.87868,94.625 137.5,94.625 Z M164.5,94.625 C165.12132,94.625 165.625,94.1213203 165.625,93.5 C165.625,92.8786797 165.12132,92.375 164.5,92.375 C163.87868,92.375 163.375,92.8786797 163.375,93.5 C163.375,94.1213203 163.87868,94.625 164.5,94.625 Z M109.5,94.625 C110.12132,94.625 110.625,94.1213203 110.625,93.5 C110.625,92.8786797 110.12132,92.375 109.5,92.375 C108.87868,92.375 108.375,92.8786797 108.375,93.5 C108.375,94.1213203 108.87868,94.625 109.5,94.625 Z M137.5,72.625 C138.12132,72.625 138.625,72.1213203 138.625,71.5 C138.625,70.8786797 138.12132,70.375 137.5,70.375 C136.87868,70.375 136.375,70.8786797 136.375,71.5 C136.375,72.1213203 136.87868,72.625 137.5,72.625 Z M164.5,47.625 C165.12132,47.625 165.625,47.1213203 165.625,46.5 C165.625,45.8786797 165.12132,45.375 164.5,45.375 C163.87868,45.375 163.375,45.8786797 163.375,46.5 C163.375,47.1213203 163.87868,47.625 164.5,47.625 Z M109.5,82.6236581 C110.12132,82.6236581 110.625,82.1199784 110.625,81.4986581 C110.625,80.8773377 110.12132,80.3736581 109.5,80.3736581 C108.87868,80.3736581 108.375,80.8773377 108.375,81.4986581 C108.375,82.1199784 108.87868,82.6236581 109.5,82.6236581 Z M87.5,82.6236581 C88.1213203,82.6236581 88.625,82.1199784 88.625,81.4986581 C88.625,80.8773377 88.1213203,80.3736581 87.5,80.3736581 C86.8786797,80.3736581 86.375,80.8773377 86.375,81.4986581 C86.375,82.1199784 86.8786797,82.6236581 87.5,82.6236581 Z M87.5,94.6236581 C88.1213203,94.6236581 88.625,94.1199784 88.625,93.4986581 C88.625,92.8773377 88.1213203,92.3736581 87.5,92.3736581 C86.8786797,92.3736581 86.375,92.8773377 86.375,93.4986581 C86.375,94.1199784 86.8786797,94.6236581 87.5,94.6236581 Z M58.5,94.6236581 C59.1213203,94.6236581 59.625,94.1199784 59.625,93.4986581 C59.625,92.8773377 59.1213203,92.3736581 58.5,92.3736581 C57.8786797,92.3736581 57.375,92.8773377 57.375,93.4986581 C57.375,94.1199784 57.8786797,94.6236581 58.5,94.6236581 Z M58.5,82.6236581 C59.1213203,82.6236581 59.625,82.1199784 59.625,81.4986581 C59.625,80.8773377 59.1213203,80.3736581 58.5,80.3736581 C57.8786797,80.3736581 57.375,80.8773377 57.375,81.4986581 C57.375,82.1199784 57.8786797,82.6236581 58.5,82.6236581 Z M28.5,82.6236581 C29.1213203,82.6236581 29.625,82.1199784 29.625,81.4986581 C29.625,80.8773377 29.1213203,80.3736581 28.5,80.3736581 C27.8786797,80.3736581 27.375,80.8773377 27.375,81.4986581 C27.375,82.1199784 27.8786797,82.6236581 28.5,82.6236581 Z M28.5,94.6236581 C29.1213203,94.6236581 29.625,94.1199784 29.625,93.4986581 C29.625,92.8773377 29.1213203,92.3736581 28.5,92.3736581 C27.8786797,92.3736581 27.375,92.8773377 27.375,93.4986581 C27.375,94.1199784 27.8786797,94.6236581 28.5,94.6236581 Z M192.5,47.625 C193.12132,47.625 193.625,47.1213203 193.625,46.5 C193.625,45.8786797 193.12132,45.375 192.5,45.375 C191.87868,45.375 191.375,45.8786797 191.375,46.5 C191.375,47.1213203 191.87868,47.625 192.5,47.625 Z M192.5,22 C193.328427,22 194,21.3284271 194,20.5 C194,19.6715729 193.328427,19 192.5,19 C191.671573,19 191,19.6715729 191,20.5 C191,21.3284271 191.671573,22 192.5,22 Z M192.5,94.625 C193.12132,94.625 193.625,94.1213203 193.625,93.5 C193.625,92.8786797 193.12132,92.375 192.5,92.375 C191.87868,92.375 191.375,92.8786797 191.375,93.5 C191.375,94.1213203 191.87868,94.625 192.5,94.625 Z"
+                    id="Rectangle-5"
+                    fill="#F55C23"
+                  />
+                  <path
+                    d="M362.999924,69.49194 L362.999924,48.696628 L362.999924,48.696628 C363.436931,48.5974065 363.82562,48.3709078 364.125004,48.0581427 L367.242677,49.9287465 C367.479467,50.0708205 367.786597,49.994038 367.928671,49.7572479 C368.070745,49.5204577 367.993962,49.2133276 367.757172,49.0712535 L364.639176,47.2004558 C364.710972,46.9803849 364.74979,46.7453991 364.74979,46.5013419 C364.74979,45.4300479 364.001844,44.5335399 362.999924,44.3060559 L362.999924,23.4611785 L362.999924,23.4611785 C363.708504,23.3422265 364.332848,22.9747274 364.78034,22.4513446 L367.242677,23.9287465 C367.479467,24.0708205 367.786597,23.994038 367.928671,23.7572479 C368.070745,23.5204577 367.993962,23.2133276 367.757172,23.0712535 L365.294748,21.5937988 C365.427039,21.2552485 365.499656,20.88678 365.499656,20.5013419 C365.499656,18.8437465 364.156631,17.5 362.499924,17.5 C360.843218,17.5 359.500193,18.8437465 359.500193,20.5013419 C359.500193,21.1091075 359.680743,21.6746805 359.991107,22.1472957 L335.767107,44.64101 L335.767107,44.64101 C335.406219,44.3944476 334.9699,44.2502684 334.499924,44.2502684 C333.257358,44.2502684 332.250059,45.2581083 332.250059,46.5013419 C332.250059,46.9292933 332.369414,47.3293527 332.576636,47.6700143 L308.725375,69.6131742 C308.372798,69.383631 307.951927,69.2502684 307.499924,69.2502684 C306.257358,69.2502684 305.250059,70.2581083 305.250059,71.5013419 C305.250059,71.5912406 305.255326,71.6799086 305.265568,71.7670536 L281.398174,80.2911227 C280.998715,79.6645016 280.297825,79.2489265 279.499924,79.2489265 C278.42911,79.2489265 277.533018,79.9974077 277.305755,81 L251.694094,81 C251.466831,79.9974077 250.570739,79.2489265 249.499924,79.2489265 C248.42911,79.2489265 247.533018,79.9974077 247.305755,81 L228.694094,81 L228.694094,81 C228.466831,79.9974077 227.570739,79.2489265 226.499924,79.2489265 C225.42911,79.2489265 224.533018,79.9974077 224.305755,81 L196.999924,81 L196.999924,82 L224.305755,82 C224.496546,82.8416934 225.158721,83.5042922 225.999924,83.6952861 L225.999924,91.3047139 C225.158721,91.4957078 224.496546,92.1583066 224.305755,93 L197.999924,93 L197.999924,94 L224.305755,94 C224.533018,95.0025923 225.42911,95.7510735 226.499924,95.7510735 C227.570739,95.7510735 228.466831,95.0025923 228.694094,94 L228.694094,94 L247.305755,94 L247.305755,94 C247.533018,95.0025923 248.42911,95.7510735 249.499924,95.7510735 C250.570739,95.7510735 251.466831,95.0025923 251.694094,94 L277.305452,94 C277.532219,95.0032662 278.428632,95.7524154 279.499924,95.7524154 C280.571217,95.7524154 281.46763,95.0032662 281.694397,94 L305.305452,94 C305.532219,95.0032662 306.428632,95.7524154 307.499924,95.7524154 C308.571217,95.7524154 309.46763,95.0032662 309.694397,94 L309.694397,94 L332.305452,94 C332.532219,95.0032662 333.428632,95.7524154 334.499924,95.7524154 C335.571217,95.7524154 336.46763,95.0032662 336.694397,94 L360.305452,94 C360.495843,94.842331 361.158274,95.5055326 361.999924,95.696628 L361.999924,143 L362.999924,143 L362.999924,95.696628 C363.841575,95.5055326 364.504006,94.842331 364.694397,94 L366.999924,94 L366.999924,93 L364.693789,93 C364.502599,92.1589443 363.840681,91.4969482 362.999924,91.3060559 L362.999924,91.3060559 L362.999924,71.50806 C362.849305,71.5829117 362.67953,71.625 362.499924,71.625 C362.320319,71.625 362.150544,71.5829117 361.999924,71.50806 L361.999924,91.3060559 C361.159168,91.4969482 360.49725,92.1589443 360.30606,93 L360.30606,93 L336.693789,93 L336.693789,93 C336.502599,92.1589443 335.840681,91.4969482 334.999924,91.3060559 L334.999924,48.696628 C336.001844,48.4691439 336.74979,47.5726359 336.74979,46.5013419 C336.74979,46.0904765 336.639775,45.7053203 336.447611,45.3737543 L336.447611,45.3737543 L360.671064,22.8805483 L360.671064,22.8805483 C361.052017,23.1741316 361.505461,23.378171 361.999924,23.4611785 L361.999924,44.3060559 L361.999924,44.3060559 C360.998005,44.5335399 360.250059,45.4300479 360.250059,46.5013419 C360.250059,47.5726359 360.998005,48.4691439 361.999924,48.696628 L361.999924,69.49194 C362.150544,69.4170883 362.320319,69.375 362.499924,69.375 C362.67953,69.375 362.849305,69.4170883 362.999924,69.49194 L362.999924,69.49194 Z M363.621394,70.5897864 L367.757172,73.0712535 C367.993962,73.2133276 368.070745,73.5204577 367.928671,73.7572479 C367.786597,73.994038 367.479467,74.0708205 367.242677,73.9287465 L363.106975,71.4473254 C363.394845,71.2624741 363.59295,70.9499958 363.621394,70.5897864 L363.621394,70.5897864 Z M247.305755,93 L228.694094,93 L228.694094,93 C228.503302,92.1583066 227.841128,91.4957078 226.999924,91.3047139 L226.999924,83.6952861 C227.841128,83.5042922 228.503302,82.8416934 228.694094,82 L247.305755,82 C247.496546,82.8416934 248.158721,83.5042922 248.999924,83.6952861 L248.999924,91.3047139 C248.158721,91.4957078 247.496546,92.1583066 247.305755,93 L247.305755,93 Z M251.694094,93 L277.30606,93 C277.49725,92.1589443 278.159168,91.4969482 278.999924,91.3060559 L278.999924,83.6952861 L278.999924,83.6952861 C278.158721,83.5042922 277.496546,82.8416934 277.305755,82 L251.694094,82 L251.694094,82 C251.503302,82.8416934 250.841128,83.5042922 249.999924,83.6952861 L249.999924,91.3047139 C250.841128,91.4957078 251.503302,92.1583066 251.694094,93 L251.694094,93 Z M279.999924,83.6952861 L279.999924,91.3060559 C280.840681,91.4969482 281.502599,92.1589443 281.693789,93 L281.693789,93 L305.30606,93 C305.49725,92.1589443 306.159168,91.4969482 306.999924,91.3060559 L306.999924,73.696628 L306.999924,73.696628 C306.413665,73.5635187 305.914362,73.2013459 305.600978,72.709126 L281.73413,81.2330006 C281.744471,81.3205598 281.74979,81.4096584 281.74979,81.5 C281.74979,82.571294 281.001844,83.467802 279.999924,83.6952861 L279.999924,83.6952861 Z M333.999924,48.696628 L333.999924,91.3060559 L333.999924,91.3060559 C333.159168,91.4969482 332.49725,92.1589443 332.30606,93 L309.693789,93 C309.502599,92.1589443 308.840681,91.4969482 307.999924,91.3060559 L307.999924,73.696628 L307.999924,73.696628 C309.001844,73.4691439 309.74979,72.5726359 309.74979,71.5013419 C309.74979,71.07267 309.630033,70.671984 309.422166,70.3309493 L333.272767,48.3883968 C332.990484,48.2042543 332.752071,47.9584208 332.576636,47.6700143 L333.999924,48.696628 Z M307.499924,94.625 C308.121245,94.625 308.624924,94.1213203 308.624924,93.5 C308.624924,92.8786797 308.121245,92.375 307.499924,92.375 C306.878604,92.375 306.374924,92.8786797 306.374924,93.5 C306.374924,94.1213203 306.878604,94.625 307.499924,94.625 Z M334.499924,94.625 C335.121245,94.625 335.624924,94.1213203 335.624924,93.5 C335.624924,92.8786797 335.121245,92.375 334.499924,92.375 C333.878604,92.375 333.374924,92.8786797 333.374924,93.5 C333.374924,94.1213203 333.878604,94.625 334.499924,94.625 Z M279.499924,94.625 C280.121245,94.625 280.624924,94.1213203 280.624924,93.5 C280.624924,92.8786797 280.121245,92.375 279.499924,92.375 C278.878604,92.375 278.374924,92.8786797 278.374924,93.5 C278.374924,94.1213203 278.878604,94.625 279.499924,94.625 Z M307.499924,72.625 C308.121245,72.625 308.624924,72.1213203 308.624924,71.5 C308.624924,70.8786797 308.121245,70.375 307.499924,70.375 C306.878604,70.375 306.374924,70.8786797 306.374924,71.5 C306.374924,72.1213203 306.878604,72.625 307.499924,72.625 Z M334.499924,47.625 C335.121245,47.625 335.624924,47.1213203 335.624924,46.5 C335.624924,45.8786797 335.121245,45.375 334.499924,45.375 C333.878604,45.375 333.374924,45.8786797 333.374924,46.5 C333.374924,47.1213203 333.878604,47.625 334.499924,47.625 Z M279.499924,82.6236581 C280.121245,82.6236581 280.624924,82.1199784 280.624924,81.4986581 C280.624924,80.8773377 280.121245,80.3736581 279.499924,80.3736581 C278.878604,80.3736581 278.374924,80.8773377 278.374924,81.4986581 C278.374924,82.1199784 278.878604,82.6236581 279.499924,82.6236581 Z M249.499924,82.6236581 C250.121245,82.6236581 250.624924,82.1199784 250.624924,81.4986581 C250.624924,80.8773377 250.121245,80.3736581 249.499924,80.3736581 C248.878604,80.3736581 248.374924,80.8773377 248.374924,81.4986581 C248.374924,82.1199784 248.878604,82.6236581 249.499924,82.6236581 Z M226.499924,82.6236581 C227.121245,82.6236581 227.624924,82.1199784 227.624924,81.4986581 C227.624924,80.8773377 227.121245,80.3736581 226.499924,80.3736581 C225.878604,80.3736581 225.374924,80.8773377 225.374924,81.4986581 C225.374924,82.1199784 225.878604,82.6236581 226.499924,82.6236581 Z M226.499924,94.6236581 C227.121245,94.6236581 227.624924,94.1199784 227.624924,93.4986581 C227.624924,92.8773377 227.121245,92.3736581 226.499924,92.3736581 C225.878604,92.3736581 225.374924,92.8773377 225.374924,93.4986581 C225.374924,94.1199784 225.878604,94.6236581 226.499924,94.6236581 Z M249.499924,94.6236581 C250.121245,94.6236581 250.624924,94.1199784 250.624924,93.4986581 C250.624924,92.8773377 250.121245,92.3736581 249.499924,92.3736581 C248.878604,92.3736581 248.374924,92.8773377 248.374924,93.4986581 C248.374924,94.1199784 248.878604,94.6236581 249.499924,94.6236581 Z M362.499924,47.625 C363.121245,47.625 363.624924,47.1213203 363.624924,46.5 C363.624924,45.8786797 363.121245,45.375 362.499924,45.375 C361.878604,45.375 361.374924,45.8786797 361.374924,46.5 C361.374924,47.1213203 361.878604,47.625 362.499924,47.625 Z M362.499924,22 C363.328352,22 363.999924,21.3284271 363.999924,20.5 C363.999924,19.6715729 363.328352,19 362.499924,19 C361.671497,19 360.999924,19.6715729 360.999924,20.5 C360.999924,21.3284271 361.671497,22 362.499924,22 Z M362.499924,94.625 C363.121245,94.625 363.624924,94.1213203 363.624924,93.5 C363.624924,92.8786797 363.121245,92.375 362.499924,92.375 C361.878604,92.375 361.374924,92.8786797 361.374924,93.5 C361.374924,94.1213203 361.878604,94.625 362.499924,94.625 Z"
+                    id="Rectangle-5-Copy"
+                    fill="#F55C23"
+                    transform="translate(282.499962, 80.250000) scale(-1, 1) translate(-282.499962, -80.250000) "
+                  />
+                  <path
+                    d="M92.2682896,101.529121 L98.4048966,101.529121 L98.9048966,101.529121 L98.9048966,100.529121 L98.4048966,100.529121 L92.2682896,100.529121 L91.7682896,100.529121 L91.7682896,101.529121 L92.2682896,101.529121 Z M30.90222,109.02779 L37.038827,109.02779 L37.538827,109.02779 L37.538827,108.02779 L37.038827,108.02779 L30.90222,108.02779 L30.40222,108.02779 L30.40222,109.02779 L30.90222,109.02779 Z M30.90222,115.02779 L37.038827,115.02779 L37.538827,115.02779 L37.538827,114.02779 L37.038827,114.02779 L30.90222,114.02779 L30.40222,114.02779 L30.40222,115.02779 L30.90222,115.02779 Z M30.90222,123.02779 L37.038827,123.02779 L37.538827,123.02779 L37.538827,122.02779 L37.038827,122.02779 L30.90222,122.02779 L30.40222,122.02779 L30.40222,123.02779 L30.90222,123.02779 Z M30.90222,101.529121 L37.038827,101.529121 L37.538827,101.529121 L37.538827,100.529121 L37.038827,100.529121 L30.90222,100.529121 L30.40222,100.529121 L30.40222,101.529121 L30.90222,101.529121 Z M51.3575766,101.529121 L57.4941835,101.529121 L57.9941835,101.529121 L57.9941835,100.529121 L57.4941835,100.529121 L51.3575766,100.529121 L50.8575766,100.529121 L50.8575766,101.529121 L51.3575766,101.529121 Z M92.2682896,131.523798 L98.4048966,131.523798 L98.9048966,131.523798 L98.9048966,130.523798 L98.4048966,130.523798 L92.2682896,130.523798 L91.7682896,130.523798 L91.7682896,131.523798 L92.2682896,131.523798 Z M92.2682896,124.025129 L98.4048966,124.025129 L98.9048966,124.025129 L98.9048966,123.025129 L98.4048966,123.025129 L92.2682896,123.025129 L91.7682896,123.025129 L91.7682896,124.025129 L92.2682896,124.025129 Z M92.2682896,116.526459 L98.4048966,116.526459 L98.9048966,116.526459 L98.9048966,115.526459 L98.4048966,115.526459 L92.2682896,115.526459 L91.7682896,115.526459 L91.7682896,116.526459 L92.2682896,116.526459 Z M71.8129331,101.529121 L77.94954,101.529121 L78.44954,101.529121 L78.44954,100.529121 L77.94954,100.529121 L71.8129331,100.529121 L71.3129331,100.529121 L71.3129331,101.529121 L71.8129331,101.529121 Z M71.8129331,108.529121 L77.94954,108.529121 L78.44954,108.529121 L78.44954,107.529121 L77.94954,107.529121 L71.8129331,107.529121 L71.3129331,107.529121 L71.3129331,108.529121 L71.8129331,108.529121 Z M91.8129331,108.529121 L97.94954,108.529121 L98.44954,108.529121 L98.44954,107.529121 L97.94954,107.529121 L91.8129331,107.529121 L91.3129331,107.529121 L91.3129331,108.529121 L91.8129331,108.529121 Z"
+                    id="bricks"
+                    fill="#F55C23"
+                  />
+                  <path
+                    d="M87.5316955,137 L87.5316955,126.72949 L76.6335576,111.72949 L59,106 L41.3664424,111.72949 L30.4683045,126.72949 L30.4683045,137 L87.5316955,137 Z"
+                    id="Polygon-3"
+                    stroke="#F55C23"
+                    fill="#FFFFFF"
+                  />
+                  <circle
+                    id="Oval-42"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="40.75"
+                    cy="111.75"
+                    r="1.75"
+                  />
+                  <circle
+                    id="Oval-42-Copy-2"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="76.75"
+                    cy="111.75"
+                    r="1.75"
+                  />
+                  <circle
+                    id="Oval-42-Copy-7"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="86.75"
+                    cy="126.75"
+                    r="1.75"
+                  />
+                  <circle
+                    id="Oval-42-Copy"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="59"
+                    cy="106"
+                    r="1.75"
+                  />
+                  <g
+                    id="pylon-copy"
+                    transform="translate(181.596857, 142.941563)"
+                    fill="#FFFFFF"
+                  >
+                    <polygon
+                      id="Polygon-3"
+                      stroke="#F55C23"
+                      transform="translate(16.364285, 4.090183) scale(1, -1) translate(-16.364285, -4.090183) "
+                      points="16.3642852 0 31.9276453 2.82624706 25.9829707 7.39921085 6.7455997 7.39921085 0.800925127 2.82624706 "
+                    />
+                    <ellipse
+                      id="Oval-153"
+                      stroke="#F65C23"
+                      cx="2.38645826"
+                      cy="5.11272896"
+                      rx="1.70461304"
+                      ry="1.70424299"
+                    />
+                    <ellipse
+                      id="Oval-153"
+                      stroke="#F65C23"
+                      cx="30.3421122"
+                      cy="5.11272896"
+                      rx="1.70461304"
+                      ry="1.70424299"
+                    />
+                  </g>
+                  <ellipse
+                    id="Oval-42-Copy-4"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    transform="translate(202.726374, 143.734238) scale(-1, 1) translate(-202.726374, -143.734238) "
+                    cx="202.726374"
+                    cy="143.734238"
+                    rx="1.72637406"
+                    ry="1.73423766"
+                  />
+                  <ellipse
+                    id="Oval-42-Copy-3"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    transform="translate(192.726374, 143.734238) scale(-1, 1) translate(-192.726374, -143.734238) "
+                    cx="192.726374"
+                    cy="143.734238"
+                    rx="1.72637406"
+                    ry="1.73423766"
+                  />
+                  <g id="Group" transform="translate(367.000000, 17.000000)">
+                    <path
+                      d="M86,54.50806 L86,74.3060559 C86.8407567,74.4969482 87.5026741,75.1589443 87.6938646,76 L93,76 L93,77 L87.6944729,77 C87.5040817,77.842331 86.8416508,78.5055326 86,78.696628 L86,126 L85,126 L85,78.696628 C84.1583492,78.5055326 83.4959183,77.842331 83.3055271,77 L83.3055271,77 L59.6944729,77 L59.6944729,77 C59.4677057,78.0032662 58.5712925,78.7524154 57.5,78.7524154 C56.4287075,78.7524154 55.5322943,78.0032662 55.3055271,77 L32.6944729,77 C32.4677057,78.0032662 31.5712925,78.7524154 30.5,78.7524154 C29.4287075,78.7524154 28.5322943,78.0032662 28.3055271,77 L4.69447293,77 L4.69447293,77 C4.46770567,78.0032662 3.57129249,78.7524154 2.5,78.7524154 C1.25743338,78.7524154 0.250134119,77.7445755 0.250134119,76.5013419 C0.250134119,75.4300479 0.99808035,74.5335399 2,74.3060559 L2,66.6952861 C0.99808035,66.467802 0.250134119,65.571294 0.250134119,64.5 C0.250134119,63.2567664 1.25743338,62.2489265 2.5,62.2489265 C3.29790038,62.2489265 3.99879007,62.6645016 4.39824959,63.2911227 L4.39824959,63.2911227 L28.2656431,54.7670536 C28.2554011,54.6799086 28.2501341,54.5912406 28.2501341,54.5013419 C28.2501341,53.2581083 29.2574334,52.2502684 30.5,52.2502684 C30.9520026,52.2502684 31.3728733,52.383631 31.7254503,52.6131742 L31.7254503,52.6131742 L55.5767111,30.6700143 L55.5767111,30.6700143 C55.3694895,30.3293527 55.2501341,29.9292933 55.2501341,29.5013419 C55.2501341,28.2581083 56.2574334,27.2502684 57.5,27.2502684 C57.9699755,27.2502684 58.4062942,27.3944476 58.7671826,27.64101 L82.9911826,5.14729569 C82.680819,4.67468048 82.5002682,4.10910746 82.5002682,3.50134191 C82.5002682,1.84374654 83.8432939,0.5 85.5,0.5 C87.1567061,0.5 88.4997318,1.84374654 88.4997318,3.50134191 C88.4997318,3.88678004 88.427115,4.25524852 88.2948233,4.59379876 L90.7572479,6.07125354 C90.994038,6.21332762 91.0708205,6.52045774 90.9287465,6.75724788 C90.7866724,6.99403801 90.4795423,7.07082054 90.2427521,6.92874646 L87.7804156,5.45134457 C87.3329231,5.97472736 86.70858,6.34222649 86,6.46117854 L86,6.46117854 L86,27.3060559 L86,27.3060559 C87.0019197,27.5335399 87.7498659,28.4300479 87.7498659,29.5013419 C87.7498659,29.7453991 87.7110476,29.9803849 87.6392516,30.2004558 L90.7572479,32.0712535 C90.994038,32.2133276 91.0708205,32.5204577 90.9287465,32.7572479 C90.7866724,32.994038 90.4795423,33.0708205 90.2427521,32.9287465 L87.1250792,31.0581427 C86.8256955,31.3709078 86.4370061,31.5974065 86,31.696628 L86,52.49194 C86.3704296,52.6760278 86.625,53.0582848 86.625,53.5 C86.625,53.5302219 86.6238083,53.5601656 86.6214693,53.5897864 L90.7572479,56.0712535 C90.994038,56.2133276 91.0708205,56.5204577 90.9287465,56.7572479 C90.7866724,56.994038 90.4795423,57.0708205 90.2427521,56.9287465 L86.1070504,54.4473254 C86.0726064,54.4694431 86.0368773,54.4897336 86,54.50806 L86,54.50806 Z M85,54.50806 L85,74.3060559 C84.1592433,74.4969482 83.4973259,75.1589443 83.3061354,76 L83.3061354,76 L59.6938646,76 L59.6938646,76 C59.5026741,75.1589443 58.8407567,74.4969482 58,74.3060559 L58,31.696628 C59.0019197,31.4691439 59.7498659,30.5726359 59.7498659,29.5013419 C59.7498659,29.0904765 59.6398508,28.7053203 59.4476867,28.3737543 L59.4476867,28.3737543 L83.6711393,5.8805483 L83.6711393,5.8805483 C84.0520923,6.1741316 84.5055363,6.37817101 85,6.46117854 L85,27.3060559 L85,27.3060559 C83.9980803,27.5335399 83.2501341,28.4300479 83.2501341,29.5013419 C83.2501341,30.5726359 83.9980803,31.4691439 85,31.696628 L85,52.49194 C85.1506199,52.4170883 85.3203948,52.375 85.5,52.375 C85.6796052,52.375 85.8493801,52.4170883 86,52.49194 L85,54.50806 Z M4.73420518,64.2330006 L28.601054,55.709126 C28.9144371,56.2013459 29.4137402,56.5635187 30,56.696628 L30,74.3060559 C29.1592433,74.4969482 28.4973259,75.1589443 28.3061354,76 L4.69386458,76 C4.50267409,75.1589443 3.84075674,74.4969482 3,74.3060559 L3,66.6952861 C4.00191965,66.467802 4.74986588,65.571294 4.74986588,64.5 C4.74986588,64.4096584 4.7445469,64.3205598 4.73420518,64.2330006 L4.73420518,64.2330006 Z M56.2728421,31.3883968 L32.4222415,53.3309493 L32.4222415,53.3309493 C32.6301083,53.671984 32.7498659,54.07267 32.7498659,54.5013419 C32.7498659,55.5726359 32.0019197,56.4691439 31,56.696628 L31,74.3060559 C31.8407567,74.4969482 32.5026741,75.1589443 32.6938646,76 L55.3061354,76 C55.4973259,75.1589443 56.1592433,74.4969482 57,74.3060559 L57,31.696628 C56.7371633,31.6369514 56.4918047,31.5312334 56.2728421,31.3883968 L56.2728421,31.3883968 Z M30.5,77.625 C31.1213203,77.625 31.625,77.1213203 31.625,76.5 C31.625,75.8786797 31.1213203,75.375 30.5,75.375 C29.8786797,75.375 29.375,75.8786797 29.375,76.5 C29.375,77.1213203 29.8786797,77.625 30.5,77.625 Z M57.5,77.625 C58.1213203,77.625 58.625,77.1213203 58.625,76.5 C58.625,75.8786797 58.1213203,75.375 57.5,75.375 C56.8786797,75.375 56.375,75.8786797 56.375,76.5 C56.375,77.1213203 56.8786797,77.625 57.5,77.625 Z M2.5,77.625 C3.12132034,77.625 3.625,77.1213203 3.625,76.5 C3.625,75.8786797 3.12132034,75.375 2.5,75.375 C1.87867966,75.375 1.375,75.8786797 1.375,76.5 C1.375,77.1213203 1.87867966,77.625 2.5,77.625 Z M30.5,55.625 C31.1213203,55.625 31.625,55.1213203 31.625,54.5 C31.625,53.8786797 31.1213203,53.375 30.5,53.375 C29.8786797,53.375 29.375,53.8786797 29.375,54.5 C29.375,55.1213203 29.8786797,55.625 30.5,55.625 Z M57.5,30.625 C58.1213203,30.625 58.625,30.1213203 58.625,29.5 C58.625,28.8786797 58.1213203,28.375 57.5,28.375 C56.8786797,28.375 56.375,28.8786797 56.375,29.5 C56.375,30.1213203 56.8786797,30.625 57.5,30.625 Z M2.5,65.6236581 C3.12132034,65.6236581 3.625,65.1199784 3.625,64.4986581 C3.625,63.8773377 3.12132034,63.3736581 2.5,63.3736581 C1.87867966,63.3736581 1.375,63.8773377 1.375,64.4986581 C1.375,65.1199784 1.87867966,65.6236581 2.5,65.6236581 Z M85.5,30.625 C86.1213203,30.625 86.625,30.1213203 86.625,29.5 C86.625,28.8786797 86.1213203,28.375 85.5,28.375 C84.8786797,28.375 84.375,28.8786797 84.375,29.5 C84.375,30.1213203 84.8786797,30.625 85.5,30.625 Z M85.5,5 C86.3284271,5 87,4.32842712 87,3.5 C87,2.67157288 86.3284271,2 85.5,2 C84.6715729,2 84,2.67157288 84,3.5 C84,4.32842712 84.6715729,5 85.5,5 Z M85.5,77.625 C86.1213203,77.625 86.625,77.1213203 86.625,76.5 C86.625,75.8786797 86.1213203,75.375 85.5,75.375 C84.8786797,75.375 84.375,75.8786797 84.375,76.5 C84.375,77.1213203 84.8786797,77.625 85.5,77.625 Z"
+                      id="Rectangle-5-Copy-3"
+                      fill="#F55C23"
+                    />
+                    <g
+                      id="pylon-copy-2"
+                      transform="translate(74.596857, 125.941563)"
+                      fill="#FFFFFF"
+                    >
+                      <polygon
+                        id="Polygon-3"
+                        stroke="#F55C23"
+                        transform="translate(16.364285, 4.090183) scale(1, -1) translate(-16.364285, -4.090183) "
+                        points="16.3642852 0 31.9276453 2.82624706 25.9829707 7.39921085 6.7455997 7.39921085 0.800925127 2.82624706 "
+                      />
+                      <ellipse
+                        id="Oval-153"
+                        stroke="#F65C23"
+                        cx="2.38645826"
+                        cy="5.11272896"
+                        rx="1.70461304"
+                        ry="1.70424299"
+                      />
+                      <ellipse
+                        id="Oval-153"
+                        stroke="#F65C23"
+                        cx="30.3421122"
+                        cy="5.11272896"
+                        rx="1.70461304"
+                        ry="1.70424299"
+                      />
+                    </g>
+                    <ellipse
+                      id="Oval-42-Copy-5"
+                      stroke="#F65C23"
+                      fill="#FFFFFF"
+                      transform="translate(95.726374, 126.734238) scale(-1, 1) translate(-95.726374, -126.734238) "
+                      cx="95.7263741"
+                      cy="126.734238"
+                      rx="1.72637406"
+                      ry="1.73423766"
+                    />
+                    <ellipse
+                      id="Oval-42-Copy-6"
+                      stroke="#F65C23"
+                      fill="#FFFFFF"
+                      transform="translate(85.726374, 126.734238) scale(-1, 1) translate(-85.726374, -126.734238) "
+                      cx="85.7263741"
+                      cy="126.734238"
+                      rx="1.72637406"
+                      ry="1.73423766"
+                    />
+                  </g>
+                  <path
+                    d="M658,69.49194 L658,48.696628 L658,48.696628 C658.437006,48.5974065 658.825695,48.3709078 659.125079,48.0581427 L662.242752,49.9287465 C662.479542,50.0708205 662.786672,49.994038 662.928746,49.7572479 C663.070821,49.5204577 662.994038,49.2133276 662.757248,49.0712535 L659.639252,47.2004558 C659.711048,46.9803849 659.749866,46.7453991 659.749866,46.5013419 C659.749866,45.4300479 659.00192,44.5335399 658,44.3060559 L658,23.4611785 L658,23.4611785 C658.70858,23.3422265 659.332923,22.9747274 659.780416,22.4513446 L662.242752,23.9287465 C662.479542,24.0708205 662.786672,23.994038 662.928746,23.7572479 C663.070821,23.5204577 662.994038,23.2133276 662.757248,23.0712535 L660.294823,21.5937988 C660.427115,21.2552485 660.499732,20.88678 660.499732,20.5013419 C660.499732,18.8437465 659.156706,17.5 657.5,17.5 C655.843294,17.5 654.500268,18.8437465 654.500268,20.5013419 C654.500268,21.1091075 654.680819,21.6746805 654.991183,22.1472957 L630.767183,44.64101 L630.767183,44.64101 C630.406294,44.3944476 629.969976,44.2502684 629.5,44.2502684 C628.257433,44.2502684 627.250134,45.2581083 627.250134,46.5013419 C627.250134,46.9292933 627.369489,47.3293527 627.576711,47.6700143 L603.72545,69.6131742 C603.372873,69.383631 602.952003,69.2502684 602.5,69.2502684 C601.257433,69.2502684 600.250134,70.2581083 600.250134,71.5013419 C600.250134,71.5912406 600.255401,71.6799086 600.265643,71.7670536 L576.39825,80.2911227 C575.99879,79.6645016 575.2979,79.2489265 574.5,79.2489265 C573.429185,79.2489265 572.533094,79.9974077 572.305831,81 L554.694169,81 C554.466906,79.9974077 553.570815,79.2489265 552.5,79.2489265 C551.429185,79.2489265 550.533094,79.9974077 550.305831,81 L525.694169,81 L525.694169,81 C525.466906,79.9974077 524.570815,79.2489265 523.5,79.2489265 C522.429185,79.2489265 521.533094,79.9974077 521.305831,81 L495.694169,81 C495.466906,79.9974077 494.570815,79.2489265 493.5,79.2489265 C492.429185,79.2489265 491.533094,79.9974077 491.305831,81 L456,81 L456,82 L491.305831,82 C491.533094,83.0025923 492.429185,83.7510735 493.5,83.7510735 C494.570815,83.7510735 495.466906,83.0025923 495.694169,82 L495.694169,82 L521.305831,82 C521.533094,83.0025923 522.429185,83.7510735 523.5,83.7510735 C524.570815,83.7510735 525.466906,83.0025923 525.694169,82 L550.305831,82 C550.533094,83.0025923 551.429185,83.7510735 552.5,83.7510735 C553.570815,83.7510735 554.466906,83.0025923 554.694169,82 L554.694169,82 L572.305831,82 C572.496622,82.8416934 573.158796,83.5042922 574,83.6952861 L574,83.6952861 L574,91.3060559 C573.159243,91.4969482 572.497326,92.1589443 572.306135,93 L554.694169,93 L554.694169,93 C554.466906,91.9974077 553.570815,91.2489265 552.5,91.2489265 C551.429185,91.2489265 550.533094,91.9974077 550.305831,93 L525.694169,93 C525.466906,91.9974077 524.570815,91.2489265 523.5,91.2489265 C522.429185,91.2489265 521.533094,91.9974077 521.305831,93 L495.694169,93 C495.466906,91.9974077 494.570815,91.2489265 493.5,91.2489265 C492.429185,91.2489265 491.533094,91.9974077 491.305831,93 L456,93 L456,94 L491.305831,94 C491.533094,95.0025923 492.429185,95.7510735 493.5,95.7510735 C494.570815,95.7510735 495.466906,95.0025923 495.694169,94 L521.305831,94 C521.533094,95.0025923 522.429185,95.7510735 523.5,95.7510735 C524.570815,95.7510735 525.466906,95.0025923 525.694169,94 L525.694169,94 L550.305831,94 C550.533094,95.0025923 551.429185,95.7510735 552.5,95.7510735 C553.570815,95.7510735 554.466906,95.0025923 554.694169,94 L572.305527,94 C572.495918,94.842331 573.158349,95.5055326 574,95.696628 L574,144 L575,144 L575,95.696628 C575.841651,95.5055326 576.504082,94.842331 576.694473,94 L600.305527,94 C600.532294,95.0032662 601.428708,95.7524154 602.5,95.7524154 C603.571292,95.7524154 604.467706,95.0032662 604.694473,94 L627.305527,94 C627.532294,95.0032662 628.428708,95.7524154 629.5,95.7524154 C630.571292,95.7524154 631.467706,95.0032662 631.694473,94 L655.305527,94 C655.495918,94.842331 656.158349,95.5055326 657,95.696628 L657,143 L658,143 L658,95.696628 C658.841651,95.5055326 659.504082,94.842331 659.694473,94 L665,94 L665,93 L659.693865,93 C659.502674,92.1589443 658.840757,91.4969482 658,91.3060559 L658,91.3060559 L658,71.50806 C657.84938,71.5829117 657.679605,71.625 657.5,71.625 C657.320395,71.625 657.15062,71.5829117 657,71.50806 L657,91.3060559 C656.159243,91.4969482 655.497326,92.1589443 655.306135,93 L655.306135,93 L631.693865,93 L631.693865,93 C631.502674,92.1589443 630.840757,91.4969482 630,91.3060559 L630,48.696628 C631.00192,48.4691439 631.749866,47.5726359 631.749866,46.5013419 C631.749866,46.0904765 631.639851,45.7053203 631.447687,45.3737543 L631.447687,45.3737543 L655.671139,22.8805483 L655.671139,22.8805483 C656.052092,23.1741316 656.505536,23.378171 657,23.4611785 L657,44.3060559 L657,44.3060559 C655.99808,44.5335399 655.250134,45.4300479 655.250134,46.5013419 C655.250134,47.5726359 655.99808,48.4691439 657,48.696628 L657,69.49194 C657.15062,69.4170883 657.320395,69.375 657.5,69.375 C657.679605,69.375 657.84938,69.4170883 658,69.49194 L658,69.49194 Z M658.621469,70.5897864 L662.757248,73.0712535 C662.994038,73.2133276 663.070821,73.5204577 662.928746,73.7572479 C662.786672,73.994038 662.479542,74.0708205 662.242752,73.9287465 L658.10705,71.4473254 C658.394921,71.2624741 658.593026,70.9499958 658.621469,70.5897864 L658.621469,70.5897864 Z M575,83.6952861 L575,91.3060559 C575.840757,91.4969482 576.502674,92.1589443 576.693865,93 L576.693865,93 L600.306135,93 C600.497326,92.1589443 601.159243,91.4969482 602,91.3060559 L602,73.696628 L602,73.696628 C601.41374,73.5635187 600.914437,73.2013459 600.601054,72.709126 L576.734205,81.2330006 C576.744547,81.3205598 576.749866,81.4096584 576.749866,81.5 C576.749866,82.571294 576.00192,83.467802 575,83.6952861 L575,83.6952861 Z M629,48.696628 L629,91.3060559 L629,91.3060559 C628.159243,91.4969482 627.497326,92.1589443 627.306135,93 L604.693865,93 C604.502674,92.1589443 603.840757,91.4969482 603,91.3060559 L603,73.696628 L603,73.696628 C604.00192,73.4691439 604.749866,72.5726359 604.749866,71.5013419 C604.749866,71.07267 604.630108,70.671984 604.422242,70.3309493 L628.272842,48.3883968 C627.990559,48.2042543 627.752146,47.9584208 627.576711,47.6700143 L629,48.696628 Z M602.5,94.625 C603.12132,94.625 603.625,94.1213203 603.625,93.5 C603.625,92.8786797 603.12132,92.375 602.5,92.375 C601.87868,92.375 601.375,92.8786797 601.375,93.5 C601.375,94.1213203 601.87868,94.625 602.5,94.625 Z M629.5,94.625 C630.12132,94.625 630.625,94.1213203 630.625,93.5 C630.625,92.8786797 630.12132,92.375 629.5,92.375 C628.87868,92.375 628.375,92.8786797 628.375,93.5 C628.375,94.1213203 628.87868,94.625 629.5,94.625 Z M574.5,94.625 C575.12132,94.625 575.625,94.1213203 575.625,93.5 C575.625,92.8786797 575.12132,92.375 574.5,92.375 C573.87868,92.375 573.375,92.8786797 573.375,93.5 C573.375,94.1213203 573.87868,94.625 574.5,94.625 Z M602.5,72.625 C603.12132,72.625 603.625,72.1213203 603.625,71.5 C603.625,70.8786797 603.12132,70.375 602.5,70.375 C601.87868,70.375 601.375,70.8786797 601.375,71.5 C601.375,72.1213203 601.87868,72.625 602.5,72.625 Z M629.5,47.625 C630.12132,47.625 630.625,47.1213203 630.625,46.5 C630.625,45.8786797 630.12132,45.375 629.5,45.375 C628.87868,45.375 628.375,45.8786797 628.375,46.5 C628.375,47.1213203 628.87868,47.625 629.5,47.625 Z M574.5,82.6236581 C575.12132,82.6236581 575.625,82.1199784 575.625,81.4986581 C575.625,80.8773377 575.12132,80.3736581 574.5,80.3736581 C573.87868,80.3736581 573.375,80.8773377 573.375,81.4986581 C573.375,82.1199784 573.87868,82.6236581 574.5,82.6236581 Z M552.5,82.6236581 C553.12132,82.6236581 553.625,82.1199784 553.625,81.4986581 C553.625,80.8773377 553.12132,80.3736581 552.5,80.3736581 C551.87868,80.3736581 551.375,80.8773377 551.375,81.4986581 C551.375,82.1199784 551.87868,82.6236581 552.5,82.6236581 Z M552.5,94.6236581 C553.12132,94.6236581 553.625,94.1199784 553.625,93.4986581 C553.625,92.8773377 553.12132,92.3736581 552.5,92.3736581 C551.87868,92.3736581 551.375,92.8773377 551.375,93.4986581 C551.375,94.1199784 551.87868,94.6236581 552.5,94.6236581 Z M523.5,94.6236581 C524.12132,94.6236581 524.625,94.1199784 524.625,93.4986581 C524.625,92.8773377 524.12132,92.3736581 523.5,92.3736581 C522.87868,92.3736581 522.375,92.8773377 522.375,93.4986581 C522.375,94.1199784 522.87868,94.6236581 523.5,94.6236581 Z M523.5,82.6236581 C524.12132,82.6236581 524.625,82.1199784 524.625,81.4986581 C524.625,80.8773377 524.12132,80.3736581 523.5,80.3736581 C522.87868,80.3736581 522.375,80.8773377 522.375,81.4986581 C522.375,82.1199784 522.87868,82.6236581 523.5,82.6236581 Z M493.5,82.6236581 C494.12132,82.6236581 494.625,82.1199784 494.625,81.4986581 C494.625,80.8773377 494.12132,80.3736581 493.5,80.3736581 C492.87868,80.3736581 492.375,80.8773377 492.375,81.4986581 C492.375,82.1199784 492.87868,82.6236581 493.5,82.6236581 Z M493.5,94.6236581 C494.12132,94.6236581 494.625,94.1199784 494.625,93.4986581 C494.625,92.8773377 494.12132,92.3736581 493.5,92.3736581 C492.87868,92.3736581 492.375,92.8773377 492.375,93.4986581 C492.375,94.1199784 492.87868,94.6236581 493.5,94.6236581 Z M657.5,47.625 C658.12132,47.625 658.625,47.1213203 658.625,46.5 C658.625,45.8786797 658.12132,45.375 657.5,45.375 C656.87868,45.375 656.375,45.8786797 656.375,46.5 C656.375,47.1213203 656.87868,47.625 657.5,47.625 Z M657.5,22 C658.328427,22 659,21.3284271 659,20.5 C659,19.6715729 658.328427,19 657.5,19 C656.671573,19 656,19.6715729 656,20.5 C656,21.3284271 656.671573,22 657.5,22 Z M657.5,94.625 C658.12132,94.625 658.625,94.1213203 658.625,93.5 C658.625,92.8786797 658.12132,92.375 657.5,92.375 C656.87868,92.375 656.375,92.8786797 656.375,93.5 C656.375,94.1213203 656.87868,94.625 657.5,94.625 Z"
+                    id="Rectangle-5-Copy-2"
+                    fill="#F55C23"
+                    transform="translate(560.500000, 80.750000) scale(-1, 1) translate(-560.500000, -80.750000) "
+                  />
+                  <path
+                    d="M563.038827,101.529121 L556.90222,101.529121 L556.40222,101.529121 L556.40222,100.529121 L556.90222,100.529121 L563.038827,100.529121 L563.538827,100.529121 L563.538827,101.529121 L563.038827,101.529121 Z M624.404897,109.02779 L618.26829,109.02779 L617.76829,109.02779 L617.76829,108.02779 L618.26829,108.02779 L624.404897,108.02779 L624.904897,108.02779 L624.904897,109.02779 L624.404897,109.02779 Z M624.404897,115.02779 L618.26829,115.02779 L617.76829,115.02779 L617.76829,114.02779 L618.26829,114.02779 L624.404897,114.02779 L624.904897,114.02779 L624.904897,115.02779 L624.404897,115.02779 Z M624.404897,123.02779 L618.26829,123.02779 L617.76829,123.02779 L617.76829,122.02779 L618.26829,122.02779 L624.404897,122.02779 L624.904897,122.02779 L624.904897,123.02779 L624.404897,123.02779 Z M624.404897,101.529121 L618.26829,101.529121 L617.76829,101.529121 L617.76829,100.529121 L618.26829,100.529121 L624.404897,100.529121 L624.904897,100.529121 L624.904897,101.529121 L624.404897,101.529121 Z M603.94954,101.529121 L597.812933,101.529121 L597.312933,101.529121 L597.312933,100.529121 L597.812933,100.529121 L603.94954,100.529121 L604.44954,100.529121 L604.44954,101.529121 L603.94954,101.529121 Z M563.038827,131.523798 L556.90222,131.523798 L556.40222,131.523798 L556.40222,130.523798 L556.90222,130.523798 L563.038827,130.523798 L563.538827,130.523798 L563.538827,131.523798 L563.038827,131.523798 Z M563.038827,124.025129 L556.90222,124.025129 L556.40222,124.025129 L556.40222,123.025129 L556.90222,123.025129 L563.038827,123.025129 L563.538827,123.025129 L563.538827,124.025129 L563.038827,124.025129 Z M563.038827,116.526459 L556.90222,116.526459 L556.40222,116.526459 L556.40222,115.526459 L556.90222,115.526459 L563.038827,115.526459 L563.538827,115.526459 L563.538827,116.526459 L563.038827,116.526459 Z M583.494184,101.529121 L577.357577,101.529121 L576.857577,101.529121 L576.857577,100.529121 L577.357577,100.529121 L583.494184,100.529121 L583.994184,100.529121 L583.994184,101.529121 L583.494184,101.529121 Z M583.494184,108.529121 L577.357577,108.529121 L576.857577,108.529121 L576.857577,107.529121 L577.357577,107.529121 L583.494184,107.529121 L583.994184,107.529121 L583.994184,108.529121 L583.494184,108.529121 Z M563.494184,108.529121 L557.357577,108.529121 L556.857577,108.529121 L556.857577,107.529121 L557.357577,107.529121 L563.494184,107.529121 L563.994184,107.529121 L563.994184,108.529121 L563.494184,108.529121 Z"
+                    id="bricks-copy"
+                    fill="#F55C23"
+                  />
+                  <path
+                    d="M556.468305,137 L556.468305,126.72949 L567.366442,111.72949 L585,106 L602.633558,111.72949 L613.531695,126.72949 L613.531695,137 L556.468305,137 Z"
+                    id="Polygon-3-Copy"
+                    stroke="#F55C23"
+                    fill="#FFFFFF"
+                  />
+                  <circle
+                    id="Oval-42-Copy-8"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="566.75"
+                    cy="111.75"
+                    r="1.75"
+                  />
+                  <circle
+                    id="Oval-42-Copy-9"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="602.75"
+                    cy="111.75"
+                    r="1.75"
+                  />
+                  <circle
+                    id="Oval-42-Copy-10"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="612.75"
+                    cy="126.75"
+                    r="1.75"
+                  />
+                  <circle
+                    id="Oval-42-Copy-11"
+                    stroke="#F65C23"
+                    fill="#FFFFFF"
+                    cx="585"
+                    cy="106"
+                    r="1.75"
+                  />
+                </g>
+              </g>
+            </svg>
+          </div>
+          <div className="brand-mountain-2">
+            <svg
+              width="514px"
+              height="175px"
+              viewBox="0 0 514 175"
+              version="1.1"
+            >
+              <path
+                fill="#fff"
+                d="M41.5,45 L78,33 L78,74.5 L2.5,92 L41.5,45 Z"
+                id="Path"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M77.5,32.5 L143,45.5 L76.5,75.5 L77.5,32.5 Z"
+                id="Path-Copy-8"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M1.5,91 L78,73.5 L87,119.5 L41.5,149 L1.5,91 Z"
+                id="Path-Copy"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M77.5000001,74.5 L143,44 L174,46 L86.0000001,119.5 L77.5000001,74.5 Z"
+                id="Path-Copy-7"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M142.5,45 L205,25.5 L229,39.5 L173.5,47.5 L142.5,45 Z"
+                id="Path-Copy-9"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M158.499999,99 L225.499999,80.5 L270.999999,87.5 L226.999999,125 L158.499999,99 Z"
+                id="Path-Copy-2"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M156.499999,101.5 L172.999999,47 L226.499999,39 L226.499999,81 L156.499999,101.5 Z"
+                id="Path-Copy-11"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M243.000001,88.5 L225.5,43.5 L288,39 L288,81.5 L243.000001,88.5 Z"
+                id="Path-Copy-12"
+                stroke="#B6CCE5"
+                transform="translate(256.750000, 63.750000) scale(-1, 1) translate(-256.750000, -63.750000) "
+              />
+              <path
+                fill="#fff"
+                d="M286.5,44.5 L342.5,64 L319.5,110.5 L270,86.5 L286.5,44.5 Z"
+                id="Path-Copy-4"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M341.5,64 L390.5,111 L396.5,155 L317,109 L341.5,64 Z"
+                id="Path-Copy-10"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M389,111 L505,149 L396,154 L389,111 Z"
+                id="Path-Copy-13"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M321,110 L397,154 L282,149 L321,110 Z"
+                id="Path-Copy-14"
+                stroke="#4A90E2"
+              />
+              <path
+                fill="#fff"
+                d="M247.5,20.5 L287,44.5 L226.5,40 L247.5,20.5 Z"
+                id="Path-Copy-5"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M221.999999,13 L248.499999,20.5 L227.499999,40 L203.999999,26 L221.999999,13 Z"
+                id="Path-Copy-6"
+                stroke="#B6CCE5"
+              />
+              <path
+                fill="#fff"
+                d="M228.499999,126.000001 L284.000003,148.499999 L224.500001,171.000001 L158.500001,148.499999 L228.499999,126.000001 Z"
+                id="fg-geometry"
+                stroke="#4A90E2"
+              />
+              <polygon
+                fill="#fff"
+                id="Path"
+                stroke="#4A90E2"
+                points="158.5 99 159.5 149 42 149 "
+              />
+              <path
+                fill="#fff"
+                d="M271.000001,86.0000002 L321.5,110.5 L283,149.5 L225,125.5 L271.000001,86.0000002 Z"
+                id="Path-Copy-3"
+                stroke="#4A90E2"
+              />
+              <path
+                fill="#fff"
+                d="M42.2061,154.838108 C45.4069681,154.838108 48.0017844,152.243855 48.0017844,149.043682 C48.0017844,145.843509 45.4069681,143.249256 42.2061,143.249256 C39.0052319,143.249256 36.4104157,145.843509 36.4104157,149.043682 C36.4104157,152.243855 39.0052319,154.838108 42.2061,154.838108 Z M159.483477,154.838108 C162.684345,154.838108 165.279162,152.243855 165.279162,149.043682 C165.279162,145.843509 162.684345,143.249256 159.483477,143.249256 C156.282609,143.249256 153.687793,145.843509 153.687793,149.043682 C153.687793,152.243855 156.282609,154.838108 159.483477,154.838108 Z M282.215617,154.838108 C285.416485,154.838108 288.011301,152.243855 288.011301,149.043682 C288.011301,145.843509 285.416485,143.249256 282.215617,143.249256 C279.014748,143.249256 276.419932,145.843509 276.419932,149.043682 C276.419932,152.243855 279.014748,154.838108 282.215617,154.838108 Z M505.860848,154.156411 C509.061716,154.156411 511.656532,151.562158 511.656532,148.361985 C511.656532,145.161811 509.061716,142.567558 505.860848,142.567558 C502.65998,142.567558 500.065164,145.161811 500.065164,148.361985 C500.065164,151.562158 502.65998,154.156411 505.860848,154.156411 Z M396.083768,159.950837 C399.284636,159.950837 401.879452,157.356584 401.879452,154.156411 C401.879452,150.956238 399.284636,148.361985 396.083768,148.361985 C392.8829,148.361985 390.288084,150.956238 390.288084,154.156411 C390.288084,157.356584 392.8829,159.950837 396.083768,159.950837 Z M319.717104,115.981368 C322.917972,115.981368 325.512788,113.387115 325.512788,110.186942 C325.512788,106.986769 322.917972,104.392516 319.717104,104.392516 C316.516235,104.392516 313.921419,106.986769 313.921419,110.186942 C313.921419,113.387115 316.516235,115.981368 319.717104,115.981368 Z M270.624248,92.8036633 C273.825116,92.8036633 276.419932,90.2094103 276.419932,87.0092371 C276.419932,83.8090639 273.825116,81.214811 270.624248,81.214811 C267.42338,81.214811 264.828563,83.8090639 264.828563,87.0092371 C264.828563,90.2094103 267.42338,92.8036633 270.624248,92.8036633 Z M229.03169,131.660403 C232.232558,131.660403 234.827374,129.06615 234.827374,125.865977 C234.827374,122.665804 232.232558,120.071551 229.03169,120.071551 C225.830822,120.071551 223.236005,122.665804 223.236005,125.865977 C223.236005,129.06615 225.830822,131.660403 229.03169,131.660403 Z M158.119787,104.392516 C161.320655,104.392516 163.915471,101.798263 163.915471,98.5980894 C163.915471,95.3979162 161.320655,92.8036633 158.119787,92.8036633 C154.918919,92.8036633 152.324103,95.3979162 152.324103,98.5980894 C152.324103,101.798263 154.918919,104.392516 158.119787,104.392516 Z M224.940618,173.925629 C228.141486,173.925629 230.736303,171.331376 230.736303,168.131203 C230.736303,164.93103 228.141486,162.336777 224.940618,162.336777 C221.73975,162.336777 219.144934,164.93103 219.144934,168.131203 C219.144934,171.331376 221.73975,173.925629 224.940618,173.925629 Z"
+                id="fg-points"
+                stroke="#4A90E2"
+              />
+              <path
+                fill="#fff"
+                d="M78.0029739,76.4429306 C79.5092648,76.4429306 80.7303548,75.2221057 80.7303548,73.7161419 C80.7303548,72.210178 79.5092648,70.9893531 78.0029739,70.9893531 C76.4966831,70.9893531 75.275593,72.210178 75.275593,73.7161419 C75.275593,75.2221057 76.4966831,76.4429306 78.0029739,76.4429306 Z M3,94.4779116 C4.50629086,94.4779116 5.72738087,93.2570867 5.72738087,91.7511228 C5.72738087,90.245159 4.50629086,89.024334 3,89.024334 C1.49370914,89.024334 0.27261913,90.245159 0.27261913,91.7511228 C0.27261913,93.2570867 1.49370914,94.4779116 3,94.4779116 Z M42.5470226,48.167594 C44.0533135,48.167594 45.2744035,46.9467691 45.2744035,45.4408052 C45.2744035,43.9348413 44.0533135,42.7140164 42.5470226,42.7140164 C41.0407318,42.7140164 39.8196417,43.9348413 39.8196417,45.4408052 C39.8196417,46.9467691 41.0407318,48.167594 42.5470226,48.167594 Z M78.0029739,35.541099 C79.5092648,35.541099 80.7303548,34.3202741 80.7303548,32.8143102 C80.7303548,31.3083464 79.5092648,30.0875214 78.0029739,30.0875214 C76.4966831,30.0875214 75.275593,31.3083464 75.275593,32.8143102 C75.275593,34.3202741 76.4966831,35.541099 78.0029739,35.541099 Z M142.77827,47.1299513 C144.28456,47.1299513 145.50565,45.9091264 145.50565,44.4031625 C145.50565,42.8971987 144.28456,41.6763737 142.77827,41.6763737 C141.271979,41.6763737 140.050889,42.8971987 140.050889,44.4031625 C140.050889,45.9091264 141.271979,47.1299513 142.77827,47.1299513 Z M86.8669617,122.116643 C88.3732526,122.116643 89.5943426,120.895818 89.5943426,119.389854 C89.5943426,117.88389 88.3732526,116.663065 86.8669617,116.663065 C85.3606709,116.663065 84.1395809,117.88389 84.1395809,119.389854 C84.1395809,120.895818 85.3606709,122.116643 86.8669617,122.116643 Z M246.418743,23.2705495 C247.925033,23.2705495 249.146123,22.0497246 249.146123,20.5437607 C249.146123,19.0377969 247.925033,17.8169719 246.418743,17.8169719 C244.912452,17.8169719 243.691362,19.0377969 243.691362,20.5437607 C243.691362,22.0497246 244.912452,23.2705495 246.418743,23.2705495 Z M173.461304,49.8567401 C174.967595,49.8567401 176.188685,48.6359151 176.188685,47.1299513 C176.188685,45.6239874 174.967595,44.4031625 173.461304,44.4031625 C171.955013,44.4031625 170.733923,45.6239874 170.733923,47.1299513 C170.733923,48.6359151 171.955013,49.8567401 173.461304,49.8567401 Z M204.144339,28.0424299 C205.65063,28.0424299 206.87172,26.8216049 206.87172,25.3156411 C206.87172,23.8096772 205.65063,22.5888523 204.144339,22.5888523 C202.638048,22.5888523 201.416958,23.8096772 201.416958,25.3156411 C201.416958,26.8216049 202.638048,28.0424299 204.144339,28.0424299 Z M225.963386,83.2599026 C227.469677,83.2599026 228.690767,82.0390776 228.690767,80.5331138 C228.690767,79.0271499 227.469677,77.806325 225.963386,77.806325 C224.457095,77.806325 223.236005,79.0271499 223.236005,80.5331138 C223.236005,82.0390776 224.457095,83.2599026 225.963386,83.2599026 Z M225.963386,41.6763737 C227.469677,41.6763737 228.690767,40.4555488 228.690767,38.949585 C228.690767,37.4436211 227.469677,36.2227962 225.963386,36.2227962 C224.457095,36.2227962 223.236005,37.4436211 223.236005,38.949585 C223.236005,40.4555488 224.457095,41.6763737 225.963386,41.6763737 Z M390.288084,113.254579 C391.794374,113.254579 393.015464,112.033754 393.015464,110.52779 C393.015464,109.021826 391.794374,107.801002 390.288084,107.801002 C388.781793,107.801002 387.560703,109.021826 387.560703,110.52779 C387.560703,112.033754 388.781793,113.254579 390.288084,113.254579 Z M342.558918,66.8991699 C344.065209,66.8991699 345.286299,65.678345 345.286299,64.1723811 C345.286299,62.6664173 344.065209,61.4455924 342.558918,61.4455924 C341.052627,61.4455924 339.831537,62.6664173 339.831537,64.1723811 C339.831537,65.678345 341.052627,66.8991699 342.558918,66.8991699 Z M286.64761,47.1299513 C288.153901,47.1299513 289.374991,45.9091264 289.374991,44.4031625 C289.374991,42.8971987 288.153901,41.6763737 286.64761,41.6763737 C285.14132,41.6763737 283.92023,42.8971987 283.92023,44.4031625 C283.92023,45.9091264 285.14132,47.1299513 286.64761,47.1299513 Z M221.872315,16.4535776 C223.378606,16.4535776 224.599696,15.2327526 224.599696,13.7267888 C224.599696,12.2208249 223.378606,11 221.872315,11 C220.366024,11 219.144934,12.2208249 219.144934,13.7267888 C219.144934,15.2327526 220.366024,16.4535776 221.872315,16.4535776 Z"
+                id="bg-points"
+                stroke="#B6CCE6"
+              />
+            </svg>
+          </div>
+        </div>
+        {/*  09-12-2016 */}
+        <div
+          id="caspian"
+          className="bg-brand block py3 absolute bottom left right"
+        />
+      </section>
+    );
+  }
 }
diff --git a/frontend/src/metabase/auth/components/BackToLogin.jsx b/frontend/src/metabase/auth/components/BackToLogin.jsx
index ba5199456e3d5c29c1fd89a812d1cc9b31a218bf..9f5fe9de223f6678b4a24715bde1d616ebe31843 100644
--- a/frontend/src/metabase/auth/components/BackToLogin.jsx
+++ b/frontend/src/metabase/auth/components/BackToLogin.jsx
@@ -1,8 +1,9 @@
-import React from 'react';
+import React from "react";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
-const BackToLogin = () =>
-    <Link to="/auth/login" className="link block">{t`Back to login`}</Link>
+const BackToLogin = () => (
+  <Link to="/auth/login" className="link block">{t`Back to login`}</Link>
+);
 
 export default BackToLogin;
diff --git a/frontend/src/metabase/auth/components/GoogleNoAccount.jsx b/frontend/src/metabase/auth/components/GoogleNoAccount.jsx
index a752c4b4caf5f3e37a839dfded9137919abc3fa8..7e9219cbe64c65be1be98f2d28412f09adb00c47 100644
--- a/frontend/src/metabase/auth/components/GoogleNoAccount.jsx
+++ b/frontend/src/metabase/auth/components/GoogleNoAccount.jsx
@@ -1,27 +1,28 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AuthScene from "./AuthScene.jsx";
 import LogoIcon from "metabase/components/LogoIcon.jsx";
-import BackToLogin from "./BackToLogin.jsx"
+import BackToLogin from "./BackToLogin.jsx";
 
-const GoogleNoAccount = () =>
-    <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
-        <div className="wrapper">
-            <div className="Login-wrapper Grid  Grid--full md-Grid--1of2">
-                <div className="Grid-cell flex layout-centered text-brand">
-                    <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
-                </div>
-                <div className="Grid-cell text-centered bordered rounded shadowed p4">
-                    <h3 className="mt4 mb2">{t`No Metabase account exists for this Google account.`}</h3>
-                    <p className="mb4 ml-auto mr-auto" style={{maxWidth: 360}}>
-                        {t`You'll need an administrator to create a Metabase account before you can use Google to log in.`}
-                    </p>
+const GoogleNoAccount = () => (
+  <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
+    <div className="wrapper">
+      <div className="Login-wrapper Grid  Grid--full md-Grid--1of2">
+        <div className="Grid-cell flex layout-centered text-brand">
+          <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
+        </div>
+        <div className="Grid-cell text-centered bordered rounded shadowed p4">
+          <h3 className="mt4 mb2">{t`No Metabase account exists for this Google account.`}</h3>
+          <p className="mb4 ml-auto mr-auto" style={{ maxWidth: 360 }}>
+            {t`You'll need an administrator to create a Metabase account before you can use Google to log in.`}
+          </p>
 
-                    <BackToLogin />
-                </div>
-            </div>
+          <BackToLogin />
         </div>
-        <AuthScene />
+      </div>
     </div>
+    <AuthScene />
+  </div>
+);
 
 export default GoogleNoAccount;
diff --git a/frontend/src/metabase/auth/components/SSOLoginButton.jsx b/frontend/src/metabase/auth/components/SSOLoginButton.jsx
index f6bc6617925181871903e7b44f5cfc5f4f87d220..14d09de059c696bbd1842ef7aacc389858ca0d42 100644
--- a/frontend/src/metabase/auth/components/SSOLoginButton.jsx
+++ b/frontend/src/metabase/auth/components/SSOLoginButton.jsx
@@ -1,28 +1,27 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon";
-import { capitalize } from "humanize-plus"
-import { t } from 'c-3po';
+import { capitalize } from "humanize-plus";
+import { t } from "c-3po";
 
 const propTypes = {
-  provider: PropTypes.string.isRequired
+  provider: PropTypes.string.isRequired,
 };
 
 class SSOLoginButton extends Component {
-  render () {
-    const { provider } = this.props
+  render() {
+    const { provider } = this.props;
     return (
-        <div className="relative z2 bg-white p2 cursor-pointer shadow-hover text-centered sm-text-left rounded block sm-inline-block bordered shadowed">
-            <div className="flex align-center">
-                <Icon className="mr1" name={provider} />
-                <h4>{t`Sign in with ${capitalize(provider)}`}</h4>
-            </div>
+      <div className="relative z2 bg-white p2 cursor-pointer shadow-hover text-centered sm-text-left rounded block sm-inline-block bordered shadowed">
+        <div className="flex align-center">
+          <Icon className="mr1" name={provider} />
+          <h4>{t`Sign in with ${capitalize(provider)}`}</h4>
         </div>
-    )
+      </div>
+    );
   }
 }
 
-
-SSOLoginButton.proptypes = propTypes
+SSOLoginButton.proptypes = propTypes;
 
 export default SSOLoginButton;
diff --git a/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx b/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx
index 467a3ed0a4c007aef70adb0b3aabd9c2fcabdd1f..ab51f7efd8dffef8eb7ecc712f9ee6b024ea93c9 100644
--- a/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx
+++ b/frontend/src/metabase/auth/containers/ForgotPasswordApp.jsx
@@ -2,9 +2,9 @@ import React, { Component } from "react";
 
 import _ from "underscore";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AuthScene from "../components/AuthScene.jsx";
-import BackToLogin from "../components/BackToLogin.jsx"
+import BackToLogin from "../components/BackToLogin.jsx";
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -15,86 +15,107 @@ import MetabaseSettings from "metabase/lib/settings";
 
 import { SessionApi } from "metabase/services";
 
-
 export default class ForgotPasswordApp extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            email: props.location.query.email || null,
-            sentNotification: false,
-            error: null
-        };
-    }
+    this.state = {
+      email: props.location.query.email || null,
+      sentNotification: false,
+      error: null,
+    };
+  }
 
-    async sendResetNotification(e) {
-        e.preventDefault();
+  async sendResetNotification(e) {
+    e.preventDefault();
 
-        if (!_.isEmpty(this.state.email)) {
-            try {
-                await SessionApi.forgot_password({"email": this.state.email});
-                this.setState({sentNotification: true, error: null});
-            } catch (error) {
-                this.setState({error: error});
-            }
-        }
+    if (!_.isEmpty(this.state.email)) {
+      try {
+        await SessionApi.forgot_password({ email: this.state.email });
+        this.setState({ sentNotification: true, error: null });
+      } catch (error) {
+        this.setState({ error: error });
+      }
     }
+  }
 
-    render() {
-        const { sentNotification, error } = this.state;
-        const valid = !_.isEmpty(this.state.email);
-        const emailConfigured = MetabaseSettings.isEmailConfigured();
+  render() {
+    const { sentNotification, error } = this.state;
+    const valid = !_.isEmpty(this.state.email);
+    const emailConfigured = MetabaseSettings.isEmailConfigured();
 
-        return (
-            <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
-                <div className="Login-wrapper wrapper Grid Grid--full md-Grid--1of2">
-                      <div className="Grid-cell flex layout-centered text-brand">
-                          <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
-                      </div>
-                      { !emailConfigured ?
-                      <div className="Grid-cell">
-                          <div className="text-centered bordered rounded shadowed p4">
-                              <h3 className="my4">{t`Please contact an administrator to have them reset your password`}</h3>
-                              <BackToLogin />
-                          </div>
-                      </div>
-                      :
-                      <div className="Grid-cell">
-                          { !sentNotification ?
-                          <div>
-                              <form className="ForgotForm bg-white Form-new bordered rounded shadowed" name="form" noValidate>
-                                  <h3 className="Login-header Form-offset mb3">{t`Forgot password`}</h3>
+    return (
+      <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
+        <div className="Login-wrapper wrapper Grid Grid--full md-Grid--1of2">
+          <div className="Grid-cell flex layout-centered text-brand">
+            <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
+          </div>
+          {!emailConfigured ? (
+            <div className="Grid-cell">
+              <div className="text-centered bordered rounded shadowed p4">
+                <h3 className="my4">{t`Please contact an administrator to have them reset your password`}</h3>
+                <BackToLogin />
+              </div>
+            </div>
+          ) : (
+            <div className="Grid-cell">
+              {!sentNotification ? (
+                <div>
+                  <form
+                    className="ForgotForm bg-white Form-new bordered rounded shadowed"
+                    name="form"
+                    noValidate
+                  >
+                    <h3 className="Login-header Form-offset mb3">{t`Forgot password`}</h3>
 
-                                  <FormMessage message={error && error.data && error.data.message}></FormMessage>
+                    <FormMessage
+                      message={error && error.data && error.data.message}
+                    />
 
-                                  <FormField key="email" fieldName="email" formError={error}>
-                                      <FormLabel title={t`Email address`}  fieldName={"email"} formError={error} />
-                                      <input className="Form-input Form-offset full" name="email" placeholder={t`The email you use for your Metabase account`} type="text" onChange={(e) => this.setState({"email": e.target.value})} defaultValue={this.state.email} autoFocus />
-                                      <span className="Form-charm"></span>
-                                  </FormField>
+                    <FormField key="email" fieldName="email" formError={error}>
+                      <FormLabel
+                        title={t`Email address`}
+                        fieldName={"email"}
+                        formError={error}
+                      />
+                      <input
+                        className="Form-input Form-offset full"
+                        name="email"
+                        placeholder={t`The email you use for your Metabase account`}
+                        type="text"
+                        onChange={e => this.setState({ email: e.target.value })}
+                        defaultValue={this.state.email}
+                        autoFocus
+                      />
+                      <span className="Form-charm" />
+                    </FormField>
 
-                                  <div className="Form-actions">
-                                      <button className={cx("Button", {"Button--primary": valid})} onClick={(e) => this.sendResetNotification(e)} disabled={!valid}>
-                                          {t`Send password reset email`}
-                                      </button>
-                                  </div>
-                              </form>
-                          </div>
-                          :
-                          <div>
-                              <div className="SuccessGroup bg-white bordered rounded shadowed">
-                                  <div className="SuccessMark">
-                                      <Icon name="check" />
-                                  </div>
-                                  <p className="SuccessText">{t`Check your email for instructions on how to reset your password.`}</p>
-                              </div>
-                          </div>
-                          }
-                      </div>
-                    }
+                    <div className="Form-actions">
+                      <button
+                        className={cx("Button", { "Button--primary": valid })}
+                        onClick={e => this.sendResetNotification(e)}
+                        disabled={!valid}
+                      >
+                        {t`Send password reset email`}
+                      </button>
                     </div>
-                <AuthScene />
+                  </form>
+                </div>
+              ) : (
+                <div>
+                  <div className="SuccessGroup bg-white bordered rounded shadowed">
+                    <div className="SuccessMark">
+                      <Icon name="check" />
+                    </div>
+                    <p className="SuccessText">{t`Check your email for instructions on how to reset your password.`}</p>
+                  </div>
+                </div>
+              )}
             </div>
-        );
-    }
+          )}
+        </div>
+        <AuthScene />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/auth/containers/LoginApp.jsx b/frontend/src/metabase/auth/containers/LoginApp.jsx
index 40c37d5c4762a5406d1c8ece648c69dd0d55529d..060acb30bbc316bd69145731caa5e7c8e10b2cc1 100644
--- a/frontend/src/metabase/auth/containers/LoginApp.jsx
+++ b/frontend/src/metabase/auth/containers/LoginApp.jsx
@@ -4,7 +4,7 @@ import { Link } from "react-router";
 import { connect } from "react-redux";
 
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AuthScene from "../components/AuthScene.jsx";
 import SSOLoginButton from "../components/SSOLoginButton.jsx";
 import FormField from "metabase/components/form/FormField.jsx";
@@ -16,171 +16,221 @@ import Utils from "metabase/lib/utils";
 
 import * as authActions from "../auth";
 
-
 const mapStateToProps = (state, props) => {
-    return {
-        loginError:       state.auth && state.auth.loginError,
-        user:             state.currentUser
-    }
-}
+  return {
+    loginError: state.auth && state.auth.loginError,
+    user: state.currentUser,
+  };
+};
 
 const mapDispatchToProps = {
-    ...authActions
-}
+  ...authActions,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class LoginApp extends Component {
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      credentials: {},
+      valid: false,
+    };
+  }
 
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            credentials: {},
-            valid: false
-        }
-    }
-
-    validateForm() {
-        let { credentials } = this.state;
-
-        let valid = true;
-
-        if (!credentials.username || !credentials.password) {
-            valid = false;
-        }
-
-        if (this.state.valid !== valid) {
-            this.setState({ valid });
-        }
-    }
+  validateForm() {
+    let { credentials } = this.state;
 
-    componentDidMount() {
-
-        this.validateForm();
-
-        const { loginGoogle, location } = this.props;
-
-        let ssoLoginButton = findDOMNode(this.refs.ssoLoginButton);
-
-        function attachGoogleAuth() {
-            // if gapi isn't loaded yet then wait 100ms and check again. Keep doing this until we're ready
-            if (!window.gapi) {
-                window.setTimeout(attachGoogleAuth, 100);
-                return;
-            }
-            try {
-                window.gapi.load('auth2', () => {
-                  let auth2 = window.gapi.auth2.init({
-                      client_id: Settings.get('google_auth_client_id'),
-                      cookiepolicy: 'single_host_origin',
-                  });
-                  auth2.attachClickHandler(ssoLoginButton, {},
-                      (googleUser) => loginGoogle(googleUser, location.query.redirect),
-                      (error) => console.error('There was an error logging in', error)
-                  );
-                })
-            } catch (error) {
-                console.error('Error attaching Google Auth handler: ', error);
-            }
-        }
-        attachGoogleAuth();
-    }
+    let valid = true;
 
-    componentDidUpdate() {
-        this.validateForm();
+    if (!credentials.username || !credentials.password) {
+      valid = false;
     }
 
-    onChangeUserName(fieldName, fieldValue) {
-        this.onChange(fieldName, fieldValue.trim())
+    if (this.state.valid !== valid) {
+      this.setState({ valid });
     }
-
-    onChange(fieldName, fieldValue) {
-        this.setState({ credentials: { ...this.state.credentials, [fieldName]: fieldValue }});
+  }
+
+  componentDidMount() {
+    this.validateForm();
+
+    const { loginGoogle, location } = this.props;
+
+    let ssoLoginButton = findDOMNode(this.refs.ssoLoginButton);
+
+    function attachGoogleAuth() {
+      // if gapi isn't loaded yet then wait 100ms and check again. Keep doing this until we're ready
+      if (!window.gapi) {
+        window.setTimeout(attachGoogleAuth, 100);
+        return;
+      }
+      try {
+        window.gapi.load("auth2", () => {
+          let auth2 = window.gapi.auth2.init({
+            client_id: Settings.get("google_auth_client_id"),
+            cookiepolicy: "single_host_origin",
+          });
+          auth2.attachClickHandler(
+            ssoLoginButton,
+            {},
+            googleUser => loginGoogle(googleUser, location.query.redirect),
+            error => console.error("There was an error logging in", error),
+          );
+        });
+      } catch (error) {
+        console.error("Error attaching Google Auth handler: ", error);
+      }
     }
-
-    formSubmitted(e) {
-        e.preventDefault();
-
-        let { login, location } = this.props;
-        let { credentials } = this.state;
-
-        login(credentials, location.query.redirect);
-    }
-
-    render() {
-
-        const { loginError } = this.props;
-
-        const ldapEnabled = Settings.ldapEnabled()
-
-        return (
-            <div className="full-height full bg-white flex flex-column flex-full md-layout-centered">
-                <div className="Login-wrapper wrapper Grid Grid--full md-Grid--1of2 relative z2">
-                    <div className="Grid-cell flex layout-centered text-brand">
-                        <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
-                    </div>
-                    <div className="Login-content Grid-cell">
-                        <form className="Form-new bg-white bordered rounded shadowed" name="form" onSubmit={(e) => this.formSubmitted(e)}>
-                            <h3 className="Login-header Form-offset">{t`Sign in to Metabase`}</h3>
-
-                            { Settings.ssoEnabled() &&
-                                <div className="mx4 mb4 py3 border-bottom relative">
-                                    <SSOLoginButton provider='google' ref="ssoLoginButton"/>
-                                    {/*<div className="g-signin2 ml1 relative z2" id="g-signin2"></div>*/}
-                                    <div className="mx1 absolute text-centered left right" style={{ bottom: -8 }}>
-                                        <span className="text-bold px3 py2 text-grey-3 bg-white">{t`OR`}</span>
-                                    </div>
-                                </div>
-                            }
-
-                            <FormMessage formError={loginError && loginError.data.message ? loginError : null} ></FormMessage>
-
-                            <FormField key="username" fieldName="username" formError={loginError}>
-                                <FormLabel
-                                    title={ldapEnabled ? t`Username or email address` : t`Email address`}
-                                    fieldName="username"
-                                    formError={loginError}
-                                />
-                                <input
-                                    className="Form-input Form-offset full py1"
-                                    name="username"
-                                    placeholder="youlooknicetoday@email.com"
-                                    type={
-                                        /*
-                                        if a user has ldap enabled, use a text input to allow for ldap username
-                                        schemes. if not and they're using built in auth, set the input type to
-                                        email so we get built in validation in modern browsers
-                                        */
-                                        ldapEnabled ? "text" : "email"
-                                    }
-                                    onChange={(e) => this.onChange("username", e.target.value)}
-                                    autoFocus
-                                />
-                                <span className="Form-charm"></span>
-                            </FormField>
-
-                            <FormField key="password" fieldName="password" formError={loginError}>
-                                <FormLabel title={t`Password`}  fieldName={"password"} formError={loginError} />
-                                <input className="Form-input Form-offset full py1" name="password" placeholder="Shh..." type="password" onChange={(e) => this.onChange("password", e.target.value)} />
-                                <span className="Form-charm"></span>
-                            </FormField>
-
-                            <div className="Form-field">
-                                <ul className="Form-offset">
-                                    <input name="remember" type="checkbox" defaultChecked /> <label className="inline-block">{t`Remember Me:`}</label>
-                                </ul>
-                            </div>
-
-                            <div className="Form-actions p2 Grid Grid--full md-Grid--1of2">
-                                <button className={cx("Button Grid-cell", {'Button--primary': this.state.valid})} disabled={!this.state.valid}>
-                                    Sign in
-                                </button>
-                                <Link to={"/auth/forgot_password"+(Utils.validEmail(this.state.credentials.username) ? "?email="+this.state.credentials.username : "")} className="Grid-cell py2 sm-py0 text-grey-3 md-text-right text-centered flex-full link" onClick={(e) => { window.OSX ? window.OSX.resetPassword() : null }}>{t`I seem to have forgotten my password`}</Link>
-                            </div>
-                        </form>
-                    </div>
+    attachGoogleAuth();
+  }
+
+  componentDidUpdate() {
+    this.validateForm();
+  }
+
+  onChangeUserName(fieldName, fieldValue) {
+    this.onChange(fieldName, fieldValue.trim());
+  }
+
+  onChange(fieldName, fieldValue) {
+    this.setState({
+      credentials: { ...this.state.credentials, [fieldName]: fieldValue },
+    });
+  }
+
+  formSubmitted(e) {
+    e.preventDefault();
+
+    let { login, location } = this.props;
+    let { credentials } = this.state;
+
+    login(credentials, location.query.redirect);
+  }
+
+  render() {
+    const { loginError } = this.props;
+    const ldapEnabled = Settings.ldapEnabled();
+
+    return (
+      <div className="full-height full bg-white flex flex-column flex-full md-layout-centered">
+        <div className="Login-wrapper wrapper Grid Grid--full md-Grid--1of2 relative z2">
+          <div className="Grid-cell flex layout-centered text-brand">
+            <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
+          </div>
+          <div className="Login-content Grid-cell">
+            <form
+              className="Form-new bg-white bordered rounded shadowed"
+              name="form"
+              onSubmit={e => this.formSubmitted(e)}
+            >
+              <h3 className="Login-header Form-offset">{t`Sign in to Metabase`}</h3>
+
+              {Settings.ssoEnabled() && (
+                <div className="mx4 mb4 py3 border-bottom relative">
+                  <SSOLoginButton provider="google" ref="ssoLoginButton" />
+                  {/*<div className="g-signin2 ml1 relative z2" id="g-signin2"></div>*/}
+                  <div
+                    className="mx1 absolute text-centered left right"
+                    style={{ bottom: -8 }}
+                  >
+                    <span className="text-bold px3 py2 text-grey-3 bg-white">{t`OR`}</span>
+                  </div>
                 </div>
-                <AuthScene />
-            </div>
-        );
-    }
+              )}
+
+              <FormMessage
+                formError={
+                  loginError && loginError.data.message ? loginError : null
+                }
+              />
+
+              <FormField
+                key="username"
+                fieldName="username"
+                formError={loginError}
+              >
+                <FormLabel
+                  title={
+                    Settings.ldapEnabled()
+                      ? t`Username or email address`
+                      : t`Email address`
+                  }
+                  fieldName={"username"}
+                  formError={loginError}
+                />
+                <input
+                  className="Form-input Form-offset full py1"
+                  name="username"
+                  placeholder="youlooknicetoday@email.com"
+                  type={
+                    /*
+                     * if a user has ldap enabled, use a text input to allow for
+                     * ldap username && schemes. if not and they're using built
+                     * in auth, set the input type to email so we get built in
+                     * validation in modern browsers
+                     * */
+                    ldapEnabled ? "text" : "email"
+                  }
+                  onChange={e => this.onChange("username", e.target.value)}
+                  autoFocus
+                />
+                <span className="Form-charm" />
+              </FormField>
+
+              <FormField
+                key="password"
+                fieldName="password"
+                formError={loginError}
+              >
+                <FormLabel
+                  title={t`Password`}
+                  fieldName={"password"}
+                  formError={loginError}
+                />
+                <input
+                  className="Form-input Form-offset full py1"
+                  name="password"
+                  placeholder="Shh..."
+                  type="password"
+                  onChange={e => this.onChange("password", e.target.value)}
+                />
+                <span className="Form-charm" />
+              </FormField>
+
+              <div className="Form-field">
+                <ul className="Form-offset">
+                  <input name="remember" type="checkbox" defaultChecked />{" "}
+                  <label className="inline-block">{t`Remember Me:`}</label>
+                </ul>
+              </div>
+
+              <div className="Form-actions p2 Grid Grid--full md-Grid--1of2">
+                <button
+                  className={cx("Button Grid-cell", {
+                    "Button--primary": this.state.valid,
+                  })}
+                  disabled={!this.state.valid}
+                >
+                  Sign in
+                </button>
+                <Link
+                  to={
+                    "/auth/forgot_password" +
+                    (Utils.validEmail(this.state.credentials.username)
+                      ? "?email=" + this.state.credentials.username
+                      : "")
+                  }
+                  className="Grid-cell py2 sm-py0 text-grey-3 md-text-right text-centered flex-full link"
+                  onClick={e => {
+                    window.OSX ? window.OSX.resetPassword() : null;
+                  }}
+                >{t`I seem to have forgotten my password`}</Link>
+              </div>
+            </form>
+          </div>
+        </div>
+        <AuthScene />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/auth/containers/LogoutApp.jsx b/frontend/src/metabase/auth/containers/LogoutApp.jsx
index 5c42f34868d43fecd1eaa9194ffba54d7e023899..2b7cd38c75c3a21e3c40b7f88a96a4d66dd45203 100644
--- a/frontend/src/metabase/auth/containers/LogoutApp.jsx
+++ b/frontend/src/metabase/auth/containers/LogoutApp.jsx
@@ -6,17 +6,16 @@ import { logout } from "../auth";
 const mapStateToProps = null;
 
 const mapDispatchToProps = {
-    logout
+  logout,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class LogoutApp extends Component {
+  componentWillMount() {
+    this.props.logout();
+  }
 
-    componentWillMount() {
-        this.props.logout();
-    }
-
-    render() {
-        return null;
-    }
+  render() {
+    return null;
+  }
 }
diff --git a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
index 091f5641c90e86f89db5724b4f598ce26dae9593..d6729eddb702478d688ffc238597632914321d05 100644
--- a/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
+++ b/frontend/src/metabase/auth/containers/PasswordResetApp.jsx
@@ -3,7 +3,7 @@ import { connect } from "react-redux";
 import { Link } from "react-router";
 
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AuthScene from "../components/AuthScene.jsx";
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
@@ -18,159 +18,214 @@ import * as authActions from "../auth";
 import { SessionApi } from "metabase/services";
 
 const mapStateToProps = (state, props) => {
-    return {
-        token:            props.params.token,
-        resetError:       state.auth && state.auth.resetError,
-        resetSuccess:     state.auth && state.auth.resetSuccess,
-        newUserJoining:   props.location.hash === "#new"
-    }
-}
+  return {
+    token: props.params.token,
+    resetError: state.auth && state.auth.resetError,
+    resetSuccess: state.auth && state.auth.resetSuccess,
+    newUserJoining: props.location.hash === "#new",
+  };
+};
 
 const mapDispatchToProps = {
-    ...authActions
-}
+  ...authActions,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class PasswordResetApp extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            credentials: {},
-            valid: false,
-            tokenValid: false
-        }
-    }
-
-    validateForm() {
-        let { credentials } = this.state;
-
-        let valid = true;
-
-        if (!credentials.password || !credentials.password2) {
-            valid = false;
-        }
-
-        if (this.state.valid !== valid) {
-            this.setState({ valid });
-        }
-    }
-
-    async componentWillMount() {
-        try {
-            let result = await SessionApi.password_reset_token_valid({token: this.props.token});
-            if (result && result.valid) {
-                this.setState({tokenValid: true});
-            }
-        } catch (error) {
-            console.log("error validating token", error);
-        }
-    }
-
-    componentDidMount() {
-        this.validateForm();
-    }
-
-    componentDidUpdate() {
-        this.validateForm();
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      credentials: {},
+      valid: false,
+      tokenValid: false,
+    };
+  }
+
+  validateForm() {
+    let { credentials } = this.state;
+
+    let valid = true;
+
+    if (!credentials.password || !credentials.password2) {
+      valid = false;
     }
 
-    onChange(fieldName, fieldValue) {
-        this.setState({ credentials: { ...this.state.credentials, [fieldName]: fieldValue }});
+    if (this.state.valid !== valid) {
+      this.setState({ valid });
     }
-
-    formSubmitted(e) {
-        e.preventDefault();
-
-        let { token, passwordReset } = this.props;
-        let { credentials } = this.state;
-
-        passwordReset(token, credentials);
+  }
+
+  async componentWillMount() {
+    try {
+      let result = await SessionApi.password_reset_token_valid({
+        token: this.props.token,
+      });
+      if (result && result.valid) {
+        this.setState({ tokenValid: true });
+      }
+    } catch (error) {
+      console.log("error validating token", error);
     }
-
-    render() {
-        const { resetError, resetSuccess, newUserJoining } = this.props;
-        const passwordComplexity = MetabaseSettings.passwordComplexity(false);
-
-        if (!this.state.tokenValid) {
-            return (
-                <div>
-                    <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
-                        <div className="wrapper">
-                            <div className="Login-wrapper Grid  Grid--full md-Grid--1of2">
-                                <div className="Grid-cell flex layout-centered text-brand">
-                                    <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
-                                </div>
-                                <div className="Grid-cell bordered rounded shadowed">
-                                    <h3 className="Login-header Form-offset mt4">{t`Whoops, that's an expired link`}</h3>
-                                    <p className="Form-offset mb4 mr4">
-                                        {t`For security reasons, password reset links expire after a little while. If you still need
+  }
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  componentDidUpdate() {
+    this.validateForm();
+  }
+
+  onChange(fieldName, fieldValue) {
+    this.setState({
+      credentials: { ...this.state.credentials, [fieldName]: fieldValue },
+    });
+  }
+
+  formSubmitted(e) {
+    e.preventDefault();
+
+    let { token, passwordReset } = this.props;
+    let { credentials } = this.state;
+
+    passwordReset(token, credentials);
+  }
+
+  render() {
+    const { resetError, resetSuccess, newUserJoining } = this.props;
+    const passwordComplexity = MetabaseSettings.passwordComplexity(false);
+
+    if (!this.state.tokenValid) {
+      return (
+        <div>
+          <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
+            <div className="wrapper">
+              <div className="Login-wrapper Grid  Grid--full md-Grid--1of2">
+                <div className="Grid-cell flex layout-centered text-brand">
+                  <LogoIcon
+                    className="Logo my4 sm-my0"
+                    width={66}
+                    height={85}
+                  />
+                </div>
+                <div className="Grid-cell bordered rounded shadowed">
+                  <h3 className="Login-header Form-offset mt4">{t`Whoops, that's an expired link`}</h3>
+                  <p className="Form-offset mb4 mr4">
+                    {t`For security reasons, password reset links expire after a little while. If you still need
                                         to reset your password, you can <Link to="/auth/forgot_password" className="link">request a new reset email</Link>.`}
-                                    </p>
-                                </div>
-                            </div>
-                        </div>
-                    </div>
-                    <AuthScene />
+                  </p>
                 </div>
-            );
-
-        } else {
-            return (
-                <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
-                    <div className="Login-wrapper wrapper Grid  Grid--full md-Grid--1of2">
-                          <div className="Grid-cell flex layout-centered text-brand">
-                              <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
-                          </div>
-                          { !resetSuccess ?
-                          <div className="Grid-cell">
-                              <form className="ForgotForm Login-wrapper bg-white Form-new bordered rounded shadowed" name="form" onSubmit={(e) => this.formSubmitted(e)} noValidate>
-                                  <h3 className="Login-header Form-offset">{t`New password`}</h3>
-
-                                  <p className="Form-offset text-grey-3 mb4">{t`To keep your data secure, passwords ${passwordComplexity}`}</p>
-
-                                  <FormMessage formError={resetError && resetError.data.message ? resetError : null} ></FormMessage>
-
-                                  <FormField key="password" fieldName="password" formError={resetError}>
-                                      <FormLabel title={t`Create a new password`}  fieldName={"password"} formError={resetError} />
-                                      <input className="Form-input Form-offset full" name="password" placeholder={t`Make sure its secure like the instructions above`} type="password" onChange={(e) => this.onChange("password", e.target.value)} autoFocus />
-                                      <span className="Form-charm"></span>
-                                  </FormField>
-
-                                  <FormField key="password2" fieldName="password2" formError={resetError}>
-                                      <FormLabel title={t`Confirm new password`}  fieldName={"password2"} formError={resetError} />
-                                      <input className="Form-input Form-offset full" name="password2" placeholder={t`Make sure it matches the one you just entered`} type="password" onChange={(e) => this.onChange("password2", e.target.value)} />
-                                      <span className="Form-charm"></span>
-                                  </FormField>
-
-                                  <div className="Form-actions">
-                                      <button className={cx("Button", {"Button--primary": this.state.valid})} disabled={!this.state.valid}>
-                                          Save new password
-                                      </button>
-                                  </div>
-                              </form>
-                          </div>
-                          :
-                          <div className="Grid-cell">
-                              <div className="SuccessGroup bg-white bordered rounded shadowed">
-                                  <div className="SuccessMark">
-                                      <Icon name="check" />
-                                  </div>
-                                  <p>{t`Your password has been reset.`}</p>
-                                  <p>
-                                      { newUserJoining ?
-                                      <Link to="/?new" className="Button Button--primary">{t`Sign in with your new password`}</Link>
-                                      :
-                                      <Link to="/" className="Button Button--primary">{t`Sign in with your new password`}</Link>
-                                      }
-                                  </p>
-                              </div>
-                          </div>
-                          }
-                    </div>
-                    <AuthScene />
+              </div>
+            </div>
+          </div>
+          <AuthScene />
+        </div>
+      );
+    } else {
+      return (
+        <div className="full-height bg-white flex flex-column flex-full md-layout-centered">
+          <div className="Login-wrapper wrapper Grid  Grid--full md-Grid--1of2">
+            <div className="Grid-cell flex layout-centered text-brand">
+              <LogoIcon className="Logo my4 sm-my0" width={66} height={85} />
+            </div>
+            {!resetSuccess ? (
+              <div className="Grid-cell">
+                <form
+                  className="ForgotForm Login-wrapper bg-white Form-new bordered rounded shadowed"
+                  name="form"
+                  onSubmit={e => this.formSubmitted(e)}
+                  noValidate
+                >
+                  <h3 className="Login-header Form-offset">{t`New password`}</h3>
+
+                  <p className="Form-offset text-grey-3 mb4">{t`To keep your data secure, passwords ${passwordComplexity}`}</p>
+
+                  <FormMessage
+                    formError={
+                      resetError && resetError.data.message ? resetError : null
+                    }
+                  />
+
+                  <FormField
+                    key="password"
+                    fieldName="password"
+                    formError={resetError}
+                  >
+                    <FormLabel
+                      title={t`Create a new password`}
+                      fieldName={"password"}
+                      formError={resetError}
+                    />
+                    <input
+                      className="Form-input Form-offset full"
+                      name="password"
+                      placeholder={t`Make sure its secure like the instructions above`}
+                      type="password"
+                      onChange={e => this.onChange("password", e.target.value)}
+                      autoFocus
+                    />
+                    <span className="Form-charm" />
+                  </FormField>
+
+                  <FormField
+                    key="password2"
+                    fieldName="password2"
+                    formError={resetError}
+                  >
+                    <FormLabel
+                      title={t`Confirm new password`}
+                      fieldName={"password2"}
+                      formError={resetError}
+                    />
+                    <input
+                      className="Form-input Form-offset full"
+                      name="password2"
+                      placeholder={t`Make sure it matches the one you just entered`}
+                      type="password"
+                      onChange={e => this.onChange("password2", e.target.value)}
+                    />
+                    <span className="Form-charm" />
+                  </FormField>
+
+                  <div className="Form-actions">
+                    <button
+                      className={cx("Button", {
+                        "Button--primary": this.state.valid,
+                      })}
+                      disabled={!this.state.valid}
+                    >
+                      Save new password
+                    </button>
+                  </div>
+                </form>
+              </div>
+            ) : (
+              <div className="Grid-cell">
+                <div className="SuccessGroup bg-white bordered rounded shadowed">
+                  <div className="SuccessMark">
+                    <Icon name="check" />
+                  </div>
+                  <p>{t`Your password has been reset.`}</p>
+                  <p>
+                    {newUserJoining ? (
+                      <Link
+                        to="/?new"
+                        className="Button Button--primary"
+                      >{t`Sign in with your new password`}</Link>
+                    ) : (
+                      <Link
+                        to="/"
+                        className="Button Button--primary"
+                      >{t`Sign in with your new password`}</Link>
+                    )}
+                  </p>
                 </div>
-            );
-        }
+              </div>
+            )}
+          </div>
+          <AuthScene />
+        </div>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/components/AccordianList.info.js b/frontend/src/metabase/components/AccordianList.info.js
index 4b056762653d20f6dffc512940a5c40793c39174..45c762688925003d3459bd3e55193b722a2daff8 100644
--- a/frontend/src/metabase/components/AccordianList.info.js
+++ b/frontend/src/metabase/components/AccordianList.info.js
@@ -2,14 +2,15 @@ import React from "react";
 import AccordianList from "metabase/components/AccordianList";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 
-const DemoPopover = ({ children }) =>
-    <PopoverWithTrigger
-        triggerElement={<button className="Button">Click me!</button>}
-        verticalAttachments={["top"]}
-        isInitiallyOpen
-    >
-        {children}
-    </PopoverWithTrigger>
+const DemoPopover = ({ children }) => (
+  <PopoverWithTrigger
+    triggerElement={<button className="Button">Click me!</button>}
+    verticalAttachments={["top"]}
+    isInitiallyOpen
+  >
+    {children}
+  </PopoverWithTrigger>
+);
 
 export const component = AccordianList;
 
@@ -21,56 +22,54 @@ An expandable and searchable list of sections and items.
 `;
 
 const sections = [
-    {
-        name: "Widgets",
-        items: [
-            { name: "Foo" },
-            { name: "Bar" },
-            { name: "Baz" },
-        ]
-    },
-    {
-        name: "Doohickeys",
-        items: [
-            { name: "Buz" },
-        ]
-    }
-]
+  {
+    name: "Widgets",
+    items: [{ name: "Foo" }, { name: "Bar" }, { name: "Baz" }],
+  },
+  {
+    name: "Doohickeys",
+    items: [{ name: "Buz" }],
+  },
+];
 
 export const examples = {
-    "Default":
-        <DemoPopover>
-            <AccordianList
-                className="text-brand"
-                sections={sections}
-                itemIsSelected={item => item.name === "Foo"}
-            />
-        </DemoPopover>,
-    "Always Expanded":
-        <DemoPopover>
-            <AccordianList
-                className="text-brand"
-                sections={sections}
-                itemIsSelected={item => item.name === "Foo"}
-                alwaysExpanded
-            />
-        </DemoPopover>,
-    "Searchable":
-        <DemoPopover>
-            <AccordianList
-                className="text-brand"
-                sections={sections}
-                itemIsSelected={item => item.name === "Foo"}
-                searchable
-            />
-        </DemoPopover>,
-    "Hide Single Section Title":
-        <DemoPopover>
-            <AccordianList
-                className="text-brand"
-                sections={sections.slice(0,1)}
-                itemIsSelected={item => item.name === "Foo"}
-                hideSingleSectionTitle
-            />
-        </DemoPopover>,
+  Default: (
+    <DemoPopover>
+      <AccordianList
+        className="text-brand"
+        sections={sections}
+        itemIsSelected={item => item.name === "Foo"}
+      />
+    </DemoPopover>
+  ),
+  "Always Expanded": (
+    <DemoPopover>
+      <AccordianList
+        className="text-brand"
+        sections={sections}
+        itemIsSelected={item => item.name === "Foo"}
+        alwaysExpanded
+      />
+    </DemoPopover>
+  ),
+  Searchable: (
+    <DemoPopover>
+      <AccordianList
+        className="text-brand"
+        sections={sections}
+        itemIsSelected={item => item.name === "Foo"}
+        searchable
+      />
+    </DemoPopover>
+  ),
+  "Hide Single Section Title": (
+    <DemoPopover>
+      <AccordianList
+        className="text-brand"
+        sections={sections.slice(0, 1)}
+        itemIsSelected={item => item.name === "Foo"}
+        hideSingleSectionTitle
+      />
+    </DemoPopover>
+  ),
 };
diff --git a/frontend/src/metabase/components/AccordianList.jsx b/frontend/src/metabase/components/AccordianList.jsx
index 2c2bc84ce1ec95c1a7cbc0e447d05e724ec3d570..ae9c3110aa572647ebeffb8a066bb7a0f04e1cb3 100644
--- a/frontend/src/metabase/components/AccordianList.jsx
+++ b/frontend/src/metabase/components/AccordianList.jsx
@@ -6,357 +6,445 @@ import _ from "underscore";
 
 import Icon from "metabase/components/Icon.jsx";
 import ListSearchField from "metabase/components/ListSearchField.jsx";
-import { List, CellMeasurer, CellMeasurerCache } from 'react-virtualized';
+import { List, CellMeasurer, CellMeasurerCache } from "react-virtualized";
 
 export default class AccordianList extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        let openSection;
-        // use initiallyOpenSection prop if present
-        if (props.initiallyOpenSection !== undefined) {
-            openSection = props.initiallyOpenSection;
-        }
-        // otherwise try to find the selected section, if any
-        if (openSection === undefined) {
-            openSection = _.findIndex(props.sections, (section, index) => this.sectionIsSelected(section, index));
-            if (openSection === -1) {
-                openSection = undefined;
-            }
-        }
-        // default to the first section
-        if (openSection === undefined) {
-            openSection = 0;
-        }
-
-        this.state = {
-            openSection,
-            searchText: ""
-        };
-
-        this._cache = new CellMeasurerCache({
-            fixedWidth: true,
-            minHeight: 10,
-        });
+    let openSection;
+    // use initiallyOpenSection prop if present
+    if (props.initiallyOpenSection !== undefined) {
+      openSection = props.initiallyOpenSection;
+    }
+    // otherwise try to find the selected section, if any
+    if (openSection === undefined) {
+      openSection = _.findIndex(props.sections, (section, index) =>
+        this.sectionIsSelected(section, index),
+      );
+      if (openSection === -1) {
+        openSection = undefined;
+      }
+    }
+    // default to the first section
+    if (openSection === undefined) {
+      openSection = 0;
     }
 
-    static propTypes = {
-        id: PropTypes.string,
-        sections: PropTypes.array.isRequired,
-        searchable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
-        initiallyOpenSection: PropTypes.number,
-        openSection: PropTypes.number,
-        onChange: PropTypes.func,
-        onChangeSection: PropTypes.func,
-        itemIsSelected: PropTypes.func,
-        itemIsClickable: PropTypes.func,
-        renderItem: PropTypes.func,
-        renderSectionIcon: PropTypes.func,
-        getItemClasses: PropTypes.func,
-        alwaysTogglable: PropTypes.bool,
-        alwaysExpanded: PropTypes.bool,
-        hideSingleSectionTitle: PropTypes.bool,
-    };
-
-    static defaultProps = {
-        style: {},
-        width: 300,
-        searchable: (section) => section.items && section.items.length > 10,
-        alwaysTogglable: false,
-        alwaysExpanded: false,
-        hideSingleSectionTitle: false,
+    this.state = {
+      openSection,
+      searchText: "",
     };
 
-    componentDidMount() {
-        // NOTE: for some reason the row heights aren't computed correctly when
-        // first rendering, so force the list to update
-        this._forceUpdateList();
-        // `scrollToRow` upon mounting, after _forceUpdateList
-        // Use list.scrollToRow instead of the scrollToIndex prop since the
-        // causes the list's scrolling to be pinned to the selected row
-        setTimeout(() => {
-            if (this._initialSelectedRowIndex != null) {
-                this._list.scrollToRow(this._initialSelectedRowIndex);
-            }
-        }, 0);
+    this._cache = new CellMeasurerCache({
+      fixedWidth: true,
+      minHeight: 10,
+    });
+  }
+
+  static propTypes = {
+    id: PropTypes.string,
+    sections: PropTypes.array.isRequired,
+    searchable: PropTypes.oneOfType([PropTypes.bool, PropTypes.func]),
+    initiallyOpenSection: PropTypes.number,
+    openSection: PropTypes.number,
+    onChange: PropTypes.func,
+    onChangeSection: PropTypes.func,
+    itemIsSelected: PropTypes.func,
+    itemIsClickable: PropTypes.func,
+    renderItem: PropTypes.func,
+    renderSectionIcon: PropTypes.func,
+    getItemClasses: PropTypes.func,
+    alwaysTogglable: PropTypes.bool,
+    alwaysExpanded: PropTypes.bool,
+    hideSingleSectionTitle: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    style: {},
+    width: 300,
+    searchable: section => section.items && section.items.length > 10,
+    alwaysTogglable: false,
+    alwaysExpanded: false,
+    hideSingleSectionTitle: false,
+  };
+
+  componentDidMount() {
+    // NOTE: for some reason the row heights aren't computed correctly when
+    // first rendering, so force the list to update
+    this._forceUpdateList();
+    // `scrollToRow` upon mounting, after _forceUpdateList
+    // Use list.scrollToRow instead of the scrollToIndex prop since the
+    // causes the list's scrolling to be pinned to the selected row
+    setTimeout(() => {
+      if (this._initialSelectedRowIndex != null) {
+        this._list.scrollToRow(this._initialSelectedRowIndex);
+      }
+    }, 0);
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    // if anything changes that affects the selected rows we need to clear the row height cache
+    if (
+      this.state.openSection !== prevState.openSection ||
+      this.state.searchText !== prevState.searchText
+    ) {
+      this._clearRowHeightCache();
     }
+  }
 
-    componentDidUpdate(prevProps, prevState) {
-        // if anything changes that affects the selected rows we need to clear the row height cache
-        if (this.state.openSection !== prevState.openSection || this.state.searchText !== prevState.searchText) {
-            this._clearRowHeightCache()
-        }
+  componentWillUnmount() {
+    // ensure _forceUpdateList is not called after unmounting
+    if (this._forceUpdateTimeout != null) {
+      clearTimeout(this._forceUpdateTimeout);
+      this._forceUpdateTimeout = null;
     }
-
-    componentWillUnmount() {
-        // ensure _forceUpdateList is not called after unmounting
-        if (this._forceUpdateTimeout != null) {
-            clearTimeout(this._forceUpdateTimeout);
-            this._forceUpdateTimeout = null;
-        }
+  }
+
+  // resets the row height cache when the displayed rows change
+  _clearRowHeightCache() {
+    this._cache.clearAll();
+    // NOTE: unclear why this needs to be async
+    this._forceUpdateTimeout = setTimeout(() => {
+      this._forceUpdateTimeout = null;
+      this._forceUpdateList();
+    });
+  }
+
+  _forceUpdateList() {
+    // NOTE: unclear why this particular set of functions works, but it does
+    this._list.invalidateCellSizeAfterRender({
+      columnIndex: 0,
+      rowIndex: 0,
+    });
+    this._list.forceUpdateGrid();
+    this.forceUpdate();
+  }
+
+  toggleSection(sectionIndex) {
+    if (this.props.onChangeSection) {
+      if (this.props.onChangeSection(sectionIndex) === false) {
+        return;
+      }
     }
 
-    // resets the row height cache when the displayed rows change
-    _clearRowHeightCache() {
-        this._cache.clearAll();
-        // NOTE: unclear why this needs to be async
-        this._forceUpdateTimeout = setTimeout(() => {
-            this._forceUpdateTimeout = null;
-            this._forceUpdateList();
-        })
+    let openSection = this.getOpenSection();
+    if (openSection === sectionIndex) {
+      sectionIndex = null;
     }
+    this.setState({ openSection: sectionIndex });
+  }
 
-    _forceUpdateList() {
-        // NOTE: unclear why this particular set of functions works, but it does
-        this._list.invalidateCellSizeAfterRender({
-            columnIndex: 0,
-            rowIndex: 0
-        });
-        this._list.forceUpdateGrid();
-        this.forceUpdate()
+  getOpenSection() {
+    if (this.props.sections.length === 1) {
+      return 0;
     }
 
-    toggleSection(sectionIndex) {
-        if (this.props.onChangeSection) {
-            if (this.props.onChangeSection(sectionIndex) === false) {
-                return;
-            }
+    let { openSection } = this.state;
+    if (openSection === undefined) {
+      for (let [index, section] of this.props.sections.entries()) {
+        if (this.sectionIsSelected(section, index)) {
+          openSection = index;
+          break;
         }
-
-        let openSection = this.getOpenSection();
-        if (openSection === sectionIndex) {
-            sectionIndex = null;
-        }
-        this.setState({ openSection: sectionIndex });
-
+      }
     }
-
-    getOpenSection() {
-        if (this.props.sections.length === 1) {
-            return 0;
-        }
-
-        let { openSection } = this.state;
-        if (openSection === undefined) {
-            for (let [index, section] of this.props.sections.entries()) {
-                if (this.sectionIsSelected(section, index)) {
-                    openSection = index;
-                    break;
-                }
-            }
-        }
-        return openSection;
+    return openSection;
+  }
+
+  sectionIsSelected(section, sectionIndex) {
+    let { sections } = this.props;
+    let selectedSection = null;
+    for (let i = 0; i < sections.length; i++) {
+      if (_.some(sections[i].items, item => this.itemIsSelected(item))) {
+        selectedSection = i;
+        break;
+      }
     }
-
-    sectionIsSelected(section, sectionIndex) {
-        let { sections } = this.props;
-        let selectedSection = null;
-        for (let i = 0; i < sections.length; i++) {
-            if (_.some(sections[i].items, (item) => this.itemIsSelected(item))) {
-                selectedSection = i;
-                break;
-            }
-        }
-        return selectedSection === sectionIndex;
+    return selectedSection === sectionIndex;
+  }
+
+  itemIsClickable(item) {
+    if (this.props.itemIsClickable) {
+      return this.props.itemIsClickable(item);
+    } else {
+      return true;
     }
+  }
 
-    itemIsClickable(item) {
-        if (this.props.itemIsClickable) {
-            return this.props.itemIsClickable(item);
-        } else {
-            return true;
-        }
+  itemIsSelected(item) {
+    if (this.props.itemIsSelected) {
+      return this.props.itemIsSelected(item);
+    } else {
+      return false;
     }
+  }
 
-    itemIsSelected(item) {
-        if (this.props.itemIsSelected) {
-            return this.props.itemIsSelected(item);
-        } else {
-            return false;
-        }
+  onChange(item) {
+    if (this.props.onChange) {
+      this.props.onChange(item);
     }
+  }
 
-    onChange(item) {
-        if (this.props.onChange) {
-            this.props.onChange(item);
-        }
+  renderItemExtra(item, itemIndex) {
+    if (this.props.renderItemExtra) {
+      return this.props.renderItemExtra(item, itemIndex);
+    } else {
+      return null;
     }
+  }
 
-    renderItemExtra(item, itemIndex) {
-        if (this.props.renderItemExtra) {
-            return this.props.renderItemExtra(item, itemIndex);
-        } else {
-            return null;
-        }
+  renderItemIcon(item, itemIndex) {
+    if (this.props.renderItemIcon) {
+      return this.props.renderItemIcon(item, itemIndex);
+    } else {
+      return null;
     }
-
-    renderItemIcon(item, itemIndex) {
-        if (this.props.renderItemIcon) {
-            return this.props.renderItemIcon(item, itemIndex);
-        } else {
-            return null;
-        }
+  }
+
+  renderSectionIcon(section, sectionIndex) {
+    if (this.props.renderSectionIcon) {
+      return (
+        <span className="List-section-icon mr2">
+          {this.props.renderSectionIcon(section, sectionIndex)}
+        </span>
+      );
+    } else {
+      return null;
     }
-
-    renderSectionIcon(section, sectionIndex) {
-        if (this.props.renderSectionIcon) {
-            return <span className="List-section-icon mr2">{this.props.renderSectionIcon(section, sectionIndex)}</span>;
-        } else {
-            return null;
+  }
+
+  getItemClasses(item, itemIndex) {
+    return (
+      this.props.getItemClasses && this.props.getItemClasses(item, itemIndex)
+    );
+  }
+
+  render() {
+    const {
+      id,
+      style,
+      searchable,
+      searchPlaceholder,
+      sections,
+      showItemArrows,
+      alwaysTogglable,
+      alwaysExpanded,
+      hideSingleSectionTitle,
+    } = this.props;
+    const { searchText } = this.state;
+
+    const openSection = this.getOpenSection();
+    const sectionIsExpanded = sectionIndex =>
+      alwaysExpanded || openSection === sectionIndex;
+    const sectionIsSearchable = sectionIndex =>
+      searchable &&
+      (typeof searchable !== "function" || searchable(sections[sectionIndex]));
+    const sectionIsTogglable = sectionIndex =>
+      alwaysTogglable || sections.length > 1;
+
+    const rows = [];
+    for (const [sectionIndex, section] of sections.entries()) {
+      const isLastSection = sectionIndex === sections.length - 1;
+      if (
+        section.name &&
+        (!hideSingleSectionTitle || sections.length > 1 || alwaysTogglable)
+      ) {
+        rows.push({ type: "header", section, sectionIndex, isLastSection });
+      } else {
+        rows.push({
+          type: "header-hidden",
+          section,
+          sectionIndex,
+          isLastSection,
+        });
+      }
+      if (
+        sectionIsSearchable(sectionIndex) &&
+        sectionIsExpanded(sectionIndex) &&
+        section.items &&
+        section.items.length > 0
+      ) {
+        rows.push({ type: "search", section, sectionIndex, isLastSection });
+      }
+      if (
+        sectionIsExpanded(sectionIndex) &&
+        section.items &&
+        section.items.length > 0
+      ) {
+        for (const [itemIndex, item] of section.items.entries()) {
+          if (
+            !searchText ||
+            item.name.toLowerCase().includes(searchText.toLowerCase())
+          ) {
+            const isLastItem = itemIndex === section.items.length - 1;
+            if (this.itemIsSelected(item)) {
+              this._initialSelectedRowIndex = rows.length;
+            }
+            rows.push({
+              type: "item",
+              section,
+              sectionIndex,
+              isLastSection,
+              item,
+              itemIndex,
+              isLastItem,
+            });
+          }
         }
+      }
     }
 
-    getItemClasses(item, itemIndex) {
-        return this.props.getItemClasses && this.props.getItemClasses(item, itemIndex);
-    }
-
-    render() {
-        const { id, style, searchable, searchPlaceholder, sections, showItemArrows, alwaysTogglable, alwaysExpanded, hideSingleSectionTitle,  } = this.props;
-        const { searchText } = this.state;
-
-        const openSection = this.getOpenSection();
-        const sectionIsExpanded = (sectionIndex) =>
-            alwaysExpanded || openSection === sectionIndex;
-        const sectionIsSearchable = (sectionIndex) =>
-            searchable && (typeof searchable !== "function" || searchable(sections[sectionIndex]));
-        const sectionIsTogglable  = (sectionIndex) =>
-            alwaysTogglable || sections.length > 1;
-
-        const rows = []
-        for (const [sectionIndex, section] of sections.entries()) {
-            const isLastSection = sectionIndex === sections.length - 1;
-            if (section.name && (!hideSingleSectionTitle || sections.length > 1 || alwaysTogglable)) {
-                rows.push({ type: "header", section, sectionIndex, isLastSection })
-            } else {
-                rows.push({ type: "header-hidden", section, sectionIndex, isLastSection })
-            }
-            if (sectionIsSearchable(sectionIndex) && sectionIsExpanded(sectionIndex) && section.items && section.items.length > 0) {
-                rows.push({ type: "search", section, sectionIndex, isLastSection })
-            }
-            if (sectionIsExpanded(sectionIndex) && section.items && section.items.length > 0) {
-                for (const [itemIndex, item] of section.items.entries()) {
-                    if (!searchText || item.name.toLowerCase().includes(searchText.toLowerCase())) {
-                        const isLastItem = itemIndex === section.items.length - 1;
-                        if (this.itemIsSelected(item)) {
-                            this._initialSelectedRowIndex = rows.length;
+    const maxHeight =
+      this.props.maxHeight > 0 && this.props.maxHeight < Infinity
+        ? this.props.maxHeight
+        : window.innerHeight;
+
+    const width = this.props.width;
+    const height = Math.min(
+      maxHeight,
+      rows.reduce(
+        (height, row, index) => height + this._cache.rowHeight({ index }),
+        0,
+      ),
+    );
+
+    return (
+      <List
+        id={id}
+        ref={list => (this._list = list)}
+        className={this.props.className}
+        style={style}
+        width={width}
+        height={height}
+        rowCount={rows.length}
+        deferredMeasurementCache={this._cache}
+        rowHeight={this._cache.rowHeight}
+        // HACK: needs to be large enough to render enough rows to fill the screen since we used
+        // the CellMeasurerCache to calculate the height
+        overscanRowCount={100}
+        // ensure `scrollToRow` scrolls the row to the top of the list
+        scrollToAlignment="start"
+        rowRenderer={({ key, index, parent, style }) => {
+          const {
+            type,
+            section,
+            sectionIndex,
+            item,
+            itemIndex,
+            isLastItem,
+          } = rows[index];
+          return (
+            <CellMeasurer
+              cache={this._cache}
+              columnIndex={0}
+              key={key}
+              rowIndex={index}
+              parent={parent}
+            >
+              {({ measure }) => (
+                <div
+                  style={style}
+                  className={cx("List-section", section.className, {
+                    "List-section--expanded": sectionIsExpanded(sectionIndex),
+                    "List-section--togglable": sectionIsTogglable(sectionIndex),
+                  })}
+                >
+                  {type === "header" ? (
+                    alwaysExpanded ? (
+                      <div className="px2 pt2 pb1 h6 text-grey-2 text-uppercase text-bold">
+                        {section.name}
+                      </div>
+                    ) : (
+                      <div
+                        className={cx(
+                          "List-section-header p2 flex align-center",
+                          {
+                            "cursor-pointer": sectionIsTogglable(sectionIndex),
+                            "border-top": sectionIndex !== 0,
+                            "border-bottom": sectionIsExpanded(sectionIndex),
+                          },
+                        )}
+                        onClick={
+                          sectionIsTogglable(sectionIndex) &&
+                          (() => this.toggleSection(sectionIndex))
                         }
-                        rows.push({ type: "item", section, sectionIndex, isLastSection, item, itemIndex, isLastItem });
-                    }
-                }
-            }
-        }
-
-        const maxHeight = (this.props.maxHeight > 0 && this.props.maxHeight < Infinity) ?
-            this.props.maxHeight :
-            window.innerHeight;
-
-        const width = this.props.width;
-        const height = Math.min(maxHeight, rows.reduce((height, row, index) =>
-            height + this._cache.rowHeight({ index })
-        , 0));
-
-        return (
-            <List
-                id={id}
-                ref={list => this._list = list}
-                className={this.props.className}
-                style={style}
-                width={width}
-                height={height}
-                rowCount={rows.length}
-
-                deferredMeasurementCache={this._cache}
-                rowHeight={this._cache.rowHeight}
-
-                // HACK: needs to be large enough to render enough rows to fill the screen since we used
-                // the CellMeasurerCache to calculate the height
-                overscanRowCount={100}
-
-                // ensure `scrollToRow` scrolls the row to the top of the list
-                scrollToAlignment="start"
-
-                rowRenderer={({ key, index, parent, style }) => {
-                    const { type, section, sectionIndex, item, itemIndex, isLastItem } = rows[index];
-                    return (
-                        <CellMeasurer
-                            cache={this._cache}
-                            columnIndex={0}
-                            key={key}
-                            rowIndex={index}
-                            parent={parent}
-                        >
-                            {({ measure }) =>
-                                <div
-                                    style={style}
-                                    className={cx("List-section", section.className, {
-                                        "List-section--expanded": sectionIsExpanded(sectionIndex),
-                                        "List-section--togglable": sectionIsTogglable(sectionIndex)
-                                    })}
-                                >
-                                    { type === "header" ? (
-                                        alwaysExpanded ?
-                                            <div className="px2 pt2 pb1 h6 text-grey-2 text-uppercase text-bold">
-                                                {section.name}
-                                            </div>
-                                        :
-                                            <div
-                                                className={cx("List-section-header p2 flex align-center", {
-                                                    "cursor-pointer": sectionIsTogglable(sectionIndex),
-                                                    "border-top": sectionIndex !== 0,
-                                                    "border-bottom": sectionIsExpanded(sectionIndex)
-                                                })}
-                                                onClick={sectionIsTogglable(sectionIndex) && (() => this.toggleSection(sectionIndex))}
-                                            >
-                                                { this.renderSectionIcon(section, sectionIndex) }
-                                                <h3 className="List-section-title">{section.name}</h3>
-                                                { sections.length > 1 && section.items && section.items.length > 0 &&
-                                                    <span className="flex-align-right">
-                                                        <Icon name={sectionIsExpanded(sectionIndex) ? "chevronup" : "chevrondown"} size={12} />
-                                                    </span>
-                                                }
-                                            </div>
-                                    ) : type === "header-hidden" ?
-                                        <div className="my1" />
-                                    : type === "search" ?
-                                        <div className="m1" style={{ border: "2px solid transparent" }}>
-                                            <ListSearchField
-                                                onChange={(val) => this.setState({searchText: val})}
-                                                searchText={this.state.searchText}
-                                                placeholder={searchPlaceholder}
-                                                autoFocus
-                                            />
-                                        </div>
-                                    : type === "item" ?
-                                        <div
-                                            className={cx("List-item flex mx1", {
-                                                'List-item--selected': this.itemIsSelected(item),
-                                                'List-item--disabled': !this.itemIsClickable(item),
-                                                "mb1": isLastItem
-                                            }, this.getItemClasses(item, itemIndex))}
-                                        >
-                                            <a
-                                                className={cx("p1 flex-full flex align-center", this.itemIsClickable(item) ? "cursor-pointer" : "cursor-default")}
-                                                onClick={this.itemIsClickable(item) && this.onChange.bind(this, item)}
-                                            >
-                                                <span className="flex align-center">{ this.renderItemIcon(item, itemIndex) }</span>
-                                                <h4 className="List-item-title ml1">{item.name}</h4>
-                                            </a>
-                                            { this.renderItemExtra(item, itemIndex) }
-                                            { showItemArrows &&
-                                                <div className="List-item-arrow flex align-center px1">
-                                                    <Icon name="chevronright" size={8} />
-                                                </div>
-                                            }
-                                        </div>
-                                    :
-                                        null
-                                    }
-                                </div>
-                            }
-                        </CellMeasurer>
-                    );
-                }}
-            />
-        );
-    }
+                      >
+                        {this.renderSectionIcon(section, sectionIndex)}
+                        <h3 className="List-section-title">{section.name}</h3>
+                        {sections.length > 1 &&
+                          section.items &&
+                          section.items.length > 0 && (
+                            <span className="flex-align-right">
+                              <Icon
+                                name={
+                                  sectionIsExpanded(sectionIndex)
+                                    ? "chevronup"
+                                    : "chevrondown"
+                                }
+                                size={12}
+                              />
+                            </span>
+                          )}
+                      </div>
+                    )
+                  ) : type === "header-hidden" ? (
+                    <div className="my1" />
+                  ) : type === "search" ? (
+                    <div
+                      className="m1"
+                      style={{ border: "2px solid transparent" }}
+                    >
+                      <ListSearchField
+                        onChange={val => this.setState({ searchText: val })}
+                        searchText={this.state.searchText}
+                        placeholder={searchPlaceholder}
+                        autoFocus
+                      />
+                    </div>
+                  ) : type === "item" ? (
+                    <div
+                      className={cx(
+                        "List-item flex mx1",
+                        {
+                          "List-item--selected": this.itemIsSelected(item),
+                          "List-item--disabled": !this.itemIsClickable(item),
+                          mb1: isLastItem,
+                        },
+                        this.getItemClasses(item, itemIndex),
+                      )}
+                    >
+                      <a
+                        className={cx(
+                          "p1 flex-full flex align-center",
+                          this.itemIsClickable(item)
+                            ? "cursor-pointer"
+                            : "cursor-default",
+                        )}
+                        onClick={
+                          this.itemIsClickable(item) &&
+                          this.onChange.bind(this, item)
+                        }
+                      >
+                        <span className="flex align-center">
+                          {this.renderItemIcon(item, itemIndex)}
+                        </span>
+                        <h4 className="List-item-title ml1">{item.name}</h4>
+                      </a>
+                      {this.renderItemExtra(item, itemIndex)}
+                      {showItemArrows && (
+                        <div className="List-item-arrow flex align-center px1">
+                          <Icon name="chevronright" size={8} />
+                        </div>
+                      )}
+                    </div>
+                  ) : null}
+                </div>
+              )}
+            </CellMeasurer>
+          );
+        }}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/ActionButton.jsx b/frontend/src/metabase/components/ActionButton.jsx
index 12c3c6011fce05fd0fc82af5c20a69561d8c409d..639cadda539130b85f54db1c4a07f0f90b137692 100644
--- a/frontend/src/metabase/components/ActionButton.jsx
+++ b/frontend/src/metabase/components/ActionButton.jsx
@@ -7,126 +7,153 @@ import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
 
 import { cancelable } from "metabase/lib/promise";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 type Props = {
-    actionFn: (...args: any[]) => Promise<any>,
-    className?: string,
-    children?: any,
-    normalText?: string,
-    activeText?: string,
-    failedText?: string,
-    successText?: string,
-    forceActiveStyle?: boolean
-}
+  actionFn: (...args: any[]) => Promise<any>,
+  className?: string,
+  children?: any,
+  normalText?: string,
+  activeText?: string,
+  failedText?: string,
+  successText?: string,
+  forceActiveStyle?: boolean,
+};
 
 type State = {
-    active: bool,
-    result: null|"success"|"failed",
-}
+  active: boolean,
+  result: null | "success" | "failed",
+};
 
 export default class ActionButton extends Component {
-    props: Props;
-    state: State;
-
-    timeout: ?any;
-    actionPromise: ?{ cancel: () => void }
+  props: Props;
+  state: State;
 
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            active: false,
-            result: null
-        };
-    }
+  timeout: ?any;
+  actionPromise: ?{ cancel: () => void };
 
-    static propTypes = {
-        actionFn: PropTypes.func.isRequired
-    };
+  constructor(props: Props) {
+    super(props);
 
-    static defaultProps = {
-        className: "Button",
-        normalText: t`Save`,
-        activeText: t`Saving...`,
-        failedText: t`Save failed`,
-        successText: t`Saved`,
-        forceActiveStyle: false
+    this.state = {
+      active: false,
+      result: null,
     };
-
-    componentWillUnmount() {
-        clearTimeout(this.timeout);
-        if (this.actionPromise) {
-            this.actionPromise.cancel();
-        }
+  }
+
+  static propTypes = {
+    actionFn: PropTypes.func.isRequired,
+  };
+
+  static defaultProps = {
+    className: "Button",
+    normalText: t`Save`,
+    activeText: t`Saving...`,
+    failedText: t`Save failed`,
+    successText: t`Saved`,
+    forceActiveStyle: false,
+  };
+
+  componentWillUnmount() {
+    clearTimeout(this.timeout);
+    if (this.actionPromise) {
+      this.actionPromise.cancel();
     }
+  }
 
-    resetStateOnTimeout = () => {
-        // clear any previously set timeouts then start a new one
-        clearTimeout(this.timeout);
-        this.timeout = setTimeout(() => this.setState({
-            active: false,
-            result: null
-        }), 5000);
-    }
-
-    onClick = (event: MouseEvent) => {
-        event.preventDefault();
-
-        // set state to active
+  resetStateOnTimeout = () => {
+    // clear any previously set timeouts then start a new one
+    clearTimeout(this.timeout);
+    this.timeout = setTimeout(
+      () =>
         this.setState({
-            active: true,
-            result: null
-        });
-
-        // run the function we want bound to this button
-        this.actionPromise = cancelable(this.props.actionFn());
-        this.actionPromise.then((success) => {
-            this.setState({
-                active: false,
-                result: "success"
-            }, this.resetStateOnTimeout);
-        }, (error) => {
-            if (!error.isCanceled) {
-                console.error(error);
-                this.setState({
-                    active: false,
-                    result: "failed"
-                }, this.resetStateOnTimeout);
-            }
-        });
-    }
-
-    render() {
-        // eslint-disable-next-line no-unused-vars
-        const { normalText, activeText, failedText, successText, actionFn, className, forceActiveStyle, children, ...props } = this.props;
-        const { active, result } = this.state;
-
-        return (
-            <Button
-                {...props}
-                    className={forceActiveStyle ? cx('Button', 'Button--waiting') : cx(className, {
-                    'Button--waiting pointer-events-none': active,
-                    'Button--success': result === 'success',
-                    'Button--danger': result === 'failed'
-                })}
-                onClick={this.onClick}
-            >
-                { active ?
-                    // TODO: loading spinner
-                    activeText
-                : result === "success" ?
-                    <span>
-                        {forceActiveStyle ? null : <Icon name='check' size={12} /> }
-                        <span className="ml1">{successText}</span>
-                    </span>
-                : result === "failed" ?
-                    failedText
-                :
-                    children || normalText
-                }
-            </Button>
+          active: false,
+          result: null,
+        }),
+      5000,
+    );
+  };
+
+  onClick = (event: MouseEvent) => {
+    event.preventDefault();
+
+    // set state to active
+    this.setState({
+      active: true,
+      result: null,
+    });
+
+    // run the function we want bound to this button
+    this.actionPromise = cancelable(this.props.actionFn());
+    this.actionPromise.then(
+      success => {
+        this.setState(
+          {
+            active: false,
+            result: "success",
+          },
+          this.resetStateOnTimeout,
         );
-    }
+      },
+      error => {
+        if (!error.isCanceled) {
+          console.error(error);
+          this.setState(
+            {
+              active: false,
+              result: "failed",
+            },
+            this.resetStateOnTimeout,
+          );
+        }
+      },
+    );
+  };
+
+  render() {
+    const {
+      normalText,
+      activeText,
+      failedText,
+      successText,
+      // eslint-disable-next-line no-unused-vars
+      actionFn,
+      className,
+      forceActiveStyle,
+      children,
+      ...props
+    } = this.props;
+    const { active, result } = this.state;
+
+    return (
+      <Button
+        {...props}
+        className={
+          forceActiveStyle
+            ? cx("Button", "Button--waiting")
+            : cx(className, {
+                "Button--waiting pointer-events-none": active,
+                "Button--success": result === "success",
+                "Button--danger": result === "failed",
+              })
+        }
+        onClick={this.onClick}
+      >
+        {active ? (
+          // TODO: loading spinner
+          activeText
+        ) : result === "success" ? (
+          <span>
+            {forceActiveStyle ? null : <Icon name="check" size={12} />}
+            <span className="ml1">{successText}</span>
+          </span>
+        ) : result === "failed" ? (
+          failedText
+        ) : (
+          children || normalText
+        )}
+      </Button>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/AdminAwareEmptyState.jsx b/frontend/src/metabase/components/AdminAwareEmptyState.jsx
index 073205bfbc827c6c67c2bb32a6281331cde56eb0..874235ffccee037861c728b5a8409bc962f04f1a 100644
--- a/frontend/src/metabase/components/AdminAwareEmptyState.jsx
+++ b/frontend/src/metabase/components/AdminAwareEmptyState.jsx
@@ -9,37 +9,43 @@ import { connect } from "react-redux";
  */
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
+  user: getUser(state, props),
 });
 
 @connect(mapStateToProps, null)
 class AdminAwareEmptyState extends Component {
-    render() {
-        const {user, title, message, adminMessage, icon, image, imageHeight, imageClassName, action, adminAction, link, adminLink, onActionClick, smallDescription = false} = this.props;
-         return (
-             <EmptyState
-                title={title}
-                message={user && user.is_superuser ?
-                    adminMessage || message :
-                    message
-                }
-                icon={icon}
-                image={image}
-                action={user && user.is_superuser ?
-                    adminAction || action :
-                    action
-                }
-                link={user && user.is_superuser ?
-                    adminLink || link :
-                    link
-                }
-                imageHeight={imageHeight}
-                imageClassName={imageClassName}
-                onActionClick={onActionClick}
-                smallDescription={smallDescription}
-            />
-         )
-    }
+  render() {
+    const {
+      user,
+      title,
+      message,
+      adminMessage,
+      icon,
+      image,
+      imageHeight,
+      imageClassName,
+      action,
+      adminAction,
+      link,
+      adminLink,
+      onActionClick,
+      smallDescription = false,
+    } = this.props;
+    return (
+      <EmptyState
+        title={title}
+        message={user && user.is_superuser ? adminMessage || message : message}
+        icon={icon}
+        image={image}
+        action={user && user.is_superuser ? adminAction || action : action}
+        link={user && user.is_superuser ? adminLink || link : link}
+        imageHeight={imageHeight}
+        imageClassName={imageClassName}
+        onActionClick={onActionClick}
+        smallDescription={smallDescription}
+      />
+    );
+  }
 }
 
-export default AdminAwareEmptyState;
\ No newline at end of file
+export default AdminAwareEmptyState;
diff --git a/frontend/src/metabase/components/AdminContentTable.jsx b/frontend/src/metabase/components/AdminContentTable.jsx
index a2014076a4757b8c28f3d01c228cab122594283f..3909c053227453775e8a08ae5ed097dee76b8b88 100644
--- a/frontend/src/metabase/components/AdminContentTable.jsx
+++ b/frontend/src/metabase/components/AdminContentTable.jsx
@@ -1,17 +1,15 @@
 import React from "react";
 
-const AdminContentTable = ({ columnTitles, children }) =>
-    <table className="ContentTable">
-        <thead>
-            <tr>
-                {columnTitles && columnTitles.map((title, index) =>
-                    <th key={index}>{title}</th>
-                 )}
-            </tr>
-        </thead>
-        <tbody>
-            {children}
-        </tbody>
-    </table>
+const AdminContentTable = ({ columnTitles, children }) => (
+  <table className="ContentTable">
+    <thead>
+      <tr>
+        {columnTitles &&
+          columnTitles.map((title, index) => <th key={index}>{title}</th>)}
+      </tr>
+    </thead>
+    <tbody>{children}</tbody>
+  </table>
+);
 
 export default AdminContentTable;
diff --git a/frontend/src/metabase/components/AdminEmptyText.jsx b/frontend/src/metabase/components/AdminEmptyText.jsx
index a9aec2383bc565e9efa277e5aa95749c71accc1d..79270885cfe9ad4801e659391024c6874f91352e 100644
--- a/frontend/src/metabase/components/AdminEmptyText.jsx
+++ b/frontend/src/metabase/components/AdminEmptyText.jsx
@@ -1,11 +1,12 @@
 import React from "react";
 import PropTypes from "prop-types";
 
-const AdminEmptyText = ({ message }) =>
-    <h2 className="text-grey-3">{message}</h2>
+const AdminEmptyText = ({ message }) => (
+  <h2 className="text-grey-3">{message}</h2>
+);
 
 AdminEmptyText.propTypes = {
-    message: PropTypes.string.isRequired
-}
+  message: PropTypes.string.isRequired,
+};
 
 export default AdminEmptyText;
diff --git a/frontend/src/metabase/components/AdminHeader.jsx b/frontend/src/metabase/components/AdminHeader.jsx
index c2507bb5f740839c0ac767b9ecd1605944281748..b85e2b86d08398bc21b08854143bf5a112660252 100644
--- a/frontend/src/metabase/components/AdminHeader.jsx
+++ b/frontend/src/metabase/components/AdminHeader.jsx
@@ -3,16 +3,16 @@ import React, { Component } from "react";
 import SaveStatus from "metabase/components/SaveStatus.jsx";
 
 export default class AdminHeader extends Component {
-    render() {
-        return (
-            <div className="MetadataEditor-header clearfix relative flex-no-shrink">
-                <div className="MetadataEditor-headerSection float-left h2 text-grey-4">
-                    {this.props.title}
-                </div>
-                <div className="MetadataEditor-headerSection absolute right float-right top bottom flex layout-centered">
-                    <SaveStatus ref="status" />
-                </div>
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className="MetadataEditor-header clearfix relative flex-no-shrink">
+        <div className="MetadataEditor-headerSection float-left h2 text-grey-4">
+          {this.props.title}
+        </div>
+        <div className="MetadataEditor-headerSection absolute right float-right top bottom flex layout-centered">
+          <SaveStatus ref="status" />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/AdminLayout.jsx b/frontend/src/metabase/components/AdminLayout.jsx
index e8502204fc05d0a03b18cd454593ea82da01a1f4..0dd4f0e199e134f21e054ebaa6cebfafb6eee551 100644
--- a/frontend/src/metabase/components/AdminLayout.jsx
+++ b/frontend/src/metabase/components/AdminLayout.jsx
@@ -3,29 +3,26 @@ import React, { Component } from "react";
 import AdminHeader from "./AdminHeader.jsx";
 
 export default class AdminLayout extends Component {
+  setSaving = () => {
+    this.refs.header.refs.status.setSaving();
+  };
+  setSaved = () => {
+    this.refs.header.refs.status.setSaved();
+  };
+  setSaveError = error => {
+    this.refs.header.refs.status.setSaveError(error);
+  };
 
-    setSaving = () => {
-        this.refs.header.refs.status.setSaving();
-    }
-    setSaved = () => {
-        this.refs.header.refs.status.setSaved();
-    }
-    setSaveError = (error) => {
-        this.refs.header.refs.status.setSaveError(error);
-    }
-
-    render() {
-        const { title, sidebar, children } = this.props;
-        return (
-            <div className="MetadataEditor full-height flex flex-column flex-full p4">
-                <AdminHeader ref="header" title={title} />
-                <div className="MetadataEditor-main flex flex-row flex-full mt2">
-                    {sidebar}
-                    <div className="px2 flex-full">
-                        {children}
-                    </div>
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const { title, sidebar, children } = this.props;
+    return (
+      <div className="MetadataEditor full-height flex flex-column flex-full p4">
+        <AdminHeader ref="header" title={title} />
+        <div className="MetadataEditor-main flex flex-row flex-full mt2">
+          {sidebar}
+          <div className="px2 flex-full">{children}</div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/AdminPaneLayout.jsx b/frontend/src/metabase/components/AdminPaneLayout.jsx
index 11cae74b5d1275776ec5685679cf9012c76b9e9e..2803465e0fda06d38c1bfd5b17066207335cd13f 100644
--- a/frontend/src/metabase/components/AdminPaneLayout.jsx
+++ b/frontend/src/metabase/components/AdminPaneLayout.jsx
@@ -3,46 +3,47 @@ import React from "react";
 import cx from "classnames";
 
 const AdminPaneTitle = ({
-    title,
-    description,
-    buttonText,
-    buttonAction,
-    buttonDisabled
-}) =>
-    <section className="clearfix px2">
-        { buttonText && buttonAction ?
-            <button
-                className={cx(
-                    "Button float-right",
-                    {"Button--primary": !buttonDisabled }
-                )}
-                disabled={buttonDisabled}
-                onClick={buttonAction}
-            >
-                {buttonText}
-            </button>
-        : null }
-        <h2 className="PageTitle">{title}</h2>
-        { description && <p className="text-measure">{description}</p> }
-    </section>
+  title,
+  description,
+  buttonText,
+  buttonAction,
+  buttonDisabled,
+}) => (
+  <section className="clearfix px2">
+    {buttonText && buttonAction ? (
+      <button
+        className={cx("Button float-right", {
+          "Button--primary": !buttonDisabled,
+        })}
+        disabled={buttonDisabled}
+        onClick={buttonAction}
+      >
+        {buttonText}
+      </button>
+    ) : null}
+    <h2 className="PageTitle">{title}</h2>
+    {description && <p className="text-measure">{description}</p>}
+  </section>
+);
 
 const AdminPaneLayout = ({
-    title,
-    description,
-    buttonText,
-    buttonAction,
-    buttonDisabled,
-    children
-}) =>
-    <div className="wrapper">
-        <AdminPaneTitle
-            title={title}
-            description={description}
-            buttonText={buttonText}
-            buttonAction={buttonAction}
-            buttonDisabled={buttonDisabled}
-        />
-        {children}
-    </div>
+  title,
+  description,
+  buttonText,
+  buttonAction,
+  buttonDisabled,
+  children,
+}) => (
+  <div className="wrapper">
+    <AdminPaneTitle
+      title={title}
+      description={description}
+      buttonText={buttonText}
+      buttonAction={buttonAction}
+      buttonDisabled={buttonDisabled}
+    />
+    {children}
+  </div>
+);
 
 export default AdminPaneLayout;
diff --git a/frontend/src/metabase/components/Alert.jsx b/frontend/src/metabase/components/Alert.jsx
index 6701928213327a81136f653f3fb75ad70ababc78..c461415ad24656e7d09a883276f3a01c1b1ea23e 100644
--- a/frontend/src/metabase/components/Alert.jsx
+++ b/frontend/src/metabase/components/Alert.jsx
@@ -1,13 +1,17 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Modal from "metabase/components/Modal.jsx";
 
-const Alert = ({ message, onClose }) =>
-    <Modal small isOpen={!!message}>
-        <div className="flex flex-column layout-centered p4">
-            <h3 className="mb4">{message}</h3>
-            <button className="Button Button--primary" onClick={onClose}>{t`Ok`}</button>
-        </div>
-    </Modal>
+const Alert = ({ message, onClose }) => (
+  <Modal small isOpen={!!message}>
+    <div className="flex flex-column layout-centered p4">
+      <h3 className="mb4">{message}</h3>
+      <button
+        className="Button Button--primary"
+        onClick={onClose}
+      >{t`Ok`}</button>
+    </div>
+  </Modal>
+);
 
 export default Alert;
diff --git a/frontend/src/metabase/components/Archived.jsx b/frontend/src/metabase/components/Archived.jsx
index bf8567735614d2445a74092353ac7525f1b8ec14..d3ab55d5b0c99ac5ddbc7eadeed9ffe268576a57 100644
--- a/frontend/src/metabase/components/Archived.jsx
+++ b/frontend/src/metabase/components/Archived.jsx
@@ -1,16 +1,23 @@
-import React from 'react';
+import React from "react";
 import EmptyState from "metabase/components/EmptyState";
 import Link from "metabase/components/Link";
-import { t } from 'c-3po';
-const Archived = ({ entityName, linkTo }) =>
-    <div className="full-height flex justify-center align-center">
-        <EmptyState
-            message={<div>
-                <div>{t`This ${entityName} has been archived`}</div>
-                <Link to={linkTo} className="my2 link" style={{fontSize: "14px"}}>{t`View the archive`}</Link>
-            </div>}
-            icon="viewArchive"
-        />
-    </div>;
+import { t } from "c-3po";
+const Archived = ({ entityName, linkTo }) => (
+  <div className="full-height flex justify-center align-center">
+    <EmptyState
+      message={
+        <div>
+          <div>{t`This ${entityName} has been archived`}</div>
+          <Link
+            to={linkTo}
+            className="my2 link"
+            style={{ fontSize: "14px" }}
+          >{t`View the archive`}</Link>
+        </div>
+      }
+      icon="viewArchive"
+    />
+  </div>
+);
 
-export default Archived;
\ No newline at end of file
+export default Archived;
diff --git a/frontend/src/metabase/components/ArchivedItem.jsx b/frontend/src/metabase/components/ArchivedItem.jsx
index 4ac81dcac0e337f72e13aff86fa35ab437791240..88a1620584e76608b1e3a0b9d01b06b61095d823 100644
--- a/frontend/src/metabase/components/ArchivedItem.jsx
+++ b/frontend/src/metabase/components/ArchivedItem.jsx
@@ -2,37 +2,46 @@
 
 import React from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon";
 import Tooltip from "metabase/components/Tooltip";
 
-const ArchivedItem = ({ name, type, icon, color = '#DEEAF1', isAdmin = false, onUnarchive }) =>
-    <div className="flex align-center p2 hover-parent hover--visibility border-bottom bg-grey-0-hover">
+const ArchivedItem = ({
+  name,
+  type,
+  icon,
+  color = "#DEEAF1",
+  isAdmin = false,
+  onUnarchive,
+}) => (
+  <div className="flex align-center p2 hover-parent hover--visibility border-bottom bg-grey-0-hover">
+    <Icon name={icon} className="mr2" style={{ color: color }} size={20} />
+    {name}
+    {isAdmin && (
+      <Tooltip
+        tooltip={
+          type === "card"
+            ? t`Unarchive this question`
+            : t`Unarchive this ${type}`
+        }
+      >
         <Icon
-            name={icon}
-            className="mr2"
-            style={{ color: color }}
-            size={20}
+          onClick={onUnarchive}
+          className="ml-auto cursor-pointer text-brand-hover hover-child"
+          name="unarchive"
         />
-        { name }
-        { isAdmin &&
-            <Tooltip tooltip={type === "card" ? t`Unarchive this question` : t`Unarchive this ${type}`}>
-                <Icon
-                    onClick={onUnarchive}
-                    className="ml-auto cursor-pointer text-brand-hover hover-child"
-                    name="unarchive"
-                />
-            </Tooltip>
-        }
-    </div>
+      </Tooltip>
+    )}
+  </div>
+);
 
 ArchivedItem.propTypes = {
-    name:        PropTypes.string.isRequired,
-    type:        PropTypes.string.isRequired,
-    icon:        PropTypes.string.isRequired,
-    color:       PropTypes.string,
-    isAdmin:     PropTypes.bool,
-    onUnarchive: PropTypes.func.isRequired
-}
+  name: PropTypes.string.isRequired,
+  type: PropTypes.string.isRequired,
+  icon: PropTypes.string.isRequired,
+  color: PropTypes.string,
+  isAdmin: PropTypes.bool,
+  onUnarchive: PropTypes.func.isRequired,
+};
 
 export default ArchivedItem;
diff --git a/frontend/src/metabase/components/BodyComponent.jsx b/frontend/src/metabase/components/BodyComponent.jsx
index eb8152f0409d7e2cdfa7e888ce44287fae1f3aa9..d5763919fe454f7f72e58291fde245f8226732c7 100644
--- a/frontend/src/metabase/components/BodyComponent.jsx
+++ b/frontend/src/metabase/components/BodyComponent.jsx
@@ -1,57 +1,67 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 
-export default ComposedComponent => class extends Component {
-    static displayName = "BodyComponent["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+export default ComposedComponent =>
+  class extends Component {
+    static displayName = "BodyComponent[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
     componentWillMount() {
-        this._element = document.createElement('div');
-        document.body.appendChild(this._element);
+      this._element = document.createElement("div");
+      document.body.appendChild(this._element);
     }
 
     componentDidMount() {
-        this._render();
+      this._render();
     }
 
     componentDidUpdate() {
-        this._render();
+      this._render();
     }
 
     componentWillUnmount() {
-        ReactDOM.unmountComponentAtNode(this._element);
-        if (this._element.parentNode) {
-            this._element.parentNode.removeChild(this._element);
-        }
+      ReactDOM.unmountComponentAtNode(this._element);
+      if (this._element.parentNode) {
+        this._element.parentNode.removeChild(this._element);
+      }
     }
 
     _render() {
-        this._element.className = this.props.className || "";
-        ReactDOM.unstable_renderSubtreeIntoContainer(this,
-            <ComposedComponent {...this.props} className={undefined} />
-        , this._element);
+      this._element.className = this.props.className || "";
+      ReactDOM.unstable_renderSubtreeIntoContainer(
+        this,
+        <ComposedComponent {...this.props} className={undefined} />,
+        this._element,
+      );
     }
 
     render() {
-        return null;
+      return null;
     }
-};
+  };
 
 /**
  * A modified version of BodyComponent HOC for Jest/Enzyme tests.
  * Simply renders the component inline instead of mutating DOM root.
  */
-export const TestBodyComponent = ComposedComponent => class extends Component {
-    static displayName = "TestBodyComponent["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+export const TestBodyComponent = ComposedComponent =>
+  class extends Component {
+    static displayName = "TestBodyComponent[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
     render() {
-        return (
-            <div
-                // because popover is normally directly attached to body element, other elements should not need
-                // to care about clicks that happen inside the popover
-                onClick={ (e) => { e.stopPropagation(); } }
-            >
-                <ComposedComponent {...this.props} className={undefined} />
-            </div>
-        )
+      return (
+        <div
+          // because popover is normally directly attached to body element, other elements should not need
+          // to care about clicks that happen inside the popover
+          onClick={e => {
+            e.stopPropagation();
+          }}
+        >
+          <ComposedComponent {...this.props} className={undefined} />
+        </div>
+      );
     }
-}
+  };
diff --git a/frontend/src/metabase/components/Breadcrumbs.css b/frontend/src/metabase/components/Breadcrumbs.css
index 9956fd12ff134a9160c277c25bb8ef5cd5036a2b..586d2d9ff1ac01d5cca6325c3562faa4c5869bb9 100644
--- a/frontend/src/metabase/components/Breadcrumbs.css
+++ b/frontend/src/metabase/components/Breadcrumbs.css
@@ -1,71 +1,70 @@
 :root {
-    --breadcrumbs-color: #BFC1C2;
-    --breadcrumb-page-color: #636060;
-    --breadcrumb-divider-spacing: 0.75em;
-    /* taken from Sidebar.css, should probably factor them out into variables */
-    --sidebar-breadcrumbs-color: #9CAEBE;
-    --sidebar-breadcrumb-page-color: #2D86D4;
+  --breadcrumbs-color: #bfc1c2;
+  --breadcrumb-page-color: #636060;
+  --breadcrumb-divider-spacing: 0.75em;
+  /* taken from Sidebar.css, should probably factor them out into variables */
+  --sidebar-breadcrumbs-color: #9caebe;
+  --sidebar-breadcrumb-page-color: #2d86d4;
 }
 
 :local(.breadcrumbs) {
-    display: flex;
-    align-items: center;
-    color: var(--breadcrumbs-color);
+  display: flex;
+  align-items: center;
+  color: var(--breadcrumbs-color);
 }
 
 :local(.breadcrumb) {
-    font-size: 0.75rem;
-    font-weight: bold;
-    text-transform: uppercase;
+  font-size: 0.75rem;
+  font-weight: bold;
+  text-transform: uppercase;
 }
 
-
 :local(.breadcrumbDivider) {
-    margin-left: var(--breadcrumb-divider-spacing);
-    margin-right: var(--breadcrumb-divider-spacing);
-    flex-shrink: 0;
+  margin-left: var(--breadcrumb-divider-spacing);
+  margin-right: var(--breadcrumb-divider-spacing);
+  flex-shrink: 0;
 }
 
 /* the breadcrumb path will always inherit the color of the breadcrumbs object */
 :local(.breadcrumb.breadcrumbPath) {
-    color: currentColor;
-    transition: color .3s linear;
+  color: currentColor;
+  transition: color 0.3s linear;
 }
 
 :local(.breadcrumb.breadcrumbPath):hover {
-    color: var(--breadcrumb-page-color);
-    transition: color .3s linear;
+  color: var(--breadcrumb-page-color);
+  transition: color 0.3s linear;
 }
 
 /* the breadcrumb page (current page) should be a different contrasting color  */
 :local(.breadcrumb.breadcrumbPage) {
-    color: var(--breadcrumb-page-color);
+  color: var(--breadcrumb-page-color);
 }
 
 :local(.sidebarBreadcrumbs) {
-    composes: flex from "style";
-    composes: breadcrumbs;
-    color: var(--sidebar-breadcrumbs-color);
-    max-width: 100%;
+  composes: flex from "style";
+  composes: breadcrumbs;
+  color: var(--sidebar-breadcrumbs-color);
+  max-width: 100%;
 }
 
 :local(.sidebarBreadcrumb) {
-    composes: breadcrumb;
-    height: 15px;
+  composes: breadcrumb;
+  height: 15px;
 }
 
 /* the breadcrumb path will always inherit the color of the breadcrumbs object */
 :local(.sidebarBreadcrumb.breadcrumbPath) {
-    color: currentColor;
-    transition: color .3s linear;
+  color: currentColor;
+  transition: color 0.3s linear;
 }
 
 :local(.sidebarBreadcrumb.breadcrumbPath):hover {
-    color: var(--sidebar-breadcrumb-page-color);
-    transition: color .3s linear;
+  color: var(--sidebar-breadcrumb-page-color);
+  transition: color 0.3s linear;
 }
 
 /* the breadcrumb page (current page) should be a different contrasting color  */
 :local(.sidebarBreadcrumb.breadcrumbPage) {
-    color: var(--sidebar-breadcrumb-page-color);
+  color: var(--sidebar-breadcrumb-page-color);
 }
diff --git a/frontend/src/metabase/components/Breadcrumbs.jsx b/frontend/src/metabase/components/Breadcrumbs.jsx
index c4b10537b02b798f920b970f3d8396baaea053e4..baa8064ca54f59d1b1880edcb3663c3a1b3aa479 100644
--- a/frontend/src/metabase/components/Breadcrumbs.jsx
+++ b/frontend/src/metabase/components/Breadcrumbs.jsx
@@ -7,75 +7,73 @@ import S from "./Breadcrumbs.css";
 import Icon from "metabase/components/Icon.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 
 export default class Breadcrumbs extends Component {
-    static propTypes = {
-        className: PropTypes.string,
-        crumbs: PropTypes.array,
-        inSidebar: PropTypes.bool,
-        placeholder: PropTypes.string
-    };
-    static defaultProps = {
-        crumbs: [],
-        inSidebar: false,
-        placeholder: null
-    };
+  static propTypes = {
+    className: PropTypes.string,
+    crumbs: PropTypes.array,
+    inSidebar: PropTypes.bool,
+    placeholder: PropTypes.string,
+  };
+  static defaultProps = {
+    crumbs: [],
+    inSidebar: false,
+    placeholder: null,
+  };
 
-    render() {
-        const {
-            className,
-            crumbs,
-            inSidebar,
-            placeholder
-        } = this.props;
+  render() {
+    const { className, crumbs, inSidebar, placeholder } = this.props;
 
-        const breadcrumbClass = inSidebar ? S.sidebarBreadcrumb : S.breadcrumb;
-        const breadcrumbsClass = inSidebar ? S.sidebarBreadcrumbs : S.breadcrumbs;
+    const breadcrumbClass = inSidebar ? S.sidebarBreadcrumb : S.breadcrumb;
+    const breadcrumbsClass = inSidebar ? S.sidebarBreadcrumbs : S.breadcrumbs;
 
-        return (
-            <section className={cx(className, breadcrumbsClass)}>
-                { crumbs.length <= 1 && placeholder ?
-                    <span className={cx(breadcrumbClass, S.breadcrumbPage)}>
-                        {placeholder}
-                    </span> :
-                    crumbs
-                        .map(breadcrumb => Array.isArray(breadcrumb) ?
-                            breadcrumb : [breadcrumb]
-                        )
-                        .map((breadcrumb, index) =>
-                            <Ellipsified
-                                key={index}
-                                tooltip={breadcrumb[0]}
-                                tooltipMaxWidth="100%"
-                                className={cx(
-                                    breadcrumbClass,
-                                    breadcrumb.length > 1 ?
-                                        S.breadcrumbPath : S.breadcrumbPage
-                                )}
-                            >
-                                { breadcrumb.length > 1 ?
-                                    <Link to={breadcrumb[1]}>{breadcrumb[0]}</Link> :
-                                    <span>{breadcrumb[0]}</span>
-                                }
-                            </Ellipsified>
-                        )
-                        .map((breadcrumb, index, breadcrumbs) =>
-                            index < breadcrumbs.length - 1 ?
-                                [
-                                    breadcrumb,
-                                    <Icon
-                                        key={`${index}-separator`}
-                                        name="chevronright"
-                                        className={S.breadcrumbDivider}
-                                        width={12}
-                                        height={12}
-                                    />
-                                ] :
-                                breadcrumb
-                        )
-                }
-            </section>
-        );
-    }
+    return (
+      <section className={cx(className, breadcrumbsClass)}>
+        {crumbs.length <= 1 && placeholder ? (
+          <span className={cx(breadcrumbClass, S.breadcrumbPage)}>
+            {placeholder}
+          </span>
+        ) : (
+          crumbs
+            .map(
+              breadcrumb =>
+                Array.isArray(breadcrumb) ? breadcrumb : [breadcrumb],
+            )
+            .map((breadcrumb, index) => (
+              <Ellipsified
+                key={index}
+                tooltip={breadcrumb[0]}
+                tooltipMaxWidth="100%"
+                className={cx(
+                  breadcrumbClass,
+                  breadcrumb.length > 1 ? S.breadcrumbPath : S.breadcrumbPage,
+                )}
+              >
+                {breadcrumb.length > 1 ? (
+                  <Link to={breadcrumb[1]}>{breadcrumb[0]}</Link>
+                ) : (
+                  <span>{breadcrumb[0]}</span>
+                )}
+              </Ellipsified>
+            ))
+            .map(
+              (breadcrumb, index, breadcrumbs) =>
+                index < breadcrumbs.length - 1
+                  ? [
+                      breadcrumb,
+                      <Icon
+                        key={`${index}-separator`}
+                        name="chevronright"
+                        className={S.breadcrumbDivider}
+                        width={12}
+                        height={12}
+                      />,
+                    ]
+                  : breadcrumb,
+            )
+        )}
+      </section>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Button.info.js b/frontend/src/metabase/components/Button.info.js
index 92e2d61bfc39aa7e7e40b8fbf380f4b24f1c3ba4..27c09c71f515bf817b007f049bf956e845962d74 100644
--- a/frontend/src/metabase/components/Button.info.js
+++ b/frontend/src/metabase/components/Button.info.js
@@ -1,6 +1,6 @@
 import React from "react";
 import Button from "metabase/components/Button";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 export const component = Button;
 
 export const description = `
@@ -8,7 +8,7 @@ Metabase's main button component.
 `;
 
 export const examples = {
-    "": <Button>{t`Clickity click`}</Button>,
-    "primary": <Button primary>{t`Clickity click`}</Button>,
-    "with an icon": <Button icon='star'>{t`Clickity click`}</Button>
+  "": <Button>{t`Clickity click`}</Button>,
+  primary: <Button primary>{t`Clickity click`}</Button>,
+  "with an icon": <Button icon="star">{t`Clickity click`}</Button>,
 };
diff --git a/frontend/src/metabase/components/Button.jsx b/frontend/src/metabase/components/Button.jsx
index a38bbd93df6262d26ab36cf0fae48dd9df88fc9c..9e2e57f4a9c999d00b812d5d5fe0c63f8c695c57 100644
--- a/frontend/src/metabase/components/Button.jsx
+++ b/frontend/src/metabase/components/Button.jsx
@@ -8,51 +8,59 @@ import cx from "classnames";
 import _ from "underscore";
 
 const BUTTON_VARIANTS = [
-    "small",
-    "medium",
-    "large",
-    "primary",
-    "warning",
-    "cancel",
-    "success",
-    "purple",
-    "borderless",
-    "onlyIcon"
+  "small",
+  "medium",
+  "large",
+  "primary",
+  "warning",
+  "cancel",
+  "success",
+  "purple",
+  "borderless",
+  "onlyIcon",
 ];
 
 const Button = ({ className, icon, iconSize, children, ...props }) => {
-    let variantClasses = BUTTON_VARIANTS.filter(variant => props[variant]).map(variant => "Button--" + variant);
-
-    return (
-        <button
-            {..._.omit(props, ...BUTTON_VARIANTS)}
-            className={cx("Button", className, variantClasses)}
-        >
-            <div className="flex layout-centered">
-                { icon && <Icon name={icon} size={iconSize ? iconSize : 14} className={cx({ "mr1": !props.onlyIcon })} />}
-                <div>{children}</div>
-            </div>
-        </button>
-    );
+  let variantClasses = BUTTON_VARIANTS.filter(variant => props[variant]).map(
+    variant => "Button--" + variant,
+  );
+
+  return (
+    <button
+      {..._.omit(props, ...BUTTON_VARIANTS)}
+      className={cx("Button", className, variantClasses)}
+    >
+      <div className="flex layout-centered">
+        {icon && (
+          <Icon
+            name={icon}
+            size={iconSize ? iconSize : 14}
+            className={cx({ mr1: !props.onlyIcon })}
+          />
+        )}
+        <div>{children}</div>
+      </div>
+    </button>
+  );
 };
 
 Button.propTypes = {
-    className: PropTypes.string,
-    icon: PropTypes.string,
-    iconSize: PropTypes.number,
-    children: PropTypes.any,
-
-    small: PropTypes.bool,
-    medium: PropTypes.bool,
-    large: PropTypes.bool,
-
-    primary: PropTypes.bool,
-    warning: PropTypes.bool,
-    cancel: PropTypes.bool,
-    purple: PropTypes.bool,
-
-    borderless: PropTypes.bool,
-    onlyIcon: PropTypes.bool,
+  className: PropTypes.string,
+  icon: PropTypes.string,
+  iconSize: PropTypes.number,
+  children: PropTypes.any,
+
+  small: PropTypes.bool,
+  medium: PropTypes.bool,
+  large: PropTypes.bool,
+
+  primary: PropTypes.bool,
+  warning: PropTypes.bool,
+  cancel: PropTypes.bool,
+  purple: PropTypes.bool,
+
+  borderless: PropTypes.bool,
+  onlyIcon: PropTypes.bool,
 };
 
 export default Button;
diff --git a/frontend/src/metabase/components/ButtonBar.jsx b/frontend/src/metabase/components/ButtonBar.jsx
index 24381f8eac5e5cba064f43e3386aa5ebf0a3924d..dae9e4a624a2beb1843dd3e44aa4b43d066a06dc 100644
--- a/frontend/src/metabase/components/ButtonBar.jsx
+++ b/frontend/src/metabase/components/ButtonBar.jsx
@@ -1,24 +1,22 @@
 import React, { Component } from "react";
 
-
 export default class ButtonBar extends Component {
+  static defaultProps = {
+    buttons: [],
+    className: "",
+  };
 
-    static defaultProps = {
-        buttons: [],
-        className: ""
-    };
-
-    render() {
-        const { buttons, className } = this.props;
+  render() {
+    const { buttons, className } = this.props;
 
-        return (
-            <div className="flex align-center">
-                {buttons.filter((v) => v && v.length > 0).map((section, sectionIndex) => 
-                    <span key={sectionIndex} className={className}>
-                        {section}
-                    </span>
-                )}
-            </div>
-        );
-    }
+    return (
+      <div className="flex align-center">
+        {buttons.filter(v => v && v.length > 0).map((section, sectionIndex) => (
+          <span key={sectionIndex} className={className}>
+            {section}
+          </span>
+        ))}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/ButtonWithStatus.jsx b/frontend/src/metabase/components/ButtonWithStatus.jsx
index d1e4e4d9ac4694629707409995f2bfa8af22d704..6b88199867f0c525b8ca71ff3577e180afbdb552 100644
--- a/frontend/src/metabase/components/ButtonWithStatus.jsx
+++ b/frontend/src/metabase/components/ButtonWithStatus.jsx
@@ -1,15 +1,14 @@
 import React, { Component } from "react";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 let defaultTitleForState = {
-    default: t`Save`,
-    inProgress: t`Saving...`,
-    completed: t`Saved!`,
-    failed: t`Saving failed.`
+  default: t`Save`,
+  inProgress: t`Saving...`,
+  completed: t`Saved!`,
+  failed: t`Saving failed.`,
 };
 
-
 // TODO Atte Keinänen 7/14/17: This could use Button component underneath and pass parameters to it
 // (Didn't want to generalize too much for the first version of this component
 
@@ -18,45 +17,55 @@ let defaultTitleForState = {
  * When the button is clicked, `inProgress` text is shown, and when the promise resolves, `completed` text is shown.
  */
 export default class ButtonWithStatus extends Component {
-    props: {
-        onClickOperation: (any) => Promise<void>,
-        titleForState?: string[],
-        disabled?: boolean,
-        className?: string,
-    }
+  props: {
+    onClickOperation: any => Promise<void>,
+    titleForState?: string[],
+    disabled?: boolean,
+    className?: string,
+  };
 
-    state = {
-        progressState: "default"
-    }
+  state = {
+    progressState: "default",
+  };
 
-    onClick = async () => {
-        this.setState({ progressState: "inProgress" });
-        try {
-            await this.props.onClickOperation();
-            this.setState({ progressState: "completed" });
-        } catch(e) {
-            console.warn('The operation triggered by click in `ButtonWithStatus` failed')
-            this.setState({ progressState: "failed" });
-            throw e;
-        } finally {
-            setTimeout(() => this.setState({ progressState: "default" }), 3000);
-        }
+  onClick = async () => {
+    this.setState({ progressState: "inProgress" });
+    try {
+      await this.props.onClickOperation();
+      this.setState({ progressState: "completed" });
+    } catch (e) {
+      console.warn(
+        "The operation triggered by click in `ButtonWithStatus` failed",
+      );
+      this.setState({ progressState: "failed" });
+      throw e;
+    } finally {
+      setTimeout(() => this.setState({ progressState: "default" }), 3000);
     }
+  };
 
-    render() {
-        const { progressState } = this.state;
-        const titleForState =  {...defaultTitleForState, ...(this.props.titleForState || {})}
-        const title = titleForState[progressState];
-        const disabled = this.props.disabled || progressState !== "default";
+  render() {
+    const { progressState } = this.state;
+    const titleForState = {
+      ...defaultTitleForState,
+      ...(this.props.titleForState || {}),
+    };
+    const title = titleForState[progressState];
+    const disabled = this.props.disabled || progressState !== "default";
 
-        return (
-            <button
-                className={cx("Button", {"Button--primary": !disabled}, {"Button--success-new": progressState === "completed"}, this.props.className)}
-                disabled={disabled} onClick={this.onClick}
-            >
-                {title}
-            </button>
-        )
-    }
+    return (
+      <button
+        className={cx(
+          "Button",
+          { "Button--primary": !disabled },
+          { "Button--success-new": progressState === "completed" },
+          this.props.className,
+        )}
+        disabled={disabled}
+        onClick={this.onClick}
+      >
+        {title}
+      </button>
+    );
+  }
 }
-
diff --git a/frontend/src/metabase/components/Calendar.css b/frontend/src/metabase/components/Calendar.css
index 67a9774394e103f068acbc1ae719e27773eb7cff..672333441a30e84b2c80019d2c6417860bf00dde 100644
--- a/frontend/src/metabase/components/Calendar.css
+++ b/frontend/src/metabase/components/Calendar.css
@@ -1,89 +1,89 @@
 .Calendar-week {
-    display: flex;
+  display: flex;
 }
 
 .Calendar-day,
 .Calendar-day-name {
-    flex: 1;
+  flex: 1;
 }
 
 .Calendar-day {
-    color: color(var(--base-grey) shade(30%));
-    position: relative;
-    border: 1px solid color(var(--base-grey) shade(20%) alpha(-50%));
-    border-radius: 0;
-    border-bottom-width: 0;
-    border-right-width: 0;
+  color: color(var(--base-grey) shade(30%));
+  position: relative;
+  border: 1px solid color(var(--base-grey) shade(20%) alpha(-50%));
+  border-radius: 0;
+  border-bottom-width: 0;
+  border-right-width: 0;
 }
 
 .Calendar-day:last-child {
-    border-right-width: 1px;
+  border-right-width: 1px;
 }
 .Calendar-week:last-child .Calendar-day {
-    border-bottom-width: 1px;
+  border-bottom-width: 1px;
 }
 
 .Calendar-day-name {
-    cursor: inherit;
+  cursor: inherit;
 }
 
 .Calendar-day--this-month {
-    color: currentcolor;
+  color: currentcolor;
 }
 
 .Calendar-day--today {
-    font-weight: 700;
+  font-weight: 700;
 }
 
 .Calendar-day:hover {
-    color: var(--purple-color);
+  color: var(--purple-color);
 }
 
 .Calendar-day-name {
-    color: inherit !important;
+  color: inherit !important;
 }
 
 .Calendar-day--selected,
 .Calendar-day--selected-end {
-    color: white !important;
-    background-color: var(--purple-color);
-    z-index: 1;
+  color: white !important;
+  background-color: var(--purple-color);
+  z-index: 1;
 }
 
 .Calendar-day--in-range {
-    background-color: #E3DAEB;
+  background-color: #e3daeb;
 }
 
 .Calendar-day--selected:after,
 .Calendar-day--selected-end:after,
 .Calendar-day--in-range:after {
-    content: "";
-    position: absolute;
-    top: -2px;
-    bottom: -1px;
-    left: -2px;
-    right: -2px;
-    border: 2px solid color(var(--purple-color) shade(25%));
-    border-radius: 4px;
-    z-index: 2;
+  content: "";
+  position: absolute;
+  top: -2px;
+  bottom: -1px;
+  left: -2px;
+  right: -2px;
+  border: 2px solid color(var(--purple-color) shade(25%));
+  border-radius: 4px;
+  z-index: 2;
 }
 
 .Calendar-day--in-range:after {
-    border-left-color: transparent;
-    border-right-color: transparent;
-    border-radius: 0px;
+  border-left-color: transparent;
+  border-right-color: transparent;
+  border-radius: 0px;
 }
 
 .Calendar-day--week-start.Calendar-day--in-range:after {
-    border-top-left-radius: 4px;
-    border-bottom-left-radius: 4px;
-    border-left-color: color(var(--purple-color) shade(25%));
+  border-top-left-radius: 4px;
+  border-bottom-left-radius: 4px;
+  border-left-color: color(var(--purple-color) shade(25%));
 }
 
 .Calendar-day--week-end.Calendar-day--in-range:after {
-    border-top-right-radius: 4px;
-    border-bottom-right-radius: 4px;
-    border-right-color: color(var(--purple-color) shade(25%));
+  border-top-right-radius: 4px;
+  border-bottom-right-radius: 4px;
+  border-right-color: color(var(--purple-color) shade(25%));
 }
 
 .circle-button {
diff --git a/frontend/src/metabase/components/Calendar.jsx b/frontend/src/metabase/components/Calendar.jsx
index b50a3926beb7bac752e9201feeeccdd8d57821fb..5018c5168aea8719ec87fdb263a764aafd788722 100644
--- a/frontend/src/metabase/components/Calendar.jsx
+++ b/frontend/src/metabase/components/Calendar.jsx
@@ -1,198 +1,224 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import "./Calendar.css";
 
-import cx from 'classnames';
-import moment from 'moment';
-import { t } from 'c-3po';
-import Icon from 'metabase/components/Icon';
+import cx from "classnames";
+import moment from "moment";
+import { t } from "c-3po";
+import Icon from "metabase/components/Icon";
 
 export default class Calendar extends Component {
-    constructor(props) {
-        super(props);
+  constructor(props) {
+    super(props);
 
-        this.state = {
-            current: moment(props.initial || undefined)
-        };
-    }
-
-    static propTypes = {
-        selected: PropTypes.object,
-        selectedEnd: PropTypes.object,
-        onChange: PropTypes.func.isRequired,
-        isRangePicker: PropTypes.bool,
-        isDual: PropTypes.bool,
+    this.state = {
+      current: moment(props.initial || undefined),
     };
-
-    static defaultProps = {
-        isRangePicker: true
-    };
-
-    componentWillReceiveProps(nextProps) {
-        if (!moment(nextProps.selected).isSame(this.props.selected, "day") ||
-            !moment(nextProps.selectedEnd).isSame(this.props.selectedEnd, "day")
-        ) {
-            let resetCurrent = false;
-            if (nextProps.selected && nextProps.selectedEnd) {
-                resetCurrent =
-                    nextProps.selected.isAfter(this.state.current, "month") &&
-                    nextProps.selectedEnd.isBefore(this.state.current, "month");
-            } else if (nextProps.selected) {
-                resetCurrent =
-                    nextProps.selected.isAfter(this.state.current, "month") ||
-                    nextProps.selected.isBefore(this.state.current, "month");
-            }
-            if (resetCurrent) {
-                this.setState({ current: nextProps.selected });
-            }
-        }
-    }
-
-    onClickDay = (date) => {
-        let { selected, selectedEnd, isRangePicker } = this.props;
-        if (!isRangePicker || !selected || selectedEnd) {
-            this.props.onChange(date.format("YYYY-MM-DD"), null);
-        } else if (!selectedEnd) {
-            if (date.isAfter(selected)) {
-                this.props.onChange(selected.format("YYYY-MM-DD"), date.format("YYYY-MM-DD"));
-            } else {
-                this.props.onChange(date.format("YYYY-MM-DD"), selected.format("YYYY-MM-DD"));
-            }
-        }
-    }
-
-    previous = () => {
-        this.setState({ current: moment(this.state.current).add(-1, "M") });
-    }
-
-    next = () => {
-        this.setState({ current: moment(this.state.current).add(1, "M") });
-    }
-
-    renderMonthHeader(current, side) {
-        return (
-            <div className="Calendar-header flex align-center">
-                { side !=="right" &&
-                    <div className="bordered rounded p1 cursor-pointer transition-border border-hover px1" onClick={this.previous}>
-                        <Icon name="chevronleft" size={10} />
-                    </div>
-                }
-                <span className="flex-full" />
-                <h4 className="cursor-pointer rounded p1">
-                    {current.format("MMMM YYYY")}
-                </h4>
-                <span className="flex-full" />
-                { side !=="left" &&
-                    <div className="bordered border-hover rounded p1 transition-border cursor-pointer px1" onClick={this.next}>
-                        <Icon name="chevronright" size={10} />
-                    </div>
-                }
-            </div>
-        )
+  }
+
+  static propTypes = {
+    selected: PropTypes.object,
+    selectedEnd: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    isRangePicker: PropTypes.bool,
+    isDual: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    isRangePicker: true,
+  };
+
+  componentWillReceiveProps(nextProps) {
+    if (
+      !moment(nextProps.selected).isSame(this.props.selected, "day") ||
+      !moment(nextProps.selectedEnd).isSame(this.props.selectedEnd, "day")
+    ) {
+      let resetCurrent = false;
+      if (nextProps.selected && nextProps.selectedEnd) {
+        resetCurrent =
+          nextProps.selected.isAfter(this.state.current, "month") &&
+          nextProps.selectedEnd.isBefore(this.state.current, "month");
+      } else if (nextProps.selected) {
+        resetCurrent =
+          nextProps.selected.isAfter(this.state.current, "month") ||
+          nextProps.selected.isBefore(this.state.current, "month");
+      }
+      if (resetCurrent) {
+        this.setState({ current: nextProps.selected });
+      }
     }
-
-    renderDayNames() {
-        const names = [t`Su`, t`Mo`, t`Tu`, t`We`, t`Th`, t`Fr`, t`Sa`];
-        return (
-            <div className="Calendar-day-names Calendar-week py1">
-                {names.map((name) => <span key={name} className="Calendar-day-name text-centered">{name}</span>)}
-            </div>
+  }
+
+  onClickDay = date => {
+    let { selected, selectedEnd, isRangePicker } = this.props;
+    if (!isRangePicker || !selected || selectedEnd) {
+      this.props.onChange(date.format("YYYY-MM-DD"), null);
+    } else if (!selectedEnd) {
+      if (date.isAfter(selected)) {
+        this.props.onChange(
+          selected.format("YYYY-MM-DD"),
+          date.format("YYYY-MM-DD"),
         );
-    }
-
-    renderWeeks(current) {
-        var weeks = [],
-            done = false,
-            date = moment(current).startOf("month").day("Sunday"),
-            monthIndex = date.month(),
-            count = 0;
-
-        while (!done) {
-            weeks.push(
-                <Week
-                    key={date.toString()}
-                    date={moment(date)}
-                    month={current}
-                    onClickDay={this.onClickDay}
-                    selected={this.props.selected}
-                    selectedEnd={this.props.selectedEnd}
-                />
-            );
-            date.add(1, "w");
-            done = count++ > 2 && monthIndex !== date.month();
-            monthIndex = date.month();
-        }
-
-        return (
-            <div className="Calendar-weeks relative">
-                {weeks}
-            </div>
+      } else {
+        this.props.onChange(
+          date.format("YYYY-MM-DD"),
+          selected.format("YYYY-MM-DD"),
         );
+      }
     }
-
-    renderCalender(current, side) {
-        return (
-            <div className={
-                cx("Calendar Grid-cell", { "Calendar--range": this.props.selected && this.props.selectedEnd })}>
-                {this.renderMonthHeader(current, side)}
-                {this.renderDayNames(current)}
-                {this.renderWeeks(current)}
-            </div>
-        );
+  };
+
+  previous = () => {
+    this.setState({ current: moment(this.state.current).add(-1, "M") });
+  };
+
+  next = () => {
+    this.setState({ current: moment(this.state.current).add(1, "M") });
+  };
+
+  renderMonthHeader(current, side) {
+    return (
+      <div className="Calendar-header flex align-center">
+        {side !== "right" && (
+          <div
+            className="bordered rounded p1 cursor-pointer transition-border border-hover px1"
+            onClick={this.previous}
+          >
+            <Icon name="chevronleft" size={10} />
+          </div>
+        )}
+        <span className="flex-full" />
+        <h4 className="cursor-pointer rounded p1">
+          {current.format("MMMM YYYY")}
+        </h4>
+        <span className="flex-full" />
+        {side !== "left" && (
+          <div
+            className="bordered border-hover rounded p1 transition-border cursor-pointer px1"
+            onClick={this.next}
+          >
+            <Icon name="chevronright" size={10} />
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  renderDayNames() {
+    const names = [t`Su`, t`Mo`, t`Tu`, t`We`, t`Th`, t`Fr`, t`Sa`];
+    return (
+      <div className="Calendar-day-names Calendar-week py1">
+        {names.map(name => (
+          <span key={name} className="Calendar-day-name text-centered">
+            {name}
+          </span>
+        ))}
+      </div>
+    );
+  }
+
+  renderWeeks(current) {
+    var weeks = [],
+      done = false,
+      date = moment(current)
+        .startOf("month")
+        .day("Sunday"),
+      monthIndex = date.month(),
+      count = 0;
+
+    while (!done) {
+      weeks.push(
+        <Week
+          key={date.toString()}
+          date={moment(date)}
+          month={current}
+          onClickDay={this.onClickDay}
+          selected={this.props.selected}
+          selectedEnd={this.props.selectedEnd}
+        />,
+      );
+      date.add(1, "w");
+      done = count++ > 2 && monthIndex !== date.month();
+      monthIndex = date.month();
     }
 
-    render() {
-        const { current } = this.state;
-        if (this.props.isDual) {
-            return (
-                <div className="Grid Grid--1of2 Grid--gutters">
-                    {this.renderCalender(current, "left")}
-                    {this.renderCalender(moment(current).add(1, "month"), "right")}
-                </div>
-            )
-        } else {
-            return this.renderCalender(current);
-        }
+    return <div className="Calendar-weeks relative">{weeks}</div>;
+  }
+
+  renderCalender(current, side) {
+    return (
+      <div
+        className={cx("Calendar Grid-cell", {
+          "Calendar--range": this.props.selected && this.props.selectedEnd,
+        })}
+      >
+        {this.renderMonthHeader(current, side)}
+        {this.renderDayNames(current)}
+        {this.renderWeeks(current)}
+      </div>
+    );
+  }
+
+  render() {
+    const { current } = this.state;
+    if (this.props.isDual) {
+      return (
+        <div className="Grid Grid--1of2 Grid--gutters">
+          {this.renderCalender(current, "left")}
+          {this.renderCalender(moment(current).add(1, "month"), "right")}
+        </div>
+      );
+    } else {
+      return this.renderCalender(current);
     }
+  }
 }
 
 class Week extends Component {
-    static propTypes = {
-        selected: PropTypes.object,
-        selectedEnd: PropTypes.object,
-        onClickDay: PropTypes.func.isRequired
+  static propTypes = {
+    selected: PropTypes.object,
+    selectedEnd: PropTypes.object,
+    onClickDay: PropTypes.func.isRequired,
+  };
+
+  render() {
+    let days = [];
+    let { date, month, selected, selectedEnd } = this.props;
+
+    for (let i = 0; i < 7; i++) {
+      let classes = cx("Calendar-day p1 cursor-pointer text-centered", {
+        "Calendar-day--today": date.isSame(new Date(), "day"),
+        "Calendar-day--this-month": date.month() === month.month(),
+        "Calendar-day--selected": selected && date.isSame(selected, "day"),
+        "Calendar-day--selected-end":
+          selectedEnd && date.isSame(selectedEnd, "day"),
+        "Calendar-day--week-start": i === 0,
+        "Calendar-day--week-end": i === 6,
+        "Calendar-day--in-range":
+          !(date.isSame(selected, "day") || date.isSame(selectedEnd, "day")) &&
+          (date.isSame(selected, "day") ||
+            date.isSame(selectedEnd, "day") ||
+            (selectedEnd &&
+              selectedEnd.isAfter(date, "day") &&
+              date.isAfter(selected, "day"))),
+      });
+      days.push(
+        <span
+          key={date.toString()}
+          className={classes}
+          onClick={this.props.onClickDay.bind(null, date)}
+        >
+          {date.date()}
+        </span>,
+      );
+      date = moment(date).add(1, "d");
     }
 
-    render() {
-        let days = [];
-        let { date, month, selected, selectedEnd } = this.props;
-
-        for (let i = 0; i < 7; i++) {
-            let classes = cx("Calendar-day p1 cursor-pointer text-centered", {
-                "Calendar-day--today": date.isSame(new Date(), "day"),
-                "Calendar-day--this-month": date.month() === month.month(),
-                "Calendar-day--selected": selected && date.isSame(selected, "day"),
-                "Calendar-day--selected-end": selectedEnd && date.isSame(selectedEnd, "day"),
-                "Calendar-day--week-start": i === 0,
-                "Calendar-day--week-end": i === 6,
-                "Calendar-day--in-range": !(date.isSame(selected, "day") || date.isSame(selectedEnd, "day")) && (
-                    date.isSame(selected, "day") || date.isSame(selectedEnd, "day") ||
-                    (selectedEnd && selectedEnd.isAfter(date, "day") && date.isAfter(selected, "day"))
-                )
-            });
-            days.push(
-                <span key={date.toString()} className={classes} onClick={this.props.onClickDay.bind(null, date)}>
-                    {date.date()}
-                </span>
-            );
-            date = moment(date).add(1, "d");
-        }
-
-        return (
-            <div className="Calendar-week" key={days[0].toString()}>
-                {days}
-            </div>
-        );
-    }
+    return (
+      <div className="Calendar-week" key={days[0].toString()}>
+        {days}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Card.jsx b/frontend/src/metabase/components/Card.jsx
index 61f39a377a90cee4f8bbbae809937cc1d08e9006..247c10dae3cc72f49dfdbe74512cfaf07d11b096 100644
--- a/frontend/src/metabase/components/Card.jsx
+++ b/frontend/src/metabase/components/Card.jsx
@@ -1,8 +1,7 @@
-import React from 'react'
+import React from "react";
 
-const Card = ({ children }) =>
-    <div className="bordered rounded shadowed bg-white">
-        { children }
-    </div>
+const Card = ({ children }) => (
+  <div className="bordered rounded shadowed bg-white">{children}</div>
+);
 
-export default Card
+export default Card;
diff --git a/frontend/src/metabase/components/ChannelSetupMessage.jsx b/frontend/src/metabase/components/ChannelSetupMessage.jsx
index 78bd045a7637856c8f9e177a84817df5fc84061d..154984aeab12c1e37822a733887921023ffd148b 100644
--- a/frontend/src/metabase/components/ChannelSetupMessage.jsx
+++ b/frontend/src/metabase/components/ChannelSetupMessage.jsx
@@ -2,41 +2,49 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import Settings from "metabase/lib/settings";
 
 export default class ChannelSetupMessage extends Component {
-    static propTypes = {
-        user: PropTypes.object.isRequired,
-        channels: PropTypes.array.isRequired
-    };
+  static propTypes = {
+    user: PropTypes.object.isRequired,
+    channels: PropTypes.array.isRequired,
+  };
 
-    static defaultProps = {
-        channels: ["email", "Slack"]
-    }
-
-    render() {
-        let { user, channels } = this.props;
-        let content;
-        if (user.is_superuser) {
-            content = (
-                <div>
-                    {channels.map(c =>
-                        <Link to={"/admin/settings/"+c.toLowerCase()} key={c.toLowerCase()} className="Button Button--primary mr1" target={window.OSX ? null : "_blank"}>{t`Configure`} {c}</Link>
-                    )}
-                </div>
-            );
+  static defaultProps = {
+    channels: ["email", "Slack"],
+  };
 
-        } else {
-            let adminEmail = Settings.get("admin_email");
-            content = (
-                <div className="mb1">
-                    <h4 className="text-grey-4">{t`Your admin's email address`}:</h4>
-                    <a className="h2 link no-decoration" href={"mailto:"+adminEmail}>{adminEmail}</a>
-                </div>
-            );
-        }
-        return content;
+  render() {
+    let { user, channels } = this.props;
+    let content;
+    if (user.is_superuser) {
+      content = (
+        <div>
+          {channels.map(c => (
+            <Link
+              to={"/admin/settings/" + c.toLowerCase()}
+              key={c.toLowerCase()}
+              className="Button Button--primary mr1"
+              target={window.OSX ? null : "_blank"}
+            >
+              {t`Configure`} {c}
+            </Link>
+          ))}
+        </div>
+      );
+    } else {
+      let adminEmail = Settings.get("admin_email");
+      content = (
+        <div className="mb1">
+          <h4 className="text-grey-4">{t`Your admin's email address`}:</h4>
+          <a className="h2 link no-decoration" href={"mailto:" + adminEmail}>
+            {adminEmail}
+          </a>
+        </div>
+      );
     }
+    return content;
+  }
 }
diff --git a/frontend/src/metabase/components/ChannelSetupModal.jsx b/frontend/src/metabase/components/ChannelSetupModal.jsx
index 76fe11a30db41fa0bbf23790acc7b7034b6a2692..667e97c2f9e308899af9de9c3049645b17d338e5 100644
--- a/frontend/src/metabase/components/ChannelSetupModal.jsx
+++ b/frontend/src/metabase/components/ChannelSetupModal.jsx
@@ -2,37 +2,55 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ModalContent from "metabase/components/ModalContent.jsx";
 import ChannelSetupMessage from "metabase/components/ChannelSetupMessage";
 
 export default class ChannelSetupModal extends Component {
-    static propTypes = {
-        onClose: PropTypes.func.isRequired,
-        user: PropTypes.object.isRequired,
-        entityNamePlural: PropTypes.string.isRequired,
-        channels: PropTypes.array,
-        fullPageModal: PropTypes.bool,
-    };
+  static propTypes = {
+    onClose: PropTypes.func.isRequired,
+    user: PropTypes.object.isRequired,
+    entityNamePlural: PropTypes.string.isRequired,
+    channels: PropTypes.array,
+    fullPageModal: PropTypes.bool,
+  };
 
-    static defaultProps = {
-        channels: ["email", "Slack"]
-    }
+  static defaultProps = {
+    channels: ["email", "Slack"],
+  };
 
-    render() {
-        const { onClose, user, entityNamePlural, fullPageModal, channels } = this.props
+  render() {
+    const {
+      onClose,
+      user,
+      entityNamePlural,
+      fullPageModal,
+      channels,
+    } = this.props;
 
-
-        return (
-            <ModalContent
-                onClose={onClose}
-                fullPageModal={fullPageModal}
-                title={user.is_superuser ? t`To send ${entityNamePlural}, you'll need to set up ${channels.join(t` or `)} integration.` : t`To send ${entityNamePlural}, an admin needs to set up ${channels.join(t` or `)} integration.`}
-            >
-                <div className={cx("ml-auto mb4", { "mr4": !fullPageModal, "mr-auto text-centered": fullPageModal })}>
-                    <ChannelSetupMessage user={this.props.user} />
-                </div>
-            </ModalContent>
-        );
-    }
+    return (
+      <ModalContent
+        onClose={onClose}
+        fullPageModal={fullPageModal}
+        title={
+          user.is_superuser
+            ? t`To send ${entityNamePlural}, you'll need to set up ${channels.join(
+                t` or `,
+              )} integration.`
+            : t`To send ${entityNamePlural}, an admin needs to set up ${channels.join(
+                t` or `,
+              )} integration.`
+        }
+      >
+        <div
+          className={cx("ml-auto mb4", {
+            mr4: !fullPageModal,
+            "mr-auto text-centered": fullPageModal,
+          })}
+        >
+          <ChannelSetupMessage user={this.props.user} />
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/CheckBox.info.js b/frontend/src/metabase/components/CheckBox.info.js
index aaec2b7213dac14c16024de4ca6a95c77ad8c37a..f730b1649e535268f03b7e4ddea97818059404f8 100644
--- a/frontend/src/metabase/components/CheckBox.info.js
+++ b/frontend/src/metabase/components/CheckBox.info.js
@@ -8,10 +8,10 @@ A standard checkbox.
 `;
 
 export const examples = {
-    "Default - Off": <CheckBox />,
-    "On - Default blue": <CheckBox checked />,
-    "Purple": <CheckBox checked color='purple' />,
-    "Yellow": <CheckBox checked color='yellow' />,
-    "Red": <CheckBox checked color='red' />,
-    "Green": <CheckBox checked color='green' />,
+  "Default - Off": <CheckBox />,
+  "On - Default blue": <CheckBox checked />,
+  Purple: <CheckBox checked color="purple" />,
+  Yellow: <CheckBox checked color="yellow" />,
+  Red: <CheckBox checked color="red" />,
+  Green: <CheckBox checked color="green" />,
 };
diff --git a/frontend/src/metabase/components/CheckBox.jsx b/frontend/src/metabase/components/CheckBox.jsx
index eca9d88515e28e3faef2276f13433dc244caabfb..7ee27d943f94034e7d9b146c61080b3be6efbf92 100644
--- a/frontend/src/metabase/components/CheckBox.jsx
+++ b/frontend/src/metabase/components/CheckBox.jsx
@@ -5,55 +5,53 @@ import Icon from "metabase/components/Icon";
 import { normal as defaultColors } from "metabase/lib/colors";
 
 export default class CheckBox extends Component {
-    static propTypes = {
-        checked: PropTypes.bool,
-        onChange: PropTypes.func,
-        color: PropTypes.oneOf(Object.keys(defaultColors)),
-        size: PropTypes.number,  // TODO - this should probably be a concrete set of options
-        padding: PropTypes.number// TODO - the component should pad itself properly based on the size
-    };
+  static propTypes = {
+    checked: PropTypes.bool,
+    onChange: PropTypes.func,
+    color: PropTypes.oneOf(Object.keys(defaultColors)),
+    size: PropTypes.number, // TODO - this should probably be a concrete set of options
+    padding: PropTypes.number, // TODO - the component should pad itself properly based on the size
+  };
 
-    static defaultProps = {
-        size: 16,
-        padding: 2,
-        color: 'blue'
-    };
+  static defaultProps = {
+    size: 16,
+    padding: 2,
+    color: "blue",
+  };
 
-    onClick() {
-        if (this.props.onChange) {
-            // TODO: use a proper event object?
-            this.props.onChange({ target: { checked: !this.props.checked }})
-        }
+  onClick() {
+    if (this.props.onChange) {
+      // TODO: use a proper event object?
+      this.props.onChange({ target: { checked: !this.props.checked } });
     }
+  }
 
-    render() {
-        const {
-            checked,
-            color,
-            padding,
-            size,
-        } = this.props;
+  render() {
+    const { checked, color, padding, size } = this.props;
 
-        const themeColor = defaultColors[color];
+    const themeColor = defaultColors[color];
 
-        const checkboxStyle = {
-            width:              size,
-            height:             size,
-            backgroundColor:    checked ? themeColor : "white",
-            border:             `2px solid ${ checked ? themeColor : '#ddd' }`,
-        };
-        return (
-            <div className="cursor-pointer" onClick={() => this.onClick()}>
-                <div style={checkboxStyle} className="flex align-center justify-center rounded">
-                    { checked && (
-                        <Icon
-                            style={{ color: checked ? 'white' : themeColor }}
-                            name="check"
-                            size={size - padding * 2}
-                        />
-                    )}
-                </div>
-            </div>
-        )
-    }
+    const checkboxStyle = {
+      width: size,
+      height: size,
+      backgroundColor: checked ? themeColor : "white",
+      border: `2px solid ${checked ? themeColor : "#ddd"}`,
+    };
+    return (
+      <div className="cursor-pointer" onClick={() => this.onClick()}>
+        <div
+          style={checkboxStyle}
+          className="flex align-center justify-center rounded"
+        >
+          {checked && (
+            <Icon
+              style={{ color: checked ? "white" : themeColor }}
+              name="check"
+              size={size - padding * 2}
+            />
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Code.jsx b/frontend/src/metabase/components/Code.jsx
index b262c0683aa68006f2d21e5ae860c850000db4da..59d9899e01fce82d586e6285976c96820ea9338b 100644
--- a/frontend/src/metabase/components/Code.jsx
+++ b/frontend/src/metabase/components/Code.jsx
@@ -3,29 +3,27 @@ import React from "react";
 import PropTypes from "prop-types";
 
 const Code = ({ children, block }) => {
-    if (block) {
-        return (
-            <div className="text-code">{children}</div>
-        );
-    } else if (typeof children === "string" && children.split(/\n/g).length > 1) {
-        return (
-            <span>
-                {children.split(/\n/g).map((line, index) => [
-                    <span className="text-code" style={{ lineHeight: "1.8em" }}>{line}</span>,
-                    <br />
-                ])}
-            </span>
-        );
-    } else {
-        return (
-            <span className="text-code">{children}</span>
-        );
-    }
-}
+  if (block) {
+    return <div className="text-code">{children}</div>;
+  } else if (typeof children === "string" && children.split(/\n/g).length > 1) {
+    return (
+      <span>
+        {children.split(/\n/g).map((line, index) => [
+          <span className="text-code" style={{ lineHeight: "1.8em" }}>
+            {line}
+          </span>,
+          <br />,
+        ])}
+      </span>
+    );
+  } else {
+    return <span className="text-code">{children}</span>;
+  }
+};
 
 Code.propTypes = {
-    children: PropTypes.any.isRequired,
-    block: PropTypes.bool
-}
+  children: PropTypes.any.isRequired,
+  block: PropTypes.bool,
+};
 
 export default Code;
diff --git a/frontend/src/metabase/components/ColorPicker.jsx b/frontend/src/metabase/components/ColorPicker.jsx
index 272ed65b5a832e4d058e540e9b4e0292e8f4fc41..28676d41ebccc3764ec86ca717e1440f055d6f18 100644
--- a/frontend/src/metabase/components/ColorPicker.jsx
+++ b/frontend/src/metabase/components/ColorPicker.jsx
@@ -7,71 +7,74 @@ import { normal } from "metabase/lib/colors";
 
 const DEFAULT_COLOR_SQUARE_SIZE = 32;
 
-const ColorSquare = ({ color, size }) =>
-    <div style={{
-        width: size,
-        height: size,
-        backgroundColor: color,
-        borderRadius: size / 8
-    }}></div>
+const ColorSquare = ({ color, size }) => (
+  <div
+    style={{
+      width: size,
+      height: size,
+      backgroundColor: color,
+      borderRadius: size / 8,
+    }}
+  />
+);
 
 class ColorPicker extends Component {
-    static defaultProps = {
-        colors: [...Object.values(normal)],
-        size: DEFAULT_COLOR_SQUARE_SIZE,
-        triggerSize: DEFAULT_COLOR_SQUARE_SIZE,
-        padding: 4
-    }
+  static defaultProps = {
+    colors: [...Object.values(normal)],
+    size: DEFAULT_COLOR_SQUARE_SIZE,
+    triggerSize: DEFAULT_COLOR_SQUARE_SIZE,
+    padding: 4,
+  };
 
-    static propTypes = {
-        colors: PropTypes.array,
-        onChange: PropTypes.func.isRequired,
-        size: PropTypes.number,
-        triggerSize: PropTypes.number,
-        value: PropTypes.string
-    }
+  static propTypes = {
+    colors: PropTypes.array,
+    onChange: PropTypes.func.isRequired,
+    size: PropTypes.number,
+    triggerSize: PropTypes.number,
+    value: PropTypes.string,
+  };
 
-    render () {
-        const { colors, onChange, padding, size, triggerSize, value } = this.props;
-        return (
-            <div className="inline-block">
-                <PopoverWithTrigger
-                    ref="colorPopover"
-                    triggerElement={
-                        <div
-                            className="bordered rounded flex align-center"
-                            style={{ padding: triggerSize / 4 }}
-                        >
-                            <ColorSquare color={value} size={triggerSize} />
-                        </div>
-                    }
-                >
-                    <div className="p1">
-                        <ol
-                            className="flex flex-wrap"
-                            style={{
-                                maxWidth: 120
-                            }}
-                        >
-                            { colors.map((color, index) =>
-                                <li
-                                    className="cursor-pointer"
-                                    style={{ padding }}
-                                    key={index}
-                                    onClick={() => {
-                                        onChange(color);
-                                        this.refs.colorPopover.close();
-                                    }}
-                                >
-                                    <ColorSquare color={color} size={size} />
-                                </li>
-                            )}
-                        </ol>
-                    </div>
-                </PopoverWithTrigger>
+  render() {
+    const { colors, onChange, padding, size, triggerSize, value } = this.props;
+    return (
+      <div className="inline-block">
+        <PopoverWithTrigger
+          ref="colorPopover"
+          triggerElement={
+            <div
+              className="bordered rounded flex align-center"
+              style={{ padding: triggerSize / 4 }}
+            >
+              <ColorSquare color={value} size={triggerSize} />
             </div>
-        );
-    }
+          }
+        >
+          <div className="p1">
+            <ol
+              className="flex flex-wrap"
+              style={{
+                maxWidth: 120,
+              }}
+            >
+              {colors.map((color, index) => (
+                <li
+                  className="cursor-pointer"
+                  style={{ padding }}
+                  key={index}
+                  onClick={() => {
+                    onChange(color);
+                    this.refs.colorPopover.close();
+                  }}
+                >
+                  <ColorSquare color={color} size={size} />
+                </li>
+              ))}
+            </ol>
+          </div>
+        </PopoverWithTrigger>
+      </div>
+    );
+  }
 }
 
 export default ColorPicker;
diff --git a/frontend/src/metabase/components/ColumnarSelector.css b/frontend/src/metabase/components/ColumnarSelector.css
index 0aa8f4763f28417c5dbe42f2a504ba125c7b1b71..26033e2b7376a9e6635456b524d84f610d0fffdf 100644
--- a/frontend/src/metabase/components/ColumnarSelector.css
+++ b/frontend/src/metabase/components/ColumnarSelector.css
@@ -1,94 +1,94 @@
-
 .ColumnarSelector {
-    display: flex;
-    background-color: #FCFCFC;
-    font-weight: 700;
+  display: flex;
+  background-color: #fcfcfc;
+  font-weight: 700;
 }
 
 .ColumnarSelector-column {
-    min-width: 180px;
-    max-height: 340px;
-    flex: 1;
+  min-width: 180px;
+  max-height: 340px;
+  flex: 1;
 }
 
 .ColumnarSelector-rows {
-    padding-top: 0.5rem;
-    padding-bottom: 0.5rem;
+  padding-top: 0.5rem;
+  padding-bottom: 0.5rem;
 }
 
 .ColumnarSelector-title {
-    color: color(var(--base-grey) shade(30%));
-    text-transform: uppercase;
-    font-size: 10px;
-    font-weight: 700;
-    padding: var(--padding-1);
-    padding-left: var(--padding-3);
+  color: color(var(--base-grey) shade(30%));
+  text-transform: uppercase;
+  font-size: 10px;
+  font-weight: 700;
+  padding: var(--padding-1);
+  padding-left: var(--padding-3);
 }
 
 .ColumnarSelector-section:first-child .ColumnarSelector-title {
-    padding-top: var(--padding-3);
+  padding-top: var(--padding-3);
 }
 
 .ColumnarSelector-description {
-    margin-top: 0.5em;
-    color: color(var(--base-grey) shade(30%));
-    max-width: 270px;
+  margin-top: 0.5em;
+  color: color(var(--base-grey) shade(30%));
+  max-width: 270px;
 }
 
 .ColumnarSelector-row {
-    padding-top: 0.75rem;
-    padding-bottom: 0.75rem;
-    padding-left: var(--padding-2);
-    padding-right: var(--padding-2);
-    display: flex;
-    align-items: center;
+  padding-top: 0.75rem;
+  padding-bottom: 0.75rem;
+  padding-left: var(--padding-2);
+  padding-right: var(--padding-2);
+  display: flex;
+  align-items: center;
 }
 
 .ColumnarSelector-row:not(.ColumnarSelector-row--disabled):hover,
 .ColumnarSelector-row:not(.ColumnarSelector-row--disabled):hover .Icon {
-    background-color: var(--brand-color) !important;
-    color: white !important;
+  background-color: var(--brand-color) !important;
+  color: white !important;
 }
 
-.ColumnarSelector-row:not(.ColumnarSelector-row--disabled):hover .ColumnarSelector-description {
-    color: rgba(255,255,255,0.50);
+.ColumnarSelector-row:not(.ColumnarSelector-row--disabled):hover
+  .ColumnarSelector-description {
+  color: rgba(255, 255, 255, 0.5);
 }
 
 .ColumnarSelector-row--selected {
-    color: inherit !important;
-    background: white;
-    border-top: var(--border-size) var(--border-style) var(--border-color);
-    border-bottom: var(--border-size) var(--border-style) var(--border-color);
+  color: inherit !important;
+  background: white;
+  border-top: var(--border-size) var(--border-style) var(--border-color);
+  border-bottom: var(--border-size) var(--border-style) var(--border-color);
 }
 
 .ColumnarSelector-row--disabled {
-    color: var(--grey-3);
+  color: var(--grey-3);
 }
 
 .ColumnarSelector-row .Icon-check {
-    visibility: hidden;
-    padding-right: 0.5rem;
+  visibility: hidden;
+  padding-right: 0.5rem;
 }
 
 .ColumnarSelector-row.ColumnarSelector-row--selected .Icon-check {
-    visibility: visible;
+  visibility: visible;
 }
 
 .ColumnarSelector-column:first-child {
-    z-index: 1;
+  z-index: 1;
 }
 
 /* only apply if there's more than one, aka the last is not the first */
 .ColumnarSelector-column:last-child:not(:first-child) {
-    background-color: white;
-    border-left:  var(--border-size) var(--border-style) var(--border-color);
-    position: relative;
-    left: -1px;
+  background-color: white;
+  border-left: var(--border-size) var(--border-style) var(--border-color);
+  position: relative;
+  left: -1px;
 }
 
 .ColumnarSelector-column:last-child .ColumnarSelector-row--selected {
-    background: inherit;
-    border-top: none;
-    border-bottom: none;
-    color: var(--brand-color);
+  background: inherit;
+  border-top: none;
+  border-bottom: none;
+  color: var(--brand-color);
 }
diff --git a/frontend/src/metabase/components/ColumnarSelector.jsx b/frontend/src/metabase/components/ColumnarSelector.jsx
index e456a839cc886f7154ce7456763c69b23fc22361..cd014ff3ba58960c0bc66cb9f774342824da3c6d 100644
--- a/frontend/src/metabase/components/ColumnarSelector.jsx
+++ b/frontend/src/metabase/components/ColumnarSelector.jsx
@@ -8,74 +8,90 @@ import Icon from "metabase/components/Icon.jsx";
 import cx from "classnames";
 
 export default class ColumnarSelector extends Component {
-    static propTypes = {
-        columns: PropTypes.array.isRequired
-    };
+  static propTypes = {
+    columns: PropTypes.array.isRequired,
+  };
 
-    render() {
-        const isItemSelected = (item, column) => column.selectedItems ?
-            column.selectedItems.includes(item) :
-            column.selectedItem === item;
-        const isItemDisabled = (item, column) => column.disabledOptionIds ?
-            column.disabledOptionIds.includes(item.id) :
-            false;
+  render() {
+    const isItemSelected = (item, column) =>
+      column.selectedItems
+        ? column.selectedItems.includes(item)
+        : column.selectedItem === item;
+    const isItemDisabled = (item, column) =>
+      column.disabledOptionIds
+        ? column.disabledOptionIds.includes(item.id)
+        : false;
 
-        var columns = this.props.columns.map((column, columnIndex) => {
-            var sectionElements;
-            if (column) {
-                var lastColumn = columnIndex === this.props.columns.length - 1;
-                var sections = column.sections || [column];
-                sectionElements = sections.map((section, sectionIndex) => {
-                    var title = section.title;
-                    var items = section.items.map((item, rowIndex) => {
-                        var itemClasses = cx({
-                            'ColumnarSelector-row': true,
-                            'ColumnarSelector-row--selected': isItemSelected(item, column),
-                            'ColumnarSelector-row--disabled': isItemDisabled(item, column),
-                            'flex': true,
-                            'no-decoration': true,
-                            'cursor-default': isItemDisabled(item, column)
-                        });
-                        var checkIcon = lastColumn ? <Icon name="check" size={14} /> : null;
-                        var descriptionElement;
-                        var description = column.itemDescriptionFn && column.itemDescriptionFn(item);
-                        if (description) {
-                            descriptionElement = <div className="ColumnarSelector-description">{description}</div>
-                        }
-                        return (
-                            <li key={rowIndex}>
-                                <a className={itemClasses} onClick={!isItemDisabled(item, column) && column.itemSelectFn.bind(null, item)}>
-                                    {checkIcon}
-                                    <div className="flex flex-column ml1">
-                                        {column.itemTitleFn(item)}
-                                        {descriptionElement}
-                                    </div>
-                                </a>
-                            </li>
-                        );
-                    });
-                    var titleElement;
-                    if (title) {
-                        titleElement = <div className="ColumnarSelector-title">{title}</div>
-                    }
-                    return (
-                        <section key={sectionIndex} className="ColumnarSelector-section">
-                            {titleElement}
-                            <ul className="ColumnarSelector-rows">{items}</ul>
-                        </section>
-                    );
-                });
+    var columns = this.props.columns.map((column, columnIndex) => {
+      var sectionElements;
+      if (column) {
+        var lastColumn = columnIndex === this.props.columns.length - 1;
+        var sections = column.sections || [column];
+        sectionElements = sections.map((section, sectionIndex) => {
+          var title = section.title;
+          var items = section.items.map((item, rowIndex) => {
+            var itemClasses = cx({
+              "ColumnarSelector-row": true,
+              "ColumnarSelector-row--selected": isItemSelected(item, column),
+              "ColumnarSelector-row--disabled": isItemDisabled(item, column),
+              flex: true,
+              "no-decoration": true,
+              "cursor-default": isItemDisabled(item, column),
+            });
+            var checkIcon = lastColumn ? <Icon name="check" size={14} /> : null;
+            var descriptionElement;
+            var description =
+              column.itemDescriptionFn && column.itemDescriptionFn(item);
+            if (description) {
+              descriptionElement = (
+                <div className="ColumnarSelector-description">
+                  {description}
+                </div>
+              );
             }
-
             return (
-                <div key={columnIndex} className="ColumnarSelector-column scroll-y scroll-show">
-                    {sectionElements}
-                </div>
+              <li key={rowIndex}>
+                <a
+                  className={itemClasses}
+                  onClick={
+                    !isItemDisabled(item, column) &&
+                    column.itemSelectFn.bind(null, item)
+                  }
+                >
+                  {checkIcon}
+                  <div className="flex flex-column ml1">
+                    {column.itemTitleFn(item)}
+                    {descriptionElement}
+                  </div>
+                </a>
+              </li>
             );
+          });
+          var titleElement;
+          if (title) {
+            titleElement = (
+              <div className="ColumnarSelector-title">{title}</div>
+            );
+          }
+          return (
+            <section key={sectionIndex} className="ColumnarSelector-section">
+              {titleElement}
+              <ul className="ColumnarSelector-rows">{items}</ul>
+            </section>
+          );
         });
+      }
+
+      return (
+        <div
+          key={columnIndex}
+          className="ColumnarSelector-column scroll-y scroll-show"
+        >
+          {sectionElements}
+        </div>
+      );
+    });
 
-        return (
-            <div className="ColumnarSelector">{columns}</div>
-        );
-    }
+    return <div className="ColumnarSelector">{columns}</div>;
+  }
 }
diff --git a/frontend/src/metabase/components/Confirm.jsx b/frontend/src/metabase/components/Confirm.jsx
index ca10c75a0508f6dcbb9561392518f92f113737e5..843c4683ea16ea88ea8ab8162280bce36f5ddafe 100644
--- a/frontend/src/metabase/components/Confirm.jsx
+++ b/frontend/src/metabase/components/Confirm.jsx
@@ -6,27 +6,31 @@ import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ConfirmContent from "./ConfirmContent.jsx";
 
 export default class Confirm extends Component {
-    static propTypes = {
-        action: PropTypes.func.isRequired,
-        title: PropTypes.string.isRequired,
-        children: PropTypes.any,
-        content: PropTypes.any,
-        triggerClasses: PropTypes.string,
-    };
+  static propTypes = {
+    action: PropTypes.func.isRequired,
+    title: PropTypes.string.isRequired,
+    children: PropTypes.any,
+    content: PropTypes.any,
+    triggerClasses: PropTypes.string,
+  };
 
-    render() {
-        const { action, children, title, content, triggerClasses } = this.props;
-        return (
-            <ModalWithTrigger ref="modal" triggerElement={children} triggerClasses={triggerClasses}>
-                <ConfirmContent
-                    title={title}
-                    content={content}
-                    onClose={() => {
-                        this.refs.modal.close();
-                    }}
-                    onAction={action}
-                />
-            </ModalWithTrigger>
-        );
-    }
+  render() {
+    const { action, children, title, content, triggerClasses } = this.props;
+    return (
+      <ModalWithTrigger
+        ref="modal"
+        triggerElement={children}
+        triggerClasses={triggerClasses}
+      >
+        <ConfirmContent
+          title={title}
+          content={content}
+          onClose={() => {
+            this.refs.modal.close();
+          }}
+          onAction={action}
+        />
+      </ModalWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/ConfirmContent.jsx b/frontend/src/metabase/components/ConfirmContent.jsx
index a8755354ff91edd90b7e4f9e7d26a1c03507bd9b..f4cbed101a770e8b85cbe056c9f2bad163355bd6 100644
--- a/frontend/src/metabase/components/ConfirmContent.jsx
+++ b/frontend/src/metabase/components/ConfirmContent.jsx
@@ -1,33 +1,53 @@
 import React from "react";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 const nop = () => {};
 
 const ConfirmContent = ({
-    title,
-    content,
-    message = t`Are you sure you want to do this?`,
-    onClose = nop,
-    onAction = nop,
-    onCancel = nop,
-    confirmButtonText = t`Yes`,
-    cancelButtonText = t`Cancel`
-}) =>
-    <ModalContent
-        title={title}
-        onClose={() => { onCancel(); onClose(); }}
-    >
-        <div className="mx4">{content}</div>
+  title,
+  content,
+  message = t`Are you sure you want to do this?`,
+  onClose = nop,
+  onAction = nop,
+  onCancel = nop,
+  confirmButtonText = t`Yes`,
+  cancelButtonText = t`Cancel`,
+}) => (
+  <ModalContent
+    title={title}
+    onClose={() => {
+      onCancel();
+      onClose();
+    }}
+  >
+    <div className="mx4">{content}</div>
 
-        <div className="Form-inputs mb4">
-            <p>{message}</p>
-        </div>
+    <div className="Form-inputs mb4">
+      <p>{message}</p>
+    </div>
 
-        <div className="Form-actions ml-auto">
-            <button className="Button" onClick={() => { onCancel(); onClose(); }}>{cancelButtonText}</button>
-            <button className="Button Button--danger ml2" onClick={() => { onAction(); onClose(); }}>{confirmButtonText}</button>
-        </div>
-    </ModalContent>
+    <div className="Form-actions ml-auto">
+      <button
+        className="Button"
+        onClick={() => {
+          onCancel();
+          onClose();
+        }}
+      >
+        {cancelButtonText}
+      </button>
+      <button
+        className="Button Button--danger ml2"
+        onClick={() => {
+          onAction();
+          onClose();
+        }}
+      >
+        {confirmButtonText}
+      </button>
+    </div>
+  </ModalContent>
+);
 
 export default ConfirmContent;
diff --git a/frontend/src/metabase/components/CopyButton.jsx b/frontend/src/metabase/components/CopyButton.jsx
index 6f8b2ed230e3caeb3df76f862c081353989806df..21ea7bb125fc0142c04aca22f2dc91ccf36c9a7d 100644
--- a/frontend/src/metabase/components/CopyButton.jsx
+++ b/frontend/src/metabase/components/CopyButton.jsx
@@ -4,43 +4,41 @@ import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon";
 import Tooltip from "metabase/components/Tooltip";
-import { t } from 'c-3po';
-import CopyToClipboard from 'react-copy-to-clipboard';
+import { t } from "c-3po";
+import CopyToClipboard from "react-copy-to-clipboard";
 
 type Props = {
-    className?: string,
-    value: string
+  className?: string,
+  value: string,
 };
 type State = {
-    copied: boolean
+  copied: boolean,
 };
 
 export default class CopyWidget extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            copied: false
-        }
-    }
-    onCopy = () => {
-        this.setState({ copied: true });
-        setTimeout(() =>
-            this.setState({ copied: false })
-        , 2000);
-    }
-    render() {
-        const { value, className, ...props } = this.props;
-        return (
-            <Tooltip tooltip={t`Copied!`} isOpen={this.state.copied}>
-                <CopyToClipboard text={value} onCopy={this.onCopy}>
-                    <div className={className}>
-                        <Icon name="copy" {...props} />
-                    </div>
-                </CopyToClipboard>
-            </Tooltip>
-        );
-    }
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      copied: false,
+    };
+  }
+  onCopy = () => {
+    this.setState({ copied: true });
+    setTimeout(() => this.setState({ copied: false }), 2000);
+  };
+  render() {
+    const { value, className, ...props } = this.props;
+    return (
+      <Tooltip tooltip={t`Copied!`} isOpen={this.state.copied}>
+        <CopyToClipboard text={value} onCopy={this.onCopy}>
+          <div className={className}>
+            <Icon name="copy" {...props} />
+          </div>
+        </CopyToClipboard>
+      </Tooltip>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/CopyWidget.jsx b/frontend/src/metabase/components/CopyWidget.jsx
index 62d5e963a2511a1a88f04c0635a853a10992e863..88b15c1654a7e822fee24ca44e4ec6938dc672eb 100644
--- a/frontend/src/metabase/components/CopyWidget.jsx
+++ b/frontend/src/metabase/components/CopyWidget.jsx
@@ -5,28 +5,28 @@ import React, { Component } from "react";
 import CopyButton from "./CopyButton";
 
 type Props = {
-    value: string
+  value: string,
 };
 
 export default class CopyWidget extends Component {
-    props: Props;
+  props: Props;
 
-    render() {
-        const { value } = this.props;
-        return (
-            <div className="flex">
-                <input
-                    className="flex-full p1 flex align-center text-grey-4 text-bold no-focus border-top border-left border-bottom border-med rounded-left"
-                    style={{ borderRight: "none" }}
-                    type="text"
-                    value={value}
-                    onClick={(e) => e.target.setSelectionRange(0, e.target.value.length)}
-                />
-                <CopyButton
-                    className="p1 flex align-center bordered border-med rounded-right text-brand bg-brand-hover text-white-hover"
-                    value={value}
-                />
-            </div>
-        );
-    }
+  render() {
+    const { value } = this.props;
+    return (
+      <div className="flex">
+        <input
+          className="flex-full p1 flex align-center text-grey-4 text-bold no-focus border-top border-left border-bottom border-med rounded-left"
+          style={{ borderRight: "none" }}
+          type="text"
+          value={value}
+          onClick={e => e.target.setSelectionRange(0, e.target.value.length)}
+        />
+        <CopyButton
+          className="p1 flex align-center bordered border-med rounded-right text-brand bg-brand-hover text-white-hover"
+          value={value}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/CreateDashboardModal.jsx b/frontend/src/metabase/components/CreateDashboardModal.jsx
index e95ab09dab51caf34d0a4b705371d2aefaf1fead..5836047e34fdde08e0ef46775eded042c6ee475c 100644
--- a/frontend/src/metabase/components/CreateDashboardModal.jsx
+++ b/frontend/src/metabase/components/CreateDashboardModal.jsx
@@ -1,108 +1,122 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import FormField from "metabase/components/FormField.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
 import Button from "metabase/components/Button.jsx";
 
 export default class CreateDashboardModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.createNewDash = this.createNewDash.bind(this);
-        this.setDescription = this.setDescription.bind(this);
-        this.setName = this.setName.bind(this);
+  constructor(props, context) {
+    super(props, context);
+    this.createNewDash = this.createNewDash.bind(this);
+    this.setDescription = this.setDescription.bind(this);
+    this.setName = this.setName.bind(this);
 
-        this.state = {
-            name: null,
-            description: null,
-            errors: null
-        };
-    }
-
-    static propTypes = {
-        createDashboardFn: PropTypes.func.isRequired,
-        onClose: PropTypes.func
+    this.state = {
+      name: null,
+      description: null,
+      errors: null,
     };
+  }
 
-    setName(event) {
-        this.setState({ name: event.target.value });
-    }
+  static propTypes = {
+    createDashboardFn: PropTypes.func.isRequired,
+    onClose: PropTypes.func,
+  };
 
-    setDescription(event) {
-        this.setState({ description: event.target.value });
-    }
+  setName(event) {
+    this.setState({ name: event.target.value });
+  }
 
-    createNewDash(event) {
-        event.preventDefault();
+  setDescription(event) {
+    this.setState({ description: event.target.value });
+  }
 
-        var name = this.state.name && this.state.name.trim();
-        var description = this.state.description && this.state.description.trim();
+  createNewDash(event) {
+    event.preventDefault();
 
-        // populate a new Dash object
-        var newDash = {
-            name: (name && name.length > 0) ? name : null,
-            description: (description && description.length > 0) ? description : null
-        };
+    var name = this.state.name && this.state.name.trim();
+    var description = this.state.description && this.state.description.trim();
 
-        // create a new dashboard
-        var component = this;
-        this.props.createDashboardFn(newDash).then(null, function(error) {
-            component.setState({
-                errors: error
-            });
-        });
-    }
+    // populate a new Dash object
+    var newDash = {
+      name: name && name.length > 0 ? name : null,
+      description: description && description.length > 0 ? description : null,
+    };
+
+    // create a new dashboard
+    var component = this;
+    this.props.createDashboardFn(newDash).then(null, function(error) {
+      component.setState({
+        errors: error,
+      });
+    });
+  }
 
-    render() {
-        var formError;
-        if (this.state.errors) {
-            var errorMessage = t`Server error encountered`;
-            if (this.state.errors.data &&
-                this.state.errors.data.message) {
-                errorMessage = this.state.errors.data.message;
-            }
+  render() {
+    var formError;
+    if (this.state.errors) {
+      var errorMessage = t`Server error encountered`;
+      if (this.state.errors.data && this.state.errors.data.message) {
+        errorMessage = this.state.errors.data.message;
+      }
 
-            // TODO: timeout display?
-            formError = (
-                <span className="text-error px2">{errorMessage}</span>
-            );
-        }
+      // TODO: timeout display?
+      formError = <span className="text-error px2">{errorMessage}</span>;
+    }
 
-        var name = this.state.name && this.state.name.trim();
+    var name = this.state.name && this.state.name.trim();
 
-        var formReady = (name !== null && name !== "");
+    var formReady = name !== null && name !== "";
 
-        return (
-            <ModalContent
-                id="CreateDashboardModal"
-                title= {t`Create dashboard`}
-                footer={[
-                    formError,
-                    <Button onClick={this.props.onClose}>{t`Cancel`}</Button>,
-                    <Button primary={formReady} disabled={!formReady} onClick={this.createNewDash}>{t`Create`}</Button>
-                ]}
-                onClose={this.props.onClose}
+    return (
+      <ModalContent
+        id="CreateDashboardModal"
+        title={t`Create dashboard`}
+        footer={[
+          formError,
+          <Button onClick={this.props.onClose}>{t`Cancel`}</Button>,
+          <Button
+            primary={formReady}
+            disabled={!formReady}
+            onClick={this.createNewDash}
+          >{t`Create`}</Button>,
+        ]}
+        onClose={this.props.onClose}
+      >
+        <form className="Modal-form" onSubmit={this.createNewDash}>
+          <div className="Form-inputs">
+            <FormField
+              displayName={t`Name`}
+              fieldName="name"
+              errors={this.state.errors}
             >
-                <form className="Modal-form" onSubmit={this.createNewDash}>
-                    <div className="Form-inputs">
-                        <FormField
-                            displayName={t`Name`}
-                            fieldName="name"
-                            errors={this.state.errors}
-                        >
-                            <input className="Form-input full" name="name" placeholder={t`What is the name of your dashboard?`} value={this.state.name} onChange={this.setName} autoFocus />
-                        </FormField>
+              <input
+                className="Form-input full"
+                name="name"
+                placeholder={t`What is the name of your dashboard?`}
+                value={this.state.name}
+                onChange={this.setName}
+                autoFocus
+              />
+            </FormField>
 
-                        <FormField
-                            displayName={t`Description`}
-                            fieldName="description"
-                            errors={this.state.errors}
-                        >
-                            <input className="Form-input full" name="description" placeholder={t`It's optional but oh, so helpful`}  value={this.state.description} onChange={this.setDescription} />
-                        </FormField>
-                    </div>
-                </form>
-            </ModalContent>
-        );
-    }
+            <FormField
+              displayName={t`Description`}
+              fieldName="description"
+              errors={this.state.errors}
+            >
+              <input
+                className="Form-input full"
+                name="description"
+                placeholder={t`It's optional but oh, so helpful`}
+                value={this.state.description}
+                onChange={this.setDescription}
+              />
+            </FormField>
+          </div>
+        </form>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/DatabaseDetailsForm.jsx b/frontend/src/metabase/components/DatabaseDetailsForm.jsx
index 83168e93082ef840431c77783926f9a988d435c4..f90ae68d62311a578519c34e08c430f766cd909a 100644
--- a/frontend/src/metabase/components/DatabaseDetailsForm.jsx
+++ b/frontend/src/metabase/components/DatabaseDetailsForm.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -11,25 +11,31 @@ import { shallowEqual } from "recompose";
 
 // TODO - this should be somewhere more centralized
 function isEmpty(str) {
-    return (!str || 0 === str.length);
+  return !str || 0 === str.length;
 }
 
 const AUTH_URL_PREFIXES = {
-    bigquery: 'https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/bigquery&client_id=',
-    bigquery_with_drive: 'https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/bigquery%20https://www.googleapis.com/auth/drive&client_id=',
-    googleanalytics: 'https://accounts.google.com/o/oauth2/auth?access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/analytics.readonly&client_id=',
+  bigquery:
+    "https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/bigquery&client_id=",
+  bigquery_with_drive:
+    "https://accounts.google.com/o/oauth2/auth?redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/bigquery%20https://www.googleapis.com/auth/drive&client_id=",
+  googleanalytics:
+    "https://accounts.google.com/o/oauth2/auth?access_type=offline&redirect_uri=urn:ietf:wg:oauth:2.0:oob&response_type=code&scope=https://www.googleapis.com/auth/analytics.readonly&client_id=",
 };
 
 const ENABLE_API_PREFIXES = {
-    googleanalytics: 'https://console.developers.google.com/apis/api/analytics.googleapis.com/overview?project='
+  googleanalytics:
+    "https://console.developers.google.com/apis/api/analytics.googleapis.com/overview?project=",
 };
 
 const CREDENTIALS_URL_PREFIXES = {
-    bigquery: 'https://console.developers.google.com/apis/credentials/oauthclient?project=',
-    googleanalytics: 'https://console.developers.google.com/apis/credentials/oauthclient?project=',
+  bigquery:
+    "https://console.developers.google.com/apis/credentials/oauthclient?project=",
+  googleanalytics:
+    "https://console.developers.google.com/apis/credentials/oauthclient?project=",
 };
 
-const isTunnelField = (field) => /^tunnel-/.test(field.name);
+const isTunnelField = field => /^tunnel-/.test(field.name);
 
 /**
  * This is a form for capturing database details for a given `engine` supplied via props.
@@ -37,295 +43,364 @@ const isTunnelField = (field) => /^tunnel-/.test(field.name);
  * function to receive the captured form input when the form is submitted.
  */
 export default class DatabaseDetailsForm extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            details: props.details || {},
-            valid: false
-        }
-    }
-
-    static propTypes = {
-        details: PropTypes.object,
-        engine: PropTypes.string.isRequired,
-        engines: PropTypes.object.isRequired,
-        formError: PropTypes.object,
-        hiddenFields: PropTypes.object,
-        isNewDatabase: PropTypes.boolean,
-        submitButtonText: PropTypes.string.isRequired,
-        submitFn: PropTypes.func.isRequired,
-        submitting: PropTypes.boolean
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      details: props.details || {},
+      valid: false,
     };
-
-    validateForm() {
-        let { engine, engines } = this.props;
-        let { details } = this.state;
-
-        let valid = true;
-
-        // name is required
-        if (!details.name) {
-            valid = false;
-        }
-
-        // go over individual fields
-        for (let field of engines[engine]['details-fields']) {
-            // tunnel fields aren't required if tunnel isn't enabled
-            if (!details["tunnel-enabled"] && isTunnelField(field)) {
-                continue;
-            } else if (field.required && isEmpty(details[field.name])) {
-                valid = false;
-                break;
-            }
-        }
-
-        if (this.state.valid !== valid) {
-            this.setState({ valid });
-        }
-    }
-
-    componentWillReceiveProps(nextProps) {
-        if (!shallowEqual(this.props.details, nextProps.details)) {
-            this.setState({ details: nextProps.details })
-        }
+  }
+
+  static propTypes = {
+    details: PropTypes.object,
+    engine: PropTypes.string.isRequired,
+    engines: PropTypes.object.isRequired,
+    formError: PropTypes.object,
+    hiddenFields: PropTypes.object,
+    isNewDatabase: PropTypes.boolean,
+    submitButtonText: PropTypes.string.isRequired,
+    submitFn: PropTypes.func.isRequired,
+    submitting: PropTypes.boolean,
+  };
+
+  validateForm() {
+    let { engine, engines } = this.props;
+    let { details } = this.state;
+
+    let valid = true;
+
+    // name is required
+    if (!details.name) {
+      valid = false;
     }
 
-    componentDidMount() {
-        this.validateForm();
+    // go over individual fields
+    for (let field of engines[engine]["details-fields"]) {
+      // tunnel fields aren't required if tunnel isn't enabled
+      if (!details["tunnel-enabled"] && isTunnelField(field)) {
+        continue;
+      } else if (field.required && isEmpty(details[field.name])) {
+        valid = false;
+        break;
+      }
     }
 
-    componentDidUpdate() {
-        this.validateForm();
+    if (this.state.valid !== valid) {
+      this.setState({ valid });
     }
+  }
 
-    onChange(fieldName, fieldValue) {
-        this.setState({ details: { ...this.state.details, [fieldName]: fieldValue }});
+  componentWillReceiveProps(nextProps) {
+    if (!shallowEqual(this.props.details, nextProps.details)) {
+      this.setState({ details: nextProps.details });
     }
+  }
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  componentDidUpdate() {
+    this.validateForm();
+  }
+
+  onChange(fieldName, fieldValue) {
+    this.setState({
+      details: { ...this.state.details, [fieldName]: fieldValue },
+    });
+  }
+
+  formSubmitted(e) {
+    e.preventDefault();
+
+    let { engine, engines, submitFn } = this.props;
+    let { details } = this.state;
+
+    let request = {
+      engine: engine,
+      name: details.name,
+      details: {},
+      // use the existing is_full_sync setting in case that "let user control scheduling" setting is enabled
+      is_full_sync: details.is_full_sync,
+    };
 
-    formSubmitted(e) {
-        e.preventDefault();
-
-        let { engine, engines, submitFn } = this.props;
-        let { details } = this.state;
-
-        let request = {
-            engine: engine,
-            name: details.name,
-            details: {},
-            // use the existing is_full_sync setting in case that "let user control scheduling" setting is enabled
-            is_full_sync: details.is_full_sync
-        };
+    for (let field of engines[engine]["details-fields"]) {
+      let val = details[field.name] === "" ? null : details[field.name];
 
-        for (let field of engines[engine]['details-fields']) {
-            let val = details[field.name] === "" ? null : details[field.name];
+      if (val && field.type === "integer") val = parseInt(val);
+      if (val == null && field.default) val = field.default;
 
-            if (val && field.type === 'integer') val = parseInt(val);
-            if (val == null && field.default)    val = field.default;
+      request.details[field.name] = val;
+    }
 
-            request.details[field.name] = val;
-        }
+    // NOTE Atte Keinänen 8/15/17: Is it a little hacky approach or not to add to the `details` field property
+    // that are not part of the details schema of current db engine?
+    request.details["let-user-control-scheduling"] =
+      details["let-user-control-scheduling"];
 
-        // NOTE Atte Keinänen 8/15/17: Is it a little hacky approach or not to add to the `details` field property
-        // that are not part of the details schema of current db engine?
-        request.details["let-user-control-scheduling"] = details["let-user-control-scheduling"];
+    submitFn(request);
+  }
 
-        submitFn(request);
-    }
+  renderFieldInput(field, fieldIndex) {
+    let { details } = this.state;
+    let value = (details && details[field.name]) || "";
 
-    renderFieldInput(field, fieldIndex) {
-        let { details } = this.state;
-        let value = details && details[field.name] || "";
-
-        switch(field.type) {
-            case 'boolean':
-                return (
-                    <div className="Form-input Form-offset full Button-group">
-                        <div className={cx('Button', details[field.name] === true ? 'Button--active' : null)} onClick={(e) => { this.onChange(field.name, true) }}>
-                            Yes
-                        </div>
-                        <div className={cx('Button', details[field.name] === false ? 'Button--danger' : null)} onClick={(e) => { this.onChange(field.name, false) }}>
-                            No
-                        </div>
-                    </div>
-                );
-            default:
-                return (
-                    <input
-                        type={field.type === 'password' ? 'password' : 'text'}
-                        className="Form-input Form-offset full"
-                        ref={field.name}
-                        name={field.name}
-                        value={value}
-                        placeholder={field.default || field.placeholder}
-                        onChange={(e) => this.onChange(field.name, e.target.value)}
-                        required={field.required}
-                        autoFocus={fieldIndex === 0}
-                    />
-                );
-        }
+    switch (field.type) {
+      case "boolean":
+        return (
+          <div className="Form-input Form-offset full Button-group">
+            <div
+              className={cx(
+                "Button",
+                details[field.name] === true ? "Button--active" : null,
+              )}
+              onClick={e => {
+                this.onChange(field.name, true);
+              }}
+            >
+              Yes
+            </div>
+            <div
+              className={cx(
+                "Button",
+                details[field.name] === false ? "Button--danger" : null,
+              )}
+              onClick={e => {
+                this.onChange(field.name, false);
+              }}
+            >
+              No
+            </div>
+          </div>
+        );
+      default:
+        return (
+          <input
+            type={field.type === "password" ? "password" : "text"}
+            className="Form-input Form-offset full"
+            ref={field.name}
+            name={field.name}
+            value={value}
+            placeholder={field.default || field.placeholder}
+            onChange={e => this.onChange(field.name, e.target.value)}
+            required={field.required}
+            autoFocus={fieldIndex === 0}
+          />
+        );
     }
-
-    renderField(field, fieldIndex) {
-        let { engine } = this.props;
-        window.ENGINE = engine;
-
-        if (field.name === "tunnel-enabled") {
-            let on = (this.state.details["tunnel-enabled"] == undefined) ? false : this.state.details["tunnel-enabled"];
-            return (
-                <FormField key={field.name} fieldName={field.name}>
-                    <div className="flex align-center Form-offset">
-                        <div className="Grid-cell--top">
-                            <Toggle value={on} onChange={(val) => this.onChange("tunnel-enabled", val)}/>
-                        </div>
-                        <div className="px2">
-                            <h3>{t`Use an SSH-tunnel for database connections`}</h3>
-                            <div style={{maxWidth: "40rem"}} className="pt1">
-                                 {t`Some database installations can only be accessed by connecting through an SSH bastion host.
+  }
+
+  renderField(field, fieldIndex) {
+    let { engine } = this.props;
+    window.ENGINE = engine;
+
+    if (field.name === "tunnel-enabled") {
+      let on =
+        this.state.details["tunnel-enabled"] == undefined
+          ? false
+          : this.state.details["tunnel-enabled"];
+      return (
+        <FormField key={field.name} fieldName={field.name}>
+          <div className="flex align-center Form-offset">
+            <div className="Grid-cell--top">
+              <Toggle
+                value={on}
+                onChange={val => this.onChange("tunnel-enabled", val)}
+              />
+            </div>
+            <div className="px2">
+              <h3>{t`Use an SSH-tunnel for database connections`}</h3>
+              <div style={{ maxWidth: "40rem" }} className="pt1">
+                {t`Some database installations can only be accessed by connecting through an SSH bastion host.
                                  This option also provides an extra layer of security when a VPN is not available.
                                  Enabling this is usually slower than a direct connection.`}
-                            </div>
-                        </div>
-                    </div>
-                </FormField>
-            )
-        } else if (isTunnelField(field) && !this.state.details["tunnel-enabled"]) {
-            // don't show tunnel fields if tunnel isn't enabled
-            return null;
-        } else if (field.name === "let-user-control-scheduling") {
-            let on = (this.state.details["let-user-control-scheduling"] == undefined) ? false : this.state.details["let-user-control-scheduling"];
-            return (
-                <FormField key={field.name} fieldName={field.name}>
-                    <div className="flex align-center Form-offset">
-                        <div className="Grid-cell--top">
-                            <Toggle value={on} onChange={(val) => this.onChange("let-user-control-scheduling", val)}/>
-                        </div>
-                        <div className="px2">
-                            <h3>{t`This is a large database, so let me choose when Metabase syncs and scans`}</h3>
-                            <div style={{maxWidth: "40rem"}} className="pt1">
-                                {t`By default, Metabase does a lightweight hourly sync, and an intensive daily scan of field values.
+              </div>
+            </div>
+          </div>
+        </FormField>
+      );
+    } else if (isTunnelField(field) && !this.state.details["tunnel-enabled"]) {
+      // don't show tunnel fields if tunnel isn't enabled
+      return null;
+    } else if (field.name === "let-user-control-scheduling") {
+      let on =
+        this.state.details["let-user-control-scheduling"] == undefined
+          ? false
+          : this.state.details["let-user-control-scheduling"];
+      return (
+        <FormField key={field.name} fieldName={field.name}>
+          <div className="flex align-center Form-offset">
+            <div className="Grid-cell--top">
+              <Toggle
+                value={on}
+                onChange={val =>
+                  this.onChange("let-user-control-scheduling", val)
+                }
+              />
+            </div>
+            <div className="px2">
+              <h3
+              >{t`This is a large database, so let me choose when Metabase syncs and scans`}</h3>
+              <div style={{ maxWidth: "40rem" }} className="pt1">
+                {t`By default, Metabase does a lightweight hourly sync, and an intensive daily scan of field values.
                                 If you have a large database, we recommend turning this on and reviewing when and how often the field value scans happen.`}
-                            </div>
-                        </div>
-                    </div>
-                </FormField>
-            );
-        } else if (field.name === 'client-id' && CREDENTIALS_URL_PREFIXES[engine]) {
-            let { details } = this.state;
-            let projectID = details && details['project-id'];
-            var credentialsURLLink;
-            // if (projectID) {
-                let credentialsURL = CREDENTIALS_URL_PREFIXES[engine] + (projectID || "");
-                credentialsURLLink = (
-                    <div className="flex align-center Form-offset">
-                        <div className="Grid-cell--top">
-                            {jt`${<a href={credentialsURL} target='_blank'>Click here</a>} to generate a Client ID and Client Secret for your project.`}
-                            {t`Choose "Other" as the application type. Name it whatever you'd like.`}
-                        </div>
-                    </div>);
-            // }
-
-            return (
-                <FormField key='client-id' field-name='client-id'>
-                    <FormLabel title={field['display-name']} field-name='client-id'></FormLabel>
-                    {credentialsURLLink}
-                    {this.renderFieldInput(field, fieldIndex)}
-                </FormField>
-            );
-        } else if (field.name === 'auth-code' && AUTH_URL_PREFIXES[engine]) {
-            let { details } = this.state;
-            const clientID = details && details['client-id'];
-            var authURLLink;
-            if (clientID) {
-                let authURL = AUTH_URL_PREFIXES[engine] + clientID;
-                authURLLink = (
-                    <div className="flex align-center Form-offset">
-                        <div className="Grid-cell--top">
-                            {jt`${<a href={authURL} target='_blank'>Click here</a>} to get an auth code`}
-                            { engine === "bigquery" &&
-                                <span> (or <a href={AUTH_URL_PREFIXES["bigquery_with_drive"] + clientID} target='_blank'>{t`with Google Drive permissions`}</a>)</span>
-                            }
-                        </div>
-                    </div>);
-            }
-
-            // for Google Analytics we need to show a link for people to go to the Console to enable the GA API
-            let enableAPILink;
-            // projectID is just the first numeric part of the clientID.
-            // e.g. clientID might be 123436115855-q8z42hilmjf8iplnnu49n7jbudmxxdf.apps.googleusercontent.com
-            // then projecID would be 12343611585
-            const projectID = clientID && (clientID.match(/^\d+/) || [])[0];
-            if (ENABLE_API_PREFIXES[engine] && projectID) {
-                // URL looks like https://console.developers.google.com/apis/api/analytics.googleapis.com/overview?project=12343611585
-                const enableAPIURL = ENABLE_API_PREFIXES[engine] + projectID;
-                enableAPILink = (
-                    <div className="flex align-center Form-offset">
-                        <div className="Grid-cell--top">
-                            {t`To use Metabase with this data you must enable API access in the Google Developers Console.`}
-                        </div>
-                        <div className="Grid-cell--top ml1">
-                            {jt`${<a href={enableAPIURL} target='_blank'>Click here</a>} to go to the console if you haven't already done so.`}
-                        </div>
-                    </div>
-                );
-            }
-
-            return (
-                <FormField key='auth-code' field-name='auth-code'>
-                    <FormLabel title={field['display-name']} field-name='auth-code'></FormLabel>
-                    {authURLLink}
-                    {this.renderFieldInput(field, fieldIndex)}
-                    {enableAPILink}
-                </FormField>
-            );
-        } else {
-            return (
-                <FormField key={field.name} fieldName={field.name}>
-                    <FormLabel title={field['display-name']} fieldName={field.name}></FormLabel>
-                    {this.renderFieldInput(field, fieldIndex)}
-                    <span className="Form-charm"></span>
-                </FormField>
-            );
-        }
-    }
-
-    render() {
-        let { engine, engines, formError, formSuccess, hiddenFields, submitButtonText, isNewDatabase, submitting } = this.props;
-        let { valid, details } = this.state;
-
-        const willProceedToNextDbCreationStep = isNewDatabase && details["let-user-control-scheduling"];
-
-        let fields = [
-            {
-                name: 'name',
-                'display-name': t`Name`,
-                placeholder: t`How would you like to refer to this database?`,
-                required: true
-            },
-            ...engines[engine]['details-fields'],
-            {
-                name: "let-user-control-scheduling",
-                required: true
-            }
-        ];
-
-        hiddenFields = hiddenFields || {};
-
-        return (
-            <form onSubmit={this.formSubmitted.bind(this)} noValidate>
-                <div className="FormInputGroup pb2">
-                    { fields.filter(field => !hiddenFields[field.name]).map((field, fieldIndex) =>
-                        this.renderField(field, fieldIndex)
-                      )}
-                </div>
-
-                <div className="Form-actions">
-                    <button className={cx("Button", {"Button--primary": valid})} disabled={!valid || submitting}>
-                        {submitting ? t`Saving...` : (willProceedToNextDbCreationStep ? t`Next` : submitButtonText)}
-                    </button>
-                    <FormMessage formError={formError} formSuccess={formSuccess}></FormMessage>
-                </div>
-            </form>
+              </div>
+            </div>
+          </div>
+        </FormField>
+      );
+    } else if (field.name === "client-id" && CREDENTIALS_URL_PREFIXES[engine]) {
+      let { details } = this.state;
+      let projectID = details && details["project-id"];
+      var credentialsURLLink;
+      // if (projectID) {
+      let credentialsURL = CREDENTIALS_URL_PREFIXES[engine] + (projectID || "");
+      credentialsURLLink = (
+        <div className="flex align-center Form-offset">
+          <div className="Grid-cell--top">
+            {jt`${(
+              <a href={credentialsURL} target="_blank">
+                Click here
+              </a>
+            )} to generate a Client ID and Client Secret for your project.`}
+            {t`Choose "Other" as the application type. Name it whatever you'd like.`}
+          </div>
+        </div>
+      );
+      // }
+
+      return (
+        <FormField key="client-id" field-name="client-id">
+          <FormLabel title={field["display-name"]} field-name="client-id" />
+          {credentialsURLLink}
+          {this.renderFieldInput(field, fieldIndex)}
+        </FormField>
+      );
+    } else if (field.name === "auth-code" && AUTH_URL_PREFIXES[engine]) {
+      let { details } = this.state;
+      const clientID = details && details["client-id"];
+      var authURLLink;
+      if (clientID) {
+        let authURL = AUTH_URL_PREFIXES[engine] + clientID;
+        authURLLink = (
+          <div className="flex align-center Form-offset">
+            <div className="Grid-cell--top">
+              {jt`${(
+                <a href={authURL} target="_blank">
+                  Click here
+                </a>
+              )} to get an auth code`}
+              {engine === "bigquery" && (
+                <span>
+                  {" "}
+                  (or{" "}
+                  <a
+                    href={AUTH_URL_PREFIXES["bigquery_with_drive"] + clientID}
+                    target="_blank"
+                  >{t`with Google Drive permissions`}</a>)
+                </span>
+              )}
+            </div>
+          </div>
+        );
+      }
+
+      // for Google Analytics we need to show a link for people to go to the Console to enable the GA API
+      let enableAPILink;
+      // projectID is just the first numeric part of the clientID.
+      // e.g. clientID might be 123436115855-q8z42hilmjf8iplnnu49n7jbudmxxdf.apps.googleusercontent.com
+      // then projecID would be 12343611585
+      const projectID = clientID && (clientID.match(/^\d+/) || [])[0];
+      if (ENABLE_API_PREFIXES[engine] && projectID) {
+        // URL looks like https://console.developers.google.com/apis/api/analytics.googleapis.com/overview?project=12343611585
+        const enableAPIURL = ENABLE_API_PREFIXES[engine] + projectID;
+        enableAPILink = (
+          <div className="flex align-center Form-offset">
+            <div className="Grid-cell--top">
+              {t`To use Metabase with this data you must enable API access in the Google Developers Console.`}
+            </div>
+            <div className="Grid-cell--top ml1">
+              {jt`${(
+                <a href={enableAPIURL} target="_blank">
+                  Click here
+                </a>
+              )} to go to the console if you haven't already done so.`}
+            </div>
+          </div>
         );
+      }
+
+      return (
+        <FormField key="auth-code" field-name="auth-code">
+          <FormLabel title={field["display-name"]} field-name="auth-code" />
+          {authURLLink}
+          {this.renderFieldInput(field, fieldIndex)}
+          {enableAPILink}
+        </FormField>
+      );
+    } else {
+      return (
+        <FormField key={field.name} fieldName={field.name}>
+          <FormLabel title={field["display-name"]} fieldName={field.name} />
+          {this.renderFieldInput(field, fieldIndex)}
+          <span className="Form-charm" />
+        </FormField>
+      );
     }
+  }
+
+  render() {
+    let {
+      engine,
+      engines,
+      formError,
+      formSuccess,
+      hiddenFields,
+      submitButtonText,
+      isNewDatabase,
+      submitting,
+    } = this.props;
+    let { valid, details } = this.state;
+
+    const willProceedToNextDbCreationStep =
+      isNewDatabase && details["let-user-control-scheduling"];
+
+    let fields = [
+      {
+        name: "name",
+        "display-name": t`Name`,
+        placeholder: t`How would you like to refer to this database?`,
+        required: true,
+      },
+      ...engines[engine]["details-fields"],
+      {
+        name: "let-user-control-scheduling",
+        required: true,
+      },
+    ];
+
+    hiddenFields = hiddenFields || {};
+
+    return (
+      <form onSubmit={this.formSubmitted.bind(this)} noValidate>
+        <div className="FormInputGroup pb2">
+          {fields
+            .filter(field => !hiddenFields[field.name])
+            .map((field, fieldIndex) => this.renderField(field, fieldIndex))}
+        </div>
+
+        <div className="Form-actions">
+          <button
+            className={cx("Button", { "Button--primary": valid })}
+            disabled={!valid || submitting}
+          >
+            {submitting
+              ? t`Saving...`
+              : willProceedToNextDbCreationStep ? t`Next` : submitButtonText}
+          </button>
+          <FormMessage formError={formError} formSuccess={formSuccess} />
+        </div>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/DeleteModalWithConfirm.jsx b/frontend/src/metabase/components/DeleteModalWithConfirm.jsx
index 6225be6549b4a4599ea20035d8012f5d4424a2e1..4d1d6b8be791747f2999360e9e9bb38efb7ed333 100644
--- a/frontend/src/metabase/components/DeleteModalWithConfirm.jsx
+++ b/frontend/src/metabase/components/DeleteModalWithConfirm.jsx
@@ -3,68 +3,83 @@ import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
 import CheckBox from "metabase/components/CheckBox.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 import _ from "underscore";
 
 export default class DeleteModalWithConfirm extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            checked: {}
-        };
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      checked: {},
+    };
 
-        _.bindAll(this, "onDelete");
-    }
+    _.bindAll(this, "onDelete");
+  }
 
-    static propTypes = {
-        title: PropTypes.string.isRequired,
-        objectType: PropTypes.string.isRequired,
-        confirmItems: PropTypes.array.isRequired,
-        onClose: PropTypes.func.isRequired,
-        onDelete: PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    objectType: PropTypes.string.isRequired,
+    confirmItems: PropTypes.array.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onDelete: PropTypes.func.isRequired,
+  };
 
-    async onDelete() {
-        await this.props.onDelete();
-        this.props.onClose();
-    }
+  async onDelete() {
+    await this.props.onDelete();
+    this.props.onClose();
+  }
 
-    render() {
-        const { title, objectType, confirmItems } = this.props;
-        const { checked } = this.state;
-        let confirmed = confirmItems.reduce((acc, item, index) => acc && checked[index], true);
-        return (
-            <ModalContent
-                title={title}
-                onClose={this.props.onClose}
-            >
-            <div className="px4">
-                <ul>
-                    {confirmItems.map((item, index) =>
-                        <li key={index} className="pb2 mb2 border-row-divider flex align-center">
-                            <span className="text-error">
-                                <CheckBox
-                                    checkColor="currentColor" borderColor={checked[index] ? "currentColor" : undefined} size={20}
-                                    checked={checked[index]}
-                                    onChange={(e) => this.setState({ checked: { ...checked, [index]: e.target.checked } })}
-                                />
-                            </span>
-                            <span className="ml2 h4">{item}</span>
-                        </li>
-                    )}
-                </ul>
-            </div>
-            <div className="Form-actions ml-auto">
-                <button className="Button" onClick={this.props.onClose}>{t`Cancel`}</button>
-                <button
-                className={cx("Button ml2", { disabled: !confirmed, "Button--danger": confirmed })}
-                onClick={this.onDelete}
-                >
-                    {t`Delete this ${objectType}`}
-                </button>
-            </div>
-            </ModalContent>
-        );
-    }
+  render() {
+    const { title, objectType, confirmItems } = this.props;
+    const { checked } = this.state;
+    let confirmed = confirmItems.reduce(
+      (acc, item, index) => acc && checked[index],
+      true,
+    );
+    return (
+      <ModalContent title={title} onClose={this.props.onClose}>
+        <div className="px4">
+          <ul>
+            {confirmItems.map((item, index) => (
+              <li
+                key={index}
+                className="pb2 mb2 border-row-divider flex align-center"
+              >
+                <span className="text-error">
+                  <CheckBox
+                    checkColor="currentColor"
+                    borderColor={checked[index] ? "currentColor" : undefined}
+                    size={20}
+                    checked={checked[index]}
+                    onChange={e =>
+                      this.setState({
+                        checked: { ...checked, [index]: e.target.checked },
+                      })
+                    }
+                  />
+                </span>
+                <span className="ml2 h4">{item}</span>
+              </li>
+            ))}
+          </ul>
+        </div>
+        <div className="Form-actions ml-auto">
+          <button
+            className="Button"
+            onClick={this.props.onClose}
+          >{t`Cancel`}</button>
+          <button
+            className={cx("Button ml2", {
+              disabled: !confirmed,
+              "Button--danger": confirmed,
+            })}
+            onClick={this.onDelete}
+          >
+            {t`Delete this ${objectType}`}
+          </button>
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/DirectionalButton.jsx b/frontend/src/metabase/components/DirectionalButton.jsx
index 8f29296c3fc37a2ddd352e477375fec26261b1a8..e17dbb4ad9618c61e2228bcab53ff906c39ad1b6 100644
--- a/frontend/src/metabase/components/DirectionalButton.jsx
+++ b/frontend/src/metabase/components/DirectionalButton.jsx
@@ -1,16 +1,17 @@
-import React from 'react'
-import Icon from 'metabase/components/Icon'
+import React from "react";
+import Icon from "metabase/components/Icon";
 
-const DirectionalButton = ({ direction = "back", onClick }) =>
-    <div
-        className="shadowed cursor-pointer text-brand-hover text-grey-4 flex align-center circle p2 bg-white transition-background transition-color"
-        onClick={onClick}
-        style={{
-            border: "1px solid #DCE1E4",
-            boxShadow: "0 2px 4px 0 #DCE1E4"
-        }}
-    >
-        <Icon name={`${direction}Arrow`} />
-    </div>
+const DirectionalButton = ({ direction = "back", onClick }) => (
+  <div
+    className="shadowed cursor-pointer text-brand-hover text-grey-4 flex align-center circle p2 bg-white transition-background transition-color"
+    onClick={onClick}
+    style={{
+      border: "1px solid #DCE1E4",
+      boxShadow: "0 2px 4px 0 #DCE1E4",
+    }}
+  >
+    <Icon name={`${direction}Arrow`} />
+  </div>
+);
 
-export default DirectionalButton
+export default DirectionalButton;
diff --git a/frontend/src/metabase/components/DisclosureTriangle.jsx b/frontend/src/metabase/components/DisclosureTriangle.jsx
index 908286bc03a9af78187d1df2f414b59439cbee56..f0a1fad26250508acddce8349311593858daa829 100644
--- a/frontend/src/metabase/components/DisclosureTriangle.jsx
+++ b/frontend/src/metabase/components/DisclosureTriangle.jsx
@@ -3,17 +3,23 @@ import { Motion, spring, presets } from "react-motion";
 
 import Icon from "metabase/components/Icon";
 
-const DisclosureTriangle = ({ open }) =>
-    <Motion defaultStyle={{ deg: 0 }} style={{ deg: open ? spring(0, presets.gentle) : spring(-90, presets.gentle) }}>
-        { motionStyle =>
-            <Icon
-                className="ml1 mr1"
-                name="expandarrow"
-                style={{
-                    transform: `rotate(${motionStyle.deg}deg)`
-                }}
-            />
-        }
-    </Motion>
+const DisclosureTriangle = ({ open }) => (
+  <Motion
+    defaultStyle={{ deg: 0 }}
+    style={{
+      deg: open ? spring(0, presets.gentle) : spring(-90, presets.gentle),
+    }}
+  >
+    {motionStyle => (
+      <Icon
+        className="ml1 mr1"
+        name="expandarrow"
+        style={{
+          transform: `rotate(${motionStyle.deg}deg)`,
+        }}
+      />
+    )}
+  </Motion>
+);
 
 export default DisclosureTriangle;
diff --git a/frontend/src/metabase/components/DownloadButton.jsx b/frontend/src/metabase/components/DownloadButton.jsx
index 49641cf6e42e4c46bc1422e4e31a4ca59b57d8d2..a3b8278b3aeb2504903a4064082a5cb924b0cb6a 100644
--- a/frontend/src/metabase/components/DownloadButton.jsx
+++ b/frontend/src/metabase/components/DownloadButton.jsx
@@ -3,41 +3,52 @@ import PropTypes from "prop-types";
 
 import Button from "metabase/components/Button.jsx";
 
-const DownloadButton = ({ className, style, children, method, url, params, extensions, ...props }) =>
-    <form className={className} style={style} method={method} action={url}>
-        { params && Object.entries(params).map(([name, value]) =>
-            <input key={name} type="hidden" name={name} value={value} />
-        )}
-        <Button
-            onClick={(e) => {
-                if (window.OSX) {
-                    // prevent form from being submitted normally
-                    e.preventDefault();
-                    // download using the API provided by the OS X app
-                    window.OSX.download(method, url, params, extensions);
-                }
-            }}
-            {...props}
-        >
-            {children}
-        </Button>
-    </form>
+const DownloadButton = ({
+  className,
+  style,
+  children,
+  method,
+  url,
+  params,
+  extensions,
+  ...props
+}) => (
+  <form className={className} style={style} method={method} action={url}>
+    {params &&
+      Object.entries(params).map(([name, value]) => (
+        <input key={name} type="hidden" name={name} value={value} />
+      ))}
+    <Button
+      onClick={e => {
+        if (window.OSX) {
+          // prevent form from being submitted normally
+          e.preventDefault();
+          // download using the API provided by the OS X app
+          window.OSX.download(method, url, params, extensions);
+        }
+      }}
+      {...props}
+    >
+      {children}
+    </Button>
+  </form>
+);
 
 DownloadButton.propTypes = {
-    className: PropTypes.string,
-    style: PropTypes.object,
-    url: PropTypes.string.isRequired,
-    method: PropTypes.string,
-    params: PropTypes.object,
-    icon: PropTypes.string,
-    extensions: PropTypes.array,
+  className: PropTypes.string,
+  style: PropTypes.object,
+  url: PropTypes.string.isRequired,
+  method: PropTypes.string,
+  params: PropTypes.object,
+  icon: PropTypes.string,
+  extensions: PropTypes.array,
 };
 
 DownloadButton.defaultProps = {
-    icon: "downarrow",
-    method: "POST",
-    params: {},
-    extensions: []
+  icon: "downarrow",
+  method: "POST",
+  params: {},
+  extensions: [],
 };
 
 export default DownloadButton;
diff --git a/frontend/src/metabase/components/EditBar.jsx b/frontend/src/metabase/components/EditBar.jsx
index 1f537102d4239a5d357b47a2759d3a378e88be7f..68851965613f6553c4c02b741e3c3f28731d005a 100644
--- a/frontend/src/metabase/components/EditBar.jsx
+++ b/frontend/src/metabase/components/EditBar.jsx
@@ -1,44 +1,37 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 
 class EditBar extends Component {
-    static propTypes = {
-        title: PropTypes.string.isRequired,
-        subtitle: PropTypes.string,
-        buttons: PropTypes.oneOfType([
-            PropTypes.element,
-            PropTypes.array
-        ]).isRequired,
-        admin: PropTypes.bool
-    }
+  static propTypes = {
+    title: PropTypes.string.isRequired,
+    subtitle: PropTypes.string,
+    buttons: PropTypes.oneOfType([PropTypes.element, PropTypes.array])
+      .isRequired,
+    admin: PropTypes.bool,
+  };
 
-    static defaultProps = {
-        admin: false
-    }
+  static defaultProps = {
+    admin: false,
+  };
 
-    render () {
-        const { admin, buttons, subtitle, title } = this.props;
-        return (
-            <div
-                className={cx(
-                    'EditHeader wrapper py1 flex align-center',
-                    { 'EditHeader--admin' : admin }
-                )}
-                ref="editHeader"
-            >
-                <span className="EditHeader-title">{title}</span>
-                { subtitle && (
-                    <span className="EditHeader-subtitle mx1">
-                        {subtitle}
-                    </span>
-                )}
-                <span className="flex-align-right flex">
-                    {buttons}
-                </span>
-            </div>
-        )
-    }
+  render() {
+    const { admin, buttons, subtitle, title } = this.props;
+    return (
+      <div
+        className={cx("EditHeader wrapper py1 flex align-center", {
+          "EditHeader--admin": admin,
+        })}
+        ref="editHeader"
+      >
+        <span className="EditHeader-title">{title}</span>
+        {subtitle && (
+          <span className="EditHeader-subtitle mx1">{subtitle}</span>
+        )}
+        <span className="flex-align-right flex">{buttons}</span>
+      </div>
+    );
+  }
 }
 
 export default EditBar;
diff --git a/frontend/src/metabase/components/Ellipsified.jsx b/frontend/src/metabase/components/Ellipsified.jsx
index 5162a76a644084013b8193d39edb1667c89096a1..ae3be87ca4d83e75c39ed1f57ce54a1ef9ec9b10 100644
--- a/frontend/src/metabase/components/Ellipsified.jsx
+++ b/frontend/src/metabase/components/Ellipsified.jsx
@@ -4,43 +4,60 @@ import ReactDOM from "react-dom";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
 export default class Ellipsified extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            isTruncated: false
-        };
-    }
-
-    static propTypes = {};
-    static defaultProps = {
-        style: {},
-        className: "",
-        showTooltip: true
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      isTruncated: false,
     };
+  }
 
-    componentDidUpdate() {
-        // Only show tooltip if title is hidden or ellipsified
-        const element = ReactDOM.findDOMNode(this.refs.content);
-        const isTruncated = element && element.offsetWidth < element.scrollWidth;
-        if (this.state.isTruncated !== isTruncated) {
-            this.setState({ isTruncated });
-        }
-    }
+  static propTypes = {};
+  static defaultProps = {
+    style: {},
+    className: "",
+    showTooltip: true,
+  };
 
-    render() {
-        const { showTooltip, children, style, className, tooltip, alwaysShowTooltip, tooltipMaxWidth } = this.props;
-        const { isTruncated } = this.state;
-        return (
-            <Tooltip
-                tooltip={tooltip || children || ' '}
-                verticalAttachments={["top", "bottom"]}
-                isEnabled={showTooltip && (isTruncated || alwaysShowTooltip) || false}
-                maxWidth={tooltipMaxWidth}
-            >
-                <div ref="content" className={className} style={{ ...style, overflow: "hidden", whiteSpace: "nowrap", textOverflow: "ellipsis" }}>
-                    {children}
-                </div>
-            </Tooltip>
-        );
+  componentDidUpdate() {
+    // Only show tooltip if title is hidden or ellipsified
+    const element = ReactDOM.findDOMNode(this.refs.content);
+    const isTruncated = element && element.offsetWidth < element.scrollWidth;
+    if (this.state.isTruncated !== isTruncated) {
+      this.setState({ isTruncated });
     }
+  }
+
+  render() {
+    const {
+      showTooltip,
+      children,
+      style,
+      className,
+      tooltip,
+      alwaysShowTooltip,
+      tooltipMaxWidth,
+    } = this.props;
+    const { isTruncated } = this.state;
+    return (
+      <Tooltip
+        tooltip={tooltip || children || " "}
+        verticalAttachments={["top", "bottom"]}
+        isEnabled={(showTooltip && (isTruncated || alwaysShowTooltip)) || false}
+        maxWidth={tooltipMaxWidth}
+      >
+        <div
+          ref="content"
+          className={className}
+          style={{
+            ...style,
+            overflow: "hidden",
+            whiteSpace: "nowrap",
+            textOverflow: "ellipsis",
+          }}
+        >
+          {children}
+        </div>
+      </Tooltip>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/EmojiIcon.jsx b/frontend/src/metabase/components/EmojiIcon.jsx
index fd1a0d0e23ed726da158a59de4f8a24fbeefed8a..91524b5236a4af41ebed636a92f43c482d4e7f14 100644
--- a/frontend/src/metabase/components/EmojiIcon.jsx
+++ b/frontend/src/metabase/components/EmojiIcon.jsx
@@ -4,16 +4,17 @@ import PropTypes from "prop-types";
 
 import { emoji } from "metabase/lib/emoji";
 
-const EmojiIcon = ({ size = 18, style, className, name }) =>
-    <span className={className} style={{ width: size, height: size, ...style }}>
-        {emoji[name].react}
-    </span>
+const EmojiIcon = ({ size = 18, style, className, name }) => (
+  <span className={className} style={{ width: size, height: size, ...style }}>
+    {emoji[name].react}
+  </span>
+);
 
 EmojiIcon.propTypes = {
-    className:  PropTypes.string,
-    style:      PropTypes.object,
-    size:       PropTypes.number,
-    name:       PropTypes.string.isRequired,
+  className: PropTypes.string,
+  style: PropTypes.object,
+  size: PropTypes.number,
+  name: PropTypes.string.isRequired,
 };
 
 export default EmojiIcon;
diff --git a/frontend/src/metabase/components/EmptyState.jsx b/frontend/src/metabase/components/EmptyState.jsx
index c55d38c4ea1083ddaaac8d6a44af1e033446f894..672e550968dec1b69bea415af5ccce6c74b6b4ff 100644
--- a/frontend/src/metabase/components/EmptyState.jsx
+++ b/frontend/src/metabase/components/EmptyState.jsx
@@ -1,6 +1,6 @@
 /* @flow */
 import React from "react";
-import {Link} from "react-router";
+import { Link } from "react-router";
 import cx from "classnames";
 /*
  * EmptyState is a component that can
@@ -11,42 +11,73 @@ import cx from "classnames";
 import Icon from "metabase/components/Icon.jsx";
 
 type EmptyStateProps = {
-    message: (string | React$Element<any>),
-    title?: string,
-    icon?: string,
-    image?: string,
-    imageHeight?: string, // for reducing ui flickering when the image is loading
-    imageClassName?: string,
-    action?: string,
-    link?: string,
-    onActionClick?: () => void,
-    smallDescription?: boolean
-}
+  message: string | React$Element<any>,
+  title?: string,
+  icon?: string,
+  image?: string,
+  imageHeight?: string, // for reducing ui flickering when the image is loading
+  imageClassName?: string,
+  action?: string,
+  link?: string,
+  onActionClick?: () => void,
+  smallDescription?: boolean,
+};
 
-const EmptyState = ({title, message, icon, image, imageHeight, 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>
-        }
-        { icon &&
-        <Icon name={icon} size={40}/>
-        }
-        { image &&
-        <img src={`${image}.png`} width="300px" height={imageHeight} alt={message} srcSet={`${image}@2x.png 2x`}
-             className={imageClassName}/>
-        }
-        <div className="flex justify-center">
-            <h2 className={cx("text-grey-2 text-normal mt2 mb4", {"text-paragraph": smallDescription})}
-                style={{lineHeight: "1.5em"}}>{message}</h2>
-        </div>
-        { 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>
-        }
+const EmptyState = ({
+  title,
+  message,
+  icon,
+  image,
+  imageHeight,
+  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>}
+    {icon && <Icon name={icon} size={40} />}
+    {image && (
+      <img
+        src={`${image}.png`}
+        width="300px"
+        height={imageHeight}
+        alt={message}
+        srcSet={`${image}@2x.png 2x`}
+        className={imageClassName}
+      />
+    )}
+    <div className="flex justify-center">
+      <h2
+        className={cx("text-grey-2 text-normal mt2 mb4", {
+          "text-paragraph": smallDescription,
+        })}
+        style={{ lineHeight: "1.5em" }}
+      >
+        {message}
+      </h2>
     </div>
-
+    {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>
+);
 
 export default EmptyState;
diff --git a/frontend/src/metabase/components/EntityMenu.info.js b/frontend/src/metabase/components/EntityMenu.info.js
index 9a5ee3e3c6e9d702d96dfdb51da9658c0393c727..b7eeaa450e8a2c965169b5503871e0763171c5c1 100644
--- a/frontend/src/metabase/components/EntityMenu.info.js
+++ b/frontend/src/metabase/components/EntityMenu.info.js
@@ -1,83 +1,122 @@
-import React from 'react'
+import React from "react";
 
-import EntityMenu from 'metabase/components/EntityMenu'
-import { t } from 'c-3po';
-export const component = EntityMenu
+import EntityMenu from "metabase/components/EntityMenu";
+import { t } from "c-3po";
+export const component = EntityMenu;
 
 export const description = `
     A menu with varios entity related options grouped by context.
-`
+`;
 
-const DemoAlignRight = ({ children }) =>
-    <div className="flex flex-full">
-            <div className="flex align-center ml-auto">
-                {children}
-            </div>
-        </div>
+const DemoAlignRight = ({ children }) => (
+  <div className="flex flex-full">
+    <div className="flex align-center ml-auto">{children}</div>
+  </div>
+);
 
 export const examples = {
-    'Edit menu': (
-        <DemoAlignRight>
-            <EntityMenu
-                triggerIcon='pencil'
-                items={[
-                    { title: t`Edit this question`, icon: "editdocument", action: () => alert(t`Action type`) },
-                    { title: t`View revision history`, icon: "history", link: '/derp' },
-                    { title: t`Move`, icon: "move", action: () => alert(t`Move action`) },
-                    { title: t`Archive`, icon: "archive", action: () => alert(t`Archive action`) }
-                ]}
-            />
-        </DemoAlignRight>
-    ),
-    'Share menu': (
-        <DemoAlignRight>
-            <EntityMenu
-                triggerIcon='share'
-                items={[
-                    { title: t`Add to dashboard`, icon: "addtodash", action: () => alert(t`Action type`) },
-                    { title: t`Download results`, icon: "download", link: '/download' },
-                    { title: t`Sharing and embedding`, icon: "embed", action: () => alert(t`Another action type`) },
-                ]}
-            />
-        </DemoAlignRight>
-    ),
-    'More menu': (
-        <DemoAlignRight>
-            <EntityMenu
-                triggerIcon='burger'
-                items={[
-                    { title: t`Get alerts about this`, icon: "alert", action: () => alert(t`Get alerts about this`) },
-                    { title: t`View the SQL`, icon: "sql", link: '/download' },
-                ]}
-            />
-        </DemoAlignRight>
-    ),
-    'Multiple menus': (
-        <DemoAlignRight>
-            <EntityMenu
-                triggerIcon='pencil'
-                items={[
-                    { title: t`Edit this question`, icon: "editdocument", action: () => alert(t`Action type`) },
-                    { title: t`View revision history`, icon: "history", link: '/derp' },
-                    { title: t`Move`, icon: "move", action: () => alert(t`Move action`) },
-                    { title: t`Archive`, icon: "archive", action: () => alert(t`Archive action`) }
-                ]}
-            />
-            <EntityMenu
-                triggerIcon='share'
-                items={[
-                    { title: t`Add to dashboard`, icon: "addtodash", action: () => alert(t`Action type`) },
-                    { title: t`Download results`, icon: "download", link: '/download' },
-                    { title: t`Sharing and embedding`, icon: "embed", action: () => alert(t`Another action type`) },
-                ]}
-            />
-            <EntityMenu
-                triggerIcon='burger'
-                items={[
-                    { title: t`Get alerts about this`, icon: "alert", action: () => alert(t`Get alerts about this`) },
-                    { title: t`View the SQL`, icon: "sql", link: '/download' },
-                ]}
-            />
-        </DemoAlignRight>
-    )
-}
+  "Edit menu": (
+    <DemoAlignRight>
+      <EntityMenu
+        triggerIcon="pencil"
+        items={[
+          {
+            title: t`Edit this question`,
+            icon: "editdocument",
+            action: () => alert(t`Action type`),
+          },
+          { title: t`View revision history`, icon: "history", link: "/derp" },
+          { title: t`Move`, icon: "move", action: () => alert(t`Move action`) },
+          {
+            title: t`Archive`,
+            icon: "archive",
+            action: () => alert(t`Archive action`),
+          },
+        ]}
+      />
+    </DemoAlignRight>
+  ),
+  "Share menu": (
+    <DemoAlignRight>
+      <EntityMenu
+        triggerIcon="share"
+        items={[
+          {
+            title: t`Add to dashboard`,
+            icon: "addtodash",
+            action: () => alert(t`Action type`),
+          },
+          { title: t`Download results`, icon: "download", link: "/download" },
+          {
+            title: t`Sharing and embedding`,
+            icon: "embed",
+            action: () => alert(t`Another action type`),
+          },
+        ]}
+      />
+    </DemoAlignRight>
+  ),
+  "More menu": (
+    <DemoAlignRight>
+      <EntityMenu
+        triggerIcon="burger"
+        items={[
+          {
+            title: t`Get alerts about this`,
+            icon: "alert",
+            action: () => alert(t`Get alerts about this`),
+          },
+          { title: t`View the SQL`, icon: "sql", link: "/download" },
+        ]}
+      />
+    </DemoAlignRight>
+  ),
+  "Multiple menus": (
+    <DemoAlignRight>
+      <EntityMenu
+        triggerIcon="pencil"
+        items={[
+          {
+            title: t`Edit this question`,
+            icon: "editdocument",
+            action: () => alert(t`Action type`),
+          },
+          { title: t`View revision history`, icon: "history", link: "/derp" },
+          { title: t`Move`, icon: "move", action: () => alert(t`Move action`) },
+          {
+            title: t`Archive`,
+            icon: "archive",
+            action: () => alert(t`Archive action`),
+          },
+        ]}
+      />
+      <EntityMenu
+        triggerIcon="share"
+        items={[
+          {
+            title: t`Add to dashboard`,
+            icon: "addtodash",
+            action: () => alert(t`Action type`),
+          },
+          { title: t`Download results`, icon: "download", link: "/download" },
+          {
+            title: t`Sharing and embedding`,
+            icon: "embed",
+            action: () => alert(t`Another action type`),
+          },
+        ]}
+      />
+      <EntityMenu
+        triggerIcon="burger"
+        items={[
+          {
+            title: t`Get alerts about this`,
+            icon: "alert",
+            action: () => alert(t`Get alerts about this`),
+          },
+          { title: t`View the SQL`, icon: "sql", link: "/download" },
+        ]}
+      />
+    </DemoAlignRight>
+  ),
+};
diff --git a/frontend/src/metabase/components/EntityMenu.jsx b/frontend/src/metabase/components/EntityMenu.jsx
index 68914a64acf39c6771c262e7c1e4de3ffb171494..90d82b95db011dec74187db1d0389f8334a07b73 100644
--- a/frontend/src/metabase/components/EntityMenu.jsx
+++ b/frontend/src/metabase/components/EntityMenu.jsx
@@ -1,124 +1,132 @@
-import React, { Component } from 'react'
-import { Motion, spring } from 'react-motion'
+import React, { Component } from "react";
+import { Motion, spring } from "react-motion";
 
-import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper'
+import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
 
-import Card from 'metabase/components/Card'
-import EntityMenuTrigger from 'metabase/components/EntityMenuTrigger'
-import EntityMenuItem from 'metabase/components/EntityMenuItem'
+import Card from "metabase/components/Card";
+import EntityMenuTrigger from "metabase/components/EntityMenuTrigger";
+import EntityMenuItem from "metabase/components/EntityMenuItem";
 
 type EntityMenuOption = {
-    icon: string,
-    title: string,
-    action?: () => void,
-    link?: string
-}
+  icon: string,
+  title: string,
+  action?: () => void,
+  link?: string,
+};
 
 type Props = {
-    items: Array<EntityMenuOption>,
-    triggerIcon: string
-}
+  items: Array<EntityMenuOption>,
+  triggerIcon: string,
+};
 
 class EntityMenu extends Component {
+  props: Props;
 
-    props: Props
+  state = {
+    open: false,
+    freezeMenu: false,
+    menuItemContent: null,
+  };
 
-    state = {
-        open: false,
-        freezeMenu: false,
-        menuItemContent: null
-    }
+  toggleMenu = () => {
+    if (this.state.freezeMenu) return;
 
-    toggleMenu = () => {
-        if (this.state.freezeMenu) return;
+    const open = !this.state.open;
+    this.setState({ open, menuItemContent: null });
+  };
 
-        const open = !this.state.open
-        this.setState({ open, menuItemContent: null })
-    }
+  setFreezeMenu = (freezeMenu: boolean) => {
+    this.setState({ freezeMenu });
+  };
 
-    setFreezeMenu = (freezeMenu: boolean) => {
-        this.setState({ freezeMenu })
-    }
+  replaceMenuWithItemContent = (menuItemContent: any) => {
+    this.setState({ menuItemContent });
+  };
 
-    replaceMenuWithItemContent = (menuItemContent: any) => {
-       this.setState({ menuItemContent })
-    }
-
-    render () {
-        const { items, triggerIcon }  = this.props
-        const { open, menuItemContent } = this.state
-        return (
-            <div className="relative">
-                <EntityMenuTrigger
-                    icon={triggerIcon}
-                    onClick={this.toggleMenu}
-                    open={open}
-                />
-                { open && (
-                    /* Note: @kdoh 10/12/17
+  render() {
+    const { items, triggerIcon } = this.props;
+    const { open, menuItemContent } = this.state;
+    return (
+      <div className="relative">
+        <EntityMenuTrigger
+          icon={triggerIcon}
+          onClick={this.toggleMenu}
+          open={open}
+        />
+        {open && (
+          /* Note: @kdoh 10/12/17
                      * React Motion has a flow type problem with children see
                      * https://github.com/chenglou/react-motion/issues/375
                      * TODO This can be removed if we upgrade to flow 0.53 and react-motion >= 0.5.1
                      */
-                    <Motion
-                        defaultStyle={{
-                            opacity: 0,
-                            translateY: 0
-                        }}
-                        style={{
-                            opacity: open ? spring(1): spring(0),
-                            translateY: open ? spring(10) : spring(0)
-                        }}
-                    >
-                        { ({ opacity, translateY }) =>
-                            <OnClickOutsideWrapper handleDismissal={this.toggleMenu}>
-                                <div
-                                    className="absolute right"
-                                    style={{
-                                        top: 35,
-                                        opacity: opacity,
-                                        transform: `translateY(${translateY}px)`
-                                    }}
-                                >
-                                    <Card>
-                                        { menuItemContent ||
-                                            <ol className="py1" style={{ minWidth: 210 }}>
-                                                {items.map(item => {
-                                                    if (item.content) {
-                                                        return (
-                                                            <li key={item.title}>
-                                                                <EntityMenuItem
-                                                                    icon={item.icon}
-                                                                    title={item.title}
-                                                                    action={() => this.replaceMenuWithItemContent(item.content(this.toggleMenu, this.setFreezeMenu))}
-                                                                />
-                                                            </li>
-                                                        )
-                                                    } else {
-                                                        return (
-                                                            <li key={item.title}>
-                                                                <EntityMenuItem
-                                                                    icon={item.icon}
-                                                                    title={item.title}
-                                                                    action={() => {item.action(); this.toggleMenu()}}
-                                                                    link={item.link}
-                                                                />
-                                                            </li>
-                                                        )
-                                                    }
-                                                })}
-                                            </ol>
-                                        }
-                                    </Card>
-                                </div>
-                            </OnClickOutsideWrapper>
-                        }
-                    </Motion>
-                )}
-            </div>
-        )
-    }
+          <Motion
+            defaultStyle={{
+              opacity: 0,
+              translateY: 0,
+            }}
+            style={{
+              opacity: open ? spring(1) : spring(0),
+              translateY: open ? spring(10) : spring(0),
+            }}
+          >
+            {({ opacity, translateY }) => (
+              <OnClickOutsideWrapper handleDismissal={this.toggleMenu}>
+                <div
+                  className="absolute right"
+                  style={{
+                    top: 35,
+                    opacity: opacity,
+                    transform: `translateY(${translateY}px)`,
+                  }}
+                >
+                  <Card>
+                    {menuItemContent || (
+                      <ol className="py1" style={{ minWidth: 210 }}>
+                        {items.map(item => {
+                          if (item.content) {
+                            return (
+                              <li key={item.title}>
+                                <EntityMenuItem
+                                  icon={item.icon}
+                                  title={item.title}
+                                  action={() =>
+                                    this.replaceMenuWithItemContent(
+                                      item.content(
+                                        this.toggleMenu,
+                                        this.setFreezeMenu,
+                                      ),
+                                    )
+                                  }
+                                />
+                              </li>
+                            );
+                          } else {
+                            return (
+                              <li key={item.title}>
+                                <EntityMenuItem
+                                  icon={item.icon}
+                                  title={item.title}
+                                  action={() => {
+                                    item.action();
+                                    this.toggleMenu();
+                                  }}
+                                  link={item.link}
+                                />
+                              </li>
+                            );
+                          }
+                        })}
+                      </ol>
+                    )}
+                  </Card>
+                </div>
+              </OnClickOutsideWrapper>
+            )}
+          </Motion>
+        )}
+      </div>
+    );
+  }
 }
 
-export default EntityMenu
-
+export default EntityMenu;
diff --git a/frontend/src/metabase/components/EntityMenuItem.jsx b/frontend/src/metabase/components/EntityMenuItem.jsx
index a5e730b01d8e7b68d9f1a617731914c80dafd8e2..aa85318347b5c92c5123a6ff24a5bb3197e3be4b 100644
--- a/frontend/src/metabase/components/EntityMenuItem.jsx
+++ b/frontend/src/metabase/components/EntityMenuItem.jsx
@@ -1,91 +1,82 @@
-import cxs from 'cxs'
-import React from 'react'
-import { Link } from 'react-router'
+import cxs from "cxs";
+import React from "react";
+import { Link } from "react-router";
 
-import Icon from 'metabase/components/Icon'
+import Icon from "metabase/components/Icon";
 
 const itemClasses = cxs({
-    display: 'flex',
-    alignItems: 'center',
-    cursor: 'pointer',
-    color: '#616D75',
-    paddingLeft: '1.45em',
-    paddingRight: '1.45em',
-    paddingTop: '0.85em',
-    paddingBottom: '0.85em',
-    textDecoration: 'none',
-    transition: 'all 300ms linear',
-    ':hover': {
-        color: '#509ee3'
-    },
-    '> .Icon': {
-        color: '#BCC5CA',
-        marginRight: '0.65em'
-    },
-    ':hover > .Icon': {
-        color: '#509ee3',
-        transition: 'all 300ms linear',
-    },
-    // icon specific tweaks
-    // the alert icon should be optically aligned  with the x-height of the text
-    '> .Icon.Icon-alert': {
-        transform: `translateY(1px)`
-    },
-    // the embed icon should be optically aligned with the x-height of the text
-    '> .Icon.Icon-embed': {
-        transform: `translateY(1px)`
-    },
-    // the download icon should be optically aligned with the x-height of the text
-    '> .Icon.Icon-download': {
-        transform: `translateY(1px)`
-    },
-    // the history icon is wider so it needs adjustement to center it with other
-    // icons
-    '> .Icon.Icon-history': {
-        transform: `translateX(-2px)`
-    }
-})
+  display: "flex",
+  alignItems: "center",
+  cursor: "pointer",
+  color: "#616D75",
+  paddingLeft: "1.45em",
+  paddingRight: "1.45em",
+  paddingTop: "0.85em",
+  paddingBottom: "0.85em",
+  textDecoration: "none",
+  transition: "all 300ms linear",
+  ":hover": {
+    color: "#509ee3",
+  },
+  "> .Icon": {
+    color: "#BCC5CA",
+    marginRight: "0.65em",
+  },
+  ":hover > .Icon": {
+    color: "#509ee3",
+    transition: "all 300ms linear",
+  },
+  // icon specific tweaks
+  // the alert icon should be optically aligned  with the x-height of the text
+  "> .Icon.Icon-alert": {
+    transform: `translateY(1px)`,
+  },
+  // the embed icon should be optically aligned with the x-height of the text
+  "> .Icon.Icon-embed": {
+    transform: `translateY(1px)`,
+  },
+  // the download icon should be optically aligned with the x-height of the text
+  "> .Icon.Icon-download": {
+    transform: `translateY(1px)`,
+  },
+  // the history icon is wider so it needs adjustement to center it with other
+  // icons
+  "> .Icon.Icon-history": {
+    transform: `translateX(-2px)`,
+  },
+});
 
-const LinkMenuItem = ({ children, link }) =>
-    <Link className={itemClasses} to={link}>
-        {children}
-    </Link>
+const LinkMenuItem = ({ children, link }) => (
+  <Link className={itemClasses} to={link}>
+    {children}
+  </Link>
+);
 
-const ActionMenuItem = ({ children, action }) =>
-    <div className={itemClasses} onClick={action}>
-        {children}
-    </div>
+const ActionMenuItem = ({ children, action }) => (
+  <div className={itemClasses} onClick={action}>
+    {children}
+  </div>
+);
 
-const EntityMenuItem = ({
-    action,
-    title,
-    icon,
-    link
-}) => {
-    if(link && action) {
-        console.warn('EntityMenuItem Error: You cannot specify both action and link props')
-        return <div></div>
-    }
+const EntityMenuItem = ({ action, title, icon, link }) => {
+  if (link && action) {
+    console.warn(
+      "EntityMenuItem Error: You cannot specify both action and link props",
+    );
+    return <div />;
+  }
 
-    const content = [
-        <Icon name={icon} />,
-        <span className="text-bold">{title}</span>
-    ]
+  const content = [
+    <Icon name={icon} />,
+    <span className="text-bold">{title}</span>,
+  ];
 
-    if(link) {
-        return (
-            <LinkMenuItem link={link}>
-                {content}
-            </LinkMenuItem>
-        )
-    }
-    if(action) {
-        return (
-            <ActionMenuItem action={action}>
-                {content}
-            </ActionMenuItem>
-        )
-    }
-}
+  if (link) {
+    return <LinkMenuItem link={link}>{content}</LinkMenuItem>;
+  }
+  if (action) {
+    return <ActionMenuItem action={action}>{content}</ActionMenuItem>;
+  }
+};
 
-export default EntityMenuItem
+export default EntityMenuItem;
diff --git a/frontend/src/metabase/components/EntityMenuTrigger.jsx b/frontend/src/metabase/components/EntityMenuTrigger.jsx
index 0ab4c6a42881976bbfdb9123a3bc25be1c5987e8..4c3d38eeebe44a4e08548b1339bf819138dfaf01 100644
--- a/frontend/src/metabase/components/EntityMenuTrigger.jsx
+++ b/frontend/src/metabase/components/EntityMenuTrigger.jsx
@@ -1,37 +1,37 @@
-import React from 'react'
-import Icon from 'metabase/components/Icon'
-import cxs from 'cxs'
+import React from "react";
+import Icon from "metabase/components/Icon";
+import cxs from "cxs";
 
 const EntityMenuTrigger = ({ icon, onClick, open }) => {
-    const interactionColor = '#F2F4F5'
-    const classes = cxs({
-        display: 'flex',
-        alignItems: 'center',
-        justifyContent: 'center',
-        width: 40,
-        height: 40,
-        borderRadius: 99,
-        cursor: 'pointer',
-        color: open ? '#509ee3' : 'inherit',
-        backgroundColor: open ? interactionColor : 'transparent',
-        ':hover': {
-            backgroundColor: interactionColor,
-            color: '#509ee3',
-            transition: 'all 300ms linear'
-        },
-        // special cases for certain icons
-        // Icon-share has a taller viewvbox than most so to optically center
-        // the icon we need to translate it upwards
-        '> .Icon.Icon-share': {
-            transform: `translateY(-2px)`
-        }
-    })
+  const interactionColor = "#F2F4F5";
+  const classes = cxs({
+    display: "flex",
+    alignItems: "center",
+    justifyContent: "center",
+    width: 40,
+    height: 40,
+    borderRadius: 99,
+    cursor: "pointer",
+    color: open ? "#509ee3" : "inherit",
+    backgroundColor: open ? interactionColor : "transparent",
+    ":hover": {
+      backgroundColor: interactionColor,
+      color: "#509ee3",
+      transition: "all 300ms linear",
+    },
+    // special cases for certain icons
+    // Icon-share has a taller viewvbox than most so to optically center
+    // the icon we need to translate it upwards
+    "> .Icon.Icon-share": {
+      transform: `translateY(-2px)`,
+    },
+  });
 
-    return (
-        <div onClick={onClick} className={classes}>
-            <Icon name={icon} className="m1" />
-        </div>
-    )
-}
+  return (
+    <div onClick={onClick} className={classes}>
+      <Icon name={icon} className="m1" />
+    </div>
+  );
+};
 
-export default EntityMenuTrigger
+export default EntityMenuTrigger;
diff --git a/frontend/src/metabase/components/Expandable.jsx b/frontend/src/metabase/components/Expandable.jsx
index 256eeb20c99e0c29ae90bacfc7bd2833170d1c31..ab81ed58b3dd62e77560fce609028529bf5caf84 100644
--- a/frontend/src/metabase/components/Expandable.jsx
+++ b/frontend/src/metabase/components/Expandable.jsx
@@ -1,34 +1,44 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
-const Expandable = (ComposedComponent) => class extends Component {
-    static displayName = "Expandable["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+const Expandable = ComposedComponent =>
+  class extends Component {
+    static displayName = "Expandable[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
     constructor(props, context) {
-        super(props, context);
-        this.state = {
-            expanded: false
-        };
-        this.expand = () => this.setState({ expanded: true });
+      super(props, context);
+      this.state = {
+        expanded: false,
+      };
+      this.expand = () => this.setState({ expanded: true });
     }
 
     static propTypes = {
-        items: PropTypes.array.isRequired,
-        initialItemLimit: PropTypes.number.isRequired
+      items: PropTypes.array.isRequired,
+      initialItemLimit: PropTypes.number.isRequired,
     };
     static defaultProps = {
-        initialItemLimit: 4
+      initialItemLimit: 4,
     };
 
     render() {
-        let { expanded } = this.state;
-        let { items, initialItemLimit } = this.props;
-        if (items.length > initialItemLimit && !expanded) {
-            items = items.slice(0, initialItemLimit - 1);
-        }
-        expanded = items.length >= this.props.items.length;
-        return <ComposedComponent {...this.props} isExpanded={expanded} onExpand={this.expand} items={items} />
+      let { expanded } = this.state;
+      let { items, initialItemLimit } = this.props;
+      if (items.length > initialItemLimit && !expanded) {
+        items = items.slice(0, initialItemLimit - 1);
+      }
+      expanded = items.length >= this.props.items.length;
+      return (
+        <ComposedComponent
+          {...this.props}
+          isExpanded={expanded}
+          onExpand={this.expand}
+          items={items}
+        />
+      );
     }
-}
+  };
 
 export default Expandable;
diff --git a/frontend/src/metabase/components/ExpandingContent.jsx b/frontend/src/metabase/components/ExpandingContent.jsx
index 205d5d27032c0cdd9d6f95a594e8964ca68b0128..9be5c298426f902e30423c900a867db70d31ef3c 100644
--- a/frontend/src/metabase/components/ExpandingContent.jsx
+++ b/frontend/src/metabase/components/ExpandingContent.jsx
@@ -1,24 +1,24 @@
 import React, { Component } from "react";
 
 class ExpandingContent extends Component {
-    constructor () {
-        super();
-        this.state = { open: false };
-    }
-    render () {
-        const { children, open } = this.props;
-        return (
-            <div
-                style={{
-                    maxHeight: open ? 'none' : 0,
-                    overflow: 'hidden',
-                    transition: 'max-height 0.3s ease'
-                }}
-            >
-                { children }
-            </div>
-        );
-    }
+  constructor() {
+    super();
+    this.state = { open: false };
+  }
+  render() {
+    const { children, open } = this.props;
+    return (
+      <div
+        style={{
+          maxHeight: open ? "none" : 0,
+          overflow: "hidden",
+          transition: "max-height 0.3s ease",
+        }}
+      >
+        {children}
+      </div>
+    );
+  }
 }
 
 export default ExpandingContent;
diff --git a/frontend/src/metabase/components/ExplicitSize.jsx b/frontend/src/metabase/components/ExplicitSize.jsx
index 9d10e95f74f14cef9c10ce22b77ebd63446d3ac1..b34e0830ad1665f3a64b7e5ace0abeccbc8aedb9 100644
--- a/frontend/src/metabase/components/ExplicitSize.jsx
+++ b/frontend/src/metabase/components/ExplicitSize.jsx
@@ -3,54 +3,65 @@ import ReactDOM from "react-dom";
 
 import ResizeObserver from "resize-observer-polyfill";
 
-export default ComposedComponent => class extends Component {
-    static displayName = "ExplicitSize["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+export default ComposedComponent =>
+  class extends Component {
+    static displayName = "ExplicitSize[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
     constructor(props, context) {
-        super(props, context);
-        this.state = {
-            width: null,
-            height: null
-        };
+      super(props, context);
+      this.state = {
+        width: null,
+        height: null,
+      };
     }
 
     componentDidMount() {
-        // media query listener, ensure re-layout when printing
+      // media query listener, ensure re-layout when printing
+      if (window.matchMedia) {
         this._mql = window.matchMedia("print");
         this._mql.addListener(this._updateSize);
+      }
 
-        // resize observer, ensure re-layout when container element changes size
-        this._ro = new ResizeObserver((entries, observer) => {
-            const element = ReactDOM.findDOMNode(this);
-            for (const entry of entries) {
-                if (entry.target === element) {
-                    this._updateSize();
-                    break;
-                }
-            }
-        });
-        this._ro.observe(ReactDOM.findDOMNode(this));
-
-        this._updateSize();
+      // resize observer, ensure re-layout when container element changes size
+      this._ro = new ResizeObserver((entries, observer) => {
+        const element = ReactDOM.findDOMNode(this);
+        for (const entry of entries) {
+          if (entry.target === element) {
+            this._updateSize();
+            break;
+          }
+        }
+      });
+      this._ro.observe(ReactDOM.findDOMNode(this));
+
+      this._updateSize();
     }
 
     componentDidUpdate() {
-        this._updateSize();
+      this._updateSize();
     }
 
     componentWillUnmount() {
+      if (this._ro) {
         this._ro.disconnect();
+      }
+      if (this._mql) {
         this._mql.removeListener(this._updateSize);
+      }
     }
 
     _updateSize = () => {
-        const { width, height } = ReactDOM.findDOMNode(this).getBoundingClientRect();
-        if (this.state.width !== width || this.state.height !== height) {
-            this.setState({ width, height });
-        }
-    }
+      const { width, height } = ReactDOM.findDOMNode(
+        this,
+      ).getBoundingClientRect();
+      if (this.state.width !== width || this.state.height !== height) {
+        this.setState({ width, height });
+      }
+    };
 
     render() {
-        return <ComposedComponent {...this.props} {...this.state} />
+      return <ComposedComponent {...this.props} {...this.state} />;
     }
-}
+  };
diff --git a/frontend/src/metabase/components/ExternalLink.jsx b/frontend/src/metabase/components/ExternalLink.jsx
index c6dea2cff0895b184c27b90cce49bf82c9aef427..1b1d47506230df56aef259f60af16cedbc25f626 100644
--- a/frontend/src/metabase/components/ExternalLink.jsx
+++ b/frontend/src/metabase/components/ExternalLink.jsx
@@ -1,18 +1,19 @@
 import React from "react";
 
-const ExternalLink = ({ href, className, children, ...props }) =>
-    <a
-        href={href}
-        className={className || "link"}
-        // open in a new tab
-        target="_blank"
-        // prevent malicious pages from navigating us away
-        rel="noopener"
-        // disables quickfilter in tables
-        onClickCapture={(e) => e.stopPropagation()}
-        {...props}
-    >
-        {children}
-    </a>
+const ExternalLink = ({ href, className, children, ...props }) => (
+  <a
+    href={href}
+    className={className || "link"}
+    // open in a new tab
+    target="_blank"
+    // prevent malicious pages from navigating us away
+    rel="noopener"
+    // disables quickfilter in tables
+    onClickCapture={e => e.stopPropagation()}
+    {...props}
+  >
+    {children}
+  </a>
+);
 
 export default ExternalLink;
diff --git a/frontend/src/metabase/components/FieldSet.jsx b/frontend/src/metabase/components/FieldSet.jsx
index adc060bd4f8dafdb17e68bd6a56feb2d5caa9ebe..1aa0b25174f84a12db846c14c37256b5439451ee 100644
--- a/frontend/src/metabase/components/FieldSet.jsx
+++ b/frontend/src/metabase/components/FieldSet.jsx
@@ -3,22 +3,28 @@ import React from "react";
 import cx from "classnames";
 
 type Props = {
-    className: string,
-    legend: string,
-    noPadding?: boolean,
-    children: React$Element<any>
-}
+  className: string,
+  legend: string,
+  noPadding?: boolean,
+  children: React$Element<any>,
+};
 
-export default function FieldSet({className = "border-brand", legend, noPadding, children}: Props) {
-    const fieldSetClassName = cx("bordered rounded", {"px2 pb2": !noPadding});
+export default function FieldSet({
+  className = "border-brand",
+  legend,
+  noPadding,
+  children,
+}: Props) {
+  const fieldSetClassName = cx("bordered rounded", { "px2 pb2": !noPadding });
 
-    return (
-        <fieldset className={cx(className, fieldSetClassName)}>
-            {legend &&
-            <legend className="h5 text-bold text-uppercase px1 text-nowrap text-grey-4">{legend}</legend>}
-            <div>
-                {children}
-            </div>
-        </fieldset>
-    );
+  return (
+    <fieldset className={cx(className, fieldSetClassName)}>
+      {legend && (
+        <legend className="h5 text-bold text-uppercase px1 text-nowrap text-grey-4">
+          {legend}
+        </legend>
+      )}
+      <div>{children}</div>
+    </fieldset>
+  );
 }
diff --git a/frontend/src/metabase/components/FieldValuesWidget.jsx b/frontend/src/metabase/components/FieldValuesWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..5bfe6afcf59420c696318418b597328c3205b705
--- /dev/null
+++ b/frontend/src/metabase/components/FieldValuesWidget.jsx
@@ -0,0 +1,373 @@
+/* @flow */
+
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import { t, jt } from "c-3po";
+
+import TokenField from "metabase/components/TokenField";
+import RemappedValue from "metabase/containers/RemappedValue";
+import LoadingSpinner from "metabase/components/LoadingSpinner";
+import Icon from "metabase/components/Icon";
+
+import AutoExpanding from "metabase/hoc/AutoExpanding";
+
+import { MetabaseApi } from "metabase/services";
+import { addRemappings, fetchFieldValues } from "metabase/redux/metadata";
+import { defer } from "metabase/lib/promise";
+import { debounce } from "underscore";
+import { stripId } from "metabase/lib/formatting";
+
+import type Field from "metabase-lib/lib/metadata/Field";
+import type { FieldId } from "metabase/meta/types/Field";
+import type { Value } from "metabase/meta/types/Dataset";
+import type { LayoutRendererProps } from "metabase/components/TokenField";
+
+const MAX_SEARCH_RESULTS = 100;
+
+const mapDispatchToProps = {
+  addRemappings,
+  fetchFieldValues,
+};
+
+type Props = {
+  value: Value[],
+  onChange: (value: Value[]) => void,
+  field: Field,
+  searchField?: Field,
+  multi?: boolean,
+  autoFocus?: boolean,
+  color?: string,
+  fetchFieldValues: (id: FieldId) => void,
+  maxResults: number,
+  style?: { [key: string]: string | number },
+  placeholder?: string,
+  maxWidth?: number,
+  minWidth?: number,
+  alwaysShowOptions?: boolean,
+};
+
+type State = {
+  loadingState: "INIT" | "LOADING" | "LOADED",
+  options: [Value, ?string][],
+  lastValue: string,
+};
+
+@AutoExpanding
+export class FieldValuesWidget extends Component {
+  props: Props;
+  state: State;
+
+  _cancel: ?() => void;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      options: [],
+      loadingState: "INIT",
+      lastValue: "",
+    };
+  }
+
+  static defaultProps = {
+    color: "purple",
+    maxResults: MAX_SEARCH_RESULTS,
+    alwaysShowOptions: true,
+    style: {},
+    maxWidth: 500,
+  };
+
+  componentWillMount() {
+    const { field, fetchFieldValues } = this.props;
+    if (field.has_field_values === "list") {
+      fetchFieldValues(field.id);
+    }
+  }
+
+  componentWillUnmount() {
+    if (this._cancel) {
+      this._cancel();
+    }
+  }
+
+  hasList() {
+    const { field } = this.props;
+    return field.has_field_values === "list" && field.values;
+  }
+
+  isSearchable() {
+    const { field, searchField } = this.props;
+    return searchField && field.has_field_values === "search";
+  }
+
+  onInputChange = (value: string) => {
+    if (value && this.isSearchable()) {
+      this._search(value);
+    }
+
+    return value;
+  };
+
+  search = async (value: string, cancelled: Promise<void>) => {
+    const { field, searchField, maxResults } = this.props;
+
+    if (!field || !searchField || !value) {
+      return;
+    }
+
+    const fieldId = (field.target || field).id;
+    const searchFieldId = searchField.id;
+    let results = await MetabaseApi.field_search(
+      {
+        value,
+        fieldId,
+        searchFieldId,
+        limit: maxResults,
+      },
+      { cancelled },
+    );
+
+    if (results && field.remappedField() === searchField) {
+      // $FlowFixMe: addRemappings provided by @connect
+      this.props.addRemappings(field.id, results);
+    }
+    return results;
+  };
+
+  _search = (value: string) => {
+    const { lastValue, options } = this.state;
+
+    // if this search is just an extension of the previous search, and the previous search
+    // wasn't truncated, then we don't need to do another search because TypeaheadListing
+    // will filter the previous result client-side
+    if (
+      lastValue &&
+      value.slice(0, lastValue.length) === lastValue &&
+      options.length < this.props.maxResults
+    ) {
+      return;
+    }
+
+    this.setState({
+      loadingState: "INIT",
+    });
+
+    if (this._cancel) {
+      this._cancel();
+    }
+
+    this._searchDebounced(value);
+  };
+
+  // $FlowFixMe
+  _searchDebounced = debounce(async (value): void => {
+    this.setState({
+      loadingState: "LOADING",
+    });
+
+    const cancelDeferred = defer();
+    this._cancel = () => {
+      this._cancel = null;
+      cancelDeferred.resolve();
+    };
+
+    let results = await this.search(value, cancelDeferred.promise);
+
+    this._cancel = null;
+
+    if (results) {
+      this.setState({
+        loadingState: "LOADED",
+        options: results,
+        lastValue: value,
+      });
+    } else {
+      this.setState({
+        loadingState: "INIT",
+        options: [],
+        lastValue: value,
+      });
+    }
+  }, 500);
+
+  renderOptions({
+    optionsList,
+    isFocused,
+    isAllSelected,
+  }: LayoutRendererProps) {
+    const { alwaysShowOptions, field, searchField } = this.props;
+    const { loadingState } = this.state;
+    if (alwaysShowOptions || isFocused) {
+      if (optionsList) {
+        return optionsList;
+      } else if (this.hasList()) {
+        if (isAllSelected) {
+          return <EveryOptionState />;
+        }
+      } else if (this.isSearchable()) {
+        if (loadingState === "INIT") {
+          return alwaysShowOptions && <SearchState />;
+        } else if (loadingState === "LOADING") {
+          return <LoadingState />;
+        } else if (loadingState === "LOADED") {
+          if (isAllSelected) {
+            return alwaysShowOptions && <SearchState />;
+          } else {
+            return <NoMatchState field={searchField || field} />;
+          }
+        }
+      }
+    }
+  }
+
+  render() {
+    const {
+      value,
+      onChange,
+      field,
+      searchField,
+      multi,
+      autoFocus,
+      color,
+    } = this.props;
+    const { loadingState } = this.state;
+
+    let { placeholder } = this.props;
+    if (!placeholder) {
+      if (this.hasList()) {
+        placeholder = t`Search the list`;
+      } else if (this.isSearchable() && searchField) {
+        const searchFieldName =
+          stripId(searchField.display_name) || searchField.display_name;
+        placeholder = t`Search by ${searchFieldName}`;
+        if (field.isID() && field !== searchField) {
+          placeholder += t` or enter an ID`;
+        }
+      } else {
+        if (field.isID()) {
+          placeholder = t`Enter an ID`;
+        } else if (field.isNumeric()) {
+          placeholder = t`Enter a number`;
+        } else {
+          placeholder = t`Enter some text`;
+        }
+      }
+    }
+
+    let options = [];
+    if (this.hasList()) {
+      options = field.values;
+    } else if (this.isSearchable() && loadingState === "LOADED") {
+      options = this.state.options;
+    } else {
+      options = [];
+    }
+
+    return (
+      <div
+        style={{
+          width: this.props.expand ? this.props.maxWidth : null,
+          minWidth: this.props.minWidth,
+          maxWidth: this.props.maxWidth,
+        }}
+      >
+        <TokenField
+          value={value.filter(v => v != null)}
+          onChange={onChange}
+          placeholder={placeholder}
+          multi={multi}
+          autoFocus={autoFocus}
+          color={color}
+          style={{
+            borderWidth: 2,
+            ...this.props.style,
+          }}
+          updateOnInputChange
+          options={options}
+          valueKey={0}
+          valueRenderer={value => (
+            <RemappedValue
+              value={value}
+              column={field}
+              round={false}
+              autoLoad={true}
+            />
+          )}
+          optionRenderer={option => (
+            <RemappedValue
+              value={option[0]}
+              column={field}
+              round={false}
+              autoLoad={false}
+            />
+          )}
+          layoutRenderer={props => (
+            <div>
+              {props.valuesList}
+              {this.renderOptions(props)}
+            </div>
+          )}
+          filterOption={(option, filterString) =>
+            (option[0] != null &&
+              String(option[0])
+                .toLowerCase()
+                .indexOf(filterString.toLowerCase()) === 0) ||
+            (option[1] != null &&
+              String(option[1])
+                .toLowerCase()
+                .indexOf(filterString.toLowerCase()) === 0)
+          }
+          onInputChange={this.onInputChange}
+          parseFreeformValue={v => {
+            // trim whitespace
+            v = String(v || "").trim();
+            // empty string is not valid
+            if (!v) {
+              return null;
+            }
+            // if the field is numeric we need to parse the string into an integer
+            if (field.isNumeric()) {
+              if (/^-?\d+(\.\d+)?$/.test(v)) {
+                return parseFloat(v);
+              } else {
+                return null;
+              }
+            }
+            return v;
+          }}
+        />
+      </div>
+    );
+  }
+}
+
+const LoadingState = () => (
+  <div className="flex layout-centered align-center" style={{ minHeight: 100 }}>
+    <LoadingSpinner size={32} />
+  </div>
+);
+
+const SearchState = () => (
+  <div className="flex layout-centered align-center" style={{ minHeight: 100 }}>
+    <Icon name="search" size={35} className="text-grey-1" />
+  </div>
+);
+
+const NoMatchState = ({ field }) => (
+  <OptionsMessage
+    message={jt`No matching ${(
+      <strong>&nbsp;{field.display_name}&nbsp;</strong>
+    )} found.`}
+  />
+);
+
+const EveryOptionState = () => (
+  <OptionsMessage
+    message={t`Including every option in your filter probably won’t do much…`}
+  />
+);
+
+const OptionsMessage = ({ message }) => (
+  <div className="flex layout-centered p4">{message}</div>
+);
+
+export default connect(null, mapDispatchToProps)(FieldValuesWidget);
diff --git a/frontend/src/metabase/components/FormField.jsx b/frontend/src/metabase/components/FormField.jsx
index a615e91d64afa863976f1ff822dc717acb07dd41..0f9729268f56344f4bff58745be6beaa9a23313c 100644
--- a/frontend/src/metabase/components/FormField.jsx
+++ b/frontend/src/metabase/components/FormField.jsx
@@ -4,47 +4,53 @@ import PropTypes from "prop-types";
 import cx from "classnames";
 
 export default class FormField extends Component {
-    static propTypes = {
-        // redux-form compatible:
-        name: PropTypes.string,
-        error: PropTypes.any,
-        visited: PropTypes.bool,
-        active: PropTypes.bool,
-
-        displayName: PropTypes.string.isRequired,
-
-        // legacy
-        fieldName: PropTypes.string,
-        errors: PropTypes.object
-    };
-
-    getError() {
-        if (this.props.error && this.props.visited !== false && this.props.active !== true) {
-            return this.props.error;
-        }
-
-        // legacy
-        if (this.props.errors &&
-            this.props.errors.data.errors &&
-            this.props.fieldName in this.props.errors.data.errors) {
-            return this.props.errors.data.errors[this.props.fieldName];
-        }
+  static propTypes = {
+    // redux-form compatible:
+    name: PropTypes.string,
+    error: PropTypes.any,
+    visited: PropTypes.bool,
+    active: PropTypes.bool,
+
+    displayName: PropTypes.string.isRequired,
+
+    // legacy
+    fieldName: PropTypes.string,
+    errors: PropTypes.object,
+  };
+
+  getError() {
+    if (
+      this.props.error &&
+      this.props.visited !== false &&
+      this.props.active !== true
+    ) {
+      return this.props.error;
     }
 
-    render() {
-        let fieldErrorMessage;
-        let fieldError = this.getError();
-        if (fieldError) {
-            fieldErrorMessage = (
-                <span className="text-error mx1">{fieldError}</span>
-            );
-        }
-
-        return (
-            <div className={cx("Form-field", { "Form--fieldError": fieldError })}>
-                <label className="Form-label" htmlFor={this.props.name}>{this.props.displayName} {fieldErrorMessage}</label>
-                {this.props.children}
-            </div>
-        );
+    // legacy
+    if (
+      this.props.errors &&
+      this.props.errors.data.errors &&
+      this.props.fieldName in this.props.errors.data.errors
+    ) {
+      return this.props.errors.data.errors[this.props.fieldName];
     }
+  }
+
+  render() {
+    let fieldErrorMessage;
+    let fieldError = this.getError();
+    if (fieldError) {
+      fieldErrorMessage = <span className="text-error mx1">{fieldError}</span>;
+    }
+
+    return (
+      <div className={cx("Form-field", { "Form--fieldError": fieldError })}>
+        <label className="Form-label" htmlFor={this.props.name}>
+          {this.props.displayName} {fieldErrorMessage}
+        </label>
+        {this.props.children}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Header.jsx b/frontend/src/metabase/components/Header.jsx
index 4faf048a7b1de7ab812c1263269a83d8b78679c8..396bb1ca6f6e195cfe9fc0b3a40bd079b539740f 100644
--- a/frontend/src/metabase/components/Header.jsx
+++ b/frontend/src/metabase/components/Header.jsx
@@ -5,139 +5,164 @@ import Input from "metabase/components/Input.jsx";
 import HeaderModal from "metabase/components/HeaderModal.jsx";
 import TitleAndDescription from "metabase/components/TitleAndDescription.jsx";
 import EditBar from "metabase/components/EditBar.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { getScrollY } from "metabase/lib/dom";
 
 export default class Header extends Component {
-    static defaultProps = {
-        headerButtons: [],
-        editingTitle: "",
-        editingSubtitle: "",
-        editingButtons: [],
-        headerClassName: "py1 lg-py2 xl-py3 wrapper"
+  static defaultProps = {
+    headerButtons: [],
+    editingTitle: "",
+    editingSubtitle: "",
+    editingButtons: [],
+    headerClassName: "py1 lg-py2 xl-py3 wrapper",
+  };
+
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      headerHeight: 0,
     };
+  }
 
-    constructor(props, context) {
-        super(props, context);
+  componentDidMount() {
+    this.updateHeaderHeight();
+  }
 
-        this.state = {
-            headerHeight: 0
-        };
+  componentWillUpdate() {
+    const modalIsOpen = !!this.props.headerModalMessage;
+    if (modalIsOpen) {
+      this.updateHeaderHeight();
     }
+  }
 
-   componentDidMount() {
-        this.updateHeaderHeight();
-   }
+  updateHeaderHeight() {
+    if (!this.refs.header) return;
 
-    componentWillUpdate() {
-        const modalIsOpen = !!this.props.headerModalMessage;
-        if (modalIsOpen) {
-            this.updateHeaderHeight()
-        }
+    const rect = ReactDOM.findDOMNode(this.refs.header).getBoundingClientRect();
+    const headerHeight = rect.top + getScrollY();
+    if (this.state.headerHeight !== headerHeight) {
+      this.setState({ headerHeight });
     }
-
-    updateHeaderHeight() {
-        if (!this.refs.header) return;
-
-        const rect = ReactDOM.findDOMNode(this.refs.header).getBoundingClientRect();
-        const headerHeight = rect.top + getScrollY();
-        if (this.state.headerHeight !== headerHeight) {
-            this.setState({ headerHeight });
-        }
+  }
+
+  setItemAttribute(attribute, event) {
+    this.props.setItemAttributeFn(attribute, event.target.value);
+  }
+
+  renderEditHeader() {
+    if (this.props.isEditing) {
+      return (
+        <EditBar
+          title={this.props.editingTitle}
+          subtitle={this.props.editingSubtitle}
+          buttons={this.props.editingButtons}
+        />
+      );
     }
-
-    setItemAttribute(attribute, event) {
-        this.props.setItemAttributeFn(attribute, event.target.value);
+  }
+
+  renderHeaderModal() {
+    return (
+      <HeaderModal
+        isOpen={!!this.props.headerModalMessage}
+        height={this.state.headerHeight}
+        title={this.props.headerModalMessage}
+        onDone={this.props.onHeaderModalDone}
+        onCancel={this.props.onHeaderModalCancel}
+      />
+    );
+  }
+
+  render() {
+    var titleAndDescription;
+    if (this.props.isEditingInfo) {
+      titleAndDescription = (
+        <div className="Header-title flex flex-column flex-full bordered rounded my1">
+          <Input
+            className="AdminInput text-bold border-bottom rounded-top h3"
+            type="text"
+            value={this.props.item.name || ""}
+            onChange={this.setItemAttribute.bind(this, "name")}
+          />
+          <Input
+            className="AdminInput rounded-bottom h4"
+            type="text"
+            value={this.props.item.description || ""}
+            onChange={this.setItemAttribute.bind(this, "description")}
+            placeholder={t`No description yet`}
+          />
+        </div>
+      );
+    } else {
+      if (this.props.item && this.props.item.id != null) {
+        titleAndDescription = (
+          <TitleAndDescription
+            title={this.props.item.name}
+            description={this.props.item.description}
+          />
+        );
+      } else {
+        titleAndDescription = (
+          <TitleAndDescription
+            title={t`New ${this.props.objectType}`}
+            description={this.props.item.description}
+          />
+        );
+      }
     }
 
-    renderEditHeader() {
-        if (this.props.isEditing) {
-            return (
-                <EditBar
-                    title={this.props.editingTitle}
-                    subtitle={this.props.editingSubtitle}
-                    buttons={this.props.editingButtons}
-                />
-            )
-        }
+    var attribution;
+    if (this.props.item && this.props.item.creator) {
+      attribution = (
+        <div className="Header-attribution">
+          {t`Asked by ${this.props.item.creator.common_name}`}
+        </div>
+      );
     }
 
-    renderHeaderModal() {
+    var headerButtons = this.props.headerButtons.map(
+      (section, sectionIndex) => {
         return (
-            <HeaderModal
-                isOpen={!!this.props.headerModalMessage}
-                height={this.state.headerHeight}
-                title={this.props.headerModalMessage}
-                onDone={this.props.onHeaderModalDone}
-                onCancel={this.props.onHeaderModalCancel}
-            />
-        );
-    }
-
-    render() {
-        var titleAndDescription;
-        if (this.props.isEditingInfo) {
-            titleAndDescription = (
-                <div className="Header-title flex flex-column flex-full bordered rounded my1">
-                    <Input className="AdminInput text-bold border-bottom rounded-top h3" type="text" value={this.props.item.name || ""} onChange={this.setItemAttribute.bind(this, "name")}/>
-                    <Input className="AdminInput rounded-bottom h4" type="text" value={this.props.item.description || ""} onChange={this.setItemAttribute.bind(this, "description")} placeholder={t`No description yet`} />
-                </div>
-            );
-        } else {
-            if (this.props.item && this.props.item.id != null) {
-                titleAndDescription = (
-                    <TitleAndDescription
-                        title={this.props.item.name}
-                        description={this.props.item.description}
-                    />
-                );
-            } else {
-                titleAndDescription = (
-                    <TitleAndDescription
-                        title={t`New ${this.props.objectType}`}
-                        description={this.props.item.description}
-                    />
-                );
-            }
-        }
-
-        var attribution;
-        if (this.props.item && this.props.item.creator) {
-            attribution = (
-                <div className="Header-attribution">
-                    {t`Asked by ${this.props.item.creator.common_name}`}
-                </div>
-            );
-        }
-
-        var headerButtons = this.props.headerButtons.map((section, sectionIndex) => {
-            return section && section.length > 0 && (
-                <span key={sectionIndex} className="Header-buttonSection flex align-center">
-                    {section.map((button, buttonIndex) =>
-                        <span key={buttonIndex} className="Header-button">
-                            {button}
-                        </span>
-                    )}
+          section &&
+          section.length > 0 && (
+            <span
+              key={sectionIndex}
+              className="Header-buttonSection flex align-center"
+            >
+              {section.map((button, buttonIndex) => (
+                <span key={buttonIndex} className="Header-button">
+                  {button}
                 </span>
-            );
-        });
-
-        return (
-            <div>
-                {this.renderEditHeader()}
-                {this.renderHeaderModal()}
-                <div className={"QueryBuilder-section flex align-center " + this.props.headerClassName} ref="header">
-                    <div className="Entity py3">
-                        {titleAndDescription}
-                        {attribution}
-                    </div>
-
-                    <div className="flex align-center flex-align-right">
-                        {headerButtons}
-                    </div>
-                </div>
-                {this.props.children}
-            </div>
+              ))}
+            </span>
+          )
         );
-    }
+      },
+    );
+
+    return (
+      <div>
+        {this.renderEditHeader()}
+        {this.renderHeaderModal()}
+        <div
+          className={
+            "QueryBuilder-section flex align-center " +
+            this.props.headerClassName
+          }
+          ref="header"
+        >
+          <div className="Entity py3">
+            {titleAndDescription}
+            {attribution}
+          </div>
+
+          <div className="flex align-center flex-align-right">
+            {headerButtons}
+          </div>
+        </div>
+        {this.props.children}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/HeaderBar.jsx b/frontend/src/metabase/components/HeaderBar.jsx
index 6f098d72f1f916cb9110ab6080dce45c11e26bb0..8776765bf25491b5bf6b99f2b4a806c6255803b9 100644
--- a/frontend/src/metabase/components/HeaderBar.jsx
+++ b/frontend/src/metabase/components/HeaderBar.jsx
@@ -2,59 +2,79 @@ import React, { Component } from "react";
 
 import Input from "metabase/components/Input.jsx";
 import TitleAndDescription from "metabase/components/TitleAndDescription.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 export default class Header extends Component {
+  static defaultProps = {
+    buttons: null,
+    className: "py1 lg-py2 xl-py3 wrapper",
+    breadcrumb: null,
+  };
 
-    static defaultProps = {
-        buttons: null,
-        className: "py1 lg-py2 xl-py3 wrapper",
-        breadcrumb: null
-    };
+  render() {
+    const {
+      isEditing,
+      name,
+      description,
+      breadcrumb,
+      buttons,
+      className,
+      badge,
+    } = this.props;
 
-    render() {
-        const { isEditing, name, description, breadcrumb, buttons, className, badge } = this.props;
-
-        let titleAndDescription;
-        if (isEditing) {
-            titleAndDescription = (
-                <div className="Header-title flex flex-column flex-full bordered rounded my1">
-                    <Input className="AdminInput text-bold border-bottom rounded-top h3" type="text" value={name} onChange={(e) => this.props.setItemAttributeFn("name", e.target.value)} />
-                    <Input className="AdminInput rounded-bottom h4" type="text" value={description} onChange={(e) => this.props.setItemAttributeFn("description", e.target.value)} placeholder={t`No description yet`} />
-                </div>
-            );
-        } else {
-            if (name && description) {
-                titleAndDescription = (
-                    <TitleAndDescription
-                        title={name}
-                        description={description}
-                    />
-                );
-            } else {
-                titleAndDescription = (
-                    <div className="flex align-baseline">
-                        <h1 className="Header-title-name my1">{name}</h1> {breadcrumb}
-                    </div>
-                );
+    let titleAndDescription;
+    if (isEditing) {
+      titleAndDescription = (
+        <div className="Header-title flex flex-column flex-full bordered rounded my1">
+          <Input
+            className="AdminInput text-bold border-bottom rounded-top h3"
+            type="text"
+            value={name}
+            onChange={e =>
+              this.props.setItemAttributeFn("name", e.target.value)
             }
-        }
-
-        return (
-            // TODO Atte Keinänen 5/16/17 Take care of the mobile layout with the multimetrics UI
-            <div className={cx("QueryBuilder-section pt2 sm-pt2 flex align-center", className)}>
-                <div className={cx("relative flex-full")}>
-                    {titleAndDescription}
-                    { badge &&
-                    <div>{badge}</div>
-                    }
-                </div>
-
-                <div className="flex-align-right hide sm-show">
-                    {buttons}
-                </div>
-            </div>
+          />
+          <Input
+            className="AdminInput rounded-bottom h4"
+            type="text"
+            value={description}
+            onChange={e =>
+              this.props.setItemAttributeFn("description", e.target.value)
+            }
+            placeholder={t`No description yet`}
+          />
+        </div>
+      );
+    } else {
+      if (name && description) {
+        titleAndDescription = (
+          <TitleAndDescription title={name} description={description} />
         );
+      } else {
+        titleAndDescription = (
+          <div className="flex align-baseline">
+            <h1 className="Header-title-name my1">{name}</h1> {breadcrumb}
+          </div>
+        );
+      }
     }
+
+    return (
+      // TODO Atte Keinänen 5/16/17 Take care of the mobile layout with the multimetrics UI
+      <div
+        className={cx(
+          "QueryBuilder-section pt2 sm-pt2 flex align-center",
+          className,
+        )}
+      >
+        <div className={cx("relative flex-full")}>
+          {titleAndDescription}
+          {badge && <div>{badge}</div>}
+        </div>
+
+        <div className="flex-align-right hide sm-show">{buttons}</div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/HeaderModal.jsx b/frontend/src/metabase/components/HeaderModal.jsx
index ee5bd922479ee919bb70a7aba6f693b9d0105654..3a6732d8e992188853cb7701a10f9c00b0d91fc9 100644
--- a/frontend/src/metabase/components/HeaderModal.jsx
+++ b/frontend/src/metabase/components/HeaderModal.jsx
@@ -2,36 +2,54 @@ import React, { Component } from "react";
 
 import BodyComponent from "metabase/components/BodyComponent";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 @BodyComponent
 export default class HeaderModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            initialTop: "-100%"
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      initialTop: "-100%",
+    };
+  }
 
-    componentDidMount() {
-        this.setState({ initialTop: 0 });
-    }
+  componentDidMount() {
+    this.setState({ initialTop: 0 });
+  }
 
-    render() {
-        const { className, height, title, onDone, onCancel, isOpen } = this.props;
-        const { initialTop } = this.state;
-        return (
-            <div
-                className={cx(className, "absolute top left right bg-brand flex flex-column layout-centered")}
-                style={{ zIndex: 4, height: height, minHeight: 50, transform: `translateY(${isOpen ? initialTop : "-100%"})`, transition: "transform 400ms ease-in-out", overflow: 'hidden' }}
-            >
-                    <h2 className="text-white pb2">{title}</h2>
-                    <div className="flex layout-centered">
-                        <button className="Button Button--borderless text-brand bg-white text-bold" onClick={onDone}>{t`Done`}</button>
-                        { onCancel && <span className="text-white mx1">or</span> }
-                        { onCancel && <a className="cursor-pointer text-white text-bold" onClick={onCancel}>{t`Cancel`}</a> }
-                    </div>
-            </div>
-        );
-    }
+  render() {
+    const { className, height, title, onDone, onCancel, isOpen } = this.props;
+    const { initialTop } = this.state;
+    return (
+      <div
+        className={cx(
+          className,
+          "absolute top left right bg-brand flex flex-column layout-centered",
+        )}
+        style={{
+          zIndex: 4,
+          height: height,
+          minHeight: 50,
+          transform: `translateY(${isOpen ? initialTop : "-100%"})`,
+          transition: "transform 400ms ease-in-out",
+          overflow: "hidden",
+        }}
+      >
+        <h2 className="text-white pb2">{title}</h2>
+        <div className="flex layout-centered">
+          <button
+            className="Button Button--borderless text-brand bg-white text-bold"
+            onClick={onDone}
+          >{t`Done`}</button>
+          {onCancel && <span className="text-white mx1">or</span>}
+          {onCancel && (
+            <a
+              className="cursor-pointer text-white text-bold"
+              onClick={onCancel}
+            >{t`Cancel`}</a>
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/HeaderWithBack.jsx b/frontend/src/metabase/components/HeaderWithBack.jsx
index b3fbfbf2b889d6c4a7533028f04f1dfa70025eee..f94f0a257211add084bc4bf10ed1d5e30edac988 100644
--- a/frontend/src/metabase/components/HeaderWithBack.jsx
+++ b/frontend/src/metabase/components/HeaderWithBack.jsx
@@ -6,23 +6,21 @@ import TitleAndDescription from "metabase/components/TitleAndDescription";
 
 const DEFAULT_BACK = () => window.history.back();
 
-const HeaderWithBack = ({ name, description, onBack }) =>
-    <div className="flex align-center">
-        { (onBack || window.history.length > 1) &&
-            <Icon
-                className="cursor-pointer text-brand mr2 flex align-center circle p2 bg-light-blue bg-brand-hover text-white-hover transition-background transition-color"
-                name="backArrow"
-                onClick={onBack || DEFAULT_BACK}
-            />
-        }
-        <TitleAndDescription
-            title={name}
-            description={description}
-        />
-    </div>
+const HeaderWithBack = ({ name, description, onBack }) => (
+  <div className="flex align-center">
+    {(onBack || window.history.length > 1) && (
+      <Icon
+        className="cursor-pointer text-brand mr2 flex align-center circle p2 bg-light-blue bg-brand-hover text-white-hover transition-background transition-color"
+        name="backArrow"
+        onClick={onBack || DEFAULT_BACK}
+      />
+    )}
+    <TitleAndDescription title={name} description={description} />
+  </div>
+);
 
 HeaderWithBack.propTypes = {
-    name: PropTypes.string.isRequired
-}
+  name: PropTypes.string.isRequired,
+};
 
 export default HeaderWithBack;
diff --git a/frontend/src/metabase/components/HistoryModal.jsx b/frontend/src/metabase/components/HistoryModal.jsx
index 8214932289a6838148b24680b454af340cb05ba7..a5cf9f406c4091bb57681f0f0b9aae861250f906 100644
--- a/frontend/src/metabase/components/HistoryModal.jsx
+++ b/frontend/src/metabase/components/HistoryModal.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ActionButton from "metabase/components/ActionButton.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
@@ -8,109 +8,124 @@ import ModalContent from "metabase/components/ModalContent.jsx";
 import moment from "moment";
 
 function formatDate(date) {
-    var m = moment(date);
-    if (m.isSame(moment(), 'day')) {
-        return t`Today, ` + m.format("h:mm a");
-    } else if (m.isSame(moment().subtract(1, "day"), "day")) {
-        return t`Yesterday, ` + m.format("h:mm a");
-    } else {
-        return m.format("MMM D YYYY, h:mm a");
-    }
+  var m = moment(date);
+  if (m.isSame(moment(), "day")) {
+    return t`Today, ` + m.format("h:mm a");
+  } else if (m.isSame(moment().subtract(1, "day"), "day")) {
+    return t`Yesterday, ` + m.format("h:mm a");
+  } else {
+    return m.format("MMM D YYYY, h:mm a");
+  }
 }
 
 export default class HistoryModal extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            error: null
-        };
-    }
+    this.state = {
+      error: null,
+    };
+  }
 
-    static propTypes = {
-        revisions: PropTypes.array,
-        entityType: PropTypes.string.isRequired,
-        entityId: PropTypes.number.isRequired,
+  static propTypes = {
+    revisions: PropTypes.array,
+    entityType: PropTypes.string.isRequired,
+    entityId: PropTypes.number.isRequired,
 
-        onFetchRevisions: PropTypes.func.isRequired,
-        onRevertToRevision: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired,
-        onReverted: PropTypes.func.isRequired
-    };
+    onFetchRevisions: PropTypes.func.isRequired,
+    onRevertToRevision: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    onReverted: PropTypes.func.isRequired,
+  };
 
-    async componentDidMount() {
-        let { entityType, entityId } = this.props;
+  async componentDidMount() {
+    let { entityType, entityId } = this.props;
 
-        try {
-            await this.props.onFetchRevisions({ entity: entityType, id: entityId });
-        } catch (error) {
-            this.setState({ error: error });
-        }
+    try {
+      await this.props.onFetchRevisions({ entity: entityType, id: entityId });
+    } catch (error) {
+      this.setState({ error: error });
     }
+  }
 
-    async revert(revision) {
-        let { entityType, entityId } = this.props;
-        try {
-            await this.props.onRevertToRevision({ entity: entityType, id: entityId, revision_id: revision.id });
-            this.props.onReverted();
-        } catch (e) {
-            console.warn("revert failed", e);
-            throw e;
-        }
+  async revert(revision) {
+    let { entityType, entityId } = this.props;
+    try {
+      await this.props.onRevertToRevision({
+        entity: entityType,
+        id: entityId,
+        revision_id: revision.id,
+      });
+      this.props.onReverted();
+    } catch (e) {
+      console.warn("revert failed", e);
+      throw e;
     }
+  }
 
-    revisionDescription(revision) {
-        if (revision.is_creation) {
-            return t`First revision.`;
-        } else if (revision.is_reversion) {
-            return t`Reverted to an earlier revision and ${revision.description}`;
-        } else {
-            return revision.description;
-        }
+  revisionDescription(revision) {
+    if (revision.is_creation) {
+      return t`First revision.`;
+    } else if (revision.is_reversion) {
+      return t`Reverted to an earlier revision and ${revision.description}`;
+    } else {
+      return revision.description;
     }
+  }
 
-    render() {
-        var { revisions } = this.props;
-        return (
-            <ModalContent
-                title={t`Revision history`}
-                onClose={() => this.props.onClose()}
-            >
-                <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">
-                            <span className="flex-half">{t`When`}</span>
-                            <span className="flex-half">{t`Who`}</span>
-                            <span className="flex-full">{t`What`}</span>
+  render() {
+    var { revisions } = this.props;
+    return (
+      <ModalContent
+        title={t`Revision history`}
+        onClose={() => this.props.onClose()}
+      >
+        <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">
+                <span className="flex-half">{t`When`}</span>
+                <span className="flex-half">{t`Who`}</span>
+                <span className="flex-full">{t`What`}</span>
+              </div>
+              <div className="px2 scroll-y">
+                {revisions.map((revision, index) => (
+                  <div
+                    key={revision.id}
+                    className="border-row-divider flex py1 px2"
+                  >
+                    <span className="flex-half">
+                      {formatDate(revision.timestamp)}
+                    </span>
+                    <span className="flex-half">
+                      {revision.user.common_name}
+                    </span>
+                    <span className="flex-full flex">
+                      <span>{this.revisionDescription(revision)}</span>
+                      {index !== 0 ? (
+                        <div className="flex-align-right pl1">
+                          <ActionButton
+                            actionFn={() => this.revert(revision)}
+                            className="Button Button--small Button--danger text-uppercase"
+                            normalText={t`Revert`}
+                            activeText={t`Reverting…`}
+                            failedText={t`Revert failed`}
+                            successText={t`Reverted`}
+                          />
                         </div>
-                        <div className="px2 scroll-y">
-                            {revisions.map((revision, index) =>
-                                <div key={revision.id} className="border-row-divider flex py1 px2">
-                                    <span className="flex-half">{formatDate(revision.timestamp)}</span>
-                                    <span className="flex-half">{revision.user.common_name}</span>
-                                    <span className="flex-full flex">
-                                        <span>{this.revisionDescription(revision)}</span>
-                                        {index !== 0 ?
-                                            <div className="flex-align-right pl1">
-                                                <ActionButton
-                                                    actionFn={() => this.revert(revision)}
-                                                    className="Button Button--small Button--danger text-uppercase"
-                                                    normalText={t`Revert`}
-                                                    activeText={t`Reverting…`}
-                                                    failedText={t`Revert failed`}
-                                                    successText={t`Reverted`}
-                                                />
-                                            </div>
-                                        : null}
-                                    </span>
-                                </div>
-                            )}
-                        </div>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </ModalContent>
-        );
-    }
+                      ) : null}
+                    </span>
+                  </div>
+                ))}
+              </div>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Icon.jsx b/frontend/src/metabase/components/Icon.jsx
index ec82633b49a6b9036f757048fdfe78b90cd16fce..23a2dc73f38a88bef0f0da8865575148f3d6315b 100644
--- a/frontend/src/metabase/components/Icon.jsx
+++ b/frontend/src/metabase/components/Icon.jsx
@@ -4,49 +4,62 @@ import React, { Component } from "react";
 import RetinaImage from "react-retina-image";
 import cx from "classnames";
 
-import { loadIcon } from 'metabase/icon_paths';
+import { loadIcon } from "metabase/icon_paths";
 
 import Tooltipify from "metabase/hoc/Tooltipify";
 
 @Tooltipify
 export default class Icon extends Component {
-    static props: {
-        name: string,
-        size?: string | number,
-        width?: string | number,
-        height?: string | number,
-        scale?: string | number,
-        tooltip?: string, // using Tooltipify
-        className?: string
-    }
+  static props: {
+    name: string,
+    size?: string | number,
+    width?: string | number,
+    height?: string | number,
+    scale?: string | number,
+    tooltip?: string, // using Tooltipify
+    className?: string,
+  };
 
-    render() {
-        const icon = loadIcon(this.props.name);
-        if (!icon) {
-            return null;
-        }
-        const className = cx(icon.attrs && icon.attrs.className, this.props.className)
-        const props = { ...icon.attrs, ...this.props, className };
-        for (const prop of ["width", "height", "size", "scale"]) {
-            if (typeof props[prop] === "string") {
-                props[prop] = parseInt(props[prop], 10);
-            }
-        }
-        if (props.size != null) {
-            props.width = props.size;
-            props.height = props.size;
-        }
-        if (props.scale != null && props.width != null && props.height != null) {
-            props.width *= props.scale;
-            props.height *= props.scale;
-        }
+  render() {
+    const icon = loadIcon(this.props.name);
+    if (!icon) {
+      return null;
+    }
+    const className = cx(
+      icon.attrs && icon.attrs.className,
+      this.props.className,
+    );
+    const props = { ...icon.attrs, ...this.props, className };
+    for (const prop of ["width", "height", "size", "scale"]) {
+      if (typeof props[prop] === "string") {
+        props[prop] = parseInt(props[prop], 10);
+      }
+    }
+    if (props.size != null) {
+      props.width = props.size;
+      props.height = props.size;
+    }
+    if (props.scale != null && props.width != null && props.height != null) {
+      props.width *= props.scale;
+      props.height *= props.scale;
+    }
 
-        if (icon.img) {
-            return (<RetinaImage forceOriginalDimensions={false} {...props} src={icon.img} />);
-        } else if (icon.svg) {
-            return (<svg {...props} dangerouslySetInnerHTML={{__html: icon.svg}}></svg>);
-        } else {
-            return (<svg {...props}><path d={icon.path} /></svg>);
-        }
+    if (icon.img) {
+      return (
+        <RetinaImage
+          forceOriginalDimensions={false}
+          {...props}
+          src={icon.img}
+        />
+      );
+    } else if (icon.svg) {
+      return <svg {...props} dangerouslySetInnerHTML={{ __html: icon.svg }} />;
+    } else {
+      return (
+        <svg {...props}>
+          <path d={icon.path} />
+        </svg>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/components/IconBorder.jsx b/frontend/src/metabase/components/IconBorder.jsx
index f249b505b2a49de2c2b6e63d5acd9c940d4cf783..5b97928adb263e65d6449a2700bafb0c7eff50b8 100644
--- a/frontend/src/metabase/components/IconBorder.jsx
+++ b/frontend/src/metabase/components/IconBorder.jsx
@@ -13,41 +13,48 @@ import cx from "classnames";
  */
 
 export default class IconBorder extends Component {
+  static propTypes = {
+    borderWidth: PropTypes.string,
+    borderStyle: PropTypes.string,
+    borderColor: PropTypes.string,
+    borderRadius: PropTypes.string,
+    style: PropTypes.object,
+    children: PropTypes.any.isRequired,
+  };
 
-    static propTypes = {
-        borderWidth: PropTypes.string,
-        borderStyle: PropTypes.string,
-        borderColor: PropTypes.string,
-        borderRadius: PropTypes.string,
-        style: PropTypes.object,
-        children: PropTypes.any.isRequired
-    };
+  static defaultProps = {
+    borderWidth: "1px",
+    borderStyle: "solid",
+    borderColor: "currentcolor",
+    borderRadius: "99px",
+    style: {},
+  };
 
-    static defaultProps = {
-        borderWidth: '1px',
-        borderStyle: 'solid',
-        borderColor: 'currentcolor',
-        borderRadius: '99px',
-        style: {},
+  render() {
+    const {
+      borderWidth,
+      borderStyle,
+      borderColor,
+      borderRadius,
+      className,
+      style,
+      children,
+    } = this.props;
+    const size = parseInt(children.props.size || children.props.width, 10) * 2;
+    const styles = {
+      width: size,
+      height: size,
+      borderWidth: borderWidth,
+      borderStyle: borderStyle,
+      borderColor: borderColor,
+      borderRadius: borderRadius,
+      ...style,
     };
 
-    render() {
-        const { borderWidth, borderStyle, borderColor, borderRadius, className, style, children } = this.props;
-        const size = parseInt(children.props.size || children.props.width, 10) * 2
-        const styles = {
-            width: size,
-            height: size,
-            borderWidth: borderWidth,
-            borderStyle: borderStyle,
-            borderColor: borderColor,
-            borderRadius: borderRadius,
-            ...style
-        }
-
-        return (
-            <div className={cx('flex layout-centered', className)} style={styles}>
-                {children}
-            </div>
-        );
-    }
+    return (
+      <div className={cx("flex layout-centered", className)} style={styles}>
+        {children}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Input.jsx b/frontend/src/metabase/components/Input.jsx
index 9d754421d076894331c8f61fc03504560bc56667..8bcc0985caf6485b761ac75778f08c7faad0523c 100644
--- a/frontend/src/metabase/components/Input.jsx
+++ b/frontend/src/metabase/components/Input.jsx
@@ -4,44 +4,60 @@ import PropTypes from "prop-types";
 import _ from "underscore";
 
 export default class Input extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.onBlur = this.onBlur.bind(this);
-        this.onChange = this.onChange.bind(this);
-        this.state = { value: props.value };
+  constructor(props, context) {
+    super(props, context);
+    this.onBlur = this.onBlur.bind(this);
+    this.onChange = this.onChange.bind(this);
+    this.state = { value: props.value };
+  }
+
+  static propTypes = {
+    type: PropTypes.string,
+    value: PropTypes.string,
+    placeholder: PropTypes.string,
+    onChange: PropTypes.func,
+    onBlurChange: PropTypes.func,
+  };
+
+  static defaultProps = {
+    type: "text",
+  };
+
+  componentWillReceiveProps(newProps) {
+    this.setState({ value: newProps.value });
+  }
+
+  onChange(event) {
+    this.setState({ value: event.target.value });
+    if (this.props.onChange) {
+      this.props.onChange(event);
     }
-
-    static propTypes = {
-        type: PropTypes.string,
-        value: PropTypes.string,
-        placeholder: PropTypes.string,
-        onChange: PropTypes.func,
-        onBlurChange: PropTypes.func
-    };
-
-    static defaultProps = {
-        type: "text"
-    };
-
-    componentWillReceiveProps(newProps) {
-        this.setState({ value: newProps.value });
-    }
-
-    onChange(event) {
-        this.setState({ value:  event.target.value });
-        if (this.props.onChange) {
-            this.props.onChange(event);
-        }
-    }
-
-    onBlur(event) {
-        if (this.props.onBlurChange && (this.props.value || "") !== event.target.value) {
-            this.props.onBlurChange(event);
-        }
-    }
-
-    render() {
-        let props = _.omit(this.props, "onBlurChange", "value", "onBlur", "onChange");
-        return <input {...props} value={this.state.value} onBlur={this.onBlur} onChange={this.onChange} />
+  }
+
+  onBlur(event) {
+    if (
+      this.props.onBlurChange &&
+      (this.props.value || "") !== event.target.value
+    ) {
+      this.props.onBlurChange(event);
     }
+  }
+
+  render() {
+    let props = _.omit(
+      this.props,
+      "onBlurChange",
+      "value",
+      "onBlur",
+      "onChange",
+    );
+    return (
+      <input
+        {...props}
+        value={this.state.value}
+        onBlur={this.onBlur}
+        onChange={this.onChange}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/LabelIcon.css b/frontend/src/metabase/components/LabelIcon.css
index 37129f4ef1398a4570c893fe2e051e0b7282c37d..8be0d6cc48eb976ab40dd00ddc058508c3a802fa 100644
--- a/frontend/src/metabase/components/LabelIcon.css
+++ b/frontend/src/metabase/components/LabelIcon.css
@@ -1,17 +1,16 @@
-
 :local(.colorIcon) {
-    composes: inline-block from "style";
-    width: 18px;
-    height: 18px;
-    border-radius: 3px;
+  composes: inline-block from "style";
+  width: 18px;
+  height: 18px;
+  border-radius: 3px;
 }
 
 :local(.emojiIcon) {
-    composes: text-centered from "style";
-    font-size: 20px;
+  composes: text-centered from "style";
+  font-size: 20px;
 }
 
 :local(.icon) {
-    composes: transition-color from "style";
-    color: currentColor;
+  composes: transition-color from "style";
+  color: currentColor;
 }
diff --git a/frontend/src/metabase/components/LabelIcon.jsx b/frontend/src/metabase/components/LabelIcon.jsx
index b62b0752479e5f49ecc6358b426a04dc51fc9390..e805c0e136627c9509999089371e102055a826a5 100644
--- a/frontend/src/metabase/components/LabelIcon.jsx
+++ b/frontend/src/metabase/components/LabelIcon.jsx
@@ -9,20 +9,27 @@ import EmojiIcon from "./EmojiIcon.jsx";
 import cx from "classnames";
 
 const LabelIcon = ({ icon, size = 18, className, style }) =>
-    !icon ?
-        null
-    : icon.charAt(0) === ":" ?
-        <EmojiIcon className={cx(S.icon, S.emojiIcon, className)} name={icon} size={size} style={style} />
-    : icon.charAt(0) === "#" ?
-        <span className={cx(S.icon, S.colorIcon, className)} style={{ backgroundColor: icon, width: size, height: size }}></span>
-    :
-        <Icon className={cx(S.icon, className)} name={icon} />
+  !icon ? null : icon.charAt(0) === ":" ? (
+    <EmojiIcon
+      className={cx(S.icon, S.emojiIcon, className)}
+      name={icon}
+      size={size}
+      style={style}
+    />
+  ) : icon.charAt(0) === "#" ? (
+    <span
+      className={cx(S.icon, S.colorIcon, className)}
+      style={{ backgroundColor: icon, width: size, height: size }}
+    />
+  ) : (
+    <Icon className={cx(S.icon, className)} name={icon} />
+  );
 
 LabelIcon.propTypes = {
-    className:  PropTypes.string,
-    style:      PropTypes.object,
-    icon:       PropTypes.string,
-    size:       PropTypes.number,
+  className: PropTypes.string,
+  style: PropTypes.object,
+  icon: PropTypes.string,
+  size: PropTypes.number,
 };
 
 export default LabelIcon;
diff --git a/frontend/src/metabase/components/LeftNavPane.jsx b/frontend/src/metabase/components/LeftNavPane.jsx
index f1cb84876af89accab19e04640ec49a505a17485..fde6cc172399b91ddf7293a53f6b92bf7fc3e0d7 100644
--- a/frontend/src/metabase/components/LeftNavPane.jsx
+++ b/frontend/src/metabase/components/LeftNavPane.jsx
@@ -1,39 +1,48 @@
 import React from "react";
 import { Link, IndexLink } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export function LeftNavPaneItem({ name, path, index = false }) {
-    return (
-        <li>
-            { index ?
-                <IndexLink to={path} className="AdminList-item flex align-center justify-between no-decoration" activeClassName="selected" >
-                    {name}
-                </IndexLink>
-            :
-                <Link to={path} className="AdminList-item flex align-center justify-between no-decoration" activeClassName="selected" >
-                    {name}
-                </Link>
-            }
-        </li>
-    );
+  return (
+    <li>
+      {index ? (
+        <IndexLink
+          to={path}
+          className="AdminList-item flex align-center justify-between no-decoration"
+          activeClassName="selected"
+        >
+          {name}
+        </IndexLink>
+      ) : (
+        <Link
+          to={path}
+          className="AdminList-item flex align-center justify-between no-decoration"
+          activeClassName="selected"
+        >
+          {name}
+        </Link>
+      )}
+    </li>
+  );
 }
 
 export function LeftNavPaneItemBack({ path }) {
-    return (
-        <li>
-            <Link to={path} className="AdminList-item flex align-center justify-between no-decoration link text-bold">
-                &lt; {t`Back`}
-            </Link>
-        </li>
-    );
+  return (
+    <li>
+      <Link
+        to={path}
+        className="AdminList-item flex align-center justify-between no-decoration link text-bold"
+      >
+        &lt; {t`Back`}
+      </Link>
+    </li>
+  );
 }
 
 export function LeftNavPane({ children }) {
-    return (
-        <div className="MetadataEditor-table-list AdminList flex-no-shrink full-height">
-            <ul className="AdminList-items pt1">
-                {children}
-            </ul>
-        </div>
-    );
+  return (
+    <div className="MetadataEditor-table-list AdminList flex-no-shrink full-height">
+      <ul className="AdminList-items pt1">{children}</ul>
+    </div>
+  );
 }
diff --git a/frontend/src/metabase/components/Link.jsx b/frontend/src/metabase/components/Link.jsx
index 33087e00876e3cad6a3055eedd51f88b5cf790fb..df456fb1ede88ed3e6938b527632ce2481ad5a45 100644
--- a/frontend/src/metabase/components/Link.jsx
+++ b/frontend/src/metabase/components/Link.jsx
@@ -1,13 +1,10 @@
 import React from "react";
 import { Link as ReactRouterLink } from "react-router";
 
-const Link = ({ to, className, children, ...props }) =>
-    <ReactRouterLink
-        to={to}
-        className={className || "link"}
-        {...props}
-    >
-        {children}
-    </ReactRouterLink>
+const Link = ({ to, className, children, ...props }) => (
+  <ReactRouterLink to={to} className={className || "link"} {...props}>
+    {children}
+  </ReactRouterLink>
+);
 
 export default Link;
diff --git a/frontend/src/metabase/components/List.css b/frontend/src/metabase/components/List.css
index b90fb3425882a78356b384da05a5ff2816e8aa5c..91ae511cd6bd3b6837ec60d319f4e14ef1cc0528 100644
--- a/frontend/src/metabase/components/List.css
+++ b/frontend/src/metabase/components/List.css
@@ -1,8 +1,8 @@
 :root {
-    --title-color: #606E7B;
-    --subtitle-color: #AAB7C3;
-    --muted-color: #DEEAF1;
-    --blue-color: #2D86D4;
+  --title-color: #606e7b;
+  --subtitle-color: #aab7c3;
+  --muted-color: #deeaf1;
+  --blue-color: #2d86d4;
 }
 
 :local(.list) {
@@ -17,185 +17,185 @@
 }
 
 :local(.list) a {
-    text-decoration: none;
+  text-decoration: none;
 }
 
 :local(.header) {
-    composes: flex flex-row from "style";
-    composes: mt4 mb2 from "style";
-    color: var(--title-color);
-    font-size: 24px;
-    min-height: 48px;
+  composes: flex flex-row from "style";
+  composes: mt4 mb2 from "style";
+  color: var(--title-color);
+  font-size: 24px;
+  min-height: 48px;
 }
 
 :local(.headerBody) {
-    composes: flex flex-full pb2 border-bottom from "style";
-    align-items: center;
-    height: 100%;
-    border-color: #EDF5FB;
+  composes: flex flex-full pb2 border-bottom from "style";
+  align-items: center;
+  height: 100%;
+  border-color: #edf5fb;
 }
 
 :local(.headerLink) {
-    composes: text-brand ml2 flex-no-shrink from "style";
-    font-size: 14px;
+  composes: text-brand ml2 flex-no-shrink from "style";
+  font-size: 14px;
 }
 
 :local(.headerButton) {
-    composes: flex ml1 align-center from "style";
-    font-size: 14px;
+  composes: flex ml1 align-center from "style";
+  font-size: 14px;
 }
 
 :local(.empty) {
-    composes: full flex justify-center from "style";
-    padding-top: 75px;
+  composes: full flex justify-center from "style";
+  padding-top: 75px;
 }
 
 :local(.item) {
-    composes: flex align-center from "style";
-    composes: relative from "style";
+  composes: flex align-center from "style";
+  composes: relative from "style";
 }
 
 :local(.itemBody) {
-    composes: border-top from "style";
-    composes: flex-full from "style";
-    max-width: 550px;
-    padding-top: 20px;
-    padding-bottom: 20px;
-    border-color: #EDF5FB;
+  composes: border-top from "style";
+  composes: flex-full from "style";
+  max-width: 550px;
+  padding-top: 20px;
+  padding-bottom: 20px;
+  border-color: #edf5fb;
 }
 
 :local(.itemTitle) {
-    composes: text-bold inline-block from "style";
-    max-width: 100%;
-    overflow: hidden;
-    color: var(--title-color);
-    font-size: 18px;
+  composes: text-bold inline-block from "style";
+  max-width: 100%;
+  overflow: hidden;
+  color: var(--title-color);
+  font-size: 18px;
 }
 
 :local(.itemName) {
-    composes: mr1 from "style";
-    composes: inline-block from "style";
-    max-width: 100%;
-    overflow: hidden;
+  composes: mr1 from "style";
+  composes: inline-block from "style";
+  max-width: 100%;
+  overflow: hidden;
 }
 
 :local(.itemName):hover {
-    color: var(--blue-color);
+  color: var(--blue-color);
 }
 
 :local(.itemSubtitle) {
-    color: var(--subtitle-color);
-    font-size: 14px;
+  color: var(--subtitle-color);
+  font-size: 14px;
 }
 
 :local(.itemSubtitleLight) {
-    color: var(--subtitle-color);
-    font-size: 14px;
+  color: var(--subtitle-color);
+  font-size: 14px;
 }
 
 :local(.itemSubtitleBold) {
-    color: var(--title-color);
+  color: var(--title-color);
 }
 
 :local(.icons) {
-    composes: flex flex-row align-center from "style";
+  composes: flex flex-row align-center from "style";
 }
 :local(.leftIcons) {
-    composes: flex-no-shrink flex layout-centered mr2 from "style";
-    composes: icons;
-    width: 48px;
+  composes: flex-no-shrink flex layout-centered mr2 from "style";
+  composes: icons;
+  width: 48px;
 }
 :local(.rightIcons) {
-    composes: icons;
+  composes: icons;
 }
 
 :local(.extraIcons) {
-    composes: icons;
-    composes: absolute top full-height from "style";
-    right: -40px;
+  composes: icons;
+  composes: absolute top full-height from "style";
+  right: -40px;
 }
 
 /* hack fix for IE 11 which was hiding the archive icon */
 @media all and (-ms-high-contrast: none), (-ms-high-contrast: active) {
   :local(.extraIcons) {
-      composes: icons;
+    composes: icons;
   }
 }
 
 :local(.icon) {
-    composes: relative from "style";
-    color: var(--muted-color);
+  composes: relative from "style";
+  color: var(--muted-color);
 }
 
 :local(.item) :local(.icon) {
-    visibility: hidden;
+  visibility: hidden;
 }
 :local(.item):hover :local(.icon) {
-    visibility: visible;
+  visibility: visible;
 }
 :local(.icon):hover {
-    color: var(--blue-color);
-    transition: color .3s linear;
+  color: var(--blue-color);
+  transition: color 0.3s linear;
 }
 
 /* ITEM CHECKBOX */
 :local(.itemCheckbox) {
-    composes: icon;
-    display: none;
-    visibility: visible !important;
-    margin-left: 10px;
+  composes: icon;
+  display: none;
+  visibility: visible !important;
+  margin-left: 10px;
 }
 :local(.item):hover :local(.itemCheckbox),
 :local(.item.selected) :local(.itemCheckbox) {
-    display: inline;
+  display: inline;
 }
 :local(.item.selected) :local(.itemCheckbox) {
-    color: var(--blue-color);
+  color: var(--blue-color);
 }
 
 /* ITEM ICON */
 :local(.itemIcon) {
-    composes: icon;
-    visibility: visible !important;
-    composes: relative from "style";
+  composes: icon;
+  visibility: visible !important;
+  composes: relative from "style";
 }
 :local(.item):hover :local(.itemIcon),
 :local(.item.selected) :local(.itemIcon) {
-    display: none;
+  display: none;
 }
 
 /* CHART ICON */
 :local(.chartIcon) {
-    composes: icon;
-    visibility: visible !important;
-    composes: relative from "style";
+  composes: icon;
+  visibility: visible !important;
+  composes: relative from "style";
 }
 
 /* ACTION ICONS */
 :local(.tagIcon),
 :local(.favoriteIcon),
 :local(.archiveIcon) {
-    composes: icon;
-    composes: mx1 from "style";
+  composes: icon;
+  composes: mx1 from "style";
 }
 
 /* TAG */
 :local(.open) :local(.tagIcon) {
-    visibility: visible;
-    color: var(--blue-color);
+  visibility: visible;
+  color: var(--blue-color);
 }
 
 /* FAVORITE */
 :local(.item.favorite) :local(.favoriteIcon) {
-    visibility: visible;
-    color: var(--blue-color);
+  visibility: visible;
+  color: var(--blue-color);
 }
 
 /* ARCHIVE */
 :local(.item.archived) :local(.archiveIcon) {
-    color: var(--blue-color);
+  color: var(--blue-color);
 }
 
 :local(.trigger) {
-    line-height: 0;
+  line-height: 0;
 }
diff --git a/frontend/src/metabase/components/List.jsx b/frontend/src/metabase/components/List.jsx
index 047518a04c3ab389e81dad992853b198fbf3b430..d94461d10caa051cbec1df45c7b59308bd37a9bf 100644
--- a/frontend/src/metabase/components/List.jsx
+++ b/frontend/src/metabase/components/List.jsx
@@ -5,13 +5,10 @@ import PropTypes from "prop-types";
 import S from "./List.css";
 import pure from "recompose/pure";
 
-const List = ({ children }) =>
-    <ul className={S.list}>
-        { children }
-    </ul>
+const List = ({ children }) => <ul className={S.list}>{children}</ul>;
 
 List.propTypes = {
-    children:   PropTypes.any.isRequired
+  children: PropTypes.any.isRequired,
 };
 
 export default pure(List);
diff --git a/frontend/src/metabase/components/ListFilterWidget.jsx b/frontend/src/metabase/components/ListFilterWidget.jsx
index 83f5f789968f4f4a6d0c83fad9d3aadb5467c90d..ed6152a68455eec282a0370ac4598b6ae6e63737 100644
--- a/frontend/src/metabase/components/ListFilterWidget.jsx
+++ b/frontend/src/metabase/components/ListFilterWidget.jsx
@@ -8,63 +8,58 @@ import Icon from "metabase/components/Icon";
 import PopoverWithTrigger from "./PopoverWithTrigger";
 
 export type ListFilterWidgetItem = {
-    id: string,
-    name: string,
-    icon: string
-}
+  id: string,
+  name: string,
+  icon: string,
+};
 
 export default class ListFilterWidget extends Component {
-    props: {
-        items: ListFilterWidgetItem[],
-        activeItem: ListFilterWidgetItem,
-        onChange: (ListFilterWidgetItem) => void
-    };
+  props: {
+    items: ListFilterWidgetItem[],
+    activeItem: ListFilterWidgetItem,
+    onChange: ListFilterWidgetItem => void,
+  };
 
-    popoverRef: PopoverWithTrigger;
-    iconRef: Icon;
+  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}
+  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();
+              }}
             >
-                <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>
-        )
-    }
+              <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/ListItem.jsx b/frontend/src/metabase/components/ListItem.jsx
index f9c87be98cf3a79bbbc1a60e4e5d31e9b878be71..0228d841ef24d73e8f4a2fc8f41e3301077beb2a 100644
--- a/frontend/src/metabase/components/ListItem.jsx
+++ b/frontend/src/metabase/components/ListItem.jsx
@@ -3,7 +3,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./List.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "./Icon.jsx";
 import Ellipsified from "./Ellipsified.jsx";
 
@@ -11,35 +11,44 @@ import cx from "classnames";
 import pure from "recompose/pure";
 
 //TODO: extend this to support functionality required for questions
-const ListItem = ({ index, name, description, placeholder, url, icon }) =>
-    <div className={cx(S.item)}>
-        <div className={S.leftIcons}>
-            { icon && <Icon className={S.chartIcon} name={icon} size={20} /> }
-        </div>
-        <div className={S.itemBody} style={ index === 0 ? {borderTop: 'none'} : {}}>
-            <div className={S.itemTitle}>
-                <Ellipsified className={S.itemName} tooltip={name} tooltipMaxWidth="100%">
-                    { url ?
-                        <Link to={url}>{name}</Link> :
-                        {name}
-                    }
-                </Ellipsified>
-            </div>
-            <div className={cx(description ? S.itemSubtitle : S.itemSubtitleLight, { "mt1" : true })}>
-                {description || placeholder || t`No description yet`}
-            </div>
-        </div>
+const ListItem = ({ index, name, description, placeholder, url, icon }) => (
+  <div className={cx(S.item)}>
+    <div className={S.leftIcons}>
+      {icon && <Icon className={S.chartIcon} name={icon} size={20} />}
     </div>
+    <div
+      className={S.itemBody}
+      style={index === 0 ? { borderTop: "none" } : {}}
+    >
+      <div className={S.itemTitle}>
+        <Ellipsified
+          className={S.itemName}
+          tooltip={name}
+          tooltipMaxWidth="100%"
+        >
+          {url ? <Link to={url}>{name}</Link> : { name }}
+        </Ellipsified>
+      </div>
+      <div
+        className={cx(description ? S.itemSubtitle : S.itemSubtitleLight, {
+          mt1: true,
+        })}
+      >
+        {description || placeholder || t`No description yet`}
+      </div>
+    </div>
+  </div>
+);
 
 ListItem.propTypes = {
-    name:               PropTypes.string.isRequired,
-    index:              PropTypes.number.isRequired,
-    url:                PropTypes.string,
-    description:        PropTypes.string,
-    placeholder:        PropTypes.string,
-    icon:               PropTypes.string,
-    isEditing:          PropTypes.bool,
-    field:              PropTypes.object
+  name: PropTypes.string.isRequired,
+  index: PropTypes.number.isRequired,
+  url: PropTypes.string,
+  description: PropTypes.string,
+  placeholder: PropTypes.string,
+  icon: PropTypes.string,
+  isEditing: PropTypes.bool,
+  field: PropTypes.object,
 };
 
 export default pure(ListItem);
diff --git a/frontend/src/metabase/components/ListSearchField.jsx b/frontend/src/metabase/components/ListSearchField.jsx
index e9808b84fc4957c322c91097359fa1d6e58b2b70..a3dfd9d9f72886fc4374cd5a8ac6a61fc94498b4 100644
--- a/frontend/src/metabase/components/ListSearchField.jsx
+++ b/frontend/src/metabase/components/ListSearchField.jsx
@@ -2,51 +2,56 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class ListSearchField extends Component {
+  static propTypes = {
+    onChange: PropTypes.func.isRequired,
+    placeholder: PropTypes.string,
+    searchText: PropTypes.string,
+    autoFocus: PropTypes.bool,
+  };
 
-    static propTypes = {
-        onChange: PropTypes.func.isRequired,
-        placeholder: PropTypes.string,
-        searchText: PropTypes.string,
-        autoFocus: PropTypes.bool
-    };
+  static defaultProps = {
+    className: "bordered rounded text-grey-2 flex flex-full align-center",
+    inputClassName: "p1 h4 input--borderless text-default flex-full",
+    placeholder: t`Find...`,
+    searchText: "",
+    autoFocus: false,
+  };
 
-    static defaultProps = {
-        className: "bordered rounded text-grey-2 flex flex-full align-center",
-        inputClassName: "p1 h4 input--borderless text-default flex-full",
-        placeholder: t`Find...`,
-        searchText: "",
-        autoFocus: false
-    };
-
-    componentDidMount() {
-        if (this.props.autoFocus) {
-            // Call focus() with a small delay because instant input focus causes an abrupt scroll to top of page
-            // when ListSearchField is used inside a popover. It seems that it takes a while for Tether library
-            // to correctly position the popover.
-            setTimeout(() => this._input && this._input.focus(), 50);
-        }
+  componentDidMount() {
+    if (this.props.autoFocus) {
+      // Call focus() with a small delay because instant input focus causes an abrupt scroll to top of page
+      // when ListSearchField is used inside a popover. It seems that it takes a while for Tether library
+      // to correctly position the popover.
+      setTimeout(() => this._input && this._input.focus(), 50);
     }
+  }
 
-    render() {
-        const { className, inputClassName, onChange, placeholder, searchText } = this.props;
+  render() {
+    const {
+      className,
+      inputClassName,
+      onChange,
+      placeholder,
+      searchText,
+    } = this.props;
 
-        return (
-            <div className={className}>
-                <span className="px1">
-                    <Icon name="search" size={16}/>
-                </span>
-                <input
-                    className={inputClassName}
-                    type="text"
-                    placeholder={placeholder}
-                    value={searchText}
-                    onChange={(e) => onChange(e.target.value)}
-                    ref={input => this._input = input}
-                />
-            </div>
-        );
-    }
+    return (
+      <div className={className}>
+        <span className="px1">
+          <Icon name="search" size={16} />
+        </span>
+        <input
+          className={inputClassName}
+          type="text"
+          placeholder={placeholder}
+          value={searchText}
+          onChange={e => onChange(e.target.value)}
+          ref={input => (this._input = input)}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
index b12b7f6b8799b6b8202667db4b4154524ec9dde5..06059e09de6584bd2739b82e87185c38d48f8cca 100644
--- a/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
+++ b/frontend/src/metabase/components/LoadingAndErrorWrapper.jsx
@@ -3,131 +3,132 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 export default class LoadingAndErrorWrapper extends Component {
-
-    state = {
-        messageIndex: 0,
-        sceneIndex: 0,
-    }
-
-    static propTypes = {
-        className:       PropTypes.string,
-        error:           PropTypes.any,
-        loading:         PropTypes.any,
-        noBackground:    PropTypes.bool,
-        noWrapper:       PropTypes.bool,
-        children:        PropTypes.any,
-        style:           PropTypes.object,
-        showSpinner:     PropTypes.bool,
-        loadingMessages: PropTypes.array,
-        messageInterval: PropTypes.number,
-        loadingScenes:   PropTypes.array
-    };
-
-    static defaultProps = {
-        className:      "flex flex-full",
-        error:          false,
-        loading:        false,
-        noBackground:   false,
-        noWrapper:      false,
-        showSpinner:    true,
-        loadingMessages: [t`Loading...`],
-        messageInterval: 6000,
-    };
-
-    getErrorMessage() {
-        const { error } = this.props;
-        return (
-            // NOTE Atte Keinänen 5/10/17 Dashboard API endpoint returns the error as JSON with `message` field
-            error.data && (error.data.message ? error.data.message : error.data) ||
-            error.statusText ||
-            error.message ||
-            t`An error occured`
-        );
-    }
-
-    componentDidMount () {
-        const { loadingMessages, messageInterval } = this.props;
-        // only start cycling if multiple messages are provided
-        if(loadingMessages.length > 1) {
-            this.cycle = setInterval(this.loadingInterval, messageInterval)
-        }
-    }
-
-    componentWillUnmount () {
-        clearInterval(this.cycle)
+  state = {
+    messageIndex: 0,
+    sceneIndex: 0,
+  };
+
+  static propTypes = {
+    className: PropTypes.string,
+    error: PropTypes.any,
+    loading: PropTypes.any,
+    noBackground: PropTypes.bool,
+    noWrapper: PropTypes.bool,
+    children: PropTypes.any,
+    style: PropTypes.object,
+    showSpinner: PropTypes.bool,
+    loadingMessages: PropTypes.array,
+    messageInterval: PropTypes.number,
+    loadingScenes: PropTypes.array,
+  };
+
+  static defaultProps = {
+    className: "flex flex-full",
+    error: false,
+    loading: false,
+    noBackground: false,
+    noWrapper: false,
+    showSpinner: true,
+    loadingMessages: [t`Loading...`],
+    messageInterval: 6000,
+  };
+
+  getErrorMessage() {
+    const { error } = this.props;
+    return (
+      // NOTE Atte Keinänen 5/10/17 Dashboard API endpoint returns the error as JSON with `message` field
+      (error.data && (error.data.message ? error.data.message : error.data)) ||
+      error.statusText ||
+      error.message ||
+      t`An error occured`
+    );
+  }
+
+  componentDidMount() {
+    const { loadingMessages, messageInterval } = this.props;
+    // only start cycling if multiple messages are provided
+    if (loadingMessages.length > 1) {
+      this.cycle = setInterval(this.loadingInterval, messageInterval);
     }
+  }
 
-    loadingInterval = () => {
-        if (this.props.loading) {
-            this.cycleLoadingMessage()
-        }
-    }
+  componentWillUnmount() {
+    clearInterval(this.cycle);
+  }
 
-    getChildren() {
-        function resolveChild(child) {
-            if (Array.isArray(child)) {
-                return child.map(resolveChild);
-            } else if (typeof child === "function") {
-                return child();
-            } else {
-                return child;
-            }
-        }
-        return resolveChild(this.props.children);
+  loadingInterval = () => {
+    if (this.props.loading) {
+      this.cycleLoadingMessage();
     }
-
-    cycleLoadingMessage = () => {
-        this.setState({
-            messageIndex: this.state.messageIndex + 1 < this.props.loadingMessages.length
-            ? this.state.messageIndex + 1
-            : 0
-        })
+  };
+
+  getChildren() {
+    function resolveChild(child) {
+      if (Array.isArray(child)) {
+        return child.map(resolveChild);
+      } else if (typeof child === "function") {
+        return child();
+      } else {
+        return child;
+      }
     }
-
-    render() {
-        const {
-            loading,
-            error,
-            noBackground,
-            noWrapper,
-            showSpinner,
-            loadingMessages,
-            loadingScenes
-        } = this.props;
-
-        const { messageIndex, sceneIndex } = this.state;
-
-        const contentClassName = cx(
-            "wrapper py4 text-brand text-centered flex-full flex flex-column layout-centered",
-            { "bg-white": !noBackground }
-        );
-
-        if (noWrapper && !error && !loading) {
-            return React.Children.only(this.getChildren());
-        }
-        return (
-            <div className={this.props.className} style={this.props.style}>
-                { error ?
-                    <div className={contentClassName}>
-                        <h2 className="text-normal text-grey-2 ie-wrap-content-fix">{this.getErrorMessage()}</h2>
-                    </div>
-                : loading ?
-                        <div className={contentClassName}>
-                            { loadingScenes && loadingScenes[sceneIndex] }
-                            { !loadingScenes && showSpinner && <LoadingSpinner /> }
-                            <h2 className="text-normal text-grey-2 mt1">
-                                {loadingMessages[messageIndex]}
-                            </h2>
-                        </div>
-
-                :
-                    this.getChildren()
-                }
-            </div>
-        );
+    return resolveChild(this.props.children);
+  }
+
+  cycleLoadingMessage = () => {
+    this.setState({
+      messageIndex:
+        this.state.messageIndex + 1 < this.props.loadingMessages.length
+          ? this.state.messageIndex + 1
+          : 0,
+    });
+  };
+
+  render() {
+    const {
+      loading,
+      error,
+      noBackground,
+      noWrapper,
+      showSpinner,
+      loadingMessages,
+      loadingScenes,
+    } = this.props;
+
+    const { messageIndex, sceneIndex } = this.state;
+
+    const contentClassName = cx(
+      "wrapper py4 text-brand text-centered flex-full flex flex-column layout-centered",
+      { "bg-white": !noBackground },
+    );
+
+    if (noWrapper && !error && !loading) {
+      return React.Children.only(this.getChildren());
     }
+    return (
+      <div className={this.props.className} style={this.props.style}>
+        {error ? (
+          <div className={contentClassName}>
+            <h2 className="text-normal text-grey-2 ie-wrap-content-fix">
+              {this.getErrorMessage()}
+            </h2>
+          </div>
+        ) : loading ? (
+          <div className={contentClassName}>
+            {loadingScenes && loadingScenes[sceneIndex]}
+            {!loadingScenes && showSpinner && <LoadingSpinner />}
+            <h2 className="text-normal text-grey-2 mt1">
+              {loadingMessages[messageIndex]}
+            </h2>
+          </div>
+        ) : (
+          this.getChildren()
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/LoadingSpinner.css b/frontend/src/metabase/components/LoadingSpinner.css
index 60e14885924f3d2ae2c1181e9e47b9f7104b7fd5..0f6b06e1190c5b5c8dd466b41aa98b22a739bfeb 100644
--- a/frontend/src/metabase/components/LoadingSpinner.css
+++ b/frontend/src/metabase/components/LoadingSpinner.css
@@ -1,4 +1,3 @@
-
 .LoadingSpinner {
   display: inline-block;
   box-sizing: border-box;
diff --git a/frontend/src/metabase/components/LoadingSpinner.jsx b/frontend/src/metabase/components/LoadingSpinner.jsx
index 38a5d0a8bae99f415cf5d3b87d25d320bba36d0f..a5f34b07d96cfe8027c1379c5d146bed21bea5fc 100644
--- a/frontend/src/metabase/components/LoadingSpinner.jsx
+++ b/frontend/src/metabase/components/LoadingSpinner.jsx
@@ -3,19 +3,22 @@ import React, { Component } from "react";
 import "./LoadingSpinner.css";
 
 export default class LoadingSpinner extends Component {
-    static defaultProps = {
-        size: 32,
-        borderWidth: 4,
-        fill: 'currentcolor',
-        spinnerClassName: 'LoadingSpinner'
-    };
+  static defaultProps = {
+    size: 32,
+    borderWidth: 4,
+    fill: "currentcolor",
+    spinnerClassName: "LoadingSpinner",
+  };
 
-    render() {
-        var { size, borderWidth, className, spinnerClassName } = this.props;
-        return (
-            <div className={className}>
-                <div className={spinnerClassName} style={{ width: size, height: size, borderWidth }}></div>
-            </div>
-        );
-    }
+  render() {
+    var { size, borderWidth, className, spinnerClassName } = this.props;
+    return (
+      <div className={className}>
+        <div
+          className={spinnerClassName}
+          style={{ width: size, height: size, borderWidth }}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/LogoIcon.jsx b/frontend/src/metabase/components/LogoIcon.jsx
index aedf88e9397e06361023bbb0a2f7487057edb6ef..c551bcb248a0f7feca1d6bf6c2dead5918778a39 100644
--- a/frontend/src/metabase/components/LogoIcon.jsx
+++ b/frontend/src/metabase/components/LogoIcon.jsx
@@ -3,24 +3,33 @@ import PropTypes from "prop-types";
 import cx from "classnames";
 
 export default class LogoIcon extends Component {
-    static defaultProps = {
-        size: 32
-    };
+  static defaultProps = {
+    size: 32,
+  };
 
-    static propTypes = {
-        size: PropTypes.number,
-        width: PropTypes.number,
-        height: PropTypes.number,
-        dark: PropTypes.bool
-    };
+  static propTypes = {
+    size: PropTypes.number,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    dark: PropTypes.bool,
+  };
 
-    render() {
-        let { dark, height, width, size } = this.props;
-        return (
-            <svg className={cx('Icon', {"text-brand": !dark }, { "text-white": dark })} viewBox="0 0 66 85" width={width || size} height={height || size} fill="currentcolor">
-                <path d="M46.8253288,70.4935014 C49.5764899,70.4935014 51.8067467,68.1774705 51.8067467,65.3205017 C51.8067467,62.4635329 49.5764899,60.147502 46.8253288,60.147502 C44.0741676,60.147502 41.8439108,62.4635329 41.8439108,65.3205017 C41.8439108,68.1774705 44.0741676,70.4935014 46.8253288,70.4935014 Z M32.8773585,84.9779005 C35.6285197,84.9779005 37.8587764,82.6618697 37.8587764,79.8049008 C37.8587764,76.947932 35.6285197,74.6319011 32.8773585,74.6319011 C30.1261973,74.6319011 27.8959405,76.947932 27.8959405,79.8049008 C27.8959405,82.6618697 30.1261973,84.9779005 32.8773585,84.9779005 Z M32.8773585,70.4935014 C35.6285197,70.4935014 37.8587764,68.1774705 37.8587764,65.3205017 C37.8587764,62.4635329 35.6285197,60.147502 32.8773585,60.147502 C30.1261973,60.147502 27.8959405,62.4635329 27.8959405,65.3205017 C27.8959405,68.1774705 30.1261973,70.4935014 32.8773585,70.4935014 Z M18.9293882,70.4935014 C21.6805494,70.4935014 23.9108062,68.1774705 23.9108062,65.3205017 C23.9108062,62.4635329 21.6805494,60.147502 18.9293882,60.147502 C16.1782271,60.147502 13.9479703,62.4635329 13.9479703,65.3205017 C13.9479703,68.1774705 16.1782271,70.4935014 18.9293882,70.4935014 Z M46.8253288,56.0091023 C49.5764899,56.0091023 51.8067467,53.6930714 51.8067467,50.8361026 C51.8067467,47.9791337 49.5764899,45.6631029 46.8253288,45.6631029 C44.0741676,45.6631029 41.8439108,47.9791337 41.8439108,50.8361026 C41.8439108,53.6930714 44.0741676,56.0091023 46.8253288,56.0091023 Z M18.9293882,56.0091023 C21.6805494,56.0091023 23.9108062,53.6930714 23.9108062,50.8361026 C23.9108062,47.9791337 21.6805494,45.6631029 18.9293882,45.6631029 C16.1782271,45.6631029 13.9479703,47.9791337 13.9479703,50.8361026 C13.9479703,53.6930714 16.1782271,56.0091023 18.9293882,56.0091023 Z M46.8253288,26.8995984 C49.5764899,26.8995984 51.8067467,24.5835675 51.8067467,21.7265987 C51.8067467,18.8696299 49.5764899,16.553599 46.8253288,16.553599 C44.0741676,16.553599 41.8439108,18.8696299 41.8439108,21.7265987 C41.8439108,24.5835675 44.0741676,26.8995984 46.8253288,26.8995984 Z M32.8773585,41.5247031 C35.6285197,41.5247031 37.8587764,39.2086723 37.8587764,36.3517034 C37.8587764,33.4947346 35.6285197,31.1787037 32.8773585,31.1787037 C30.1261973,31.1787037 27.8959405,33.4947346 27.8959405,36.3517034 C27.8959405,39.2086723 30.1261973,41.5247031 32.8773585,41.5247031 Z M32.8773585,10.3459994 C35.6285197,10.3459994 37.8587764,8.02996853 37.8587764,5.17299969 C37.8587764,2.31603085 35.6285197,0 32.8773585,0 C30.1261973,0 27.8959405,2.31603085 27.8959405,5.17299969 C27.8959405,8.02996853 30.1261973,10.3459994 32.8773585,10.3459994 Z M32.8773585,26.8995984 C35.6285197,26.8995984 37.8587764,24.5835675 37.8587764,21.7265987 C37.8587764,18.8696299 35.6285197,16.553599 32.8773585,16.553599 C30.1261973,16.553599 27.8959405,18.8696299 27.8959405,21.7265987 C27.8959405,24.5835675 30.1261973,26.8995984 32.8773585,26.8995984 Z M18.9293882,26.8995984 C21.6805494,26.8995984 23.9108062,24.5835675 23.9108062,21.7265987 C23.9108062,18.8696299 21.6805494,16.553599 18.9293882,16.553599 C16.1782271,16.553599 13.9479703,18.8696299 13.9479703,21.7265987 C13.9479703,24.5835675 16.1782271,26.8995984 18.9293882,26.8995984 Z" opacity="0.2"></path>
-                <path d="M60.773299,70.4935014 C63.5244602,70.4935014 65.754717,68.1774705 65.754717,65.3205017 C65.754717,62.4635329 63.5244602,60.147502 60.773299,60.147502 C58.0221379,60.147502 55.7918811,62.4635329 55.7918811,65.3205017 C55.7918811,68.1774705 58.0221379,70.4935014 60.773299,70.4935014 Z M4.98141795,70.3527958 C7.73257912,70.3527958 9.96283591,68.0367649 9.96283591,65.1797961 C9.96283591,62.3228273 7.73257912,60.0067964 4.98141795,60.0067964 C2.23025679,60.0067964 0,62.3228273 0,65.1797961 C0,68.0367649 2.23025679,70.3527958 4.98141795,70.3527958 Z M60.773299,56.0091023 C63.5244602,56.0091023 65.754717,53.6930714 65.754717,50.8361026 C65.754717,47.9791337 63.5244602,45.6631029 60.773299,45.6631029 C58.0221379,45.6631029 55.7918811,47.9791337 55.7918811,50.8361026 C55.7918811,53.6930714 58.0221379,56.0091023 60.773299,56.0091023 Z M32.8773585,56.0091023 C35.6285197,56.0091023 37.8587764,53.6930714 37.8587764,50.8361026 C37.8587764,47.9791337 35.6285197,45.6631029 32.8773585,45.6631029 C30.1261973,45.6631029 27.8959405,47.9791337 27.8959405,50.8361026 C27.8959405,53.6930714 30.1261973,56.0091023 32.8773585,56.0091023 Z M4.98141795,55.8683967 C7.73257912,55.8683967 9.96283591,53.5523658 9.96283591,50.695397 C9.96283591,47.8384281 7.73257912,45.5223973 4.98141795,45.5223973 C2.23025679,45.5223973 0,47.8384281 0,50.695397 C0,53.5523658 2.23025679,55.8683967 4.98141795,55.8683967 Z M60.773299,41.5247031 C63.5244602,41.5247031 65.754717,39.2086723 65.754717,36.3517034 C65.754717,33.4947346 63.5244602,31.1787037 60.773299,31.1787037 C58.0221379,31.1787037 55.7918811,33.4947346 55.7918811,36.3517034 C55.7918811,39.2086723 58.0221379,41.5247031 60.773299,41.5247031 Z M46.8253288,41.5247031 C49.5764899,41.5247031 51.8067467,39.2086723 51.8067467,36.3517034 C51.8067467,33.4947346 49.5764899,31.1787037 46.8253288,31.1787037 C44.0741676,31.1787037 41.8439108,33.4947346 41.8439108,36.3517034 C41.8439108,39.2086723 44.0741676,41.5247031 46.8253288,41.5247031 Z M60.773299,26.8995984 C63.5244602,26.8995984 65.754717,24.5835675 65.754717,21.7265987 C65.754717,18.8696299 63.5244602,16.553599 60.773299,16.553599 C58.0221379,16.553599 55.7918811,18.8696299 55.7918811,21.7265987 C55.7918811,24.5835675 58.0221379,26.8995984 60.773299,26.8995984 Z M18.9293882,41.5247031 C21.6805494,41.5247031 23.9108062,39.2086723 23.9108062,36.3517034 C23.9108062,33.4947346 21.6805494,31.1787037 18.9293882,31.1787037 C16.1782271,31.1787037 13.9479703,33.4947346 13.9479703,36.3517034 C13.9479703,39.2086723 16.1782271,41.5247031 18.9293882,41.5247031 Z M4.98141795,41.3839975 C7.73257912,41.3839975 9.96283591,39.0679667 9.96283591,36.2109978 C9.96283591,33.354029 7.73257912,31.0379981 4.98141795,31.0379981 C2.23025679,31.0379981 0,33.354029 0,36.2109978 C0,39.0679667 2.23025679,41.3839975 4.98141795,41.3839975 Z M4.98141795,26.8995984 C7.73257912,26.8995984 9.96283591,24.5835675 9.96283591,21.7265987 C9.96283591,18.8696299 7.73257912,16.553599 4.98141795,16.553599 C2.23025679,16.553599 0,18.8696299 0,21.7265987 C0,24.5835675 2.23025679,26.8995984 4.98141795,26.8995984 Z"></path>
-            </svg>
-        );
-    }
+  render() {
+    let { dark, height, width, size } = this.props;
+    return (
+      <svg
+        className={cx("Icon", { "text-brand": !dark }, { "text-white": dark })}
+        viewBox="0 0 66 85"
+        width={width || size}
+        height={height || size}
+        fill="currentcolor"
+      >
+        <path
+          d="M46.8253288,70.4935014 C49.5764899,70.4935014 51.8067467,68.1774705 51.8067467,65.3205017 C51.8067467,62.4635329 49.5764899,60.147502 46.8253288,60.147502 C44.0741676,60.147502 41.8439108,62.4635329 41.8439108,65.3205017 C41.8439108,68.1774705 44.0741676,70.4935014 46.8253288,70.4935014 Z M32.8773585,84.9779005 C35.6285197,84.9779005 37.8587764,82.6618697 37.8587764,79.8049008 C37.8587764,76.947932 35.6285197,74.6319011 32.8773585,74.6319011 C30.1261973,74.6319011 27.8959405,76.947932 27.8959405,79.8049008 C27.8959405,82.6618697 30.1261973,84.9779005 32.8773585,84.9779005 Z M32.8773585,70.4935014 C35.6285197,70.4935014 37.8587764,68.1774705 37.8587764,65.3205017 C37.8587764,62.4635329 35.6285197,60.147502 32.8773585,60.147502 C30.1261973,60.147502 27.8959405,62.4635329 27.8959405,65.3205017 C27.8959405,68.1774705 30.1261973,70.4935014 32.8773585,70.4935014 Z M18.9293882,70.4935014 C21.6805494,70.4935014 23.9108062,68.1774705 23.9108062,65.3205017 C23.9108062,62.4635329 21.6805494,60.147502 18.9293882,60.147502 C16.1782271,60.147502 13.9479703,62.4635329 13.9479703,65.3205017 C13.9479703,68.1774705 16.1782271,70.4935014 18.9293882,70.4935014 Z M46.8253288,56.0091023 C49.5764899,56.0091023 51.8067467,53.6930714 51.8067467,50.8361026 C51.8067467,47.9791337 49.5764899,45.6631029 46.8253288,45.6631029 C44.0741676,45.6631029 41.8439108,47.9791337 41.8439108,50.8361026 C41.8439108,53.6930714 44.0741676,56.0091023 46.8253288,56.0091023 Z M18.9293882,56.0091023 C21.6805494,56.0091023 23.9108062,53.6930714 23.9108062,50.8361026 C23.9108062,47.9791337 21.6805494,45.6631029 18.9293882,45.6631029 C16.1782271,45.6631029 13.9479703,47.9791337 13.9479703,50.8361026 C13.9479703,53.6930714 16.1782271,56.0091023 18.9293882,56.0091023 Z M46.8253288,26.8995984 C49.5764899,26.8995984 51.8067467,24.5835675 51.8067467,21.7265987 C51.8067467,18.8696299 49.5764899,16.553599 46.8253288,16.553599 C44.0741676,16.553599 41.8439108,18.8696299 41.8439108,21.7265987 C41.8439108,24.5835675 44.0741676,26.8995984 46.8253288,26.8995984 Z M32.8773585,41.5247031 C35.6285197,41.5247031 37.8587764,39.2086723 37.8587764,36.3517034 C37.8587764,33.4947346 35.6285197,31.1787037 32.8773585,31.1787037 C30.1261973,31.1787037 27.8959405,33.4947346 27.8959405,36.3517034 C27.8959405,39.2086723 30.1261973,41.5247031 32.8773585,41.5247031 Z M32.8773585,10.3459994 C35.6285197,10.3459994 37.8587764,8.02996853 37.8587764,5.17299969 C37.8587764,2.31603085 35.6285197,0 32.8773585,0 C30.1261973,0 27.8959405,2.31603085 27.8959405,5.17299969 C27.8959405,8.02996853 30.1261973,10.3459994 32.8773585,10.3459994 Z M32.8773585,26.8995984 C35.6285197,26.8995984 37.8587764,24.5835675 37.8587764,21.7265987 C37.8587764,18.8696299 35.6285197,16.553599 32.8773585,16.553599 C30.1261973,16.553599 27.8959405,18.8696299 27.8959405,21.7265987 C27.8959405,24.5835675 30.1261973,26.8995984 32.8773585,26.8995984 Z M18.9293882,26.8995984 C21.6805494,26.8995984 23.9108062,24.5835675 23.9108062,21.7265987 C23.9108062,18.8696299 21.6805494,16.553599 18.9293882,16.553599 C16.1782271,16.553599 13.9479703,18.8696299 13.9479703,21.7265987 C13.9479703,24.5835675 16.1782271,26.8995984 18.9293882,26.8995984 Z"
+          opacity="0.2"
+        />
+        <path d="M60.773299,70.4935014 C63.5244602,70.4935014 65.754717,68.1774705 65.754717,65.3205017 C65.754717,62.4635329 63.5244602,60.147502 60.773299,60.147502 C58.0221379,60.147502 55.7918811,62.4635329 55.7918811,65.3205017 C55.7918811,68.1774705 58.0221379,70.4935014 60.773299,70.4935014 Z M4.98141795,70.3527958 C7.73257912,70.3527958 9.96283591,68.0367649 9.96283591,65.1797961 C9.96283591,62.3228273 7.73257912,60.0067964 4.98141795,60.0067964 C2.23025679,60.0067964 0,62.3228273 0,65.1797961 C0,68.0367649 2.23025679,70.3527958 4.98141795,70.3527958 Z M60.773299,56.0091023 C63.5244602,56.0091023 65.754717,53.6930714 65.754717,50.8361026 C65.754717,47.9791337 63.5244602,45.6631029 60.773299,45.6631029 C58.0221379,45.6631029 55.7918811,47.9791337 55.7918811,50.8361026 C55.7918811,53.6930714 58.0221379,56.0091023 60.773299,56.0091023 Z M32.8773585,56.0091023 C35.6285197,56.0091023 37.8587764,53.6930714 37.8587764,50.8361026 C37.8587764,47.9791337 35.6285197,45.6631029 32.8773585,45.6631029 C30.1261973,45.6631029 27.8959405,47.9791337 27.8959405,50.8361026 C27.8959405,53.6930714 30.1261973,56.0091023 32.8773585,56.0091023 Z M4.98141795,55.8683967 C7.73257912,55.8683967 9.96283591,53.5523658 9.96283591,50.695397 C9.96283591,47.8384281 7.73257912,45.5223973 4.98141795,45.5223973 C2.23025679,45.5223973 0,47.8384281 0,50.695397 C0,53.5523658 2.23025679,55.8683967 4.98141795,55.8683967 Z M60.773299,41.5247031 C63.5244602,41.5247031 65.754717,39.2086723 65.754717,36.3517034 C65.754717,33.4947346 63.5244602,31.1787037 60.773299,31.1787037 C58.0221379,31.1787037 55.7918811,33.4947346 55.7918811,36.3517034 C55.7918811,39.2086723 58.0221379,41.5247031 60.773299,41.5247031 Z M46.8253288,41.5247031 C49.5764899,41.5247031 51.8067467,39.2086723 51.8067467,36.3517034 C51.8067467,33.4947346 49.5764899,31.1787037 46.8253288,31.1787037 C44.0741676,31.1787037 41.8439108,33.4947346 41.8439108,36.3517034 C41.8439108,39.2086723 44.0741676,41.5247031 46.8253288,41.5247031 Z M60.773299,26.8995984 C63.5244602,26.8995984 65.754717,24.5835675 65.754717,21.7265987 C65.754717,18.8696299 63.5244602,16.553599 60.773299,16.553599 C58.0221379,16.553599 55.7918811,18.8696299 55.7918811,21.7265987 C55.7918811,24.5835675 58.0221379,26.8995984 60.773299,26.8995984 Z M18.9293882,41.5247031 C21.6805494,41.5247031 23.9108062,39.2086723 23.9108062,36.3517034 C23.9108062,33.4947346 21.6805494,31.1787037 18.9293882,31.1787037 C16.1782271,31.1787037 13.9479703,33.4947346 13.9479703,36.3517034 C13.9479703,39.2086723 16.1782271,41.5247031 18.9293882,41.5247031 Z M4.98141795,41.3839975 C7.73257912,41.3839975 9.96283591,39.0679667 9.96283591,36.2109978 C9.96283591,33.354029 7.73257912,31.0379981 4.98141795,31.0379981 C2.23025679,31.0379981 0,33.354029 0,36.2109978 C0,39.0679667 2.23025679,41.3839975 4.98141795,41.3839975 Z M4.98141795,26.8995984 C7.73257912,26.8995984 9.96283591,24.5835675 9.96283591,21.7265987 C9.96283591,18.8696299 7.73257912,16.553599 4.98141795,16.553599 C2.23025679,16.553599 0,18.8696299 0,21.7265987 C0,24.5835675 2.23025679,26.8995984 4.98141795,26.8995984 Z" />
+      </svg>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Logs.jsx b/frontend/src/metabase/components/Logs.jsx
index 5bf86b55f272465fd7e4b6cb105cf1686cd768bb..246ab02a5c2e0bd1c876be1e43bf8d6e51966dd6 100644
--- a/frontend/src/metabase/components/Logs.jsx
+++ b/frontend/src/metabase/components/Logs.jsx
@@ -11,65 +11,74 @@ import "react-ansi-style/inject-css";
 import _ from "underscore";
 
 export default class Logs extends Component {
-    constructor() {
-        super();
-        this.state = {
-            logs: [],
-            scrollToBottom: true
-        };
+  constructor() {
+    super();
+    this.state = {
+      logs: [],
+      scrollToBottom: true,
+    };
 
-        this._onScroll = () => {
-            this.scrolling = true;
-            this._onScrollDebounced();
-        }
-        this._onScrollDebounced = _.debounce(() => {
-            let elem = ReactDOM.findDOMNode(this).parentNode;
-            let scrollToBottom = Math.abs(elem.scrollTop - (elem.scrollHeight - elem.offsetHeight)) < 10;
-            this.setState({ scrollToBottom }, () => {
-                this.scrolling = false;
-            });
-        }, 500);
-    }
+    this._onScroll = () => {
+      this.scrolling = true;
+      this._onScrollDebounced();
+    };
+    this._onScrollDebounced = _.debounce(() => {
+      let elem = ReactDOM.findDOMNode(this).parentNode;
+      let scrollToBottom =
+        Math.abs(elem.scrollTop - (elem.scrollHeight - elem.offsetHeight)) < 10;
+      this.setState({ scrollToBottom }, () => {
+        this.scrolling = false;
+      });
+    }, 500);
+  }
 
-    async fetchLogs() {
-        let logs = await UtilApi.logs();
-        this.setState({ logs: logs.reverse() })
-    }
+  async fetchLogs() {
+    let logs = await UtilApi.logs();
+    this.setState({ logs: logs.reverse() });
+  }
 
-    componentWillMount() {
-        this.timer = setInterval(this.fetchLogs.bind(this), 1000);
-    }
+  componentWillMount() {
+    this.timer = setInterval(this.fetchLogs.bind(this), 1000);
+  }
 
-    componentDidMount() {
-        let elem = ReactDOM.findDOMNode(this).parentNode;
-        elem.addEventListener("scroll", this._onScroll, false);
-    }
+  componentDidMount() {
+    let elem = ReactDOM.findDOMNode(this).parentNode;
+    elem.addEventListener("scroll", this._onScroll, false);
+  }
 
-    componentDidUpdate() {
-        let elem = ReactDOM.findDOMNode(this).parentNode;
-        if (!this.scrolling && this.state.scrollToBottom) {
-            if (elem.scrollTop !== elem.scrollHeight - elem.offsetHeight) {
-                elem.scrollTop = elem.scrollHeight - elem.offsetHeight;
-            }
-        }
+  componentDidUpdate() {
+    let elem = ReactDOM.findDOMNode(this).parentNode;
+    if (!this.scrolling && this.state.scrollToBottom) {
+      if (elem.scrollTop !== elem.scrollHeight - elem.offsetHeight) {
+        elem.scrollTop = elem.scrollHeight - elem.offsetHeight;
+      }
     }
+  }
 
-    componentWillUnmount() {
-        let elem = ReactDOM.findDOMNode(this).parentNode;
-        elem.removeEventListener("scroll", this._onScroll, false);
-        clearTimeout(this.timer);
-    }
+  componentWillUnmount() {
+    let elem = ReactDOM.findDOMNode(this).parentNode;
+    elem.removeEventListener("scroll", this._onScroll, false);
+    clearTimeout(this.timer);
+  }
 
-    render() {
-        let { logs } = this.state;
-        return (
-            <LoadingAndErrorWrapper loading={!logs || logs.length === 0}>
-                {() =>
-                    <div style={{ backgroundColor: "black", fontFamily: "monospace", fontSize: "14px", whiteSpace: "pre-line", padding: "0.5em" }}>
-                        {reactAnsiStyle(React, logs.join("\n"))}
-                    </div>
-                }
-            </LoadingAndErrorWrapper>
-        );
-    }
+  render() {
+    let { logs } = this.state;
+    return (
+      <LoadingAndErrorWrapper loading={!logs || logs.length === 0}>
+        {() => (
+          <div
+            style={{
+              backgroundColor: "black",
+              fontFamily: "monospace",
+              fontSize: "14px",
+              whiteSpace: "pre-line",
+              padding: "0.5em",
+            }}
+          >
+            {reactAnsiStyle(React, logs.join("\n"))}
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Modal.jsx b/frontend/src/metabase/components/Modal.jsx
index 9f31ee072f8109b51089cf0223e1b258e2d44e1d..7714591c5af05ae42766c937b17df5184fc473ec 100644
--- a/frontend/src/metabase/components/Modal.jsx
+++ b/frontend/src/metabase/components/Modal.jsx
@@ -14,205 +14,229 @@ import ModalContent from "./ModalContent";
 import _ from "underscore";
 
 function getModalContent(props) {
-    if (React.Children.count(props.children) > 1 ||
-        props.title != null || props.footer != null
-    ) {
-        return <ModalContent {..._.omit(props, "className", "style")} />
-    } else {
-        return React.Children.only(props.children);
-    }
+  if (
+    React.Children.count(props.children) > 1 ||
+    props.title != null ||
+    props.footer != null
+  ) {
+    return <ModalContent {..._.omit(props, "className", "style")} />;
+  } else {
+    return React.Children.only(props.children);
+  }
 }
 
 export class WindowModal extends Component {
-    static propTypes = {
-        isOpen: PropTypes.bool
-    };
-
-    static defaultProps = {
-        className: "Modal",
-        backdropClassName: "Modal-backdrop"
-    };
-
-    componentWillMount() {
-        this._modalElement = document.createElement('span');
-        this._modalElement.className = 'ModalContainer';
-        document.querySelector('body').appendChild(this._modalElement);
-    }
-
-    componentDidMount() {
-        this._renderPopover();
-    }
-
-    componentDidUpdate() {
-        this._renderPopover();
-    }
-
-    componentWillUnmount() {
-        ReactDOM.unmountComponentAtNode(this._modalElement);
-        if (this._modalElement.parentNode) {
-            this._modalElement.parentNode.removeChild(this._modalElement);
-        }
-    }
-
-    handleDismissal() {
-        if (this.props.onClose) {
-            this.props.onClose()
-        }
-    }
-
-    _modalComponent() {
-        const className = cx(this.props.className, ...["small", "medium", "wide", "tall"].filter(type => this.props[type]).map(type => `Modal--${type}`))
-        return (
-            <OnClickOutsideWrapper handleDismissal={this.handleDismissal.bind(this)}>
-                <div className={cx(className, 'relative bordered bg-white rounded')}>
-                    { getModalContent({
-                        ...this.props,
-                        fullPageModal: false,
-                        formModal: !!this.props.form
-                    }) }
-                </div>
-            </OnClickOutsideWrapper>
-        );
-    }
-
-    _renderPopover() {
-        const { backdropClassName, isOpen, style } = this.props;
-        const backdropClassnames = 'flex justify-center align-center fixed top left bottom right';
-        ReactDOM.unstable_renderSubtreeIntoContainer(this,
-            <ReactCSSTransitionGroup transitionName="Modal" transitionAppear={true} transitionAppearTimeout={250} transitionEnterTimeout={250} transitionLeaveTimeout={250}>
-                { isOpen &&
-                    <div key="modal" className={cx(backdropClassName, backdropClassnames)} style={style}>
-                        {this._modalComponent()}
-                    </div>
-                }
-            </ReactCSSTransitionGroup>
-        , this._modalElement);
-    }
-
-    render() {
-        return null;
-    }
+  static propTypes = {
+    isOpen: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    className: "Modal",
+    backdropClassName: "Modal-backdrop",
+  };
+
+  componentWillMount() {
+    this._modalElement = document.createElement("span");
+    this._modalElement.className = "ModalContainer";
+    document.querySelector("body").appendChild(this._modalElement);
+  }
+
+  componentDidMount() {
+    this._renderPopover();
+  }
+
+  componentDidUpdate() {
+    this._renderPopover();
+  }
+
+  componentWillUnmount() {
+    ReactDOM.unmountComponentAtNode(this._modalElement);
+    if (this._modalElement.parentNode) {
+      this._modalElement.parentNode.removeChild(this._modalElement);
+    }
+  }
+
+  handleDismissal() {
+    if (this.props.onClose) {
+      this.props.onClose();
+    }
+  }
+
+  _modalComponent() {
+    const className = cx(
+      this.props.className,
+      ...["small", "medium", "wide", "tall"]
+        .filter(type => this.props[type])
+        .map(type => `Modal--${type}`),
+    );
+    return (
+      <OnClickOutsideWrapper handleDismissal={this.handleDismissal.bind(this)}>
+        <div className={cx(className, "relative bordered bg-white rounded")}>
+          {getModalContent({
+            ...this.props,
+            fullPageModal: false,
+            formModal: !!this.props.form,
+          })}
+        </div>
+      </OnClickOutsideWrapper>
+    );
+  }
+
+  _renderPopover() {
+    const { backdropClassName, isOpen, style } = this.props;
+    const backdropClassnames =
+      "flex justify-center align-center fixed top left bottom right";
+    ReactDOM.unstable_renderSubtreeIntoContainer(
+      this,
+      <ReactCSSTransitionGroup
+        transitionName="Modal"
+        transitionAppear={true}
+        transitionAppearTimeout={250}
+        transitionEnterTimeout={250}
+        transitionLeaveTimeout={250}
+      >
+        {isOpen && (
+          <div
+            key="modal"
+            className={cx(backdropClassName, backdropClassnames)}
+            style={style}
+          >
+            {this._modalComponent()}
+          </div>
+        )}
+      </ReactCSSTransitionGroup>,
+      this._modalElement,
+    );
+  }
+
+  render() {
+    return null;
+  }
 }
 
 import routeless from "metabase/hoc/Routeless";
 
 export class FullPageModal extends Component {
-    componentDidMount() {
-        this._modalElement = document.createElement("div");
-        this._modalElement.className = "Modal--full";
-        document.querySelector('body').appendChild(this._modalElement);
-
-        // save the scroll position, scroll to the top left, and disable scrolling
-        this._scrollX = getScrollX();
-        this._scrollY = getScrollY();
-        window.scrollTo(0,0);
-        document.body.style.overflow = "hidden";
-
-        this.componentDidUpdate();
-    }
-
-    componentDidUpdate() {
-        // set the top of the modal to the bottom of the nav
-        let nav = document.body.querySelector(".Nav");
-        if (nav) {
-            this._modalElement.style.top = nav.getBoundingClientRect().bottom + "px";
+  componentDidMount() {
+    this._modalElement = document.createElement("div");
+    this._modalElement.className = "Modal--full";
+    document.querySelector("body").appendChild(this._modalElement);
+
+    // save the scroll position, scroll to the top left, and disable scrolling
+    this._scrollX = getScrollX();
+    this._scrollY = getScrollY();
+    window.scrollTo(0, 0);
+    document.body.style.overflow = "hidden";
+
+    this.componentDidUpdate();
+  }
+
+  componentDidUpdate() {
+    // set the top of the modal to the bottom of the nav
+    let nav = document.body.querySelector(".Nav");
+    if (nav) {
+      this._modalElement.style.top = nav.getBoundingClientRect().bottom + "px";
+    }
+    this._renderModal(true);
+  }
+
+  componentWillUnmount() {
+    this._renderModal(false);
+
+    // restore scroll position and scrolling
+    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(() => {
+      ReactDOM.unmountComponentAtNode(this._modalElement);
+      this._modalElement.parentNode.removeChild(this._modalElement);
+    }, 300);
+  }
+
+  _renderModal(open) {
+    ReactDOM.unstable_renderSubtreeIntoContainer(
+      this,
+      <Motion
+        defaultStyle={{ opacity: 0, top: 20 }}
+        style={
+          open
+            ? { opacity: spring(1), top: spring(0) }
+            : { opacity: spring(0), top: spring(20) }
         }
-        this._renderModal(true)
-    }
-
-    componentWillUnmount() {
-        this._renderModal(false);
-
-        // restore scroll position and scrolling
-        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(() => {
-            ReactDOM.unmountComponentAtNode(this._modalElement);
-            this._modalElement.parentNode.removeChild(this._modalElement);
-        }, 300);
-    }
-
-    _renderModal(open) {
-        ReactDOM.unstable_renderSubtreeIntoContainer(this,
-            <Motion defaultStyle={{ opacity: 0, top: 20 }} style={open ?
-                { opacity: spring(1), top: spring(0) } :
-                { opacity: spring(0), top: spring(20) }
-            }>
-                { motionStyle =>
-                    <div className="full-height relative scroll-y" style={motionStyle}>
-                        { getModalContent({
-                            ...this.props,
-                            fullPageModal: true,
-                            formModal: !!this.props.form
-                        }) }
-                    </div>
-                }
-            </Motion>
-        , this._modalElement);
-    }
-
-    render() {
-        return null;
-    }
+      >
+        {motionStyle => (
+          <div className="full-height relative scroll-y" style={motionStyle}>
+            {getModalContent({
+              ...this.props,
+              fullPageModal: true,
+              formModal: !!this.props.form,
+            })}
+          </div>
+        )}
+      </Motion>,
+      this._modalElement,
+    );
+  }
+
+  render() {
+    return null;
+  }
 }
 
 export class InlineModal extends Component {
-    render() {
-        return (
-            <div>
-                {this.props.isOpen ? <FullPageModal {...this.props} /> : null}
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div>{this.props.isOpen ? <FullPageModal {...this.props} /> : null}</div>
+    );
+  }
 }
 
 /**
  * A modified version of Modal for Jest/Enzyme tests. Renders the modal content inline instead of document root.
  */
 export class TestModal extends Component {
-    static defaultProps = {
-        isOpen: true
-    }
-
-    render() {
-        if (this.props.isOpen) {
-            return (
-                <div
-                    className="test-modal"
-                    onClick={e => e.stopPropagation()}
-                >
-                    { getModalContent({
-                        ...this.props,
-                        fullPageModal: true,
-                        formModal: !!this.props.form
-                    }) }
-                </div>
-            )
-        } else {
-            return null;
-        }
+  static defaultProps = {
+    isOpen: true,
+  };
+
+  render() {
+    if (this.props.isOpen) {
+      return (
+        <div className="test-modal" onClick={e => e.stopPropagation()}>
+          {getModalContent({
+            ...this.props,
+            fullPageModal: true,
+            formModal: !!this.props.form,
+          })}
+        </div>
+      );
+    } else {
+      return null;
     }
+  }
 }
 
 // the "routeless" version should only be used for non-inline modals
 const RoutelessFullPageModal = routeless(FullPageModal);
 
 const Modal = ({ full, inline, ...props }) =>
-    full ?
-        (props.isOpen ? <RoutelessFullPageModal {...props} /> : null)
-    : inline ?
-        <InlineModal {...props} />
-    :
-        <WindowModal {...props} />;
+  full ? (
+    props.isOpen ? (
+      <RoutelessFullPageModal {...props} />
+    ) : null
+  ) : inline ? (
+    <InlineModal {...props} />
+  ) : (
+    <WindowModal {...props} />
+  );
 
 Modal.defaultProps = {
-    isOpen: true,
+  isOpen: true,
 };
 
 export default Modal;
diff --git a/frontend/src/metabase/components/ModalContent.jsx b/frontend/src/metabase/components/ModalContent.jsx
index 111d2d7ba314df806153627df7a22920299fdb2b..82def4b22b53eaabfd88f64339b42a6691feebee 100644
--- a/frontend/src/metabase/components/ModalContent.jsx
+++ b/frontend/src/metabase/components/ModalContent.jsx
@@ -4,87 +4,116 @@ import cx from "classnames";
 import Icon from "metabase/components/Icon.jsx";
 
 export default class ModalContent extends Component {
-    static propTypes = {
-        id: PropTypes.string,
-        title: PropTypes.string,
-        onClose: PropTypes.func.isRequired,
-        fullPageModal: PropTypes.bool,
-        formModal: PropTypes.bool
-    };
+  static propTypes = {
+    id: PropTypes.string,
+    title: PropTypes.string,
+    onClose: PropTypes.func.isRequired,
+    fullPageModal: PropTypes.bool,
+    formModal: PropTypes.bool,
+  };
 
-    static defaultProps = {
-    };
+  static defaultProps = {};
 
-    render() {
-        const { title, footer, onClose, children, className, fullPageModal, formModal } = this.props;
+  render() {
+    const {
+      title,
+      footer,
+      onClose,
+      children,
+      className,
+      fullPageModal,
+      formModal,
+    } = this.props;
 
-        return (
-            <div
-                id={this.props.id}
-                className={cx("ModalContent NewForm flex-full flex flex-column relative", className, { "full-height": fullPageModal && !formModal })}
-            >
-                { onClose &&
-                    <Icon
-                        className="text-grey-2 text-grey-4-hover cursor-pointer absolute m2 p2 top right"
-                        name="close"
-                        size={fullPageModal ? 24 : 16}
-                        onClick={onClose}
-                    />
-                }
-                { title &&
-                    <ModalHeader fullPageModal={fullPageModal} formModal={formModal}>
-                        {title}
-                    </ModalHeader>
-                }
-                <ModalBody fullPageModal={fullPageModal} formModal={formModal}>
-                    {children}
-                </ModalBody>
-                { footer &&
-                    <ModalFooter fullPageModal={fullPageModal} formModal={formModal}>
-                        {footer}
-                    </ModalFooter>
-                }
-            </div>
-        );
-    }
+    return (
+      <div
+        id={this.props.id}
+        className={cx(
+          "ModalContent NewForm flex-full flex flex-column relative",
+          className,
+          { "full-height": fullPageModal && !formModal },
+        )}
+      >
+        {onClose && (
+          <Icon
+            className="text-grey-2 text-grey-4-hover cursor-pointer absolute m2 p2 top right"
+            name="close"
+            size={fullPageModal ? 24 : 16}
+            onClick={onClose}
+          />
+        )}
+        {title && (
+          <ModalHeader fullPageModal={fullPageModal} formModal={formModal}>
+            {title}
+          </ModalHeader>
+        )}
+        <ModalBody fullPageModal={fullPageModal} formModal={formModal}>
+          {children}
+        </ModalBody>
+        {footer && (
+          <ModalFooter fullPageModal={fullPageModal} formModal={formModal}>
+            {footer}
+          </ModalFooter>
+        )}
+      </div>
+    );
+  }
 }
 
 const FORM_WIDTH = 500 + 32 * 2; // includes padding
 
-export const ModalHeader = ({ children, fullPageModal, formModal }) =>
-    <div className={cx("ModalHeader flex-no-shrink px4 py4 full")}>
-        <h2 className={cx("text-bold", { "text-centered": fullPageModal }, { "mr4": !fullPageModal})}>{children}</h2>
-    </div>
-
+export const ModalHeader = ({ children, fullPageModal, formModal }) => (
+  <div className={cx("ModalHeader flex-no-shrink px4 py4 full")}>
+    <h2
+      className={cx(
+        "text-bold",
+        { "text-centered": fullPageModal },
+        { mr4: !fullPageModal },
+      )}
+    >
+      {children}
+    </h2>
+  </div>
+);
 
-export const ModalBody = ({ children, fullPageModal, formModal }) =>
+export const ModalBody = ({ children, fullPageModal, formModal }) => (
+  <div
+    className={cx("ModalBody", {
+      px4: formModal,
+      "flex flex-full flex-basis-auto": !formModal,
+    })}
+  >
     <div
-        className={cx("ModalBody", { "px4": formModal, "flex flex-full flex-basis-auto": !formModal })}
+      className="flex-full ml-auto mr-auto flex flex-column"
+      style={{ maxWidth: formModal && fullPageModal ? FORM_WIDTH : undefined }}
     >
-        <div
-            className="flex-full ml-auto mr-auto flex flex-column"
-            style={{ maxWidth: (formModal && fullPageModal) ? FORM_WIDTH : undefined }}
-        >
-            {children}
-        </div>
+      {children}
     </div>
+  </div>
+);
 
-
-export const ModalFooter = ({ children, fullPageModal, formModal }) =>
+export const ModalFooter = ({ children, fullPageModal, formModal }) => (
+  <div
+    className={cx(
+      "ModalFooter flex-no-shrink px4",
+      fullPageModal ? "py4" : "py2",
+      {
+        "border-top": !fullPageModal || (fullPageModal && !formModal),
+      },
+    )}
+  >
     <div
-        className={cx("ModalFooter flex-no-shrink px4", fullPageModal ? "py4" : "py2", {
-            "border-top": !fullPageModal || (fullPageModal && !formModal),
-        })}
+      className="flex-full ml-auto mr-auto flex"
+      style={{ maxWidth: formModal && fullPageModal ? FORM_WIDTH : undefined }}
     >
-        <div
-            className="flex-full ml-auto mr-auto flex"
-            style={{ maxWidth: (formModal && fullPageModal) ? FORM_WIDTH : undefined }}
-        >
-            <div className="flex-full" />
-            { Array.isArray(children) ?
-                children.map((child, index) => <span key={index} className="ml2">{child}</span>)
-            :
-                children
-            }
-        </div>
+      <div className="flex-full" />
+      {Array.isArray(children)
+        ? children.map((child, index) => (
+            <span key={index} className="ml2">
+              {child}
+            </span>
+          ))
+        : children}
     </div>
+  </div>
+);
diff --git a/frontend/src/metabase/components/ModalWithTrigger.jsx b/frontend/src/metabase/components/ModalWithTrigger.jsx
index e82e9df5f6b4100b55dccd77c5eea0c5add04c50..fd89cf9df6ef4cbd55dc41268fb32e44127901bf 100644
--- a/frontend/src/metabase/components/ModalWithTrigger.jsx
+++ b/frontend/src/metabase/components/ModalWithTrigger.jsx
@@ -1,4 +1,4 @@
-import Triggerable from './Triggerable.jsx';
-import Modal from './Modal.jsx';
+import Triggerable from "./Triggerable.jsx";
+import Modal from "./Modal.jsx";
 
 export default Triggerable(Modal);
diff --git a/frontend/src/metabase/components/NewsletterForm.jsx b/frontend/src/metabase/components/NewsletterForm.jsx
index 4c884350624dc04cad3eb13cd3796a63e0828733..93f9cd285b8e1886ac7c7b84c49f54e35242f6ad 100644
--- a/frontend/src/metabase/components/NewsletterForm.jsx
+++ b/frontend/src/metabase/components/NewsletterForm.jsx
@@ -2,87 +2,111 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
-import Icon from 'metabase/components/Icon.jsx';
+import { t } from "c-3po";
+import Icon from "metabase/components/Icon.jsx";
 
 export default class NewsletterForm extends Component {
+  constructor(props, context) {
+    super(props, context);
 
-    constructor(props, context) {
-        super(props, context);
+    this.state = { submitted: false };
 
-        this.state = { submitted: false };
+    this.styles = {
+      container: {
+        borderWidth: "2px",
+      },
 
-        this.styles = {
-            container: {
-                borderWidth: "2px"
-            },
+      input: {
+        fontSize: "1.1rem",
+        color: "#676C72",
+        width: "350px",
+      },
 
-            input: {
-                fontSize: '1.1rem',
-                color: '#676C72',
-                width: "350px"
-            },
-
-            label: {
-                top: "-12px"
-            }
-        }
-    }
-
-    static propTypes = {
-        initialEmail: PropTypes.string.isRequired
+      label: {
+        top: "-12px",
+      },
     };
+  }
 
-    subscribeUser(e) {
-        e.preventDefault();
+  static propTypes = {
+    initialEmail: PropTypes.string.isRequired,
+  };
 
-        var formData = new FormData();
-        formData.append("EMAIL", ReactDOM.findDOMNode(this.refs.email).value);
-        formData.append("b_869fec0e4689e8fd1db91e795_b9664113a8", "");
+  subscribeUser(e) {
+    e.preventDefault();
 
-        let req = new XMLHttpRequest();
-        req.open("POST", "https://metabase.us10.list-manage.com/subscribe/post?u=869fec0e4689e8fd1db91e795&id=b9664113a8");
-        req.send(formData);
+    var formData = new FormData();
+    formData.append("EMAIL", ReactDOM.findDOMNode(this.refs.email).value);
+    formData.append("b_869fec0e4689e8fd1db91e795_b9664113a8", "");
 
-        this.setState({submitted: true});
-    }
+    let req = new XMLHttpRequest();
+    req.open(
+      "POST",
+      "https://metabase.us10.list-manage.com/subscribe/post?u=869fec0e4689e8fd1db91e795&id=b9664113a8",
+    );
+    req.send(formData);
 
-    render() {
-        const { initialEmail } = this.props;
-        const { submitted } = this.state;
+    this.setState({ submitted: true });
+  }
 
-        return (
-            <div style={this.styles.container} className="bordered rounded p4 relative">
-                <div style={this.styles.label} className="absolute text-centered left right">
-                    <div className="px3 bg-white h5 text-bold text-grey-4 text-uppercase inline-block">
-                      <Icon className="mr1 float-left" name="mail" size={16} />
-                      <span className="inline-block" style={{marginTop: 1}}>{t`Metabase Newsletter`}</span>
-                    </div>
-                </div>
+  render() {
+    const { initialEmail } = this.props;
+    const { submitted } = this.state;
 
-                <div className="MB-Newsletter sm-float-right">
-                    <div>
-                        <div style={{color: "#878E95"}} className="text-grey-4 h3 pb3">
-                            {t`Get infrequent emails about new releases and feature updates.`}
-                        </div>
+    return (
+      <div
+        style={this.styles.container}
+        className="bordered rounded p4 relative"
+      >
+        <div
+          style={this.styles.label}
+          className="absolute text-centered left right"
+        >
+          <div className="px3 bg-white h5 text-bold text-grey-4 text-uppercase inline-block">
+            <Icon className="mr1 float-left" name="mail" size={16} />
+            <span
+              className="inline-block"
+              style={{ marginTop: 1 }}
+            >{t`Metabase Newsletter`}</span>
+          </div>
+        </div>
 
-                        <form onSubmit={this.subscribeUser.bind(this)} noValidate>
-                            <div>
-                                { !submitted ?
-                                    <div className="">
-                                        <input ref="email" style={this.styles.input} className="AdminInput bordered rounded h3 inline-block" type="email" defaultValue={initialEmail} placeholder={t`Email address`} />
-                                        <input className="Button float-right inline-block ml1" type="submit" value={t`Subscribe`} name="subscribe" />
-                                    </div>
-                                :
-                                    <div className="text-success text-centered text-bold h3 p1">
-                                        <Icon className="mr2" name="check" size={16} />{t`You're subscribed. Thanks for using Metabase!`}
-                                    </div>
-                                }
-                            </div>
-                        </form>
-                    </div>
-                </div>
+        <div className="MB-Newsletter sm-float-right">
+          <div>
+            <div style={{ color: "#878E95" }} className="text-grey-4 h3 pb3">
+              {t`Get infrequent emails about new releases and feature updates.`}
             </div>
-        );
-    }
+
+            <form onSubmit={this.subscribeUser.bind(this)} noValidate>
+              <div>
+                {!submitted ? (
+                  <div className="">
+                    <input
+                      ref="email"
+                      style={this.styles.input}
+                      className="AdminInput bordered rounded h3 inline-block"
+                      type="email"
+                      defaultValue={initialEmail}
+                      placeholder={t`Email address`}
+                    />
+                    <input
+                      className="Button float-right inline-block ml1"
+                      type="submit"
+                      value={t`Subscribe`}
+                      name="subscribe"
+                    />
+                  </div>
+                ) : (
+                  <div className="text-success text-centered text-bold h3 p1">
+                    <Icon className="mr2" name="check" size={16} />
+                    {t`You're subscribed. Thanks for using Metabase!`}
+                  </div>
+                )}
+              </div>
+            </form>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/NotFound.jsx b/frontend/src/metabase/components/NotFound.jsx
index 432e39addb9267bd9218f1486862ff3fe70ef977..28279e7d8b0a8f42530a47e170228e039b7c4a1b 100644
--- a/frontend/src/metabase/components/NotFound.jsx
+++ b/frontend/src/metabase/components/NotFound.jsx
@@ -1,31 +1,37 @@
 import React, { Component } from "react";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import * as Urls from "metabase/lib/urls";
 
 export default class NotFound extends Component {
-    render() {
-        return (
-            <div className="layout-centered flex full">
-                <div className="p4 text-bold">
-                    <h1 className="text-brand text-light mb3">{t`We're a little lost...`}</h1>
-                    <p className="h4 mb1">{t`The page you asked for couldn't be found`}.</p>
-                    <p className="h4">{t`You might've been tricked by a ninja, but in all likelihood, you were just given a bad link.`}</p>
-                    <p className="h4 my4">{t`You can always:`}</p>
-                    <div className="flex align-center">
-                        <Link to={Urls.question()} className="Button Button--primary">
-                            <div className="p1">{t`Ask a new question.`}</div>
-                        </Link>
-                        <span className="mx2">{t`or`}</span>
-                        <a className="Button Button--withIcon" target="_blank" href="http://tv.giphy.com/kitten">
-                            <div className="p1 flex align-center relative">
-                                <span className="h2">😸</span>
-                                <span className="ml1">{t`Take a kitten break.`}</span>
-                            </div>
-                        </a>
-                    </div>
-                </div>
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className="layout-centered flex full">
+        <div className="p4 text-bold">
+          <h1 className="text-brand text-light mb3">{t`We're a little lost...`}</h1>
+          <p className="h4 mb1">
+            {t`The page you asked for couldn't be found`}.
+          </p>
+          <p className="h4">{t`You might've been tricked by a ninja, but in all likelihood, you were just given a bad link.`}</p>
+          <p className="h4 my4">{t`You can always:`}</p>
+          <div className="flex align-center">
+            <Link to={Urls.question()} className="Button Button--primary">
+              <div className="p1">{t`Ask a new question.`}</div>
+            </Link>
+            <span className="mx2">{t`or`}</span>
+            <a
+              className="Button Button--withIcon"
+              target="_blank"
+              href="https://giphy.com/tv/search/kitten"
+            >
+              <div className="p1 flex align-center relative">
+                <span className="h2">😸</span>
+                <span className="ml1">{t`Take a kitten break.`}</span>
+              </div>
+            </a>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/NumericInput.jsx b/frontend/src/metabase/components/NumericInput.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b626c79240776d85298abaa2d9b73ac3bbb05f34
--- /dev/null
+++ b/frontend/src/metabase/components/NumericInput.jsx
@@ -0,0 +1,25 @@
+/* @flow */
+
+import React from "react";
+
+import Input from "metabase/components/Input.jsx";
+
+type Props = {
+  value: ?(number | string),
+  onChange: (value: ?number) => void,
+};
+
+const NumericInput = ({ value, onChange, ...props }: Props) => (
+  <Input
+    value={value == null ? "" : String(value)}
+    onBlurChange={({ target: { value } }) => {
+      value = value ? parseInt(value, 10) : null;
+      if (!isNaN(value)) {
+        onChange(value);
+      }
+    }}
+    {...props}
+  />
+);
+
+export default NumericInput;
diff --git a/frontend/src/metabase/components/OnClickOutsideWrapper.jsx b/frontend/src/metabase/components/OnClickOutsideWrapper.jsx
index 700ff3fa880ba49a609b6a4ebfc237eef8ffbfc3..270ef2dfa07dabaf03c3987628538c0581319f13 100644
--- a/frontend/src/metabase/components/OnClickOutsideWrapper.jsx
+++ b/frontend/src/metabase/components/OnClickOutsideWrapper.jsx
@@ -8,69 +8,70 @@ import { KEYCODE_ESCAPE } from "metabase/lib/keyboard";
 const popoverStack = [];
 
 export default class OnClickOutsideWrapper extends Component {
-    static propTypes = {
-        handleDismissal: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    handleDismissal: PropTypes.func.isRequired,
+  };
 
-    static defaultProps = {
-        dismissOnClickOutside: true,
-        dismissOnEscape: true
-    };
+  static defaultProps = {
+    dismissOnClickOutside: true,
+    dismissOnEscape: true,
+  };
 
-    componentDidMount() {
-        // necessary to ignore click events that fire immediately, causing modals/popovers to close prematurely
-        this._timeout = setTimeout(() => {
-            popoverStack.push(this);
+  componentDidMount() {
+    // necessary to ignore click events that fire immediately, causing modals/popovers to close prematurely
+    this._timeout = setTimeout(() => {
+      popoverStack.push(this);
 
-            // HACK: set the z-index of the parent element to ensure it"s always on top
-            // NOTE: this actually doesn"t seem to be working correctly for popovers since PopoverBody creates a stacking context
-            ReactDOM.findDOMNode(this).parentNode.style.zIndex = popoverStack.length + 2; // HACK: add 2 to ensure it"s in front of main and nav elements
+      // HACK: set the z-index of the parent element to ensure it"s always on top
+      // NOTE: this actually doesn"t seem to be working correctly for popovers since PopoverBody creates a stacking context
+      ReactDOM.findDOMNode(this).parentNode.style.zIndex =
+        popoverStack.length + 2; // HACK: add 2 to ensure it"s in front of main and nav elements
 
-            if (this.props.dismissOnEscape) {
-                document.addEventListener("keydown", this._handleKeyPress, false);
-            }
-            if (this.props.dismissOnClickOutside) {
-                window.addEventListener("mousedown", this._handleClick, true);
-            }
-        }, 0);
-    }
+      if (this.props.dismissOnEscape) {
+        document.addEventListener("keydown", this._handleKeyPress, false);
+      }
+      if (this.props.dismissOnClickOutside) {
+        window.addEventListener("mousedown", this._handleClick, true);
+      }
+    }, 0);
+  }
 
-    componentWillUnmount() {
-        document.removeEventListener("keydown", this._handleKeyPress, false);
-        window.removeEventListener("mousedown", this._handleClick, true);
-        clearTimeout(this._timeout);
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this._handleKeyPress, false);
+    window.removeEventListener("mousedown", this._handleClick, true);
+    clearTimeout(this._timeout);
 
-        // remove from the stack after a delay, if it is removed through some other
-        // means this will happen too early causing parent modal to close
-        setTimeout(() => {
-            var index = popoverStack.indexOf(this);
-            if (index >= 0) {
-                popoverStack.splice(index, 1);
-            }
-        }, 0);
-    }
+    // remove from the stack after a delay, if it is removed through some other
+    // means this will happen too early causing parent modal to close
+    setTimeout(() => {
+      var index = popoverStack.indexOf(this);
+      if (index >= 0) {
+        popoverStack.splice(index, 1);
+      }
+    }, 0);
+  }
 
-    _handleClick = (e) => {
-        if (!ReactDOM.findDOMNode(this).contains(e.target)) {
-            setTimeout(this._handleDismissal, 0);
-        }
+  _handleClick = e => {
+    if (!ReactDOM.findDOMNode(this).contains(e.target)) {
+      setTimeout(this._handleDismissal, 0);
     }
+  };
 
-    _handleKeyPress = (e) => {
-        if (e.keyCode === KEYCODE_ESCAPE) {
-            e.preventDefault();
-            this._handleDismissal();
-        }
+  _handleKeyPress = e => {
+    if (e.keyCode === KEYCODE_ESCAPE) {
+      e.preventDefault();
+      this._handleDismissal();
     }
+  };
 
-    _handleDismissal = (e) => {
-        // only propagate event for the popover on top of the stack
-        if (this === popoverStack[popoverStack.length - 1]) {
-            this.props.handleDismissal(e);
-        }
+  _handleDismissal = e => {
+    // only propagate event for the popover on top of the stack
+    if (this === popoverStack[popoverStack.length - 1]) {
+      this.props.handleDismissal(e);
     }
+  };
 
-    render() {
-        return React.Children.only(this.props.children);
-    }
+  render() {
+    return React.Children.only(this.props.children);
+  }
 }
diff --git a/frontend/src/metabase/components/PasswordReveal.jsx b/frontend/src/metabase/components/PasswordReveal.jsx
index a5b0ac226433dafdecfec4ef906385e485ac6f03..98f896e928280fe6e0431c120d226f75c308527a 100644
--- a/frontend/src/metabase/components/PasswordReveal.jsx
+++ b/frontend/src/metabase/components/PasswordReveal.jsx
@@ -1,74 +1,78 @@
 /* flow */
 import React, { Component } from "react";
-import CopyButton from 'metabase/components/CopyButton';
-import { t } from 'c-3po';
+import CopyButton from "metabase/components/CopyButton";
+import { t } from "c-3po";
 
 type State = {
-    visible: boolean
-}
+  visible: boolean,
+};
 
 type Props = {
-    password: string
-}
+  password: string,
+};
 
 const styles = {
-    input: {
-        fontSize: '1.2rem',
-        letterSpacing: '2',
-        color: '#676C72',
-        outline: "none"
-    }
-}
+  input: {
+    fontSize: "1.2rem",
+    letterSpacing: "2",
+    color: "#676C72",
+    outline: "none",
+  },
+};
 
-const Label = () =>
-    <div style={{ top: -12 }} className="absolute text-centered left right">
-        <span className="px1 bg-white h6 text-bold text-grey-3 text-uppercase">
-            {t`Temporary Password`}
-        </span>
-    </div>
+const Label = () => (
+  <div style={{ top: -12 }} className="absolute text-centered left right">
+    <span className="px1 bg-white h6 text-bold text-grey-3 text-uppercase">
+      {t`Temporary Password`}
+    </span>
+  </div>
+);
 
 export default class PasswordReveal extends Component {
+  props: Props;
+  state: State = { visible: false };
 
-    props: Props
-    state: State = { visible: false };
-
-    render() {
-        const { password } = this.props;
-        const { visible } = this.state;
-
-        return (
-            <div style={{ borderWidth: 2 }} className="bordered rounded flex align-center  p3 relative">
+  render() {
+    const { password } = this.props;
+    const { visible } = this.state;
 
-                <Label />
+    return (
+      <div
+        style={{ borderWidth: 2 }}
+        className="bordered rounded flex align-center  p3 relative"
+      >
+        <Label />
 
-                { visible ?
-                    <input
-                        ref="input"
-                        style={styles.input}
-                        className="text-grey-2 text-normal mr3 borderless"
-                        value={password}
-                        onClick={({ target }) => target.setSelectionRange(0, target.value.length) }
-                    />
-                :
-                    <span style={styles.input} className="mr3">
-                        &#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;
-                    </span>
-                }
+        {visible ? (
+          <input
+            ref="input"
+            style={styles.input}
+            className="text-grey-2 text-normal mr3 borderless"
+            value={password}
+            onClick={({ target }) =>
+              target.setSelectionRange(0, target.value.length)
+            }
+          />
+        ) : (
+          <span style={styles.input} className="mr3">
+            &#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;&#9679;
+          </span>
+        )}
 
-                <div className="ml-auto flex align-center">
-                    <a
-                        className="link text-bold mr2"
-                        onClick={() => this.setState({ visible: !visible })}
-                    >
-                        { visible ? t`Hide` : t`Show` }
-                    </a>
+        <div className="ml-auto flex align-center">
+          <a
+            className="link text-bold mr2"
+            onClick={() => this.setState({ visible: !visible })}
+          >
+            {visible ? t`Hide` : t`Show`}
+          </a>
 
-                    <CopyButton
-                        className="text-brand-hover cursor-pointer"
-                        value={password}
-                    />
-                </div>
-            </div>
-        );
-    }
+          <CopyButton
+            className="text-brand-hover cursor-pointer"
+            value={password}
+          />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Popover.css b/frontend/src/metabase/components/Popover.css
index 1ab5700ee64823aa6d7efd25f1310a58fdfddb3f..a17586adbb7eb4053c0e5e4eda9f4751e730bce3 100644
--- a/frontend/src/metabase/components/Popover.css
+++ b/frontend/src/metabase/components/Popover.css
@@ -1,20 +1,20 @@
 /* afaik popover needs a positioning context to be able to calculate the transform */
 .PopoverContainer {
-	pointer-events: none;
-	position: absolute;
-	z-index: 4;
+  pointer-events: none;
+  position: absolute;
+  z-index: 4;
 }
 
 .PopoverBody {
-	pointer-events: auto;
-	min-width: 1em; /* ewwwwwwww */
-	border: 1px solid #ddd;
-	box-shadow: 0 1px 7px rgba(0, 0, 0, .18);
-	background-color: #fff;
-	border-radius: 4px;
-	display: flex;
-	flex-direction: column;
-	overflow: hidden;
+  pointer-events: auto;
+  min-width: 1em; /* ewwwwwwww */
+  border: 1px solid #ddd;
+  box-shadow: 0 1px 7px rgba(0, 0, 0, 0.18);
+  background-color: #fff;
+  border-radius: 4px;
+  display: flex;
+  flex-direction: column;
+  overflow: hidden;
   /* add a max-width so that long strings don't cause the popover to expand
    * see issue #4930 */
   max-width: 500px;
@@ -29,155 +29,155 @@
 }
 
 .PopoverBody.PopoverBody--tooltip {
-	color: white;
-	font-weight: bold;
-	background-color: rgb(76,71,71);
-	border: none;
-	pointer-events: none;
+  color: white;
+  font-weight: bold;
+  background-color: rgb(76, 71, 71);
+  border: none;
+  pointer-events: none;
 }
 
 /* shared arrow styles */
 .PopoverBody--withArrow:before,
 .PopoverBody--withArrow:after {
-	position: absolute;
-	content: '';
-	display: block;
-	border-left: 10px solid transparent;
-	border-right: 10px solid transparent;
-	border-top: 10px solid transparent;
-	border-bottom: 10px solid transparent;
-	pointer-events: none;
+  position: absolute;
+  content: "";
+  display: block;
+  border-left: 10px solid transparent;
+  border-right: 10px solid transparent;
+  border-top: 10px solid transparent;
+  border-bottom: 10px solid transparent;
+  pointer-events: none;
 }
 
 .PopoverBody .Form-input {
-	font-size: 1rem;
+  font-size: 1rem;
 }
 
 .PopoverBody .Form-field {
-	margin-bottom: 0.75rem;
+  margin-bottom: 0.75rem;
 }
 
 .PopoverHeader {
-	display: flex;
-	border-bottom: 1px solid var(--border-color);
-	min-width: 400px;
+  display: flex;
+  border-bottom: 1px solid var(--border-color);
+  min-width: 400px;
 }
 
 .PopoverHeader-item {
-	flex: 1;
-	position: relative;
-	top: 1px; /* to overlap bottom border */
-	text-align: center;
-	padding: 1em;
+  flex: 1;
+  position: relative;
+  top: 1px; /* to overlap bottom border */
+  text-align: center;
+  padding: 1em;
 
-	text-transform: uppercase;
-	font-size: 0.8em;
-	font-weight: 700;
-	color: color(var(--base-grey) shade(30%));
-	border-bottom: 2px solid transparent;
+  text-transform: uppercase;
+  font-size: 0.8em;
+  font-weight: 700;
+  color: color(var(--base-grey) shade(30%));
+  border-bottom: 2px solid transparent;
 }
 
 .PopoverHeader-item.selected {
-	color: currentcolor;
-	border-color: currentcolor;
+  color: currentcolor;
+  border-color: currentcolor;
 }
 
 .PopoverHeader-item--withArrow {
-	margin-right: 8px;
+  margin-right: 8px;
 }
 
 .PopoverHeader-item--withArrow:before,
 .PopoverHeader-item--withArrow:after {
-	position: absolute;
-	content: '';
-	display: block;
-	border-left: 8px solid transparent;
-	border-right: 8px solid transparent;
-	border-top: 8px solid transparent;
-	border-bottom: 8px solid transparent;
-	top: 50%;
-	margin-top: -8px;
+  position: absolute;
+  content: "";
+  display: block;
+  border-left: 8px solid transparent;
+  border-right: 8px solid transparent;
+  border-top: 8px solid transparent;
+  border-bottom: 8px solid transparent;
+  top: 50%;
+  margin-top: -8px;
 }
 
 /* create a slightly larger arrow on the right for border purposes */
 .PopoverHeader-item--withArrow:before {
-	right: -16px;
-	border-left-color: #ddd;
+  right: -16px;
+  border-left-color: #ddd;
 }
 
 /* create a smaller inset arrow on the right */
 .PopoverHeader-item--withArrow:after {
-	right: -15px;
-	border-left-color: #fff;
+  right: -15px;
+  border-left-color: #fff;
 }
 
 /* create a slightly larger arrow on the top for border purposes */
 .tether-element-attached-top .PopoverBody--withArrow:before {
-	top: -20px;
-	border-bottom-color: #ddd;
+  top: -20px;
+  border-bottom-color: #ddd;
 }
 .tether-element-attached-top .PopoverBody--tooltip:before {
-	border-bottom: none;
+  border-bottom: none;
 }
 
 /* create a smaller inset arrow on the top */
 .tether-element-attached-top .PopoverBody--withArrow:after {
-	top: -18px;
-	border-bottom-color: #fff;
+  top: -18px;
+  border-bottom-color: #fff;
 }
 .tether-element-attached-top .PopoverBody--tooltip:after {
-	border-bottom-color: rgb(76,71,71);
+  border-bottom-color: rgb(76, 71, 71);
 }
 
 /* create a slightly larger arrow on the bottom for border purposes */
 .tether-element-attached-bottom .PopoverBody--withArrow:before {
-	bottom: -20px;
-	border-top-color: #ddd;
+  bottom: -20px;
+  border-top-color: #ddd;
 }
 .tether-element-attached-bottom .PopoverBody--tooltip:before {
-	border-top: none;
+  border-top: none;
 }
 
 /* create a smaller inset arrow on the bottom */
 .tether-element-attached-bottom .PopoverBody--withArrow:after {
-	bottom: -18px;
-	border-top-color: #fff;
+  bottom: -18px;
+  border-top-color: #fff;
 }
 .tether-element-attached-bottom .PopoverBody--tooltip:after {
-	border-top-color: rgb(76,71,71);
+  border-top-color: rgb(76, 71, 71);
 }
 
 /* if the tether element is attached right, move our arrows right */
 .tether-target-attached-right .PopoverBody--withArrow:before,
 .tether-target-attached-right .PopoverBody--withArrow:after {
-	right: 12px;
+  right: 12px;
 }
 
 /* if the tether element is attached center, move our arrows to the center */
 .tether-element-attached-center .PopoverBody--withArrow:before,
 .tether-element-attached-center .PopoverBody--withArrow:after {
-	margin-left: 50%;
-	left: -10px;
+  margin-left: 50%;
+  left: -10px;
 }
 
 .tether-element-attached-right .PopoverBody--withArrow:before,
 .tether-element-attached-right .PopoverBody--withArrow:after {
-	right: 12px;
+  right: 12px;
 }
 
 .tether-element-attached-left .PopoverBody--withArrow:before,
 .tether-element-attached-left .PopoverBody--withArrow:after {
-	left: 12px;
+  left: 12px;
 }
 
 #popover-event-target {
-	position: fixed;
-	width: 6px;
-	height: 6px;
-	pointer-events: none;
+  position: fixed;
+  width: 6px;
+  height: 6px;
+  pointer-events: none;
 }
 
- /* transition classes */
+/* transition classes */
 
 .Popover-appear,
 .Popover-enter {
diff --git a/frontend/src/metabase/components/Popover.jsx b/frontend/src/metabase/components/Popover.jsx
index 1bf668a999a2a923d850dd53d0f525f69e2e414e..b37ea39e7e2a6c06f86c95f80e5153a2986bb00a 100644
--- a/frontend/src/metabase/components/Popover.jsx
+++ b/frontend/src/metabase/components/Popover.jsx
@@ -21,338 +21,340 @@ const PAGE_PADDING = 10;
 const POPOVER_BODY_PADDING = 2;
 
 export default class Popover extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            width: null,
-            height: null
-        };
-
-        this.handleDismissal = this.handleDismissal.bind(this);
-    }
-
-    static propTypes = {
-        id: PropTypes.string,
-        isOpen: PropTypes.bool,
-        hasArrow: PropTypes.bool,
-        // target: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
-        tetherOptions: PropTypes.object,
-        // used to prevent popovers from being taller than the screen
-        sizeToFit: PropTypes.bool,
-        pinInitialAttachment: PropTypes.bool,
-        // most popovers have a max-width to prevent them from being overly wide
-        // in the case their content is of an unexpected length
-        // noMaxWidth allows that to be overridden in cases where popovers should
-        // expand  alongside their contents contents
-        autoWidth: PropTypes.bool
+    this.state = {
+      width: null,
+      height: null,
     };
 
-    static defaultProps = {
-        isOpen: true,
-        hasArrow: true,
-        verticalAttachments: ["top", "bottom"],
-        horizontalAttachments: ["center", "left", "right"],
-        targetOffsetX: 24,
-        targetOffsetY: 5,
-        sizeToFit: false,
-        autoWidth: false
-    };
-
-    _getPopoverElement() {
-        if (!this._popoverElement) {
-            this._popoverElement = document.createElement('span');
-            this._popoverElement.className = 'PopoverContainer';
-            document.body.appendChild(this._popoverElement);
-            this._timer = setInterval(() => {
-                const { width, height } = this._popoverElement.getBoundingClientRect();
-                if (this.state.width !== width || this.state.height !== height) {
-                    this.setState({ width, height });
-                }
-            }, 100);
+    this.handleDismissal = this.handleDismissal.bind(this);
+  }
+
+  static propTypes = {
+    id: PropTypes.string,
+    isOpen: PropTypes.bool,
+    hasArrow: PropTypes.bool,
+    // target: PropTypes.oneOfType([PropTypes.func, PropTypes.node]),
+    tetherOptions: PropTypes.object,
+    // used to prevent popovers from being taller than the screen
+    sizeToFit: PropTypes.bool,
+    pinInitialAttachment: PropTypes.bool,
+    // most popovers have a max-width to prevent them from being overly wide
+    // in the case their content is of an unexpected length
+    // noMaxWidth allows that to be overridden in cases where popovers should
+    // expand  alongside their contents contents
+    autoWidth: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    isOpen: true,
+    hasArrow: true,
+    verticalAttachments: ["top", "bottom"],
+    horizontalAttachments: ["center", "left", "right"],
+    targetOffsetX: 24,
+    targetOffsetY: 5,
+    sizeToFit: false,
+    autoWidth: false,
+  };
+
+  _getPopoverElement() {
+    if (!this._popoverElement) {
+      this._popoverElement = document.createElement("span");
+      this._popoverElement.className = "PopoverContainer";
+      document.body.appendChild(this._popoverElement);
+      this._timer = setInterval(() => {
+        const { width, height } = this._popoverElement.getBoundingClientRect();
+        if (this.state.width !== width || this.state.height !== height) {
+          this.setState({ width, height });
         }
-        return this._popoverElement;
+      }, 100);
     }
+    return this._popoverElement;
+  }
 
-    componentDidMount() {
-        this._renderPopover(this.props.isOpen);
-    }
+  componentDidMount() {
+    this._renderPopover(this.props.isOpen);
+  }
 
-    componentDidUpdate() {
-        this._renderPopover(this.props.isOpen);
-    }
+  componentDidUpdate() {
+    this._renderPopover(this.props.isOpen);
+  }
 
-    componentWillUnmount() {
-        if (this._tether) {
-            this._tether.destroy();
-            delete this._tether;
-        }
-        if (this._popoverElement) {
-            this._renderPopover(false);
-            setTimeout(() => {
-                ReactDOM.unmountComponentAtNode(this._popoverElement);
-                if (this._popoverElement.parentNode) {
-                    this._popoverElement.parentNode.removeChild(this._popoverElement);
-                }
-                clearInterval(this._timer);
-                delete this._popoverElement, this._timer;
-            }, POPOVER_TRANSITION_LEAVE);
-        }
+  componentWillUnmount() {
+    if (this._tether) {
+      this._tether.destroy();
+      delete this._tether;
     }
-
-    handleDismissal(...args) {
-        if (this.props.onClose) {
-            this.props.onClose(...args)
+    if (this._popoverElement) {
+      this._renderPopover(false);
+      setTimeout(() => {
+        ReactDOM.unmountComponentAtNode(this._popoverElement);
+        if (this._popoverElement.parentNode) {
+          this._popoverElement.parentNode.removeChild(this._popoverElement);
         }
+        clearInterval(this._timer);
+        delete this._popoverElement, this._timer;
+      }, POPOVER_TRANSITION_LEAVE);
     }
+  }
 
-    _popoverComponent() {
-        const childProps = {
-            maxHeight: this._getMaxHeight()
-        };
-        return (
-            <OnClickOutsideWrapper
-                handleDismissal={this.handleDismissal}
-                dismissOnEscape={this.props.dismissOnEscape}
-                dismissOnClickOutside={this.props.dismissOnClickOutside}
-            >
-                <div
-                    id={this.props.id}
-                    className={cx(
-                        "PopoverBody",
-                        {
-                            "PopoverBody--withArrow": this.props.hasArrow,
-                            "PopoverBody--autoWidth": this.props.autoWidth
-                        },
-                        // TODO kdoh 10/16/2017 we should eventually remove this
-                        this.props.className
-                    )}
-                    style={this.props.style}
-                >
-                    { typeof this.props.children === "function" ?
-                        this.props.children(childProps)
-                    : React.Children.count(this.props.children) === 1 ?
-                        React.cloneElement(React.Children.only(this.props.children), childProps)
-                    :
-                        this.props.children
-                    }
-                </div>
-            </OnClickOutsideWrapper>
-        );
+  handleDismissal(...args) {
+    if (this.props.onClose) {
+      this.props.onClose(...args);
     }
+  }
 
-    _setTetherOptions(tetherOptions, o) {
-        if (o) {
-            tetherOptions = {
-                ...tetherOptions,
-                attachment: `${o.attachmentY} ${o.attachmentX}`,
-                targetAttachment: `${o.targetAttachmentY} ${o.targetAttachmentX}`,
-                targetOffset: `${o.offsetY}px ${o.offsetX}px`
-            }
-        }
-        if (this._tether) {
-            this._tether.setOptions(tetherOptions);
-        } else {
-            this._tether = new Tether(tetherOptions);
-        }
+  _popoverComponent() {
+    const childProps = {
+      maxHeight: this._getMaxHeight(),
+    };
+    return (
+      <OnClickOutsideWrapper
+        handleDismissal={this.handleDismissal}
+        dismissOnEscape={this.props.dismissOnEscape}
+        dismissOnClickOutside={this.props.dismissOnClickOutside}
+      >
+        <div
+          id={this.props.id}
+          className={cx(
+            "PopoverBody",
+            {
+              "PopoverBody--withArrow": this.props.hasArrow,
+              "PopoverBody--autoWidth": this.props.autoWidth,
+            },
+            // TODO kdoh 10/16/2017 we should eventually remove this
+            this.props.className,
+          )}
+          style={this.props.style}
+        >
+          {typeof this.props.children === "function"
+            ? this.props.children(childProps)
+            : React.Children.count(this.props.children) === 1
+              ? React.cloneElement(
+                  React.Children.only(this.props.children),
+                  childProps,
+                )
+              : this.props.children}
+        </div>
+      </OnClickOutsideWrapper>
+    );
+  }
+
+  _setTetherOptions(tetherOptions, o) {
+    if (o) {
+      tetherOptions = {
+        ...tetherOptions,
+        attachment: `${o.attachmentY} ${o.attachmentX}`,
+        targetAttachment: `${o.targetAttachmentY} ${o.targetAttachmentX}`,
+        targetOffset: `${o.offsetY}px ${o.offsetX}px`,
+      };
     }
-
-    _getMaxHeight() {
-        const { top, bottom } = this._getTarget().getBoundingClientRect();
-
-        let attachments;
-        if (this.props.pinInitialAttachment && this._best) {
-            // if we have a pinned attachment only use that
-            attachments = [this._best.attachmentY]
-        } else {
-            // otherwise use the verticalAttachments prop
-            attachments = this.props.verticalAttachments;
-        }
-
-        const availableHeights = attachments.map(attachmentY =>
-            attachmentY === "top" ?
-                window.innerHeight - bottom - this.props.targetOffsetY - PAGE_PADDING
-            : attachmentY === "bottom" ?
-                top - this.props.targetOffsetY - PAGE_PADDING
-            :
-                0
-        );
-
-        // get the largest available height, then subtract .PopoverBody's border and padding
-        return Math.max(...availableHeights) - POPOVER_BODY_PADDING;
+    if (this._tether) {
+      this._tether.setOptions(tetherOptions);
+    } else {
+      this._tether = new Tether(tetherOptions);
     }
-
-    _getBestAttachmentOptions(tetherOptions, options, attachments, offscreenProps, getAttachmentOptions) {
-        let best = { ...options };
-        let bestOffScreen = -Infinity;
-        // try each attachment until one is entirely on screen, or pick the least bad one
-        for (let attachment of attachments) {
-            // compute the options for this attachment position then set it
-            let options = getAttachmentOptions(best, attachment);
-            this._setTetherOptions(tetherOptions, options);
-
-            // get bounds within *document*
-            let elementRect = Tether.Utils.getBounds(tetherOptions.element);
-
-            // get bounds within *window*
-            let doc = document.documentElement;
-            let left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
-            let top = (window.pageYOffset || doc.scrollTop)  - (doc.clientTop || 0);
-            elementRect.top -= top;
-            elementRect.bottom += top;
-            elementRect.left -= left;
-            elementRect.right += left;
-
-            // test to see how much of the popover is off-screen
-            let offScreen = offscreenProps.map(prop => Math.min(elementRect[prop], 0)).reduce((a, b) => a + b);
-            // if none then we're done, otherwise check to see if it's the best option so far
-            if (offScreen === 0) {
-                best = options;
-                break;
-            } else if (offScreen > bestOffScreen) {
-                best = options;
-                bestOffScreen = offScreen;
-            }
-        }
-        return best;
+  }
+
+  _getMaxHeight() {
+    const { top, bottom } = this._getTarget().getBoundingClientRect();
+
+    let attachments;
+    if (this.props.pinInitialAttachment && this._best) {
+      // if we have a pinned attachment only use that
+      attachments = [this._best.attachmentY];
+    } else {
+      // otherwise use the verticalAttachments prop
+      attachments = this.props.verticalAttachments;
     }
 
-    _getTarget() {
-        let target;
-        if (this.props.targetEvent) {
-            // create a fake element at the event coordinates
-            target = document.getElementById("popover-event-target");
-            if (!target) {
-                target = document.createElement("div");
-                target.id = "popover-event-target";
-                document.body.appendChild(target);
-
-            }
-            target.style.left = (this.props.targetEvent.clientX - 3) + "px";
-            target.style.top = (this.props.targetEvent.clientY - 3) + "px";
-        } else if (this.props.target) {
-            if (typeof this.props.target === "function") {
-                target = ReactDOM.findDOMNode(this.props.target());
-            } else {
-                target = ReactDOM.findDOMNode(this.props.target);
-            }
-        }
-        if (target == null) {
-            target = ReactDOM.findDOMNode(this).parentNode;
-        }
-        return target;
+    const availableHeights = attachments.map(
+      attachmentY =>
+        attachmentY === "top"
+          ? window.innerHeight -
+            bottom -
+            this.props.targetOffsetY -
+            PAGE_PADDING
+          : attachmentY === "bottom"
+            ? top - this.props.targetOffsetY - PAGE_PADDING
+            : 0,
+    );
+
+    // get the largest available height, then subtract .PopoverBody's border and padding
+    return Math.max(...availableHeights) - POPOVER_BODY_PADDING;
+  }
+
+  _getBestAttachmentOptions(
+    tetherOptions,
+    options,
+    attachments,
+    offscreenProps,
+    getAttachmentOptions,
+  ) {
+    let best = { ...options };
+    let bestOffScreen = -Infinity;
+    // try each attachment until one is entirely on screen, or pick the least bad one
+    for (let attachment of attachments) {
+      // compute the options for this attachment position then set it
+      let options = getAttachmentOptions(best, attachment);
+      this._setTetherOptions(tetherOptions, options);
+
+      // get bounds within *document*
+      let elementRect = Tether.Utils.getBounds(tetherOptions.element);
+
+      // get bounds within *window*
+      let doc = document.documentElement;
+      let left = (window.pageXOffset || doc.scrollLeft) - (doc.clientLeft || 0);
+      let top = (window.pageYOffset || doc.scrollTop) - (doc.clientTop || 0);
+      elementRect.top -= top;
+      elementRect.bottom += top;
+      elementRect.left -= left;
+      elementRect.right += left;
+
+      // test to see how much of the popover is off-screen
+      let offScreen = offscreenProps
+        .map(prop => Math.min(elementRect[prop], 0))
+        .reduce((a, b) => a + b);
+      // if none then we're done, otherwise check to see if it's the best option so far
+      if (offScreen === 0) {
+        best = options;
+        break;
+      } else if (offScreen > bestOffScreen) {
+        best = options;
+        bestOffScreen = offScreen;
+      }
+    }
+    return best;
+  }
+
+  _getTarget() {
+    let target;
+    if (this.props.targetEvent) {
+      // create a fake element at the event coordinates
+      target = document.getElementById("popover-event-target");
+      if (!target) {
+        target = document.createElement("div");
+        target.id = "popover-event-target";
+        document.body.appendChild(target);
+      }
+      target.style.left = this.props.targetEvent.clientX - 3 + "px";
+      target.style.top = this.props.targetEvent.clientY - 3 + "px";
+    } else if (this.props.target) {
+      if (typeof this.props.target === "function") {
+        target = ReactDOM.findDOMNode(this.props.target());
+      } else {
+        target = ReactDOM.findDOMNode(this.props.target);
+      }
     }
+    if (target == null) {
+      target = ReactDOM.findDOMNode(this).parentNode;
+    }
+    return target;
+  }
+
+  _renderPopover(isOpen) {
+    // popover is open, lets do this!
+    const popoverElement = this._getPopoverElement();
+    ReactDOM.unstable_renderSubtreeIntoContainer(
+      this,
+      <ReactCSSTransitionGroup
+        transitionName="Popover"
+        transitionAppear
+        transitionEnter
+        transitionLeave
+        transitionAppearTimeout={POPOVER_TRANSITION_ENTER}
+        transitionEnterTimeout={POPOVER_TRANSITION_ENTER}
+        transitionLeaveTimeout={POPOVER_TRANSITION_LEAVE}
+      >
+        {isOpen ? this._popoverComponent() : null}
+      </ReactCSSTransitionGroup>,
+      popoverElement,
+    );
+
+    if (isOpen) {
+      var tetherOptions = {
+        element: popoverElement,
+        target: this._getTarget(),
+      };
+
+      if (this.props.tetherOptions) {
+        this._setTetherOptions({
+          ...tetherOptions,
+          ...this.props.tetherOptions,
+        });
+      } else {
+        if (!this._best || !this.props.pinInitialAttachment) {
+          let best = {
+            attachmentX: "center",
+            attachmentY: "top",
+            targetAttachmentX: "center",
+            targetAttachmentY: "bottom",
+            offsetX: 0,
+            offsetY: 0,
+          };
+
+          // horizontal
+          best = this._getBestAttachmentOptions(
+            tetherOptions,
+            best,
+            this.props.horizontalAttachments,
+            ["left", "right"],
+            (best, attachmentX) => ({
+              ...best,
+              attachmentX: attachmentX,
+              targetAttachmentX: "center",
+              offsetX: {
+                center: 0,
+                left: -this.props.targetOffsetX,
+                right: this.props.targetOffsetX,
+              }[attachmentX],
+            }),
+          );
+
+          // vertical
+          best = this._getBestAttachmentOptions(
+            tetherOptions,
+            best,
+            this.props.verticalAttachments,
+            ["top", "bottom"],
+            (best, attachmentY) => ({
+              ...best,
+              attachmentY: attachmentY,
+              targetAttachmentY: attachmentY === "top" ? "bottom" : "top",
+              offsetY: {
+                top: this.props.targetOffsetY,
+                bottom: -this.props.targetOffsetY,
+              }[attachmentY],
+            }),
+          );
+
+          this._best = best;
+        }
 
-    _renderPopover(isOpen) {
-        // popover is open, lets do this!
-        const popoverElement = this._getPopoverElement();
-        ReactDOM.unstable_renderSubtreeIntoContainer(this,
-            <ReactCSSTransitionGroup
-                transitionName="Popover"
-                transitionAppear
-                transitionEnter
-                transitionLeave
-                transitionAppearTimeout={POPOVER_TRANSITION_ENTER}
-                transitionEnterTimeout={POPOVER_TRANSITION_ENTER}
-                transitionLeaveTimeout={POPOVER_TRANSITION_LEAVE}
-            >
-                { isOpen ? this._popoverComponent() : null }
-            </ReactCSSTransitionGroup>
-        , popoverElement);
-
-        if (isOpen) {
-            var tetherOptions = {
-                element: popoverElement,
-                target: this._getTarget()
-            };
-
-            if (this.props.tetherOptions) {
-                this._setTetherOptions({
-                    ...tetherOptions,
-                    ...this.props.tetherOptions
-                });
-            } else {
-                if (!this._best || !this.props.pinInitialAttachment) {
-                    let best = {
-                        attachmentX: "center",
-                        attachmentY: "top",
-                        targetAttachmentX: "center",
-                        targetAttachmentY: "bottom",
-                        offsetX: 0,
-                        offsetY: 0
-                    };
-
-                    // horizontal
-                    best = this._getBestAttachmentOptions(
-                        tetherOptions, best, this.props.horizontalAttachments, ["left", "right"],
-                        (best, attachmentX) => ({
-                            ...best,
-                            attachmentX: attachmentX,
-                            targetAttachmentX: "center",
-                            offsetX: ({ "center": 0, "left": -(this.props.targetOffsetX), "right": this.props.targetOffsetX })[attachmentX]
-                        })
-                    );
-
-                    // vertical
-                    best = this._getBestAttachmentOptions(
-                        tetherOptions, best, this.props.verticalAttachments, ["top", "bottom"],
-                        (best, attachmentY) => ({
-                            ...best,
-                            attachmentY: attachmentY,
-                            targetAttachmentY: (attachmentY === "top" ? "bottom" : "top"),
-                            offsetY: ({ "top": this.props.targetOffsetY, "bottom": -(this.props.targetOffsetY) })[attachmentY]
-                        })
-                    );
-
-                    this._best = best;
-                }
-
-                // finally set the best options
-                this._setTetherOptions(tetherOptions, this._best);
-            }
-
-            if (this.props.sizeToFit) {
-                const body = tetherOptions.element.querySelector(".PopoverBody");
-                if (this._tether.attachment.top === "top") {
-                    if (constrainToScreen(body, "bottom", PAGE_PADDING)) {
-                        body.classList.add("scroll-y");
-                        body.classList.add("scroll-show");
-                    }
-                } else if (this._tether.attachment.top === "bottom") {
-                    if (constrainToScreen(body, "top", PAGE_PADDING)) {
-                        body.classList.add("scroll-y");
-                        body.classList.add("scroll-show");
-                    }
-                }
-            }
+        // finally set the best options
+        this._setTetherOptions(tetherOptions, this._best);
+      }
+
+      if (this.props.sizeToFit) {
+        const body = tetherOptions.element.querySelector(".PopoverBody");
+        if (this._tether.attachment.top === "top") {
+          if (constrainToScreen(body, "bottom", PAGE_PADDING)) {
+            body.classList.add("scroll-y");
+            body.classList.add("scroll-show");
+          }
+        } else if (this._tether.attachment.top === "bottom") {
+          if (constrainToScreen(body, "top", PAGE_PADDING)) {
+            body.classList.add("scroll-y");
+            body.classList.add("scroll-show");
+          }
         }
+      }
     }
+  }
 
-    render() {
-        return <span className="hide" />;
-    }
+  render() {
+    return <span className="hide" />;
+  }
 }
-
-/**
- * A modified version of TestPopover for Jest/Enzyme tests.
- * Simply renders the popover body inline instead of mutating DOM root.
- */
-export const TestPopover = (props) =>
-    (props.isOpen === undefined || props.isOpen) ?
-        <OnClickOutsideWrapper
-            handleDismissal={(...args) => { props.onClose && props.onClose(...args) }}
-            dismissOnEscape={props.dismissOnEscape}
-            dismissOnClickOutside={props.dismissOnClickOutside}
-        >
-            <div
-                id={props.id}
-                className={cx("TestPopover TestPopoverBody", props.className)}
-                style={props.style}
-                // because popover is normally directly attached to body element, other elements should not need
-                // to care about clicks that happen inside the popover
-                onClick={ (e) => { e.stopPropagation(); } }
-            >
-                { typeof props.children === "function" ? props.children({ maxHeight: 500 }) : props.children}
-            </div>
-        </OnClickOutsideWrapper>
-        : null
diff --git a/frontend/src/metabase/components/PopoverWithTrigger.jsx b/frontend/src/metabase/components/PopoverWithTrigger.jsx
index 87096ca924282ed1eb420c9469debc65077497cf..8385d166964f2335bb14ae525bcbc187b64e7dba 100644
--- a/frontend/src/metabase/components/PopoverWithTrigger.jsx
+++ b/frontend/src/metabase/components/PopoverWithTrigger.jsx
@@ -1,4 +1,4 @@
-import Triggerable from './Triggerable.jsx';
-import Popover from './Popover.jsx';
+import Triggerable from "./Triggerable.jsx";
+import Popover from "./Popover.jsx";
 
 export default Triggerable(Popover);
diff --git a/frontend/src/metabase/components/ProgressBar.jsx b/frontend/src/metabase/components/ProgressBar.jsx
index a10bf6de3685c4257ba41b8bedc1036a245bb689..a40db0bbf335fa85d090d9297c22b4cbe972eff0 100644
--- a/frontend/src/metabase/components/ProgressBar.jsx
+++ b/frontend/src/metabase/components/ProgressBar.jsx
@@ -2,19 +2,22 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 export default class ProgressBar extends Component {
-    static propTypes = {
-        percentage: PropTypes.number.isRequired
-    };
+  static propTypes = {
+    percentage: PropTypes.number.isRequired,
+  };
 
-    static defaultProps = {
-        className: "ProgressBar"
-    };
+  static defaultProps = {
+    className: "ProgressBar",
+  };
 
-    render() {
-        return (
-            <div className={this.props.className}>
-                <div className="ProgressBar-progress" style={{"width": (this.props.percentage * 100) + "%"}}></div>
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className={this.props.className}>
+        <div
+          className="ProgressBar-progress"
+          style={{ width: this.props.percentage * 100 + "%" }}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/QueryButton.css b/frontend/src/metabase/components/QueryButton.css
index 460d7909bdc7ccd98a143af109975adb757cf103..f1f8f79115dc318db626a4cee24d22fac6914161 100644
--- a/frontend/src/metabase/components/QueryButton.css
+++ b/frontend/src/metabase/components/QueryButton.css
@@ -1,16 +1,16 @@
 :local(.queryButton) {
-    composes: flex align-center no-decoration py1 from "style";
+  composes: flex align-center no-decoration py1 from "style";
 }
 
 :local(.queryButtonText) {
-    composes: flex-full mx2 text-default from "style";
-    max-width: 100%;
+  composes: flex-full mx2 text-default from "style";
+  max-width: 100%;
 }
 
 :local(.queryButtonCircle) {
-    composes: flex align-center justify-center text-brand from "style";
-    border: 1px solid currentColor;
-    border-radius: 99px;
-    width: 1.25rem;
-    height: 1.25rem;
-}
\ No newline at end of file
+  composes: flex align-center justify-center text-brand from "style";
+  border: 1px solid currentColor;
+  border-radius: 99px;
+  width: 1.25rem;
+  height: 1.25rem;
+}
diff --git a/frontend/src/metabase/components/QueryButton.jsx b/frontend/src/metabase/components/QueryButton.jsx
index 58985a6ae905c2ee55a6e00e4f5e7d0735d44d40..c02ffb44720f7c7ff3396d7c026278886785d1fa 100644
--- a/frontend/src/metabase/components/QueryButton.jsx
+++ b/frontend/src/metabase/components/QueryButton.jsx
@@ -8,36 +8,28 @@ import S from "./QueryButton.css";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const QueryButton = ({
-    className,
-    text,
-    icon,
-    iconClass,
-    onClick,
-    link,
-}) => 
-    <div className={className}>
-        <Link className={S.queryButton} onClick={onClick} to={link}>
-            <Icon 
-                className={iconClass} 
-                size={20} 
-                {...(typeof icon === 'string' ? { name: icon } : icon)} 
-            />
-            <span className={cx(S.queryButtonText, 'text-brand-hover')}>
-                {text}
-            </span>
-            <span className={S.queryButtonCircle}>
-                <Icon size={8} name="chevronright" />
-            </span>
-        </Link>
-    </div>;
+const QueryButton = ({ className, text, icon, iconClass, onClick, link }) => (
+  <div className={className}>
+    <Link className={S.queryButton} onClick={onClick} to={link}>
+      <Icon
+        className={iconClass}
+        size={20}
+        {...(typeof icon === "string" ? { name: icon } : icon)}
+      />
+      <span className={cx(S.queryButtonText, "text-brand-hover")}>{text}</span>
+      <span className={S.queryButtonCircle}>
+        <Icon size={8} name="chevronright" />
+      </span>
+    </Link>
+  </div>
+);
 QueryButton.propTypes = {
-    className: PropTypes.string,
-    icon: PropTypes.any.isRequired,
-    text: PropTypes.string.isRequired,
-    iconClass: PropTypes.string,
-    onClick: PropTypes.func,
-    link: PropTypes.string
+  className: PropTypes.string,
+  icon: PropTypes.any.isRequired,
+  text: PropTypes.string.isRequired,
+  iconClass: PropTypes.string,
+  onClick: PropTypes.func,
+  link: PropTypes.string,
 };
 
 export default pure(QueryButton);
diff --git a/frontend/src/metabase/components/QuestionSavedModal.jsx b/frontend/src/metabase/components/QuestionSavedModal.jsx
index f5a0a51932e4f6724b7db5025b4beabb9a0ffb21..4af499f8af042b87067287d0169d1690d56df344 100644
--- a/frontend/src/metabase/components/QuestionSavedModal.jsx
+++ b/frontend/src/metabase/components/QuestionSavedModal.jsx
@@ -2,27 +2,33 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import ModalContent from "metabase/components/ModalContent.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class QuestionSavedModal extends Component {
-    static propTypes = {
-        addToDashboardFn: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    addToDashboardFn: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
 
-    render() {
-        return (
-            <ModalContent
-                id="QuestionSavedModal"
-                title={t`Saved! Add this to a dashboard?`}
-                onClose={this.props.onClose}
-                className="Modal-content Modal-content--small NewForm"
-            >
-                <div className="Form-inputs mb4">
-                    <button className="Button Button--primary" onClick={this.props.addToDashboardFn}>{t`Yes please!`}</button>
-                    <button className="Button ml3" onClick={this.props.onClose}>{t`Not now`}</button>
-                </div>
-            </ModalContent>
-        );
-    }
+  render() {
+    return (
+      <ModalContent
+        id="QuestionSavedModal"
+        title={t`Saved! Add this to a dashboard?`}
+        onClose={this.props.onClose}
+        className="Modal-content Modal-content--small NewForm"
+      >
+        <div className="Form-inputs mb4">
+          <button
+            className="Button Button--primary"
+            onClick={this.props.addToDashboardFn}
+          >{t`Yes please!`}</button>
+          <button
+            className="Button ml3"
+            onClick={this.props.onClose}
+          >{t`Not now`}</button>
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Radio.jsx b/frontend/src/metabase/components/Radio.jsx
index 0a5c7eb9c80429d334215e67eead3f01ce46a996..53843c39b7b6f5767e6a2cc91542926cd2d77dc7 100644
--- a/frontend/src/metabase/components/Radio.jsx
+++ b/frontend/src/metabase/components/Radio.jsx
@@ -5,58 +5,77 @@ import cx from "classnames";
 import _ from "underscore";
 
 export default class Radio extends Component {
-    static propTypes = {
-        value: PropTypes.any,
-        options: PropTypes.array.isRequired,
-        onChange: PropTypes.func,
-        optionNameFn: PropTypes.func,
-        optionValueFn: PropTypes.func,
-        optionKeyFn: PropTypes.func,
-        isVertical: PropTypes.bool,
-        showButtons: PropTypes.bool,
-    };
+  static propTypes = {
+    value: PropTypes.any,
+    options: PropTypes.array.isRequired,
+    onChange: PropTypes.func,
+    optionNameFn: PropTypes.func,
+    optionValueFn: PropTypes.func,
+    optionKeyFn: PropTypes.func,
+    isVertical: PropTypes.bool,
+    showButtons: PropTypes.bool,
+  };
 
-    static defaultProps = {
-        optionNameFn: (option) => option.name,
-        optionValueFn: (option) => option.value,
-        optionKeyFn: (option) => option.value,
-        isVertical: false
-    };
+  static defaultProps = {
+    optionNameFn: option => option.name,
+    optionValueFn: option => option.value,
+    optionKeyFn: option => option.value,
+    isVertical: false,
+  };
 
-    constructor(props, context) {
-        super(props, context);
-        this._id = _.uniqueId("radio-");
-    }
+  constructor(props, context) {
+    super(props, context);
+    this._id = _.uniqueId("radio-");
+  }
 
-    render() {
-        const { value, options, onChange, optionNameFn, optionValueFn, optionKeyFn, isVertical, className } = this.props;
-        // show buttons for vertical only by default
-        const showButtons = this.props.showButtons != undefined ? this.props.showButtons : isVertical;
-        return (
-            <ul className={cx(className, "flex", { "flex-column": isVertical, "text-bold h3": !showButtons })}>
-                {options.map(option =>
-                    <li
-                        key={optionKeyFn(option)}
-                        className={cx("flex align-center cursor-pointer mt1 mr2", { "text-brand-hover": !showButtons })}
-                        onClick={(e) => onChange(optionValueFn(option))}
-                    >
-                        <input
-                            className="Form-radio"
-                            type="radio"
-                            name={this._id}
-                            value={optionValueFn(option)}
-                            checked={value === optionValueFn(option)}
-                            id={this._id+"-"+optionKeyFn(option)}
-                        />
-                        { showButtons &&
-                            <label htmlFor={this._id+"-"+optionKeyFn(option)} />
-                        }
-                        <span className={cx({ "text-brand": value === optionValueFn(option) })}>
-                            {optionNameFn(option)}
-                        </span>
-                    </li>
-                )}
-            </ul>
-        )
-    }
+  render() {
+    const {
+      value,
+      options,
+      onChange,
+      optionNameFn,
+      optionValueFn,
+      optionKeyFn,
+      isVertical,
+      className,
+    } = this.props;
+    // show buttons for vertical only by default
+    const showButtons =
+      this.props.showButtons != undefined ? this.props.showButtons : isVertical;
+    return (
+      <ul
+        className={cx(className, "flex", {
+          "flex-column": isVertical,
+          "text-bold h3": !showButtons,
+        })}
+      >
+        {options.map(option => (
+          <li
+            key={optionKeyFn(option)}
+            className={cx("flex align-center cursor-pointer mt1 mr2", {
+              "text-brand-hover": !showButtons,
+            })}
+            onClick={e => onChange(optionValueFn(option))}
+          >
+            <input
+              className="Form-radio"
+              type="radio"
+              name={this._id}
+              value={optionValueFn(option)}
+              checked={value === optionValueFn(option)}
+              id={this._id + "-" + optionKeyFn(option)}
+            />
+            {showButtons && (
+              <label htmlFor={this._id + "-" + optionKeyFn(option)} />
+            )}
+            <span
+              className={cx({ "text-brand": value === optionValueFn(option) })}
+            >
+              {optionNameFn(option)}
+            </span>
+          </li>
+        ))}
+      </ul>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/SaveStatus.jsx b/frontend/src/metabase/components/SaveStatus.jsx
index 2fe068f0cfc6fdbbf94ea838e72d405a41c12b76..a554185807c650ed79d55e4d009f72f3dbdac58b 100644
--- a/frontend/src/metabase/components/SaveStatus.jsx
+++ b/frontend/src/metabase/components/SaveStatus.jsx
@@ -2,51 +2,66 @@ import React, { Component } from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import _ from "underscore";
 
 export default class SaveStatus extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            saving: false,
-            recentlySavedTimeout: null,
-            error: null
-        };
+    this.state = {
+      saving: false,
+      recentlySavedTimeout: null,
+      error: null,
+    };
 
-        _.bindAll(this, "setSaving", "setSaved", "setSaveError");
-    }
+    _.bindAll(this, "setSaving", "setSaved", "setSaveError");
+  }
 
-    setSaving() {
-        clearTimeout(this.state.recentlySavedTimeout);
-        this.setState({ saving: true, recentlySavedTimeout: null, error: null });
-    }
+  setSaving() {
+    clearTimeout(this.state.recentlySavedTimeout);
+    this.setState({ saving: true, recentlySavedTimeout: null, error: null });
+  }
 
-    setSaved() {
-        clearTimeout(this.state.recentlySavedTimeout);
-        var recentlySavedTimeout = setTimeout(() => this.setState({ recentlySavedTimeout: null }), 5000);
-        this.setState({ saving: false, recentlySavedTimeout: recentlySavedTimeout, error: null });
-    }
+  setSaved() {
+    clearTimeout(this.state.recentlySavedTimeout);
+    var recentlySavedTimeout = setTimeout(
+      () => this.setState({ recentlySavedTimeout: null }),
+      5000,
+    );
+    this.setState({
+      saving: false,
+      recentlySavedTimeout: recentlySavedTimeout,
+      error: null,
+    });
+  }
 
-    setSaveError(error) {
-        this.setState({ saving: false, recentlySavedTimeout: null, error: error });
-    }
+  setSaveError(error) {
+    this.setState({ saving: false, recentlySavedTimeout: null, error: error });
+  }
 
-    render() {
-        if (this.state.saving) {
-            return (<div className="SaveStatus mx2 px2 border-right"><LoadingSpinner size={24} /></div>);
-        } else if (this.state.error) {
-            return (<div className="SaveStatus mx2 px2 border-right text-error">{t`Error:`} {String(this.state.error.message || this.state.error)}</div>)
-        } else if (this.state.recentlySavedTimeout != null) {
-            return (
-                <div className="SaveStatus mx2 px2 border-right flex align-center text-success">
-                    <Icon name="check" size={16} />
-                    <div className="ml1 h3 text-bold">{t`Saved`}</div>
-                </div>
-            )
-        } else {
-            return null;
-        }
+  render() {
+    if (this.state.saving) {
+      return (
+        <div className="SaveStatus mx2 px2 border-right">
+          <LoadingSpinner size={24} />
+        </div>
+      );
+    } else if (this.state.error) {
+      return (
+        <div className="SaveStatus mx2 px2 border-right text-error">
+          {t`Error:`} {String(this.state.error.message || this.state.error)}
+        </div>
+      );
+    } else if (this.state.recentlySavedTimeout != null) {
+      return (
+        <div className="SaveStatus mx2 px2 border-right flex align-center text-success">
+          <Icon name="check" size={16} />
+          <div className="ml1 h3 text-bold">{t`Saved`}</div>
+        </div>
+      );
+    } else {
+      return null;
     }
+  }
 }
diff --git a/frontend/src/metabase/components/SchedulePicker.jsx b/frontend/src/metabase/components/SchedulePicker.jsx
index 888aff1935d2c3d1285f87c01426f684928a525f..19345ccb46a81533ace968abc70476f2211719a0 100644
--- a/frontend/src/metabase/components/SchedulePicker.jsx
+++ b/frontend/src/metabase/components/SchedulePicker.jsx
@@ -6,32 +6,33 @@ import Select from "metabase/components/Select.jsx";
 
 import Settings from "metabase/lib/settings";
 import { capitalize } from "metabase/lib/formatting";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import _ from "underscore";
 
-export const HOUR_OPTIONS = _.times(12, (n) => (
-    { name: (n === 0 ? 12 : n)+":00", value: n }
-));
+export const HOUR_OPTIONS = _.times(12, n => ({
+  name: (n === 0 ? 12 : n) + ":00",
+  value: n,
+}));
 
 export const AM_PM_OPTIONS = [
-    { name: "AM", value: 0 },
-    { name: "PM", value: 1 }
+  { name: "AM", value: 0 },
+  { name: "PM", value: 1 },
 ];
 
 export const DAY_OF_WEEK_OPTIONS = [
-    { name: t`"Sunday`, value: "sun" },
-    { name: t`Monday`, value: "mon" },
-    { name: t`Tuesday`, value: "tue" },
-    { name: t`Wednesday`, value: "wed" },
-    { name: t`Thursday`, value: "thu" },
-    { name: t`Friday`, value: "fri" },
-    { name: t`Saturday`, value: "sat" }
+  { name: t`"Sunday`, value: "sun" },
+  { name: t`Monday`, value: "mon" },
+  { name: t`Tuesday`, value: "tue" },
+  { name: t`Wednesday`, value: "wed" },
+  { name: t`Thursday`, value: "thu" },
+  { name: t`Friday`, value: "fri" },
+  { name: t`Saturday`, value: "sat" },
 ];
 
 export const MONTH_DAY_OPTIONS = [
-    { name: t`First`, value: "first" },
-    { name: t`Last`, value: "last" },
-    { name: t`15th (Midpoint)`, value: "mid" }
+  { name: t`First`, value: "first" },
+  { name: t`Last`, value: "last" },
+  { name: t`15th (Midpoint)`, value: "mid" },
 ];
 
 /**
@@ -40,176 +41,202 @@ export const MONTH_DAY_OPTIONS = [
  * TODO Atte Keinänen 6/30/17: This could use text input fields instead of dropdown for time (hour + AM/PM) pickers
  */
 export default class SchedulePicker extends Component {
-    // TODO: How does this tread an empty schedule?
-
-    static propTypes = {
-        // the currently chosen schedule, e.g. { schedule_day: "mon", schedule_frame: "null", schedule_hour: 4, schedule_type: "daily" }
-        schedule: PropTypes.object.isRequired,
-        // TODO: hourly option?
-        // available schedules, e.g. [ "daily", "weekly", "monthly"]
-        scheduleOptions: PropTypes.array.isRequired,
-        // text before Daily/Weekly/Monthly... option
-        textBeforeInterval: PropTypes.string,
-        // text prepended to "12:00 PM PST, your Metabase timezone"
-        textBeforeSendTime: PropTypes.string,
-        onScheduleChange: PropTypes.func.isRequired,
+  // TODO: How does this tread an empty schedule?
+
+  static propTypes = {
+    // the currently chosen schedule, e.g. { schedule_day: "mon", schedule_frame: "null", schedule_hour: 4, schedule_type: "daily" }
+    schedule: PropTypes.object.isRequired,
+    // TODO: hourly option?
+    // available schedules, e.g. [ "daily", "weekly", "monthly"]
+    scheduleOptions: PropTypes.array.isRequired,
+    // text before Daily/Weekly/Monthly... option
+    textBeforeInterval: PropTypes.string,
+    // text prepended to "12:00 PM PST, your Metabase timezone"
+    textBeforeSendTime: PropTypes.string,
+    onScheduleChange: PropTypes.func.isRequired,
+  };
+
+  onPropertyChange(name, value) {
+    let newSchedule = {
+      ...this.props.schedule,
+      [name]: value,
     };
 
-    onPropertyChange(name, value) {
-        let newSchedule = {
-            ...this.props.schedule,
-            [name]: value
+    if (name === "schedule_type") {
+      // clear out other values than schedule_type for hourly schedule
+      if (value === "hourly") {
+        newSchedule = {
+          ...newSchedule,
+          schedule_day: null,
+          schedule_frame: null,
+          schedule_hour: null,
         };
+      }
 
-        if (name === "schedule_type") {
-            // clear out other values than schedule_type for hourly schedule
-            if (value === "hourly") {
-                newSchedule = { ...newSchedule, "schedule_day": null, "schedule_frame": null, "schedule_hour": null };
-            }
-
-            // default to midnight for all schedules other than hourly
-            if (value !== "hourly") {
-                newSchedule = { ...newSchedule, "schedule_hour": newSchedule.schedule_hour || 0 }
-            }
-
-            // clear out other values than schedule_type and schedule_day for daily schedule
-            if (value === "daily") {
-                newSchedule = { ...newSchedule, "schedule_day": null, "schedule_frame": null };
-            }
-
-            // default to Monday when user wants a weekly schedule + clear out schedule_frame
-            if (value === "weekly") {
-                newSchedule = { ...newSchedule, "schedule_day": "mon", "schedule_frame": null };
-            }
-
-            // default to First, Monday when user wants a monthly schedule
-            if (value === "monthly") {
-                newSchedule = { ...newSchedule, "schedule_frame": "first", "schedule_day": "mon" };
-            }
-        }
-        else if (name === "schedule_frame") {
-            // when the monthly schedule frame is the 15th, clear out the schedule_day
-            if (value === "mid") {
-                newSchedule = { ...newSchedule, "schedule_day": null };
-            }
-        }
-
-        const changedProp = { name, value };
-        this.props.onScheduleChange(newSchedule, changedProp)
-    }
-
-    renderMonthlyPicker() {
-        let { schedule } = this.props;
-
-        let DAY_OPTIONS = DAY_OF_WEEK_OPTIONS.slice(0);
-        DAY_OPTIONS.unshift({ name: t`Calendar Day`, value: null });
-
-        return (
-            <span className="mt1">
-                <span className="h4 text-bold mx1">on the</span>
-                <Select
-                    value={_.find(MONTH_DAY_OPTIONS, (o) => o.value === schedule.schedule_frame)}
-                    options={MONTH_DAY_OPTIONS}
-                    optionNameFn={o => o.name}
-                    className="h4 text-bold bg-white"
-                    optionValueFn={o => o.value}
-                    onChange={(o) => this.onPropertyChange("schedule_frame", o) }
-                />
-                { schedule.schedule_frame !== "mid" &&
-                    <span className="mt1 mx1">
-                        <Select
-                            value={_.find(DAY_OPTIONS, (o) => o.value === schedule.schedule_day)}
-                            options={DAY_OPTIONS}
-                            optionNameFn={o => o.name}
-                            optionValueFn={o => o.value}
-                            className="h4 text-bold bg-white"
-                            onChange={(o) => this.onPropertyChange("schedule_day", o) }
-                        />
-                    </span>
-                }
-            </span>
-        );
-    }
-
-    renderDayPicker() {
-        let { schedule } = this.props;
-
-        return (
-            <span className="mt1">
-                <span className="h4 text-bold mx1">on</span>
-                <Select
-                    value={_.find(DAY_OF_WEEK_OPTIONS, (o) => o.value === schedule.schedule_day)}
-                    options={DAY_OF_WEEK_OPTIONS}
-                    optionNameFn={o => o.name}
-                    optionValueFn={o => o.value}
-                    className="h4 text-bold bg-white"
-                    onChange={(o) => this.onPropertyChange("schedule_day", o) }
-                />
-            </span>
-        );
-    }
-
-    renderHourPicker() {
-        let { schedule, textBeforeSendTime } = this.props;
-
-        let hourOfDay = isNaN(schedule.schedule_hour) ? 8 : schedule.schedule_hour;
-        let hour = hourOfDay % 12;
-        let amPm = hourOfDay >= 12 ? 1 : 0;
-        let timezone = Settings.get("timezone_short");
-        return (
-            <div className="mt1">
-                <span className="h4 text-bold mr1">at</span>
-                <Select
-                    className="mr1 h4 text-bold bg-white"
-                    value={_.find(HOUR_OPTIONS, (o) => o.value === hour)}
-                    options={HOUR_OPTIONS}
-                    optionNameFn={o => o.name}
-                    optionValueFn={o => o.value}
-                    onChange={(o) => this.onPropertyChange("schedule_hour", o + amPm * 12) }
-                />
-                <Select
-                    value={_.find(AM_PM_OPTIONS, (o) => o.value === amPm)}
-                    options={AM_PM_OPTIONS}
-                    optionNameFn={o => o.name}
-                    optionValueFn={o => o.value}
-                    onChange={(o) => this.onPropertyChange("schedule_hour", hour + o * 12) }
-                    className="h4 text-bold bg-white"
-                />
-                { textBeforeSendTime &&
-                    <div className="mt2 h4 text-bold text-grey-3 border-top pt2">
-                        {textBeforeSendTime} {hour === 0 ? 12 : hour}:00 {amPm ? "PM" : "AM"} {timezone}, {t`your Metabase timezone`}.
-                    </div>
-                }
-            </div>
-        );
+      // default to midnight for all schedules other than hourly
+      if (value !== "hourly") {
+        newSchedule = {
+          ...newSchedule,
+          schedule_hour: newSchedule.schedule_hour || 0,
+        };
+      }
+
+      // clear out other values than schedule_type and schedule_day for daily schedule
+      if (value === "daily") {
+        newSchedule = {
+          ...newSchedule,
+          schedule_day: null,
+          schedule_frame: null,
+        };
+      }
+
+      // default to Monday when user wants a weekly schedule + clear out schedule_frame
+      if (value === "weekly") {
+        newSchedule = {
+          ...newSchedule,
+          schedule_day: "mon",
+          schedule_frame: null,
+        };
+      }
+
+      // default to First, Monday when user wants a monthly schedule
+      if (value === "monthly") {
+        newSchedule = {
+          ...newSchedule,
+          schedule_frame: "first",
+          schedule_day: "mon",
+        };
+      }
+    } else if (name === "schedule_frame") {
+      // when the monthly schedule frame is the 15th, clear out the schedule_day
+      if (value === "mid") {
+        newSchedule = { ...newSchedule, schedule_day: null };
+      }
     }
 
-    render() {
-        let { schedule, scheduleOptions, textBeforeInterval } = this.props;
-
-        const scheduleType = schedule.schedule_type;
-
-        return (
-            <div className="mt1">
-                <span className="h4 text-bold mr1">{ textBeforeInterval }</span>
-                <Select
-                    className="h4 text-bold bg-white"
-                    value={scheduleType}
-                    options={scheduleOptions}
-                    optionNameFn={o => capitalize(o)}
-                    optionValueFn={o => o}
-                    onChange={(o) => this.onPropertyChange("schedule_type", o)}
-                />
-                { scheduleType === "monthly" &&
-                    this.renderMonthlyPicker()
-                }
-                { scheduleType === "weekly" &&
-                    this.renderDayPicker()
-                }
-                { (scheduleType === "daily" || scheduleType === "weekly" || scheduleType === "monthly") &&
-                    this.renderHourPicker()
-                }
-            </div>
-        );
-    }
+    const changedProp = { name, value };
+    this.props.onScheduleChange(newSchedule, changedProp);
+  }
+
+  renderMonthlyPicker() {
+    let { schedule } = this.props;
+
+    let DAY_OPTIONS = DAY_OF_WEEK_OPTIONS.slice(0);
+    DAY_OPTIONS.unshift({ name: t`Calendar Day`, value: null });
+
+    return (
+      <span className="mt1">
+        <span className="h4 text-bold mx1">on the</span>
+        <Select
+          value={_.find(
+            MONTH_DAY_OPTIONS,
+            o => o.value === schedule.schedule_frame,
+          )}
+          options={MONTH_DAY_OPTIONS}
+          optionNameFn={o => o.name}
+          className="h4 text-bold bg-white"
+          optionValueFn={o => o.value}
+          onChange={o => this.onPropertyChange("schedule_frame", o)}
+        />
+        {schedule.schedule_frame !== "mid" && (
+          <span className="mt1 mx1">
+            <Select
+              value={_.find(
+                DAY_OPTIONS,
+                o => o.value === schedule.schedule_day,
+              )}
+              options={DAY_OPTIONS}
+              optionNameFn={o => o.name}
+              optionValueFn={o => o.value}
+              className="h4 text-bold bg-white"
+              onChange={o => this.onPropertyChange("schedule_day", o)}
+            />
+          </span>
+        )}
+      </span>
+    );
+  }
+
+  renderDayPicker() {
+    let { schedule } = this.props;
+
+    return (
+      <span className="mt1">
+        <span className="h4 text-bold mx1">on</span>
+        <Select
+          value={_.find(
+            DAY_OF_WEEK_OPTIONS,
+            o => o.value === schedule.schedule_day,
+          )}
+          options={DAY_OF_WEEK_OPTIONS}
+          optionNameFn={o => o.name}
+          optionValueFn={o => o.value}
+          className="h4 text-bold bg-white"
+          onChange={o => this.onPropertyChange("schedule_day", o)}
+        />
+      </span>
+    );
+  }
+
+  renderHourPicker() {
+    let { schedule, textBeforeSendTime } = this.props;
+
+    let hourOfDay = isNaN(schedule.schedule_hour) ? 8 : schedule.schedule_hour;
+    let hour = hourOfDay % 12;
+    let amPm = hourOfDay >= 12 ? 1 : 0;
+    let timezone = Settings.get("timezone_short");
+    return (
+      <div className="mt1">
+        <span className="h4 text-bold mr1">at</span>
+        <Select
+          className="mr1 h4 text-bold bg-white"
+          value={_.find(HOUR_OPTIONS, o => o.value === hour)}
+          options={HOUR_OPTIONS}
+          optionNameFn={o => o.name}
+          optionValueFn={o => o.value}
+          onChange={o => this.onPropertyChange("schedule_hour", o + amPm * 12)}
+        />
+        <Select
+          value={_.find(AM_PM_OPTIONS, o => o.value === amPm)}
+          options={AM_PM_OPTIONS}
+          optionNameFn={o => o.name}
+          optionValueFn={o => o.value}
+          onChange={o => this.onPropertyChange("schedule_hour", hour + o * 12)}
+          className="h4 text-bold bg-white"
+        />
+        {textBeforeSendTime && (
+          <div className="mt2 h4 text-bold text-grey-3 border-top pt2">
+            {textBeforeSendTime} {hour === 0 ? 12 : hour}:00{" "}
+            {amPm ? "PM" : "AM"} {timezone}, {t`your Metabase timezone`}.
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  render() {
+    let { schedule, scheduleOptions, textBeforeInterval } = this.props;
+
+    const scheduleType = schedule.schedule_type;
+
+    return (
+      <div className="mt1">
+        <span className="h4 text-bold mr1">{textBeforeInterval}</span>
+        <Select
+          className="h4 text-bold bg-white"
+          value={scheduleType}
+          options={scheduleOptions}
+          optionNameFn={o => capitalize(o)}
+          optionValueFn={o => o}
+          onChange={o => this.onPropertyChange("schedule_type", o)}
+        />
+        {scheduleType === "monthly" && this.renderMonthlyPicker()}
+        {scheduleType === "weekly" && this.renderDayPicker()}
+        {(scheduleType === "daily" ||
+          scheduleType === "weekly" ||
+          scheduleType === "monthly") &&
+          this.renderHourPicker()}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/SearchHeader.css b/frontend/src/metabase/components/SearchHeader.css
index 95e1e4440d4a04bbb9627e048f6dfe8ff56cf40d..b8058e28950d0bd5c1e504292b5271e9729536da 100644
--- a/frontend/src/metabase/components/SearchHeader.css
+++ b/frontend/src/metabase/components/SearchHeader.css
@@ -1,15 +1,15 @@
-@import '../questions/Questions.css';
+@import "../questions/Questions.css";
 
 :local(.searchIcon) {
-    color: var(--muted-color);
+  color: var(--muted-color);
 }
 
 :local(.searchBox) {
-    composes: borderless from "style";
-    color: var(--title-color);
-    font-size: 20px;
-    width: 100%;
+  composes: borderless from "style";
+  color: var(--title-color);
+  font-size: 20px;
+  width: 100%;
 }
 :local(.searchBox)::-webkit-input-placeholder {
-    color: var(--subtitle-color);
+  color: var(--subtitle-color);
 }
diff --git a/frontend/src/metabase/components/SearchHeader.jsx b/frontend/src/metabase/components/SearchHeader.jsx
index 9a912fb2d2a75da4bd7bcfbc981477295256f231..902cb36a2731000abb0654d4d4ddbf99bc168f7f 100644
--- a/frontend/src/metabase/components/SearchHeader.jsx
+++ b/frontend/src/metabase/components/SearchHeader.jsx
@@ -4,36 +4,44 @@ import PropTypes from "prop-types";
 import S from "./SearchHeader.css";
 import Icon from "metabase/components/Icon.jsx";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
-const SearchHeader = ({ searchText, setSearchText, autoFocus, inputRef, resetSearchText }) =>
-    <div className="flex align-center">
-        <Icon className={S.searchIcon} name="search" size={18} />
-        <input
-            className={cx("input bg-transparent", S.searchBox)}
-            type="text"
-            placeholder={t`Filter this list...`}
-            value={searchText}
-            onChange={(e) => setSearchText(e.target.value)}
-            autoFocus={!!autoFocus}
-            ref={inputRef || (() => {})}
+const SearchHeader = ({
+  searchText,
+  setSearchText,
+  autoFocus,
+  inputRef,
+  resetSearchText,
+}) => (
+  <div className="flex align-center">
+    <Icon className={S.searchIcon} name="search" size={18} />
+    <input
+      className={cx("input bg-transparent", S.searchBox)}
+      type="text"
+      placeholder={t`Filter this list...`}
+      value={searchText}
+      onChange={e => setSearchText(e.target.value)}
+      autoFocus={!!autoFocus}
+      ref={inputRef || (() => {})}
+    />
+    {resetSearchText &&
+      searchText !== "" && (
+        <Icon
+          name="close"
+          className="cursor-pointer text-grey-2"
+          size={18}
+          onClick={resetSearchText}
         />
-        { resetSearchText && searchText !== "" &&
-            <Icon
-                name="close"
-                className="cursor-pointer text-grey-2"
-                size={18}
-                onClick={resetSearchText}
-            />
-        }
-    </div>
+      )}
+  </div>
+);
 
 SearchHeader.propTypes = {
-    searchText: PropTypes.string.isRequired,
-    setSearchText: PropTypes.func.isRequired,
-    autoFocus: PropTypes.bool,
-    inputRef: PropTypes.func,
-    resetSearchText: PropTypes.func
+  searchText: PropTypes.string.isRequired,
+  setSearchText: PropTypes.func.isRequired,
+  autoFocus: PropTypes.bool,
+  inputRef: PropTypes.func,
+  resetSearchText: PropTypes.func,
 };
 
 export default SearchHeader;
diff --git a/frontend/src/metabase/components/Select.info.js b/frontend/src/metabase/components/Select.info.js
index a05fc7adaefb8f0087f2e84d084c3a146e419104..8abe0ed4a7b981c2af2f0736e3ab37e87a893d71 100644
--- a/frontend/src/metabase/components/Select.info.js
+++ b/frontend/src/metabase/components/Select.info.js
@@ -1,33 +1,29 @@
-import React from 'react'
-import { t } from 'c-3po';
-import Select, { Option } from 'metabase/components/Select'
+import React from "react";
+import { t } from "c-3po";
+import Select, { Option } from "metabase/components/Select";
 
-export const component = Select
+export const component = Select;
 
 const fixture = [
-    { name: t`Blue` },
-    { name: t`Green` },
-    { name: t`Red` },
-    { name: t`Yellow` },
-]
+  { name: t`Blue` },
+  { name: t`Green` },
+  { name: t`Red` },
+  { name: t`Yellow` },
+];
 
 export const description = t`
     A component used to make a selection
-`
+`;
 
 export const examples = {
-    'Default': (
-        <Select onChange={() => alert(t`Selected`)}>
-            { fixture.map(f => <Option name={f.name}>{f.name}</Option>)}
-        </Select>
-    ),
-    'With search': (
-        <Select
-            searchProp='name'
-            onChange={() => alert(t`Selected`)}
-        >
-            { fixture.map(f => <Option name={f.name}>{f.name}</Option>)}
-        </Select>
-    ),
-}
-
+  Default: (
+    <Select onChange={() => alert(t`Selected`)}>
+      {fixture.map(f => <Option name={f.name}>{f.name}</Option>)}
+    </Select>
+  ),
+  "With search": (
+    <Select searchProp="name" onChange={() => alert(t`Selected`)}>
+      {fixture.map(f => <Option name={f.name}>{f.name}</Option>)}
+    </Select>
+  ),
+};
diff --git a/frontend/src/metabase/components/Select.jsx b/frontend/src/metabase/components/Select.jsx
index 83a27fefd6dd2460ca571c4aa5dd449a7c4bc636..a2a138b068aa3e7f1a790f4433af2e269d7bb80e 100644
--- a/frontend/src/metabase/components/Select.jsx
+++ b/frontend/src/metabase/components/Select.jsx
@@ -2,9 +2,9 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
-import { List } from 'react-virtualized'
+import { List } from "react-virtualized";
 import "react-virtualized/styles.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ColumnarSelector from "metabase/components/ColumnarSelector.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
@@ -12,316 +12,370 @@ import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import cx from "classnames";
 
 export default class Select extends Component {
-    static propTypes = {
-        children: PropTypes.any
-    };
+  static propTypes = {
+    children: PropTypes.any,
+  };
 
-    render() {
-        if (this.props.children) {
-            return <BrowserSelect {...this.props} />;
-        } else {
-            return <LegacySelect {...this.props} />;
-        }
+  render() {
+    if (this.props.children) {
+      return <BrowserSelect {...this.props} />;
+    } else {
+      return <LegacySelect {...this.props} />;
     }
+  }
 }
 
 class BrowserSelect extends Component {
+  state = {
+    inputValue: "",
+  };
 
-    state = {
-        inputValue: ""
-    }
+  static propTypes = {
+    children: PropTypes.array.isRequired,
+    onChange: PropTypes.func.isRequired,
+    value: PropTypes.any,
+    searchProp: PropTypes.string,
+    searchCaseInsensitive: PropTypes.bool,
+    isInitiallyOpen: PropTypes.bool,
+    placeholder: PropTypes.string,
+    // NOTE - @kdoh
+    // seems too generic for us?
+    triggerElement: PropTypes.any,
+    height: PropTypes.number,
+    width: PropTypes.number,
+    rowHeight: PropTypes.number,
+    // TODO - @kdoh
+    // we should not allow this
+    className: PropTypes.string,
+    compact: PropTypes.bool,
+  };
+  static defaultProps = {
+    className: "",
+    width: 320,
+    height: 320,
+    rowHeight: 40,
+  };
 
-    static propTypes = {
-        children: PropTypes.array.isRequired,
-        onChange: PropTypes.func.isRequired,
-        value: PropTypes.any,
-        searchProp: PropTypes.string,
-        searchCaseInsensitive: PropTypes.bool,
-        isInitiallyOpen: PropTypes.bool,
-        placeholder: PropTypes.string,
-        // NOTE - @kdoh
-        // seems too generic for us?
-        triggerElement: PropTypes.any,
-        height: PropTypes.number,
-        width: PropTypes.number,
-        rowHeight: PropTypes.number,
-        // TODO - @kdoh
-        // we should not allow this
-        className: PropTypes.string,
-        compact: PropTypes.bool,
-    }
-    static defaultProps = {
-        className: "",
-        width: 320,
-        height: 320,
-        rowHeight: 40,
-    }
+  isSelected(otherValue) {
+    const { value } = this.props;
+    return (
+      value === otherValue ||
+      ((value == null || value === "") &&
+        (otherValue == null || otherValue === ""))
+    );
+  }
 
-    isSelected(otherValue) {
-        const { value } = this.props;
-        return (value === otherValue || ((value == null || value === "") && (otherValue == null || otherValue === "")))
-    }
+  render() {
+    const {
+      className,
+      value,
+      onChange,
+      searchProp,
+      searchCaseInsensitive,
+      isInitiallyOpen,
+      placeholder,
+      triggerElement,
+      width,
+      height,
+      rowHeight,
+    } = this.props;
 
-    render() {
-        const {
-            className,
-            value,
-            onChange,
-            searchProp,
-            searchCaseInsensitive,
-            isInitiallyOpen,
-            placeholder,
-            triggerElement,
-            width,
-            height,
-            rowHeight
-        } = this.props;
+    let children = this.props.children;
 
-        let children = this.props.children
+    let selectedName;
+    for (const child of children) {
+      if (this.isSelected(child.props.value)) {
+        selectedName = child.props.children;
+      }
+    }
+    if (selectedName == null && placeholder) {
+      selectedName = placeholder;
+    }
 
-        let selectedName;
-        for (const child of children) {
-            if (this.isSelected(child.props.value)) {
-                selectedName = child.props.children;
-            }
-        }
-        if (selectedName == null && placeholder) {
-            selectedName = placeholder;
+    const { inputValue } = this.state;
+    let filter = () => true;
+    if (searchProp && inputValue) {
+      filter = child => {
+        let childValue = String(child.props[searchProp] || "");
+        if (!inputValue) {
+          return false;
+        } else if (searchCaseInsensitive) {
+          return childValue.toLowerCase().startsWith(inputValue.toLowerCase());
+        } else {
+          return childValue.startsWith(inputValue);
         }
+      };
+    }
 
-        const { inputValue } = this.state;
-        let filter = () => true;
-        if (searchProp && inputValue) {
-            filter = (child) => {
-                let childValue = String(child.props[searchProp] || "");
-                if (!inputValue) {
-                    return false;
-                } else if (searchCaseInsensitive) {
-                    return childValue.toLowerCase().startsWith(inputValue.toLowerCase())
-                } else {
-                    return childValue.startsWith(inputValue);
-                }
-            }
-        }
+    // make sure we filter by the search query
+    children = children.filter(filter);
 
-        // make sure we filter by the search query
-        children = children.filter(filter)
+    let extraProps = {};
+    if (this.props.compact) {
+      extraProps = {
+        tetherOptions: {
+          attachment: `top left`,
+          targetAttachment: `bottom left`,
+          targetOffset: `0px 0px`,
+        },
+        hasArrow: false,
+      };
+    }
 
-        let extraProps = {}
-        if (this.props.compact) {
-          extraProps = {
-            tetherOptions: {
-                attachment: `top left`,
-                targetAttachment: `bottom left`,
-                targetOffset: `0px 0px`
-            },
-            hasArrow: false
-          }
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        className={className}
+        triggerElement={
+          triggerElement || (
+            <SelectButton hasValue={!!value}>{selectedName}</SelectButton>
+          )
         }
+        triggerClasses={className}
+        verticalAttachments={["top"]}
+        isInitiallyOpen={isInitiallyOpen}
+        {...extraProps}
+      >
+        <div className="flex flex-column">
+          {searchProp && (
+            <input
+              className="AdminSelect m1 flex-full"
+              value={inputValue}
+              onChange={e => this.setState({ inputValue: e.target.value })}
+              autoFocus
+            />
+          )}
+          <List
+            width={width}
+            height={
+              // check to see if the height of the number of rows is less than the provided (or default)
+              // height. if so, set the height to the number of rows * the row height so that
+              // large blank spaces at the bottom are prevented
+              children.length * rowHeight < height
+                ? children.length * rowHeight
+                : height
+            }
+            rowHeight={rowHeight}
+            rowCount={children.length}
+            rowRenderer={({ index, key, style }) => {
+              const child = children[index];
 
-        return (
-            <PopoverWithTrigger
-                ref="popover"
-                className={className}
-                triggerElement={triggerElement || <SelectButton hasValue={!!value}>{selectedName}</SelectButton>}
-                triggerClasses={className}
-                verticalAttachments={["top"]}
-                isInitiallyOpen={isInitiallyOpen}
-                {...extraProps}
-            >
-                <div className="flex flex-column">
-                    { searchProp &&
-                        <input
-                            className="AdminSelect m1 flex-full"
-                            value={inputValue}
-                            onChange={(e) => this.setState({ inputValue: e.target.value })}
-                            autoFocus
-                        />
-                    }
-                    <List
-                        width={width}
-                        height={
-                            // check to see if the height of the number of rows is less than the provided (or default)
-                            // height. if so, set the height to the number of rows * the row height so that
-                            // large blank spaces at the bottom are prevented
-                            children.length * rowHeight < height ? children.length * rowHeight : height
-                        }
-                        rowHeight={rowHeight}
-                        rowCount={children.length}
-                        rowRenderer={({index, key, style}) => {
-                            const child = children[index]
-
-                            /*
+              /*
                              * for each child we need to add props based on
                              * the parent's onClick and the current selection
                              * status, so we use cloneElement here
                             * */
-                            return (
-                                <div key={key} style={style} onClick={e => e.stopPropagation()}>
-                                    {React.cloneElement(children[index], {
-                                        selected: this.isSelected(child.props.value),
-                                        onClick: () => {
-                                            if (!child.props.disabled) {
-                                                onChange({ target: { value: child.props.value }});
-                                            }
-                                            this.refs.popover.close()
-                                        }
-                                    })}
-                                </div>
-                            )
-                        }}
-                    />
+              return (
+                <div key={key} style={style} onClick={e => e.stopPropagation()}>
+                  {React.cloneElement(children[index], {
+                    selected: this.isSelected(child.props.value),
+                    onClick: () => {
+                      if (!child.props.disabled) {
+                        onChange({ target: { value: child.props.value } });
+                      }
+                      this.refs.popover.close();
+                    },
+                  })}
                 </div>
-            </PopoverWithTrigger>
-        );
-    }
+              );
+            }}
+          />
+        </div>
+      </PopoverWithTrigger>
+    );
+  }
 }
 
-export const SelectButton = ({ hasValue, children }) =>
-    <div className={"AdminSelect flex align-center " + (!hasValue ? " text-grey-3" : "")}>
-        <span className="AdminSelect-content mr1">{children}</span>
-        <Icon className="AdminSelect-chevron flex-align-right" name="chevrondown" size={12} />
-    </div>
+export const SelectButton = ({ hasValue, children }) => (
+  <div
+    className={
+      "AdminSelect border-med flex align-center " +
+      (!hasValue ? " text-grey-3" : "")
+    }
+  >
+    <span className="AdminSelect-content mr1">{children}</span>
+    <Icon
+      className="AdminSelect-chevron flex-align-right"
+      name="chevrondown"
+      size={12}
+    />
+  </div>
+);
 
 SelectButton.propTypes = {
-    hasValue: PropTypes.bool,
-    children: PropTypes.any,
+  hasValue: PropTypes.bool,
+  children: PropTypes.any,
 };
 
 export class Option extends Component {
-    static propTypes = {
-        children:   PropTypes.any,
-        selected:   PropTypes.bool,
-        disabled:   PropTypes.bool,
-        onClick:    PropTypes.func,
-        icon:       PropTypes.string,
-        iconColor:  PropTypes.string,
-        iconSize:   PropTypes.number,
-    };
+  static propTypes = {
+    children: PropTypes.any,
+    selected: PropTypes.bool,
+    disabled: PropTypes.bool,
+    onClick: PropTypes.func,
+    icon: PropTypes.string,
+    iconColor: PropTypes.string,
+    iconSize: PropTypes.number,
+  };
 
-    render() {
-        const { children, selected, disabled, icon, iconColor, iconSize, onClick } = this.props;
-        return (
-            <div
-                onClick={onClick}
-                className={cx("ColumnarSelector-row flex align-center cursor-pointer no-decoration relative", {
-                    "ColumnarSelector-row--selected": selected,
-                    "disabled": disabled
-                })}
-            >
-                <Icon name="check" size={14} style={{ position: 'absolute' }} />
-                { icon &&
-                    <Icon
-                        name={icon}
-                        size={iconSize}
-                        style={{
-                            position: 'absolute',
-                            color: iconColor,
-                            visibility: !selected ? "visible" : "hidden"
-                        }}
-                    />
-                }
-                <span className="ml4 no-decoration">{children}</span>
-            </div>
-        );
-    }
+  render() {
+    const {
+      children,
+      selected,
+      disabled,
+      icon,
+      iconColor,
+      iconSize,
+      onClick,
+    } = this.props;
+    return (
+      <div
+        onClick={onClick}
+        className={cx(
+          "ColumnarSelector-row flex align-center cursor-pointer no-decoration relative",
+          {
+            "ColumnarSelector-row--selected": selected,
+            disabled: disabled,
+          },
+        )}
+      >
+        <Icon name="check" size={14} style={{ position: "absolute" }} />
+        {icon && (
+          <Icon
+            name={icon}
+            size={iconSize}
+            style={{
+              position: "absolute",
+              color: iconColor,
+              visibility: !selected ? "visible" : "hidden",
+            }}
+          />
+        )}
+        <span className="ml4 no-decoration">{children}</span>
+      </div>
+    );
+  }
 }
 
 class LegacySelect extends Component {
-    static propTypes = {
-        value: PropTypes.any,
-        values: PropTypes.array,
-        options: PropTypes.array.isRequired,
-        disabledOptionIds: PropTypes.array,
-        placeholder: PropTypes.string,
-        emptyPlaceholder: PropTypes.string,
-        onChange: PropTypes.func,
-        optionNameFn: PropTypes.func,
-        optionValueFn: PropTypes.func,
-        className: PropTypes.string,
-        isInitiallyOpen: PropTypes.bool,
-        disabled: PropTypes.bool,
-        //TODO: clean up hardcoded "AdminSelect" class on trigger to avoid this workaround
-        triggerClasses: PropTypes.string
-    };
+  static propTypes = {
+    value: PropTypes.any,
+    values: PropTypes.array,
+    options: PropTypes.array.isRequired,
+    disabledOptionIds: PropTypes.array,
+    placeholder: PropTypes.string,
+    emptyPlaceholder: PropTypes.string,
+    onChange: PropTypes.func,
+    optionNameFn: PropTypes.func,
+    optionValueFn: PropTypes.func,
+    className: PropTypes.string,
+    isInitiallyOpen: PropTypes.bool,
+    disabled: PropTypes.bool,
+    //TODO: clean up hardcoded "AdminSelect" class on trigger to avoid this workaround
+    triggerClasses: PropTypes.string,
+  };
 
-    static defaultProps = {
-        placeholder: "",
-        emptyPlaceholder: t`Nothing to select`,
-        disabledOptionIds: [],
-        optionNameFn: (option) => option.name,
-        optionValueFn: (option) => option,
-        isInitiallyOpen: false,
-    };
+  static defaultProps = {
+    placeholder: "",
+    emptyPlaceholder: t`Nothing to select`,
+    disabledOptionIds: [],
+    optionNameFn: option => option.name,
+    optionValueFn: option => option,
+    isInitiallyOpen: false,
+  };
 
-    toggle() {
-        this.refs.popover.toggle();
-    }
+  toggle() {
+    this.refs.popover.toggle();
+  }
 
-    render() {
-        const { className, value, values, onChange, options, disabledOptionIds, optionNameFn, optionValueFn, placeholder, emptyPlaceholder, isInitiallyOpen, disabled } = this.props;
+  render() {
+    const {
+      className,
+      value,
+      values,
+      onChange,
+      options,
+      disabledOptionIds,
+      optionNameFn,
+      optionValueFn,
+      placeholder,
+      emptyPlaceholder,
+      isInitiallyOpen,
+      disabled,
+    } = this.props;
 
-        var selectedName = value ?
-            optionNameFn(value) :
-            options && options.length > 0 ?
-                placeholder :
-                emptyPlaceholder;
+    var selectedName = value
+      ? optionNameFn(value)
+      : options && options.length > 0 ? placeholder : emptyPlaceholder;
 
-        var triggerElement = (
-            <div className={cx("flex align-center", !value && (!values || values.length === 0) ? " text-grey-2" : "")}>
-                { values && values.length !== 0 ?
-                    values
-                        .map(value => optionNameFn(value))
-                        .sort()
-                        .map((name, index) => <span key={index} className="mr1">{`${name}${index !== (values.length - 1) ? ',   ' : ''}`}</span>) :
-                    <span className="mr1">{selectedName}</span>
-                }
-                <Icon className="flex-align-right" name="chevrondown" size={12}/>
-            </div>
-        );
+    var triggerElement = (
+      <div
+        className={cx(
+          "flex align-center",
+          !value && (!values || values.length === 0) ? " text-grey-2" : "",
+        )}
+      >
+        {values && values.length !== 0 ? (
+          values
+            .map(value => optionNameFn(value))
+            .sort()
+            .map((name, index) => (
+              <span key={index} className="mr1">{`${name}${
+                index !== values.length - 1 ? ",   " : ""
+              }`}</span>
+            ))
+        ) : (
+          <span className="mr1">{selectedName}</span>
+        )}
+        <Icon className="flex-align-right" name="chevrondown" size={12} />
+      </div>
+    );
 
-        var sections = {};
-        options.forEach(function (option) {
-            var sectionName = option.section || "";
-            sections[sectionName] = sections[sectionName] || { title: sectionName || undefined, items: [] };
-            sections[sectionName].items.push(option);
-        });
-        sections = Object.keys(sections).map((sectionName) => sections[sectionName]);
+    var sections = {};
+    options.forEach(function(option) {
+      var sectionName = option.section || "";
+      sections[sectionName] = sections[sectionName] || {
+        title: sectionName || undefined,
+        items: [],
+      };
+      sections[sectionName].items.push(option);
+    });
+    sections = Object.keys(sections).map(sectionName => sections[sectionName]);
 
-        var columns = [
-            {
-                selectedItem: value,
-                selectedItems: values,
-                sections: sections,
-                disabledOptionIds: disabledOptionIds,
-                itemTitleFn: optionNameFn,
-                itemDescriptionFn: (item) => item.description,
-                itemSelectFn: (item) => {
-                    onChange(optionValueFn(item));
-                    if (!values) {
-                        this.toggle();
-                    }
-                }
-            }
-        ];
+    var columns = [
+      {
+        selectedItem: value,
+        selectedItems: values,
+        sections: sections,
+        disabledOptionIds: disabledOptionIds,
+        itemTitleFn: optionNameFn,
+        itemDescriptionFn: item => item.description,
+        itemSelectFn: item => {
+          onChange(optionValueFn(item));
+          if (!values) {
+            this.toggle();
+          }
+        },
+      },
+    ];
 
-        const disablePopover = disabled || !options || options.length === 0;
+    const disablePopover = disabled || !options || options.length === 0;
 
-        return (
-            <PopoverWithTrigger
-                ref="popover"
-                className={className}
-                triggerElement={triggerElement}
-                triggerClasses={this.props.triggerClasses || cx("AdminSelect", this.props.className)}
-                isInitiallyOpen={isInitiallyOpen}
-                disabled={disablePopover}
-            >
-                <div onClick={(e) => e.stopPropagation()}>
-                    <ColumnarSelector
-                        columns={columns}
-                    />
-                </div>
-            </PopoverWithTrigger>
-        );
-    }
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        className={className}
+        triggerElement={triggerElement}
+        triggerClasses={
+          this.props.triggerClasses || cx("AdminSelect", this.props.className)
+        }
+        isInitiallyOpen={isInitiallyOpen}
+        disabled={disablePopover}
+      >
+        <div onClick={e => e.stopPropagation()}>
+          <ColumnarSelector columns={columns} />
+        </div>
+      </PopoverWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/SelectButton.jsx b/frontend/src/metabase/components/SelectButton.jsx
index e14ed5577c0c8171bd9faf7758b5058e085a07be..876bb74645242757266cab3bccaa67db50300251 100644
--- a/frontend/src/metabase/components/SelectButton.jsx
+++ b/frontend/src/metabase/components/SelectButton.jsx
@@ -6,16 +6,25 @@ import Icon from "metabase/components/Icon.jsx";
 
 import cx from "classnames";
 
-const SelectButton = ({ className, children, hasValue = true }) =>
-    <div className={cx(className, "AdminSelect flex align-center", { "text-grey-3": !hasValue })}>
-        <span className="AdminSelect-content mr1">{children}</span>
-        <Icon className="AdminSelect-chevron flex-align-right" name="chevrondown" size={12} />
-    </div>
+const SelectButton = ({ className, children, hasValue = true }) => (
+  <div
+    className={cx(className, "AdminSelect flex align-center", {
+      "text-grey-3": !hasValue,
+    })}
+  >
+    <span className="AdminSelect-content mr1">{children}</span>
+    <Icon
+      className="AdminSelect-chevron flex-align-right"
+      name="chevrondown"
+      size={12}
+    />
+  </div>
+);
 
 SelectButton.propTypes = {
-    className: PropTypes.string,
-    children: PropTypes.any,
-    hasValue: PropTypes.any
+  className: PropTypes.string,
+  children: PropTypes.any,
+  hasValue: PropTypes.any,
 };
 
 export default SelectButton;
diff --git a/frontend/src/metabase/components/ShrinkableList.jsx b/frontend/src/metabase/components/ShrinkableList.jsx
index 3bae345fb74b2ab3b774c8c616c29f21bf51a92c..0b16343ea45f3d3a51be8b67eae2f703f48631e4 100644
--- a/frontend/src/metabase/components/ShrinkableList.jsx
+++ b/frontend/src/metabase/components/ShrinkableList.jsx
@@ -6,55 +6,52 @@ import ReactDOM from "react-dom";
 import ExplicitSize from "metabase/components/ExplicitSize";
 
 type Props = {
-    className?: string,
-    items: any[],
-    renderItem: (item: any) => any,
-    renderItemSmall: (item: any) => any
+  className?: string,
+  items: any[],
+  renderItem: (item: any) => any,
+  renderItemSmall: (item: any) => any,
 };
 
 type State = {
-    isShrunk: ?boolean
+  isShrunk: ?boolean,
 };
 
 @ExplicitSize
 export default class ShrinkableList extends Component {
-    props: Props;
-    state: State = {
-        isShrunk: null
-    }
-
-    componentWillReceiveProps() {
-        this.setState({
-            isShrunk: null
-        })
-    }
-
-    componentDidMount() {
-        this.componentDidUpdate();
-    }
-
-    componentDidUpdate() {
-        const container = ReactDOM.findDOMNode(this)
-        const { isShrunk } = this.state;
-        if (container && isShrunk === null) {
-            this.setState({
-                isShrunk: container.scrollWidth !== container.offsetWidth
-            })
-        }
-    }
-
-    render() {
-        const { items, className, renderItemSmall, renderItem } = this.props;
-        const { isShrunk } = this.state;
-        return (
-            <div className={className}>
-                { items.map(item =>
-                    isShrunk ?
-                        renderItemSmall(item)
-                    :
-                        renderItem(item)
-                )}
-            </div>
-        );
+  props: Props;
+  state: State = {
+    isShrunk: null,
+  };
+
+  componentWillReceiveProps() {
+    this.setState({
+      isShrunk: null,
+    });
+  }
+
+  componentDidMount() {
+    this.componentDidUpdate();
+  }
+
+  componentDidUpdate() {
+    const container = ReactDOM.findDOMNode(this);
+    const { isShrunk } = this.state;
+    if (container && isShrunk === null) {
+      this.setState({
+        isShrunk: container.scrollWidth !== container.offsetWidth,
+      });
     }
+  }
+
+  render() {
+    const { items, className, renderItemSmall, renderItem } = this.props;
+    const { isShrunk } = this.state;
+    return (
+      <div className={className}>
+        {items.map(
+          item => (isShrunk ? renderItemSmall(item) : renderItem(item)),
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Sidebar.css b/frontend/src/metabase/components/Sidebar.css
index 4f1ab3dd404046795916c00c43acd53fb9726cf8..18dff1da157d4fb1c2378ac1df01878608b8cc74 100644
--- a/frontend/src/metabase/components/Sidebar.css
+++ b/frontend/src/metabase/components/Sidebar.css
@@ -1,5 +1,5 @@
 :root {
-    --item-padding: 45px;
+  --item-padding: 45px;
 }
 
 :local(.sidebar-padding) {
@@ -13,33 +13,33 @@
 }
 
 :local(.sidebar) {
-    composes: py2 from "style";
-    width: 345px;
-    background-color: rgb(248, 252, 253);
-    border-right: 1px solid rgb(223, 238, 245);
-    color: #606E7B;
+  composes: py2 from "style";
+  width: 345px;
+  background-color: rgb(248, 252, 253);
+  border-right: 1px solid rgb(223, 238, 245);
+  color: #606e7b;
 }
 
 :local(.sidebar) a {
-    text-decoration: none;
+  text-decoration: none;
 }
 
 :local(.breadcrumbs) {
-    composes: sidebar-padding;
+  composes: sidebar-padding;
 }
 
 :local(.item),
 :local(.sectionTitle) {
-    composes: flex align-center from "style";
-    composes: py2 from "style";
-    composes: sidebar-padding;
+  composes: flex align-center from "style";
+  composes: py2 from "style";
+  composes: sidebar-padding;
 }
 
 :local(.item) {
-    composes: transition-color from "style";
-    composes: transition-background from "style";
-    font-size: 1em;
-    color: #CFE4F5;
+  composes: transition-color from "style";
+  composes: transition-background from "style";
+  font-size: 1em;
+  color: #cfe4f5;
 }
 
 :local(.item) :local(.icon) {
@@ -47,46 +47,44 @@
 }
 
 :local(.sectionTitle) {
-    composes: my1 from "style";
-    composes: text-bold from "style";
-    font-size: 16px;
+  composes: my1 from "style";
+  composes: text-bold from "style";
+  font-size: 16px;
 }
 
-
 :local(.item.selected),
 :local(.item.selected) :local(.icon),
 :local(.sectionTitle.selected),
 :local(.item):hover,
 :local(.item):hover :local(.icon),
 :local(.sectionTitle):hover {
-    background-color: #E3F0F9;
-    color: #2D86D4;
+  background-color: #e3f0f9;
+  color: #2d86d4;
 }
 
 :local(.divider) {
-    composes: my2 from "style";
-    composes: border-bottom from "style";
-    composes: sidebar-margin;
+  composes: my2 from "style";
+  composes: border-bottom from "style";
+  composes: sidebar-margin;
 }
 
 :local(.name) {
-    composes: ml2 text-bold from "style";
-    color: #9CAEBE;
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    overflow-x: hidden;
+  composes: ml2 text-bold from "style";
+  color: #9caebe;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow-x: hidden;
 }
 
 :local(.item):hover :local(.name),
 :local(.item.selected) :local(.name) {
-    color: #2D86D4;
+  color: #2d86d4;
 }
 
 :local(.icon) {
-    composes: flex-no-shrink from "style";
+  composes: flex-no-shrink from "style";
 }
 
-
 :local(.noLabelsMessage) {
   composes: relative from "style";
   composes: text-centered from "style";
diff --git a/frontend/src/metabase/components/SidebarItem.jsx b/frontend/src/metabase/components/SidebarItem.jsx
index adfbc982612e11ddbaa9d24e1a95b5c4f2657c22..ff77101a75ea94cdc09c43f208779064a920be23 100644
--- a/frontend/src/metabase/components/SidebarItem.jsx
+++ b/frontend/src/metabase/components/SidebarItem.jsx
@@ -8,20 +8,20 @@ import LabelIcon from "./LabelIcon.jsx";
 
 import pure from "recompose/pure";
 
-
-const SidebarItem = ({ name, sidebar, icon, href }) =>
-    <li>
-        <Link to={href} className={S.item} activeClassName={S.selected}>
-            <LabelIcon className={S.icon} icon={icon}/>
-            <span className={S.name}>{sidebar || name}</span>
-        </Link>
-    </li>
+const SidebarItem = ({ name, sidebar, icon, href }) => (
+  <li>
+    <Link to={href} className={S.item} activeClassName={S.selected}>
+      <LabelIcon className={S.icon} icon={icon} />
+      <span className={S.name}>{sidebar || name}</span>
+    </Link>
+  </li>
+);
 
 SidebarItem.propTypes = {
-    name:  PropTypes.string.isRequired,
-    sidebar:  PropTypes.string,
-    icon:  PropTypes.string.isRequired,
-    href:  PropTypes.string.isRequired,
+  name: PropTypes.string.isRequired,
+  sidebar: PropTypes.string,
+  icon: PropTypes.string.isRequired,
+  href: PropTypes.string.isRequired,
 };
 
 export default pure(SidebarItem);
diff --git a/frontend/src/metabase/components/SidebarLayout.jsx b/frontend/src/metabase/components/SidebarLayout.jsx
index 6a72feeadcc938d2c852b430f77693b77c41fada..a017796c72c5313f1dbc186f5661282187c5b360 100644
--- a/frontend/src/metabase/components/SidebarLayout.jsx
+++ b/frontend/src/metabase/components/SidebarLayout.jsx
@@ -2,27 +2,41 @@
 import React from "react";
 import PropTypes from "prop-types";
 
-const SidebarLayout = ({ className, style, sidebar, children }) =>
-    <div className={className} style={{ ...style, display: "flex", flexDirection: "row"}}>
-        { React.cloneElement(
-            sidebar,
-            { style: { flexShrink: 0 },
-              className: 'scroll-y scroll-show scroll--light scroll-show--hover'
-            },
-            sidebar.props.children
-        )}
-        { children && React.cloneElement(
-            React.Children.only(children),
-            { style: { flex: 1, overflowY: 'auto', display: 'flex', flexDirection: 'column', height: '100%' }},
-            React.Children.only(children).props.children
-        )}
-    </div>
+const SidebarLayout = ({ className, style, sidebar, children }) => (
+  <div
+    className={className}
+    style={{ ...style, display: "flex", flexDirection: "row" }}
+  >
+    {React.cloneElement(
+      sidebar,
+      {
+        style: { flexShrink: 0 },
+        className: "scroll-y scroll-show scroll--light scroll-show--hover",
+      },
+      sidebar.props.children,
+    )}
+    {children &&
+      React.cloneElement(
+        React.Children.only(children),
+        {
+          style: {
+            flex: 1,
+            overflowY: "auto",
+            display: "flex",
+            flexDirection: "column",
+            height: "100%",
+          },
+        },
+        React.Children.only(children).props.children,
+      )}
+  </div>
+);
 
 SidebarLayout.propTypes = {
-    className:  PropTypes.string,
-    style:      PropTypes.object,
-    sidebar:    PropTypes.element.isRequired,
-    children:   PropTypes.element.isRequired,
+  className: PropTypes.string,
+  style: PropTypes.object,
+  sidebar: PropTypes.element.isRequired,
+  children: PropTypes.element.isRequired,
 };
 
 export default SidebarLayout;
diff --git a/frontend/src/metabase/components/SortableItemList.css b/frontend/src/metabase/components/SortableItemList.css
index 7520477ba665413a948c7acf669a3bf2ea43205e..5b0335d1ec6ccceaa00339b852f81708dd998722 100644
--- a/frontend/src/metabase/components/SortableItemList.css
+++ b/frontend/src/metabase/components/SortableItemList.css
@@ -1,3 +1,3 @@
 .SortableItemList-list {
-    overflow-y: auto;
+  overflow-y: auto;
 }
diff --git a/frontend/src/metabase/components/SortableItemList.jsx b/frontend/src/metabase/components/SortableItemList.jsx
index 07a90d9b7a95a94d82fbf3495ad4b2c420fb1b19..d4cae406e412768179ead6afa3a4edb58d55ec42 100644
--- a/frontend/src/metabase/components/SortableItemList.jsx
+++ b/frontend/src/metabase/components/SortableItemList.jsx
@@ -2,76 +2,97 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import "./SortableItemList.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
-import Radio from 'metabase/components/Radio.jsx';
+import Radio from "metabase/components/Radio.jsx";
 
 export default class SortableItemList extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            sort: props.initialSort || "Last Modified"
-        };
-    }
-
-    static propTypes = {
-        items: PropTypes.array.isRequired,
-        clickItemFn: PropTypes.func,
-        showIcons: PropTypes.bool
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      sort: props.initialSort || "Last Modified",
     };
+  }
 
-    onClickItem(item) {
-        if (this.props.onClickItemFn) {
-            this.props.onClickItemFn(item);
-        }
-    }
+  static propTypes = {
+    items: PropTypes.array.isRequired,
+    clickItemFn: PropTypes.func,
+    showIcons: PropTypes.bool,
+  };
 
-    render() {
-        var items;
-        if (this.state.sort === "Last Modified") {
-            items = this.props.items.slice().sort((a, b) => b.updated_at - a.updated_at);
-        } else if (this.state.sort === "Alphabetical Order") {
-            items = this.props.items.slice().sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
-        }
-
-        return (
-            <div className="SortableItemList">
-                <div className="flex align-center px2 pb3 border-bottom">
-                    <h5 className="text-bold text-uppercase text-grey-3 ml2 mt1 mr2">Sort by</h5>
-                    <Radio
-                        value={this.state.sort}
-                        options={["Last Modified", /*"Most Popular",*/  "Alphabetical Order"]}
-                        onChange={(sort) => this.setState({ sort })}
-                        optionNameFn={o => o}
-                        optionValueFn={o => o}
-                        optionKeyFn={o => o}
-                    />
-                </div>
+  onClickItem(item) {
+    if (this.props.onClickItemFn) {
+      this.props.onClickItemFn(item);
+    }
+  }
 
-                <ul className="SortableItemList-list px2 pb2">
-                    {items.map(item =>
-                        <li key={item.id} className="border-row-divider">
-                            <a className="no-decoration flex p2" onClick={() => this.onClickItem(item)}>
-                                <div className="flex align-center flex-full mr2">
-                                    {this.props.showIcons ?
-                                        <div className="mr2"><Icon name={'illustration-'+item.display} size={48} /></div>
-                                    : null}
-                                    <div className="text-brand-hover">
-                                        <h3 className="mb1">{item.name}</h3>
-                                        <h4 className="text-grey-3">{item.description || t`No description yet`}</h4>
-                                    </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>
-                    )}
-                </ul>
-            </div>
+  render() {
+    var items;
+    if (this.state.sort === "Last Modified") {
+      items = this.props.items
+        .slice()
+        .sort((a, b) => b.updated_at - a.updated_at);
+    } else if (this.state.sort === "Alphabetical Order") {
+      items = this.props.items
+        .slice()
+        .sort((a, b) =>
+          a.name.toLowerCase().localeCompare(b.name.toLowerCase()),
         );
     }
+
+    return (
+      <div className="SortableItemList">
+        <div className="flex align-center px2 pb3 border-bottom">
+          <h5 className="text-bold text-uppercase text-grey-3 ml2 mt1 mr2">
+            Sort by
+          </h5>
+          <Radio
+            value={this.state.sort}
+            options={[
+              "Last Modified",
+              /*"Most Popular",*/ "Alphabetical Order",
+            ]}
+            onChange={sort => this.setState({ sort })}
+            optionNameFn={o => o}
+            optionValueFn={o => o}
+            optionKeyFn={o => o}
+          />
+        </div>
+
+        <ul className="SortableItemList-list px2 pb2">
+          {items.map(item => (
+            <li key={item.id} className="border-row-divider">
+              <a
+                className="no-decoration flex p2"
+                onClick={() => this.onClickItem(item)}
+              >
+                <div className="flex align-center flex-full mr2">
+                  {this.props.showIcons ? (
+                    <div className="mr2">
+                      <Icon name={"illustration-" + item.display} size={48} />
+                    </div>
+                  ) : null}
+                  <div className="text-brand-hover">
+                    <h3 className="mb1">{item.name}</h3>
+                    <h4 className="text-grey-3">
+                      {item.description || t`No description yet`}
+                    </h4>
+                  </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>
+          ))}
+        </ul>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/StackedCheckBox.info.js b/frontend/src/metabase/components/StackedCheckBox.info.js
index 433c096e213c6d7dd417381b7a5201ad7c2430a5..795c49814b020169cce0e9669da54864b3561a20 100644
--- a/frontend/src/metabase/components/StackedCheckBox.info.js
+++ b/frontend/src/metabase/components/StackedCheckBox.info.js
@@ -8,7 +8,7 @@ A stacked checkbox, representing "all" items.
 `;
 
 export const examples = {
-    "Off - Default": <StackedCheckBox />,
-    "Checked": <StackedCheckBox checked />,
-    "Checked with color": <StackedCheckBox checked color='purple' />,
+  "Off - Default": <StackedCheckBox />,
+  Checked: <StackedCheckBox checked />,
+  "Checked with color": <StackedCheckBox checked color="purple" />,
 };
diff --git a/frontend/src/metabase/components/StackedCheckBox.jsx b/frontend/src/metabase/components/StackedCheckBox.jsx
index 1567dfd9e8ddc2504f97ad709083f78009b50336..f265e6af629079ca79c5e35c0752b5be292edc38 100644
--- a/frontend/src/metabase/components/StackedCheckBox.jsx
+++ b/frontend/src/metabase/components/StackedCheckBox.jsx
@@ -3,19 +3,20 @@ import CheckBox from "metabase/components/CheckBox.jsx";
 
 const OFFSET = 4;
 
-const StackedCheckBox = (props) =>
-    <div className="relative">
-        <span
-            className="absolute"
-            style={{
-                top: -OFFSET,
-                left: OFFSET,
-                zIndex: -1
-            }}
-        >
-            <CheckBox {...props} />
-        </span>
-        <CheckBox {...props} />
-    </div>
+const StackedCheckBox = props => (
+  <div className="relative">
+    <span
+      className="absolute"
+      style={{
+        top: -OFFSET,
+        left: OFFSET,
+        zIndex: -1,
+      }}
+    >
+      <CheckBox {...props} />
+    </span>
+    <CheckBox {...props} />
+  </div>
+);
 
 export default StackedCheckBox;
diff --git a/frontend/src/metabase/components/StepIndicators.jsx b/frontend/src/metabase/components/StepIndicators.jsx
index d4ea90c44e3c93fab81e39793ca8c913bd820046..993f4be37a65712ddeea1c7df200b94dfcfe0fbe 100644
--- a/frontend/src/metabase/components/StepIndicators.jsx
+++ b/frontend/src/metabase/components/StepIndicators.jsx
@@ -1,43 +1,42 @@
 /* @flow */
-import React from 'react'
+import React from "react";
 
-import { normal } from 'metabase/lib/colors'
+import { normal } from "metabase/lib/colors";
 
 type Props = {
-    activeDotColor?: string,
-    currentStep: number,
-    dotSize?: number,
-    goToStep?: (step: number) => void,
-    steps: []
-}
+  activeDotColor?: string,
+  currentStep: number,
+  dotSize?: number,
+  goToStep?: (step: number) => void,
+  steps: [],
+};
 
 const StepIndicators = ({
-    activeDotColor = normal.blue,
-    currentStep = 0,
-    dotSize = 8,
-    goToStep,
-    steps,
-}: Props) =>
-    <ol className="flex">
-        {
-            steps.map((step, index) =>
-                <li
-                    onClick={() => goToStep && goToStep(index + 1)}
-                    style={{
-                        width: dotSize,
-                        height: dotSize,
-                        borderRadius: 99,
-                        cursor: 'pointer',
-                        marginLeft: 2,
-                        marginRight: 2,
-                        backgroundColor: index + 1 === currentStep ? activeDotColor : '#D8D8D8',
-                        transition: 'background 600ms ease-in'
-                    }}
-                    key={index}
-                >
-                </li>
-            )
-        }
-    </ol>
+  activeDotColor = normal.blue,
+  currentStep = 0,
+  dotSize = 8,
+  goToStep,
+  steps,
+}: Props) => (
+  <ol className="flex">
+    {steps.map((step, index) => (
+      <li
+        onClick={() => goToStep && goToStep(index + 1)}
+        style={{
+          width: dotSize,
+          height: dotSize,
+          borderRadius: 99,
+          cursor: "pointer",
+          marginLeft: 2,
+          marginRight: 2,
+          backgroundColor:
+            index + 1 === currentStep ? activeDotColor : "#D8D8D8",
+          transition: "background 600ms ease-in",
+        }}
+        key={index}
+      />
+    ))}
+  </ol>
+);
 
 export default StepIndicators;
diff --git a/frontend/src/metabase/components/TermWithDefinition.jsx b/frontend/src/metabase/components/TermWithDefinition.jsx
index bca65ee4e184219cfb44632521ae38b9bfb5298b..3c081499c434c51392c0602e4704634168fd4ddb 100644
--- a/frontend/src/metabase/components/TermWithDefinition.jsx
+++ b/frontend/src/metabase/components/TermWithDefinition.jsx
@@ -1,16 +1,19 @@
-import React from 'react'
+import React from "react";
 import cxs from "cxs";
 import Tooltip from "metabase/components/Tooltip";
 
 const termStyles = cxs({
-    textDecoration: "none",
-    borderBottom: '1px dotted #DCE1E4'
-})
-export const TermWithDefinition = ({ children, definition, link }) =>
-    <Tooltip tooltip={definition}>
-        { link
-            ? <a href={link} className={termStyles} target="_blank">{ children }</a>
-            : <span className={termStyles}>{ children }</span>
-        }
-    </Tooltip>
-
+  textDecoration: "none",
+  borderBottom: "1px dotted #DCE1E4",
+});
+export const TermWithDefinition = ({ children, definition, link }) => (
+  <Tooltip tooltip={definition}>
+    {link ? (
+      <a href={link} className={termStyles} target="_blank">
+        {children}
+      </a>
+    ) : (
+      <span className={termStyles}>{children}</span>
+    )}
+  </Tooltip>
+);
diff --git a/frontend/src/metabase/components/TextEditor.jsx b/frontend/src/metabase/components/TextEditor.jsx
index d23fc3505aec4e4ef33e88f50cba0692e54751b6..38602be4817de58434eedd438499534522ab0660 100644
--- a/frontend/src/metabase/components/TextEditor.jsx
+++ b/frontend/src/metabase/components/TextEditor.jsx
@@ -13,104 +13,109 @@ const SCROLL_MARGIN = 8;
 const LINE_HEIGHT = 16;
 
 export default class TextEditor extends Component {
-
-    static propTypes = {
-        mode: PropTypes.string,
-        theme: PropTypes.string,
-        value: PropTypes.string,
-        defaultValue: PropTypes.string,
-        onChange: PropTypes.func
-    };
-
-    static defaultProps = {
-        mode: "ace/mode/plain_text",
-        theme: null
-    };
-
-    componentWillReceiveProps(nextProps) {
-        if (this._editor && nextProps.value != null && nextProps.value !== this._editor.getValue()) {
-            this._editor.setValue(nextProps.value);
-            this._editor.clearSelection();
-        }
-    }
-
-    _update() {
-        let element = ReactDOM.findDOMNode(this);
-
-        this._updateValue();
-
-        this._editor.getSession().setMode(this.props.mode);
-        this._editor.setTheme(this.props.theme);
-
-        // read only
-        this._editor.setReadOnly(this.props.readOnly);
-        element.classList[this.props.readOnly ? "add" : "remove"]("read-only");
-
-        this._updateSize();
-    }
-
-    _updateValue() {
-        if (this._editor) {
-            this.value = this._editor.getValue();
-        }
+  static propTypes = {
+    mode: PropTypes.string,
+    theme: PropTypes.string,
+    value: PropTypes.string,
+    defaultValue: PropTypes.string,
+    onChange: PropTypes.func,
+  };
+
+  static defaultProps = {
+    mode: "ace/mode/plain_text",
+    theme: null,
+  };
+
+  componentWillReceiveProps(nextProps) {
+    if (
+      this._editor &&
+      nextProps.value != null &&
+      nextProps.value !== this._editor.getValue()
+    ) {
+      this._editor.setValue(nextProps.value);
+      this._editor.clearSelection();
     }
+  }
 
-    _updateSize() {
-        const doc = this._editor.getSession().getDocument();
-        const element = ReactDOM.findDOMNode(this);
-        element.style.height = 2 * SCROLL_MARGIN + LINE_HEIGHT * doc.getLength() + "px";
-        this._editor.resize();
-    }
-
-    onChange = (e) => {
-        this._update();
-        if (this.props.onChange) {
-            this.props.onChange(this.value);
-        }
-    }
-
-    componentDidMount() {
-        let element = ReactDOM.findDOMNode(this);
-        this._editor = ace.edit(element);
-
-        window.editor = this._editor;
+  _update() {
+    let element = ReactDOM.findDOMNode(this);
 
-        // listen to onChange events
-        this._editor.getSession().on("change", this.onChange);
+    this._updateValue();
 
-        // misc options, copied from NativeQueryEditor
-        this._editor.setOptions({
-            enableBasicAutocompletion: true,
-            enableSnippets: true,
-            enableLiveAutocompletion: true,
-            showPrintMargin: false,
-            highlightActiveLine: false,
-            highlightGutterLine: false,
-            showLineNumbers: true,
-            // wrap: true
-        });
-        this._editor.renderer.setScrollMargin(SCROLL_MARGIN, SCROLL_MARGIN)
+    this._editor.getSession().setMode(this.props.mode);
+    this._editor.setTheme(this.props.theme);
 
-        // initialize the content
-        this._editor.setValue((this.props.value != null ? this.props.value : this.props.defaultValue) || "");
+    // read only
+    this._editor.setReadOnly(this.props.readOnly);
+    element.classList[this.props.readOnly ? "add" : "remove"]("read-only");
 
-        // clear the editor selection, otherwise we start with the whole editor selected
-        this._editor.clearSelection();
+    this._updateSize();
+  }
 
-        // hmmm, this could be dangerous
-        // this._editor.focus();
-
-        this._update();
-    }
-
-    componentDidUpdate() {
-        this._update();
+  _updateValue() {
+    if (this._editor) {
+      this.value = this._editor.getValue();
     }
-
-    render() {
-        const { className, style } = this.props
-        return (
-            <div className={className} style={style} />
-        );
+  }
+
+  _updateSize() {
+    const doc = this._editor.getSession().getDocument();
+    const element = ReactDOM.findDOMNode(this);
+    element.style.height =
+      2 * SCROLL_MARGIN + LINE_HEIGHT * doc.getLength() + "px";
+    this._editor.resize();
+  }
+
+  onChange = e => {
+    this._update();
+    if (this.props.onChange) {
+      this.props.onChange(this.value);
     }
+  };
+
+  componentDidMount() {
+    let element = ReactDOM.findDOMNode(this);
+    this._editor = ace.edit(element);
+
+    window.editor = this._editor;
+
+    // listen to onChange events
+    this._editor.getSession().on("change", this.onChange);
+
+    // misc options, copied from NativeQueryEditor
+    this._editor.setOptions({
+      enableBasicAutocompletion: true,
+      enableSnippets: true,
+      enableLiveAutocompletion: true,
+      showPrintMargin: false,
+      highlightActiveLine: false,
+      highlightGutterLine: false,
+      showLineNumbers: true,
+      // wrap: true
+    });
+    this._editor.renderer.setScrollMargin(SCROLL_MARGIN, SCROLL_MARGIN);
+
+    // initialize the content
+    this._editor.setValue(
+      (this.props.value != null ? this.props.value : this.props.defaultValue) ||
+        "",
+    );
+
+    // clear the editor selection, otherwise we start with the whole editor selected
+    this._editor.clearSelection();
+
+    // hmmm, this could be dangerous
+    // this._editor.focus();
+
+    this._update();
+  }
+
+  componentDidUpdate() {
+    this._update();
+  }
+
+  render() {
+    const { className, style } = this.props;
+    return <div className={className} style={style} />;
+  }
 }
diff --git a/frontend/src/metabase/components/TitleAndDescription.jsx b/frontend/src/metabase/components/TitleAndDescription.jsx
index 2099c33a3fda6fa6cee99ef86ee8a561775109ac..277447ad8fc411c88817698c7909478e2cf3a675 100644
--- a/frontend/src/metabase/components/TitleAndDescription.jsx
+++ b/frontend/src/metabase/components/TitleAndDescription.jsx
@@ -1,5 +1,5 @@
 /* @flow */
-import React from 'react';
+import React from "react";
 import cx from "classnames";
 import pure from "recompose/pure";
 
@@ -7,18 +7,19 @@ import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
 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'}>
-                <Icon name='info' style={{ marginTop: 3 }}/>
-            </Tooltip>
-        }
-    </div>;
+  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"}>
+        <Icon name="info" style={{ marginTop: 3 }} />
+      </Tooltip>
+    )}
+  </div>
+);
 
 export default pure(TitleAndDescription);
diff --git a/frontend/src/metabase/components/Toggle.css b/frontend/src/metabase/components/Toggle.css
index 7e99e70d4ed9ae76d7b7d17db7d579cca770eaf0..8bcda78577d880ee322c591b509f8ba79bb5902d 100644
--- a/frontend/src/metabase/components/Toggle.css
+++ b/frontend/src/metabase/components/Toggle.css
@@ -1,40 +1,40 @@
 :local(.toggle) {
-    position: relative;
-    display: inline-block;
-    color: var(--brand-color);
-    box-sizing: border-box;
-    width: 48px;
-    height: 24px;
-    border-radius: 99px;
-    border: 1px solid #EAEAEA;
-    background-color: #F7F7F7;
-    transition: all 0.3s;
+  position: relative;
+  display: inline-block;
+  color: var(--brand-color);
+  box-sizing: border-box;
+  width: 48px;
+  height: 24px;
+  border-radius: 99px;
+  border: 1px solid #eaeaea;
+  background-color: #f7f7f7;
+  transition: all 0.3s;
 }
 
 :local(.toggle.selected) {
-    background-color: currentColor;
+  background-color: currentColor;
 }
 
 :local(.toggle):after {
-    content: "";
-    width: 20px;
-    height: 20px;
-    border-radius: 99px;
-    position: absolute;
-    top: 1px;
-    left: 1px;
-    background-color: #D9D9D9;
-    transition: all 0.3s;
+  content: "";
+  width: 20px;
+  height: 20px;
+  border-radius: 99px;
+  position: absolute;
+  top: 1px;
+  left: 1px;
+  background-color: #d9d9d9;
+  transition: all 0.3s;
 }
 
 :local(.toggle.selected):after {
-    content: "";
-    width: 20px;
-    height: 20px;
-    border-radius: 99px;
-    position: absolute;
-    top: 1px;
-    left: 25px;
+  content: "";
+  width: 20px;
+  height: 20px;
+  border-radius: 99px;
+  position: absolute;
+  top: 1px;
+  left: 25px;
 
-    background-color: white;
+  background-color: white;
 }
diff --git a/frontend/src/metabase/components/Toggle.info.js b/frontend/src/metabase/components/Toggle.info.js
index 55ac49dd26cd1cb66fcbd6010fb285531639db25..753a97723e2fc075196a7d51ca0d2b0ad96f81c2 100644
--- a/frontend/src/metabase/components/Toggle.info.js
+++ b/frontend/src/metabase/components/Toggle.info.js
@@ -8,6 +8,6 @@ A standard toggle.
 `;
 
 export const examples = {
-    "off": <Toggle value={false} />,
-    "on": <Toggle value={true} />
+  off: <Toggle value={false} />,
+  on: <Toggle value={true} />,
 };
diff --git a/frontend/src/metabase/components/Toggle.jsx b/frontend/src/metabase/components/Toggle.jsx
index 7617f8723622bf3ba192ef9175a87a4b0756ebd9..2d8c9367fe7a39861b8316555e76ed0982218407 100644
--- a/frontend/src/metabase/components/Toggle.jsx
+++ b/frontend/src/metabase/components/Toggle.jsx
@@ -6,29 +6,35 @@ import styles from "./Toggle.css";
 import cx from "classnames";
 
 export default class Toggle extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.onClick = this.onClick.bind(this);
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.onClick = this.onClick.bind(this);
+  }
 
-    static propTypes = {
-        value: PropTypes.bool.isRequired,
-        onChange: PropTypes.func
-    };
+  static propTypes = {
+    value: PropTypes.bool.isRequired,
+    onChange: PropTypes.func,
+  };
 
-    onClick() {
-        if (this.props.onChange) {
-            this.props.onChange(!this.props.value);
-        }
+  onClick() {
+    if (this.props.onChange) {
+      this.props.onChange(!this.props.value);
     }
+  }
 
-    render() {
-        return (
-            <a
-                className={cx(styles.toggle, "no-decoration", { [styles.selected]: this.props.value }) + " " + (this.props.className||"")}
-                style={{color: this.props.color || null}}
-                onClick={this.props.onChange ? this.onClick : null}
-            />
-        );
-    }
+  render() {
+    return (
+      <a
+        className={
+          cx(styles.toggle, "no-decoration", {
+            [styles.selected]: this.props.value,
+          }) +
+          " " +
+          (this.props.className || "")
+        }
+        style={{ color: this.props.color || null }}
+        onClick={this.props.onChange ? this.onClick : null}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/ToggleLarge.jsx b/frontend/src/metabase/components/ToggleLarge.jsx
index 2ea0c5e7c0b1b484ffdf3a09d888e66af0f58c9b..964b9d624feb626978100ca8d9c6d11ca5acc5d1 100644
--- a/frontend/src/metabase/components/ToggleLarge.jsx
+++ b/frontend/src/metabase/components/ToggleLarge.jsx
@@ -2,24 +2,34 @@ import React from "react";
 
 import cx from "classnames";
 
-const ToggleLarge = ({ style, className, value, onChange, textLeft, textRight }) =>
+const ToggleLarge = ({
+  style,
+  className,
+  value,
+  onChange,
+  textLeft,
+  textRight,
+}) => (
+  <div
+    className={cx(className, "bg-grey-1 flex relative text-bold", {
+      "cursor-pointer": onChange,
+    })}
+    style={{ borderRadius: 8, ...style }}
+    onClick={() => onChange({ target: { value: !value } })}
+  >
     <div
-        className={cx(className, "bg-grey-1 flex relative text-bold", {
-            "cursor-pointer": onChange
-        })}
-        style={{ borderRadius: 8, ...style }}
-        onClick={() => onChange({ target: { value: !value }})}
-    >
-        <div
-            className="absolute bg-white z1"
-            style={{
-                borderRadius: 6,
-                top: 3, bottom: 3,
-                width: "50%", [value ? "left" : "right"]: 3,
-            }}
-        />
-        <div className="flex-full flex layout-centered z2">{textLeft}</div>
-        <div className="flex-full flex layout-centered z2">{textRight}</div>
-    </div>
+      className="absolute bg-white z1"
+      style={{
+        borderRadius: 6,
+        top: 3,
+        bottom: 3,
+        width: "50%",
+        [value ? "left" : "right"]: 3,
+      }}
+    />
+    <div className="flex-full flex layout-centered z2">{textLeft}</div>
+    <div className="flex-full flex layout-centered z2">{textRight}</div>
+  </div>
+);
 
 export default ToggleLarge;
diff --git a/frontend/src/metabase/components/TokenField.info.js b/frontend/src/metabase/components/TokenField.info.js
new file mode 100644
index 0000000000000000000000000000000000000000..2e81613cd7942da3aeddfbcf8ae84bc4701fab3b
--- /dev/null
+++ b/frontend/src/metabase/components/TokenField.info.js
@@ -0,0 +1,51 @@
+import React from "react";
+import TokenField from "metabase/components/TokenField";
+
+export const component = TokenField;
+
+export const description = `
+Token field picker with searching
+`;
+
+class TokenFieldWithStateAndDefaults extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      value: props.value || [],
+    };
+  }
+  render() {
+    return (
+      <TokenField
+        value={this.state.value}
+        options={[]}
+        onChange={value => this.setState({ value })}
+        multi
+        valueKey={option => option}
+        labelKey={option => option}
+        layoutRenderer={({ valuesList, optionsList }) => (
+          <div>
+            {valuesList}
+            {optionsList}
+          </div>
+        )}
+        {...this.props}
+      />
+    );
+  }
+}
+
+export const examples = {
+  "": (
+    <TokenFieldWithStateAndDefaults
+      options={["Doohickey", "Gadget", "Gizmo", "Widget"]}
+    />
+  ),
+  updateOnInputChange: (
+    <TokenFieldWithStateAndDefaults
+      options={["Doohickey", "Gadget", "Gizmo", "Widget"]}
+      updateOnInputChange
+      parseFreeformValue={value => value}
+    />
+  ),
+};
diff --git a/frontend/src/metabase/components/TokenField.jsx b/frontend/src/metabase/components/TokenField.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..d3559b67d2fa4f4bc09e1f538141a4afbc5c3b22
--- /dev/null
+++ b/frontend/src/metabase/components/TokenField.jsx
@@ -0,0 +1,675 @@
+/* @flow */
+/* eslint "react/prop-types": "warn" */
+import React, { Component } from "react";
+import PropTypes from "prop-types";
+import { findDOMNode } from "react-dom";
+import _ from "underscore";
+import cx from "classnames";
+import cxs from "cxs";
+
+import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
+import Icon from "metabase/components/Icon";
+import Popover from "metabase/components/Popover";
+
+import {
+  KEYCODE_ESCAPE,
+  KEYCODE_ENTER,
+  KEYCODE_COMMA,
+  KEYCODE_TAB,
+  KEYCODE_UP,
+  KEYCODE_DOWN,
+  KEYCODE_BACKSPACE,
+} from "metabase/lib/keyboard";
+import { isObscured } from "metabase/lib/dom";
+
+const inputBoxClasses = cxs({
+  maxHeight: 130,
+  overflow: "scroll",
+});
+
+type Value = any;
+type Option = any;
+
+export type LayoutRendererProps = {
+  valuesList: React$Element<any>,
+  optionsList: ?React$Element<any>,
+  isFocused: boolean,
+  isAllSelected: boolean,
+  onClose: () => void,
+};
+
+type Props = {
+  value: Value[],
+  onChange: (value: Value[]) => void,
+
+  options: Option[],
+
+  placeholder?: string,
+  autoFocus?: boolean,
+  multi?: boolean,
+
+  style: { [key: string]: string | number },
+  color: string,
+
+  valueKey: string | number | (() => any),
+  labelKey: string | number | (() => string),
+
+  removeSelected?: boolean,
+  filterOption: (option: Option, searchValue: string) => boolean,
+
+  onInputChange?: string => string,
+  onInputKeyDown?: (event: SyntheticKeyboardEvent) => void,
+  onFocus?: () => void,
+  onBlur?: () => void,
+
+  updateOnInputChange: boolean,
+  // if provided, parseFreeformValue parses the input string into a value,
+  // or returns null to indicate an invalid value
+  parseFreeformValue: (value: string) => ?Value,
+
+  valueRenderer: (value: Value) => React$Element<any>,
+  optionRenderer: (option: Option) => React$Element<any>,
+  layoutRenderer: (props: LayoutRendererProps) => React$Element<any>,
+};
+
+type State = {
+  inputValue: string,
+  searchValue: string,
+  filteredOptions: Option[],
+  selectedOptionValue: ?Value,
+  isFocused: boolean,
+  isAllSelected: boolean,
+  listIsHovered: boolean,
+};
+
+// somewhat matches react-select's API: https://github.com/JedWatson/react-select
+export default class TokenField extends Component {
+  props: Props;
+  state: State;
+
+  scrollElement: ?HTMLDivElement = null;
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      inputValue: "",
+      searchValue: "",
+      filteredOptions: [],
+      selectedOptionValue: null,
+      isFocused: props.autoFocus || false,
+      isAllSelected: false,
+      listIsHovered: false,
+    };
+  }
+
+  static propTypes = {
+    value: PropTypes.array,
+    options: PropTypes.array,
+    placeholder: PropTypes.string,
+    autoFocus: PropTypes.bool,
+    multi: PropTypes.bool,
+
+    style: PropTypes.object,
+    color: PropTypes.string,
+
+    valueKey: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.number,
+      PropTypes.func,
+    ]),
+    labelKey: PropTypes.oneOfType([
+      PropTypes.string,
+      PropTypes.number,
+      PropTypes.func,
+    ]),
+
+    removeSelected: PropTypes.bool,
+    filterOption: PropTypes.func,
+
+    onChange: PropTypes.func.isRequired,
+    onInputChange: PropTypes.func,
+    onInputKeyDown: PropTypes.func,
+    onFocus: PropTypes.func,
+    onBlur: PropTypes.func,
+
+    updateOnInputChange: PropTypes.bool,
+    // if provided, parseFreeformValue parses the input string into a value,
+    // or returns null to indicate an invalid value
+    parseFreeformValue: PropTypes.func,
+
+    valueRenderer: PropTypes.func.isRequired, // TODO: default
+    optionRenderer: PropTypes.func.isRequired, // TODO: default
+    layoutRenderer: PropTypes.func,
+  };
+
+  static defaultProps = {
+    removeSelected: true,
+
+    // $FlowFixMe
+    valueKey: "value",
+    labelKey: "label",
+
+    valueRenderer: value => <span>{value}</span>,
+    optionRenderer: option => <span>{option}</span>,
+    layoutRenderer: props => <DefaultTokenFieldLayout {...props} />,
+
+    color: "brand",
+  };
+
+  componentWillMount() {
+    this._updateFilteredValues(this.props);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    this._updateFilteredValues((nextProps: Props));
+  }
+
+  setInputValue(inputValue: string, setSearchValue: boolean = true) {
+    const newState: { inputValue: string, searchValue?: string } = {
+      inputValue,
+    };
+    if (setSearchValue) {
+      newState.searchValue = inputValue;
+    }
+    this.setState(newState, () => this._updateFilteredValues(this.props));
+  }
+
+  clearInputValue(clearSearchValue: boolean = true) {
+    this.setInputValue("", clearSearchValue);
+  }
+
+  _value(option: Option) {
+    const { valueKey } = this.props;
+    return typeof valueKey === "function" ? valueKey(option) : option[valueKey];
+  }
+
+  _label(option: Option) {
+    const { labelKey } = this.props;
+    return typeof labelKey === "function" ? labelKey(option) : option[labelKey];
+  }
+
+  _isLastFreeformValue(inputValue: string) {
+    const { value, parseFreeformValue, updateOnInputChange } = this.props;
+    if (parseFreeformValue && updateOnInputChange) {
+      const freeformValue = parseFreeformValue(inputValue);
+      const currentLastValue = value[value.length - 1];
+      // check to see if the current last value is the same as the inputValue, in which case we should replace it or remove it
+      return currentLastValue === freeformValue;
+    }
+  }
+
+  _updateFilteredValues = (props: Props) => {
+    let { options, value, removeSelected, filterOption } = props;
+    let { searchValue, selectedOptionValue } = this.state;
+    let selectedValues = new Set(value.map(v => JSON.stringify(v)));
+
+    if (!filterOption) {
+      filterOption = (option, searchValue) =>
+        String(this._label(option) || "").indexOf(searchValue) >= 0;
+    }
+
+    let selectedCount = 0;
+    let filteredOptions = options.filter(option => {
+      const isSelected = selectedValues.has(
+        JSON.stringify(this._value(option)),
+      );
+      const isLastFreeform =
+        this._isLastFreeformValue(this._value(option)) &&
+        this._isLastFreeformValue(searchValue);
+      const isMatching = filterOption(option, searchValue);
+      if (isSelected) {
+        selectedCount++;
+      }
+      // filter out options who have already been selected, unless:
+      return (
+        // remove selected is disabled
+        (!removeSelected ||
+          // or it's not in the selectedValues
+          !isSelected ||
+          // or it's the current "freeform" value, which updates as we type
+          isLastFreeform) &&
+        // and it's matching
+        isMatching
+      );
+    });
+
+    if (
+      selectedOptionValue == null ||
+      !_.find(filteredOptions, option =>
+        this._valueIsEqual(selectedOptionValue, this._value(option)),
+      )
+    ) {
+      // if there are results based on the user's typing...
+      if (filteredOptions.length > 0) {
+        // select the first option in the list and set the selected option to that
+        selectedOptionValue = this._value(filteredOptions[0]);
+      } else {
+        selectedOptionValue = null;
+      }
+    }
+
+    this.setState({
+      filteredOptions,
+      selectedOptionValue,
+      isAllSelected: options.length > 0 && selectedCount === options.length,
+    });
+  };
+
+  onInputChange = ({ target: { value } }: SyntheticInputEvent) => {
+    const {
+      updateOnInputChange,
+      onInputChange,
+      parseFreeformValue,
+    } = this.props;
+
+    if (onInputChange) {
+      value = onInputChange(value) || "";
+    }
+
+    // update the input value
+    this.setInputValue(value);
+
+    // if updateOnInputChange is true and parseFreeformValue is enabled then try adding/updating the freeform value immediately
+    if (updateOnInputChange && parseFreeformValue) {
+      const replaceLast = this._isLastFreeformValue(this.state.inputValue);
+      // call parseFreeformValue to make sure we can add it
+      const freeformValue = parseFreeformValue(value);
+      if (freeformValue != null) {
+        // if so, add it, replacing the last value if necessary
+        this.addValue(freeformValue, replaceLast);
+      } else {
+        // otherwise remove the value if necessary, e.x. after deleting
+        if (replaceLast) {
+          this.removeValue(parseFreeformValue(this.state.inputValue));
+        }
+      }
+    }
+  };
+
+  // capture events on the input to allow for convenient keyboard shortcuts
+  onInputKeyDown = (event: SyntheticKeyboardEvent) => {
+    if (this.props.onInputKeyDown) {
+      this.props.onInputKeyDown(event);
+    }
+
+    const keyCode = event.keyCode;
+
+    const { filteredOptions, selectedOptionValue } = this.state;
+
+    // enter, tab, comma
+    if (
+      keyCode === KEYCODE_ESCAPE ||
+      keyCode === KEYCODE_TAB ||
+      keyCode === KEYCODE_COMMA ||
+      keyCode === KEYCODE_ENTER
+    ) {
+      if (this.addSelectedOption(event)) {
+        event.stopPropagation();
+      }
+    } else if (event.keyCode === KEYCODE_UP) {
+      // up arrow
+      event.preventDefault();
+      let index = _.findIndex(filteredOptions, option =>
+        this._valueIsEqual(selectedOptionValue, this._value(option)),
+      );
+      if (index > 0) {
+        this.setState({
+          selectedOptionValue: this._value(filteredOptions[index - 1]),
+        });
+      }
+    } else if (keyCode === KEYCODE_DOWN) {
+      // down arrow
+      event.preventDefault();
+      let index = _.findIndex(filteredOptions, option =>
+        this._valueIsEqual(selectedOptionValue, this._value(option)),
+      );
+      if (index >= 0 && index < filteredOptions.length - 1) {
+        this.setState({
+          selectedOptionValue: this._value(filteredOptions[index + 1]),
+        });
+      }
+    } else if (keyCode === KEYCODE_BACKSPACE) {
+      // backspace
+      let { value } = this.props;
+      if (!this.state.inputValue && value.length > 0) {
+        this.removeValue(value[value.length - 1]);
+      }
+    }
+  };
+
+  onInputFocus = () => {
+    if (this.props.onFocus) {
+      this.props.onFocus();
+    }
+    this.setState({ isFocused: true, searchValue: this.state.inputValue }, () =>
+      this._updateFilteredValues(this.props),
+    );
+  };
+
+  onInputBlur = () => {
+    if (this.props.onBlur) {
+      this.props.onBlur();
+    }
+    this.setState({ isFocused: false });
+  };
+
+  onInputPaste = (e: SyntheticClipboardEvent) => {
+    if (this.props.parseFreeformValue) {
+      e.preventDefault();
+      const string = e.clipboardData.getData("Text");
+      const values = this.props.multi
+        ? string
+            .split(/\n|,/g)
+            .map(this.props.parseFreeformValue)
+            .filter(s => s)
+        : [string];
+      if (values.length > 0) {
+        this.addValue(values);
+      }
+    }
+  };
+
+  onMouseDownCapture = (e: SyntheticMouseEvent) => {
+    let input = findDOMNode(this.refs.input);
+    input.focus();
+    // prevents clicks from blurring input while still allowing text selection:
+    if (input !== e.target) {
+      e.preventDefault();
+    }
+  };
+
+  onClose = () => {
+    this.setState({ isFocused: false });
+  };
+
+  addSelectedOption(e: SyntheticKeyboardEvent) {
+    const { multi } = this.props;
+    const { filteredOptions, selectedOptionValue } = this.state;
+    let input = findDOMNode(this.refs.input);
+    let option = _.find(filteredOptions, option =>
+      this._valueIsEqual(selectedOptionValue, this._value(option)),
+    );
+    if (option) {
+      this.addOption(option);
+      // clear the input if the option is the same as the last value
+      if (this._isLastFreeformValue(this._value(option))) {
+        // also clear the search
+        this.clearInputValue(true);
+      } else {
+        // only clear the search if this was the last option
+        this.clearInputValue(filteredOptions.length === 1);
+      }
+      return true;
+    } else if (this.props.parseFreeformValue) {
+      // if we previously updated on input change then we don't need to do it again,
+      if (this.props.updateOnInputChange) {
+        // if multi=true also prevent the input from changing due to this key press
+        if (multi) {
+          e.preventDefault();
+        }
+        // and clear the input
+        this.clearInputValue();
+        // return false so we don't stop the keyDown from propagating in case we're listening
+        // for it, e.x. in the filter popover this allows enter to commit the filter
+        return false;
+      } else {
+        const value = this.props.parseFreeformValue(input.value);
+        if (value != null && (multi || value !== this.props.value[0])) {
+          this.addValue(value);
+          this.clearInputValue();
+          return true;
+        }
+      }
+    }
+  }
+
+  addOption = (option: Option) => {
+    const replaceLast = this._isLastFreeformValue(this.state.inputValue);
+    // add the option's value to the current value
+    this.addValue(this._value(option), replaceLast);
+  };
+
+  addValue(valueToAdd: Value, replaceLast: boolean = false) {
+    const { value, onChange, multi } = this.props;
+    if (!Array.isArray(valueToAdd)) {
+      valueToAdd = [valueToAdd];
+    }
+    if (multi) {
+      if (replaceLast) {
+        onChange(dedup(value.slice(0, -1).concat(valueToAdd)));
+      } else {
+        onChange(dedup(value.concat(valueToAdd)));
+      }
+    } else {
+      onChange(valueToAdd.slice(0, 1));
+    }
+    // reset the input value
+    // setTimeout(() =>
+    //   this.setInputValue("")
+    // )
+  }
+
+  removeValue(valueToRemove: Value) {
+    const { value, onChange } = this.props;
+    const values = value.filter(v => !this._valueIsEqual(v, valueToRemove));
+    onChange(values);
+    // reset the input value
+    // this.setInputValue("");
+  }
+
+  _valueIsEqual(v1: any, v2: any) {
+    return JSON.stringify(v1) === JSON.stringify(v2);
+  }
+
+  componentDidUpdate(prevProps: Props, prevState: State) {
+    if (
+      prevState.selectedOptionValue !== this.state.selectedOptionValue &&
+      this.scrollElement != null
+    ) {
+      const element = findDOMNode(this.scrollElement);
+      if (element && isObscured(element)) {
+        element.scrollIntoView(element);
+      }
+    }
+    // if we added a valkue then scroll to the last item (the input)
+    if (this.props.value.length > prevProps.value.length) {
+      let input = findDOMNode(this.refs.input);
+      if (input && isObscured(input)) {
+        input.scrollIntoView(input);
+      }
+    }
+  }
+
+  render() {
+    let {
+      value,
+      placeholder,
+      multi,
+      optionRenderer,
+      valueRenderer,
+      layoutRenderer,
+      color,
+      parseFreeformValue,
+      updateOnInputChange,
+    } = this.props;
+    let {
+      inputValue,
+      searchValue,
+      filteredOptions,
+      isFocused,
+      isAllSelected,
+      selectedOptionValue,
+    } = this.state;
+
+    if (!multi && isFocused) {
+      inputValue = inputValue || value[0];
+      value = [];
+    }
+
+    // if we have a value and updateOnInputChange is enabled, and the last value matches the inputValue
+    if (
+      value.length > 0 &&
+      updateOnInputChange &&
+      parseFreeformValue &&
+      value[value.length - 1] === parseFreeformValue(inputValue)
+    ) {
+      if (isFocused) {
+        // if focused, don't render the last value
+        value = value.slice(0, -1);
+      } else {
+        // if not focused, don't render the inputValue
+        inputValue = "";
+      }
+    }
+
+    // if not focused we won't get key events to accept the selected value, so don't render as selected
+    if (!isFocused) {
+      selectedOptionValue = null;
+    }
+
+    // don't show the placeholder if we already have a value
+    if (value.length > 0) {
+      placeholder = null;
+    }
+
+    const valuesList = (
+      <ul
+        className={cx(
+          "m1 p0 pb1 bordered rounded flex flex-wrap bg-white scroll-x scroll-y",
+          inputBoxClasses,
+          {
+            [`border-grey-2`]: this.state.isFocused,
+          },
+        )}
+        style={this.props.style}
+        onMouseDownCapture={this.onMouseDownCapture}
+      >
+        {value.map((v, index) => (
+          <li
+            key={v}
+            className={cx(
+              `mt1 ml1 py1 pl2 rounded bg-grey-05`,
+              multi ? "pr1" : "pr2",
+            )}
+          >
+            <span className="text-bold">{valueRenderer(v)}</span>
+            {multi && (
+              <a
+                className="text-grey-3 text-default-hover px1"
+                onClick={e => {
+                  this.removeValue(v);
+                  e.preventDefault();
+                }}
+                onMouseDown={e => e.preventDefault()}
+              >
+                <Icon name="close" className="" size={12} />
+              </a>
+            )}
+          </li>
+        ))}
+        <li className="flex-full mr1 py1 pl1 mt1 bg-white">
+          <input
+            ref="input"
+            className="full h4 text-bold text-default no-focus borderless"
+            // set size to be small enough that it fits in a parameter.
+            size={10}
+            placeholder={placeholder}
+            value={inputValue}
+            autoFocus={isFocused}
+            onKeyDown={this.onInputKeyDown}
+            onChange={this.onInputChange}
+            onFocus={this.onInputFocus}
+            onBlur={this.onInputBlur}
+            onPaste={this.onInputPaste}
+          />
+        </li>
+      </ul>
+    );
+
+    const optionsList =
+      filteredOptions.length === 0 ? null : (
+        <ul
+          className="ml1 scroll-y scroll-show"
+          style={{ maxHeight: 300 }}
+          onMouseEnter={() => this.setState({ listIsHovered: true })}
+          onMouseLeave={() => this.setState({ listIsHovered: false })}
+        >
+          {filteredOptions.map(option => (
+            <li key={this._value(option)}>
+              <div
+                ref={
+                  this._valueIsEqual(selectedOptionValue, this._value(option))
+                    ? _ => (this.scrollElement = _)
+                    : null
+                }
+                className={cx(
+                  `py1 pl1 pr2 block rounded text-bold text-${color}-hover inline-block full cursor-pointer`,
+                  `bg-grey-0-hover`,
+                  {
+                    [`text-${color} bg-grey-0`]:
+                      !this.state.listIsHovered &&
+                      this._valueIsEqual(
+                        selectedOptionValue,
+                        this._value(option),
+                      ),
+                  },
+                )}
+                onClick={e => {
+                  this.addOption(option);
+                  // clear the input value, and search value if last option
+                  this.clearInputValue(filteredOptions.length === 1);
+                  e.preventDefault();
+                }}
+                onMouseDown={e => e.preventDefault()}
+              >
+                {optionRenderer(option)}
+              </div>
+            </li>
+          ))}
+        </ul>
+      );
+
+    return layoutRenderer({
+      valuesList,
+      optionsList,
+      isFocused,
+      isAllSelected,
+      isFiltered: !!searchValue,
+      onClose: this.onClose,
+    });
+  }
+}
+
+const dedup = array => Array.from(new Set(array));
+
+const DefaultTokenFieldLayout = ({
+  valuesList,
+  optionsList,
+  isFocused,
+  onClose,
+}) => (
+  <OnClickOutsideWrapper handleDismissal={onClose}>
+    <div>
+      {valuesList}
+      <Popover
+        isOpen={isFocused && !!optionsList}
+        hasArrow={false}
+        tetherOptions={{
+          attachment: "top left",
+          targetAttachment: "bottom left",
+          targetOffset: "10 0",
+        }}
+      >
+        {optionsList}
+      </Popover>
+    </div>
+  </OnClickOutsideWrapper>
+);
+
+DefaultTokenFieldLayout.propTypes = {
+  valuesList: PropTypes.element.isRequired,
+  optionsList: PropTypes.element,
+  isFocused: PropTypes.bool,
+  onClose: PropTypes.func,
+};
diff --git a/frontend/src/metabase/components/Tooltip.jsx b/frontend/src/metabase/components/Tooltip.jsx
index 86c0295a78675c0c4ee1e137c04e49312361b260..eee89c03cbd8f99f92bd37c3deec03630a5b8b19 100644
--- a/frontend/src/metabase/components/Tooltip.jsx
+++ b/frontend/src/metabase/components/Tooltip.jsx
@@ -5,90 +5,97 @@ import ReactDOM from "react-dom";
 import TooltipPopover from "./TooltipPopover.jsx";
 
 export default class Tooltip extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            isOpen: false,
-            isHovered: false
-        };
-    }
-
-    static propTypes = {
-        tooltip: PropTypes.node,
-        children: PropTypes.element.isRequired,
-        isEnabled: PropTypes.bool,
-        verticalAttachments: PropTypes.array,
-        isOpen: PropTypes.bool
+    this.state = {
+      isOpen: false,
+      isHovered: false,
     };
-
-    static defaultProps = {
-        isEnabled: true,
-        verticalAttachments: ["top", "bottom"]
-    };
-
-    componentDidMount() {
-        let elem = ReactDOM.findDOMNode(this);
-
-        elem.addEventListener("mouseenter", this._onMouseEnter, false);
-        elem.addEventListener("mouseleave", this._onMouseLeave, false);
-
-        // HACK: These two event listeners ensure that if a click on the child causes the tooltip to
-        // unmount (e.x. navigating away) then the popover is removed by the time this component
-        // unmounts. Previously we were seeing difficult to debug error messages like
-        // "Cannot read property 'componentDidUpdate' of null"
-        elem.addEventListener("mousedown", this._onMouseDown, true);
-        elem.addEventListener("mouseup", this._onMouseUp, true);
-
-        this._element = document.createElement('div');
-        this.componentDidUpdate();
-    }
-
-    componentDidUpdate() {
-        const { isEnabled, tooltip } = this.props;
-        const isOpen = this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
-        if (tooltip && isEnabled && isOpen) {
-            ReactDOM.render(
-                <TooltipPopover isOpen={true} target={this} {...this.props} children={this.props.tooltip} />,
-                this._element
-            );
-        } else {
-            ReactDOM.unmountComponentAtNode(this._element);
-        }
-    }
-
-    componentWillUnmount() {
-        let elem = ReactDOM.findDOMNode(this);
-        elem.removeEventListener("mouseenter", this._onMouseEnter, false);
-        elem.removeEventListener("mouseleave", this._onMouseLeave, false);
-        elem.removeEventListener("mousedown", this._onMouseDown, true);
-        elem.removeEventListener("mouseup", this._onMouseUp, true);
-        ReactDOM.unmountComponentAtNode(this._element);
-        clearTimeout(this.timer);
-    }
-
-    _onMouseEnter = (e) => {
-        this.setState({ isOpen: true, isHovered: true });
-    }
-
-    _onMouseLeave = (e) => {
-        this.setState({ isOpen: false, isHovered: false });
-    }
-
-    _onMouseDown = (e) => {
-        this.setState({ isOpen: false });
-    }
-
-    _onMouseUp = (e) => {
-        // This is in a timeout to ensure the component has a chance to fully unmount
-        this.timer = setTimeout(() =>
-                this.setState({ isOpen: this.state.isHovered })
-            , 0);
-    }
-
-    render() {
-        return React.Children.only(this.props.children);
+  }
+
+  static propTypes = {
+    tooltip: PropTypes.node,
+    children: PropTypes.element.isRequired,
+    isEnabled: PropTypes.bool,
+    verticalAttachments: PropTypes.array,
+    isOpen: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    isEnabled: true,
+    verticalAttachments: ["top", "bottom"],
+  };
+
+  componentDidMount() {
+    let elem = ReactDOM.findDOMNode(this);
+
+    elem.addEventListener("mouseenter", this._onMouseEnter, false);
+    elem.addEventListener("mouseleave", this._onMouseLeave, false);
+
+    // HACK: These two event listeners ensure that if a click on the child causes the tooltip to
+    // unmount (e.x. navigating away) then the popover is removed by the time this component
+    // unmounts. Previously we were seeing difficult to debug error messages like
+    // "Cannot read property 'componentDidUpdate' of null"
+    elem.addEventListener("mousedown", this._onMouseDown, true);
+    elem.addEventListener("mouseup", this._onMouseUp, true);
+
+    this._element = document.createElement("div");
+    this.componentDidUpdate();
+  }
+
+  componentDidUpdate() {
+    const { isEnabled, tooltip } = this.props;
+    const isOpen =
+      this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
+    if (tooltip && isEnabled && isOpen) {
+      ReactDOM.render(
+        <TooltipPopover
+          isOpen={true}
+          target={this}
+          {...this.props}
+          children={this.props.tooltip}
+        />,
+        this._element,
+      );
+    } else {
+      ReactDOM.unmountComponentAtNode(this._element);
     }
+  }
+
+  componentWillUnmount() {
+    let elem = ReactDOM.findDOMNode(this);
+    elem.removeEventListener("mouseenter", this._onMouseEnter, false);
+    elem.removeEventListener("mouseleave", this._onMouseLeave, false);
+    elem.removeEventListener("mousedown", this._onMouseDown, true);
+    elem.removeEventListener("mouseup", this._onMouseUp, true);
+    ReactDOM.unmountComponentAtNode(this._element);
+    clearTimeout(this.timer);
+  }
+
+  _onMouseEnter = e => {
+    this.setState({ isOpen: true, isHovered: true });
+  };
+
+  _onMouseLeave = e => {
+    this.setState({ isOpen: false, isHovered: false });
+  };
+
+  _onMouseDown = e => {
+    this.setState({ isOpen: false });
+  };
+
+  _onMouseUp = e => {
+    // This is in a timeout to ensure the component has a chance to fully unmount
+    this.timer = setTimeout(
+      () => this.setState({ isOpen: this.state.isHovered }),
+      0,
+    );
+  };
+
+  render() {
+    return React.Children.only(this.props.children);
+  }
 }
 
 /**
@@ -98,63 +105,78 @@ export default class Tooltip extends Component {
  * The test tooltip can only be toggled with `jestWrapper.simulate("mouseenter")` and `jestWrapper.simulate("mouseleave")`.
  */
 export class TestTooltip extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            isOpen: false,
-            isHovered: false
-        };
-    }
-
-    static propTypes = {
-        tooltip: PropTypes.node,
-        children: PropTypes.element.isRequired,
-        isEnabled: PropTypes.bool,
-        verticalAttachments: PropTypes.array,
-        isOpen: PropTypes.bool
-    };
+  constructor(props, context) {
+    super(props, context);
 
-    static defaultProps = {
-        isEnabled: true,
-        verticalAttachments: ["top", "bottom"]
+    this.state = {
+      isOpen: false,
+      isHovered: false,
     };
-
-    _onMouseEnter = (e) => {
-        this.setState({ isOpen: true, isHovered: true });
-    }
-
-    _onMouseLeave = (e) => {
-        this.setState({ isOpen: false, isHovered: false });
-    }
-
-    render() {
-        const { isEnabled, tooltip } = this.props;
-        const isOpen = this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
-
-        return (
-            <div>
-                <TestTooltipTarget
-                    onMouseEnter={this._onMouseEnter}
-                    onMouseLeave={this._onMouseLeave}
-                >
-                    {this.props.children}
-                </TestTooltipTarget>
-
-                { tooltip && isEnabled && isOpen &&
-                    <TestTooltipContent>
-                        <TooltipPopover isOpen={true} target={this} {...this.props} children={this.props.tooltip} />
-                        {this.props.tooltip}
-                    </TestTooltipContent>
-                }
-            </div>
-        )
-    }
+  }
+
+  static propTypes = {
+    tooltip: PropTypes.node,
+    children: PropTypes.element.isRequired,
+    isEnabled: PropTypes.bool,
+    verticalAttachments: PropTypes.array,
+    isOpen: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    isEnabled: true,
+    verticalAttachments: ["top", "bottom"],
+  };
+
+  _onMouseEnter = e => {
+    this.setState({ isOpen: true, isHovered: true });
+  };
+
+  _onMouseLeave = e => {
+    this.setState({ isOpen: false, isHovered: false });
+  };
+
+  render() {
+    const { isEnabled, tooltip } = this.props;
+    const isOpen =
+      this.props.isOpen != null ? this.props.isOpen : this.state.isOpen;
+
+    return (
+      <div>
+        <TestTooltipTarget
+          onMouseEnter={this._onMouseEnter}
+          onMouseLeave={this._onMouseLeave}
+        >
+          {this.props.children}
+        </TestTooltipTarget>
+
+        {tooltip &&
+          isEnabled &&
+          isOpen && (
+            <TestTooltipContent>
+              <TooltipPopover
+                isOpen={true}
+                target={this}
+                {...this.props}
+                children={this.props.tooltip}
+              />
+              {this.props.tooltip}
+            </TestTooltipContent>
+          )}
+      </div>
+    );
+  }
 }
 
-export const TestTooltipTarget = ({ children, onMouseEnter, onMouseLeave }) =>
-    <div className="test-tooltip-hover-area" onMouseEnter={onMouseEnter} onMouseLeave={onMouseLeave}>
-        {children}
-    </div>
-
-export const TestTooltipContent = ({ children }) => <div className="test-tooltip-content">{children}</div>
+export const TestTooltipTarget = ({ children, onMouseEnter, onMouseLeave }) => (
+  <div
+    className="test-tooltip-hover-area"
+    onMouseEnter={onMouseEnter}
+    onMouseLeave={onMouseLeave}
+  >
+    {children}
+  </div>
+);
+
+export const TestTooltipContent = ({ children }) => (
+  <div className="test-tooltip-content">{children}</div>
+);
diff --git a/frontend/src/metabase/components/TooltipPopover.jsx b/frontend/src/metabase/components/TooltipPopover.jsx
index 5fceead08de609725c46ed812fc92d966d24c337..af6dd1ae3d5416bca53a6bb8d7b0115ede0749ba 100644
--- a/frontend/src/metabase/components/TooltipPopover.jsx
+++ b/frontend/src/metabase/components/TooltipPopover.jsx
@@ -9,41 +9,33 @@ import Popover from "./Popover.jsx";
 // we use the number of words as an approximation
 const CONDITIONAL_WORD_COUNT = 10;
 
-const wordCount = (string) => string.split(' ').length;
+const wordCount = string => string.split(" ").length;
 
 const TooltipPopover = ({ children, maxWidth, ...props }) => {
-
-    let popoverContent;
-
-    if (typeof children === "string")  {
-        const needsSpace = wordCount(children) > CONDITIONAL_WORD_COUNT;
-        popoverContent = (
-            <div
-                className={cx(
-                    { 'py1 px2': !needsSpace },
-                    { 'py2 px3': needsSpace }
-                )}
-                style={{
-                    maxWidth: maxWidth || "12em",
-                    lineHeight: needsSpace ? 1.54 : 1
-                }}
-            >
-                {children}
-            </div>
-        );
-    } else {
-        popoverContent = children;
-    }
-
-    return (
-        <Popover
-            className="PopoverBody--tooltip"
-            targetOffsetY={10}
-            {...props}
-        >
-            {popoverContent}
-        </Popover>
-    )
-}
+  let popoverContent;
+
+  if (typeof children === "string") {
+    const needsSpace = wordCount(children) > CONDITIONAL_WORD_COUNT;
+    popoverContent = (
+      <div
+        className={cx({ "py1 px2": !needsSpace }, { "py2 px3": needsSpace })}
+        style={{
+          maxWidth: maxWidth || "12em",
+          lineHeight: needsSpace ? 1.54 : 1,
+        }}
+      >
+        {children}
+      </div>
+    );
+  } else {
+    popoverContent = children;
+  }
+
+  return (
+    <Popover className="PopoverBody--tooltip" targetOffsetY={10} {...props}>
+      {popoverContent}
+    </Popover>
+  );
+};
 
 export default pure(TooltipPopover);
diff --git a/frontend/src/metabase/components/Triggerable.jsx b/frontend/src/metabase/components/Triggerable.jsx
index 4da08c27d6193962ef62e09c2a9f8eb5fad3fef9..d4e0a9a726559221d066af5fd870509b54021e66 100644
--- a/frontend/src/metabase/components/Triggerable.jsx
+++ b/frontend/src/metabase/components/Triggerable.jsx
@@ -9,129 +9,152 @@ import cx from "classnames";
 
 // higher order component that takes a component which takes props "isOpen" and optionally "onClose"
 // and returns a component that renders a <a> element "trigger", and tracks whether that component is open or not
-export default ComposedComponent => class extends Component {
-    static displayName = "Triggerable["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+export default ComposedComponent =>
+  class extends Component {
+    static displayName = "Triggerable[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
     constructor(props, context) {
-        super(props, context);
+      super(props, context);
 
-        this.state = {
-            isOpen: props.isInitiallyOpen || false
-        }
+      this.state = {
+        isOpen: props.isInitiallyOpen || false,
+      };
 
-        this._startCheckObscured = this._startCheckObscured.bind(this);
-        this._stopCheckObscured = this._stopCheckObscured.bind(this);
-        this.onClose = this.onClose.bind(this);
+      this._startCheckObscured = this._startCheckObscured.bind(this);
+      this._stopCheckObscured = this._stopCheckObscured.bind(this);
+      this.onClose = this.onClose.bind(this);
     }
 
     static defaultProps = {
-        closeOnObscuredTrigger: false
+      closeOnObscuredTrigger: false,
     };
 
     open() {
-        this.toggle(true);
+      this.toggle(true);
     }
 
     close() {
-        this.toggle(false);
+      this.toggle(false);
     }
 
     toggle(isOpen = !this.state.isOpen) {
-        this.setState({ isOpen });
+      this.setState({ isOpen });
     }
 
     onClose(e) {
-        // don't close if clicked the actual trigger, it will toggle
-        if (e && e.target && ReactDOM.findDOMNode(this.refs.trigger).contains(e.target)) {
-            return;
-        }
-
-        if (this.props.onClose) {
-            this.props.onClose(e)
-        }
-
-        this.close();
+      // don't close if clicked the actual trigger, it will toggle
+      if (
+        e &&
+        e.target &&
+        ReactDOM.findDOMNode(this.refs.trigger).contains(e.target)
+      ) {
+        return;
+      }
+
+      if (this.props.onClose) {
+        this.props.onClose(e);
+      }
+
+      this.close();
     }
 
     target() {
-        if (this.props.target) {
-            return this.props.target();
-        } else {
-            return this.refs.trigger;
-        }
+      if (this.props.target) {
+        return this.props.target();
+      } else {
+        return this.refs.trigger;
+      }
     }
 
     componentDidMount() {
-        this.componentDidUpdate();
+      this.componentDidUpdate();
     }
 
     componentDidUpdate() {
-        if (this.state.isOpen && this.props.closeOnObscuredTrigger) {
-            this._startCheckObscured();
-        } else {
-            this._stopCheckObscured();
-        }
+      if (this.state.isOpen && this.props.closeOnObscuredTrigger) {
+        this._startCheckObscured();
+      } else {
+        this._stopCheckObscured();
+      }
     }
 
     componentWillUnmount() {
-        this._stopCheckObscured();
+      this._stopCheckObscured();
     }
 
     _startCheckObscured() {
-        if (this._offscreenTimer == null) {
-            this._offscreenTimer = setInterval(() => {
-                let trigger = ReactDOM.findDOMNode(this.refs.trigger);
-                if (isObscured(trigger)) {
-                    this.close();
-                }
-            }, 250);
-        }
+      if (this._offscreenTimer == null) {
+        this._offscreenTimer = setInterval(() => {
+          let trigger = ReactDOM.findDOMNode(this.refs.trigger);
+          if (isObscured(trigger)) {
+            this.close();
+          }
+        }, 250);
+      }
     }
     _stopCheckObscured() {
-        if (this._offscreenTimer != null) {
-            clearInterval(this._offscreenTimer);
-            this._offscreenTimer = null;
-        }
+      if (this._offscreenTimer != null) {
+        clearInterval(this._offscreenTimer);
+        this._offscreenTimer = null;
+      }
     }
 
     render() {
-        const { triggerId, triggerClasses, triggerStyle, triggerClassesOpen } = this.props;
-        const { isOpen } = this.state;
-
-        let { triggerElement } = this.props;
-        if (triggerElement && triggerElement.type === Tooltip) {
-            // Disables tooltip when open:
-            triggerElement = React.cloneElement(triggerElement, { isEnabled: triggerElement.props.isEnabled && !isOpen });
-        }
-
-        // if we have a single child which isn't an HTML element and doesn't have an onClose prop go ahead and inject it directly
-        let { children } = this.props;
-        if (React.Children.count(children) === 1 && React.Children.only(children).props.onClose === undefined && typeof React.Children.only(children).type !== "string") {
-            children = React.cloneElement(children, { onClose: this.onClose });
-        }
-
-        return (
-            <a
-                id={triggerId}
-                ref="trigger"
-                onClick={(event) => {
-                    event.preventDefault()
-                    !this.props.disabled && this.toggle()
-                }}
-                className={cx(triggerClasses, isOpen && triggerClassesOpen, "no-decoration", {
-                    'cursor-default': this.props.disabled
-                })}
-                style={triggerStyle}
-            >
-                {triggerElement}
-                <ComposedComponent
-                    {...this.props}
-                    children={children}
-                    isOpen={isOpen}
-                    onClose={this.onClose}
-                    target={() => this.target()}
-                />
-            </a>
-        );
+      const {
+        triggerId,
+        triggerClasses,
+        triggerStyle,
+        triggerClassesOpen,
+      } = this.props;
+      const { isOpen } = this.state;
+
+      let { triggerElement } = this.props;
+      if (triggerElement && triggerElement.type === Tooltip) {
+        // Disables tooltip when open:
+        triggerElement = React.cloneElement(triggerElement, {
+          isEnabled: triggerElement.props.isEnabled && !isOpen,
+        });
+      }
+
+      // if we have a single child which isn't an HTML element and doesn't have an onClose prop go ahead and inject it directly
+      let { children } = this.props;
+      if (
+        React.Children.count(children) === 1 &&
+        React.Children.only(children).props.onClose === undefined &&
+        typeof React.Children.only(children).type !== "string"
+      ) {
+        children = React.cloneElement(children, { onClose: this.onClose });
+      }
+
+      return (
+        <a
+          id={triggerId}
+          ref="trigger"
+          onClick={event => {
+            event.preventDefault();
+            !this.props.disabled && this.toggle();
+          }}
+          className={cx(
+            triggerClasses,
+            isOpen && triggerClassesOpen,
+            "no-decoration",
+            {
+              "cursor-default": this.props.disabled,
+            },
+          )}
+          style={triggerStyle}
+        >
+          {triggerElement}
+          <ComposedComponent
+            {...this.props}
+            children={children}
+            isOpen={isOpen}
+            onClose={this.onClose}
+            target={() => this.target()}
+          />
+        </a>
+      );
     }
-};
+  };
diff --git a/frontend/src/metabase/components/Unauthorized.jsx b/frontend/src/metabase/components/Unauthorized.jsx
index 4da2fdcfa40ea0663342c5e119570e38dac1ce41..d0917fc0e8c177b82973ae283986aea30f59a101 100644
--- a/frontend/src/metabase/components/Unauthorized.jsx
+++ b/frontend/src/metabase/components/Unauthorized.jsx
@@ -1,14 +1,14 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 
 export default class Unauthorized extends Component {
-    render() {
-        return (
-            <div className="flex layout-centered flex-full flex-column text-grey-2">
-                <Icon name="key" size={100} />
-                <h1 className="mt4">{t`Sorry, you don’t have permission to see that.`}</h1>
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className="flex layout-centered flex-full flex-column text-grey-2">
+        <Icon name="key" size={100} />
+        <h1 className="mt4">{t`Sorry, you don’t have permission to see that.`}</h1>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/UserAvatar.jsx b/frontend/src/metabase/components/UserAvatar.jsx
index 0c002cd4d32931636033ea4ab727e69c0b27de9b..5809d1a972d938a6e42961d88e8cdd72cc2e480b 100644
--- a/frontend/src/metabase/components/UserAvatar.jsx
+++ b/frontend/src/metabase/components/UserAvatar.jsx
@@ -1,54 +1,59 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import cx from 'classnames';
+import cx from "classnames";
 
 export default class UserAvatar extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.styles = {
-            fontSize: '0.85rem',
-            borderWidth: '1px',
-            borderStyle: 'solid',
-            borderRadius: '99px',
-            width: '2rem',
-            height: '2rem',
-        }
-    }
-
-    static propTypes = {
-        background: PropTypes.string,
-        user: PropTypes.object.isRequired
-    };
-
-    static defaultProps = {
-        background: 'bg-brand'
+  constructor(props, context) {
+    super(props, context);
+    this.styles = {
+      fontSize: "0.85rem",
+      borderWidth: "1px",
+      borderStyle: "solid",
+      borderRadius: "99px",
+      width: "2rem",
+      height: "2rem",
     };
+  }
 
-    userInitials() {
-        const { first_name, last_name } = this.props.user;
+  static propTypes = {
+    background: PropTypes.string,
+    user: PropTypes.object.isRequired,
+  };
 
-        function initial(name) {
-            return typeof name !== 'undefined' && name.length ? name.substring(0, 1).toUpperCase() : '';
-        }
+  static defaultProps = {
+    background: "bg-brand",
+  };
 
-        const initials = initial(first_name) + initial(last_name);
+  userInitials() {
+    const { first_name, last_name } = this.props.user;
 
-        return initials.length ? initials : '?';
+    function initial(name) {
+      return typeof name !== "undefined" && name.length
+        ? name.substring(0, 1).toUpperCase()
+        : "";
     }
 
-    render() {
-        const { background } = this.props;
-        const classes = {
-            'flex': true,
-            'align-center': true,
-            'justify-center': true
-        }
-        classes[background] = true;
-
-        return (
-            <div className={cx(classes)} style={Object.assign(this.styles, this.props.style)}>
-                {this.userInitials()}
-            </div>
-        )
-    }
+    const initials = initial(first_name) + initial(last_name);
+
+    return initials.length ? initials : "?";
+  }
+
+  render() {
+    const { background } = this.props;
+    const classes = {
+      flex: true,
+      "align-center": true,
+      "justify-center": true,
+    };
+    classes[background] = true;
+
+    return (
+      <div
+        className={cx(classes)}
+        style={Object.assign(this.styles, this.props.style)}
+      >
+        {this.userInitials()}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/Value.jsx b/frontend/src/metabase/components/Value.jsx
index 10721fb453df9f0ad531734a93c10af64c0e4bcd..f95822caeee9cfe2e7e6fdbd316ae634ec7433b9 100644
--- a/frontend/src/metabase/components/Value.jsx
+++ b/frontend/src/metabase/components/Value.jsx
@@ -2,22 +2,27 @@
 
 import React from "react";
 
+import RemappedValue from "metabase/containers/RemappedValue";
+
 import { formatValue } from "metabase/lib/formatting";
 
 import type { Value as ValueType } from "metabase/meta/types/Dataset";
-import type { FormattingOptions } from "metabase/lib/formatting"
+import type { FormattingOptions } from "metabase/lib/formatting";
 
 type Props = {
-    value: ValueType
+  value: ValueType,
 } & FormattingOptions;
 
 const Value = ({ value, ...options }: Props) => {
-    let formatted = formatValue(value, { ...options, jsx: true });
-    if (React.isValidElement(formatted)) {
-        return formatted;
-    } else {
-        return <span>{formatted}</span>
-    }
-}
+  if (options.remap) {
+    return <RemappedValue value={value} {...options} />;
+  }
+  let formatted = formatValue(value, { ...options, jsx: true });
+  if (React.isValidElement(formatted)) {
+    return formatted;
+  } else {
+    return <span>{formatted}</span>;
+  }
+};
 
 export default Value;
diff --git a/frontend/src/metabase/components/__mocks__/Popover.jsx b/frontend/src/metabase/components/__mocks__/Popover.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..546384b2319b3ca420613f6ea7e988d1fd17eeca
--- /dev/null
+++ b/frontend/src/metabase/components/__mocks__/Popover.jsx
@@ -0,0 +1,36 @@
+import React from "react";
+
+import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
+import cx from "classnames";
+
+/**
+ * A modified version of TestPopover for Jest/Enzyme tests.
+ * Simply renders the popover body inline instead of mutating DOM root.
+ */
+const TestPopover = props =>
+  props.isOpen === undefined || props.isOpen ? (
+    <OnClickOutsideWrapper
+      handleDismissal={(...args) => {
+        props.onClose && props.onClose(...args);
+      }}
+      dismissOnEscape={props.dismissOnEscape}
+      dismissOnClickOutside={props.dismissOnClickOutside}
+    >
+      <div
+        id={props.id}
+        className={cx("TestPopover TestPopoverBody", props.className)}
+        style={props.style}
+        // because popover is normally directly attached to body element, other elements should not need
+        // to care about clicks that happen inside the popover
+        onClick={e => {
+          e.stopPropagation();
+        }}
+      >
+        {typeof props.children === "function"
+          ? props.children({ maxHeight: 500 })
+          : props.children}
+      </div>
+    </OnClickOutsideWrapper>
+  ) : null;
+
+export default TestPopover;
diff --git a/frontend/src/metabase/components/form/FormField.jsx b/frontend/src/metabase/components/form/FormField.jsx
index 4e4066d20969dddfbf9b7808754712fd0b4c15cf..18fcd85f04ecf91d12cf9ce241b53bf75c93effe 100644
--- a/frontend/src/metabase/components/form/FormField.jsx
+++ b/frontend/src/metabase/components/form/FormField.jsx
@@ -4,21 +4,21 @@ import PropTypes from "prop-types";
 import cx from "classnames";
 
 export default class FormField extends Component {
-    static propTypes = {
-        fieldName: PropTypes.string.isRequired
-    };
+  static propTypes = {
+    fieldName: PropTypes.string.isRequired,
+  };
 
-    render() {
-        let { children, className, fieldName, formError, error } = this.props;
+  render() {
+    let { children, className, fieldName, formError, error } = this.props;
 
-        const classes = cx('Form-field', className, {
-            'Form--fieldError': (error === true || (formError && formError.data.errors && fieldName in formError.data.errors))
-        });
+    const classes = cx("Form-field", className, {
+      "Form--fieldError":
+        error === true ||
+        (formError &&
+          formError.data.errors &&
+          fieldName in formError.data.errors),
+    });
 
-        return (
-            <div className={classes}>
-            	{children}
-            </div>
-        );
-    }
+    return <div className={classes}>{children}</div>;
+  }
 }
diff --git a/frontend/src/metabase/components/form/FormLabel.jsx b/frontend/src/metabase/components/form/FormLabel.jsx
index 5a14bed3229ad96489b1a4040543389a9e1a9149..709a38fe4a054af5e3071399793786ae4cd45cd0 100644
--- a/frontend/src/metabase/components/form/FormLabel.jsx
+++ b/frontend/src/metabase/components/form/FormLabel.jsx
@@ -2,27 +2,31 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 
-
 export default class FormLabel extends Component {
-    static propTypes = {
-        fieldName: PropTypes.string.isRequired,
-        formError: PropTypes.object,
-        message: PropTypes.string,
-    };
-
-    static defaultProps = {
-        offset: true
-    };
+  static propTypes = {
+    fieldName: PropTypes.string.isRequired,
+    formError: PropTypes.object,
+    message: PropTypes.string,
+  };
 
-    render() {
-        let { fieldName, formError, message, offset, title } = this.props;
+  static defaultProps = {
+    offset: true,
+  };
 
-        if (!message) {
-            message = (formError && formError.data.errors && fieldName in formError.data.errors) ? formError.data.errors[fieldName] : undefined;
-        }
+  render() {
+    let { fieldName, formError, message, offset, title } = this.props;
 
-        return (
-            <label className={cx("Form-label", {"Form-offset": offset})}>{title}: { message !== undefined ? <span>{message}</span> : null }</label>
-        );
+    if (!message) {
+      message =
+        formError && formError.data.errors && fieldName in formError.data.errors
+          ? formError.data.errors[fieldName]
+          : undefined;
     }
+
+    return (
+      <label className={cx("Form-label", { "Form-offset": offset })}>
+        {title}: {message !== undefined ? <span>{message}</span> : null}
+      </label>
+    );
+  }
 }
diff --git a/frontend/src/metabase/components/form/FormMessage.jsx b/frontend/src/metabase/components/form/FormMessage.jsx
index 6cac785ee55aef711933666cb40a7d42daa1a38d..d524e82b1009efec43700d3bf01db35ae3e6d499 100644
--- a/frontend/src/metabase/components/form/FormMessage.jsx
+++ b/frontend/src/metabase/components/form/FormMessage.jsx
@@ -1,35 +1,33 @@
 import React, { Component } from "react";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 export const SERVER_ERROR_MESSAGE = t`Server error encountered`;
 export const UNKNOWN_ERROR_MESSAGE = t`Unknown error encountered`;
 
 export default class FormMessage extends Component {
-    render() {
-        let { className, formError, formSuccess, message } = this.props;
+  render() {
+    let { className, formError, formSuccess, message } = this.props;
 
-        if (!message) {
-            if (formError) {
-                if (formError.data && formError.data.message) {
-                    message = formError.data.message;
-                } else if (formError.status >= 400) {
-                    message = SERVER_ERROR_MESSAGE;
-                } else {
-                    message = UNKNOWN_ERROR_MESSAGE;
-                }
-            } else if (formSuccess && formSuccess.data.message) {
-                message = formSuccess.data.message;
-            }
+    if (!message) {
+      if (formError) {
+        if (formError.data && formError.data.message) {
+          message = formError.data.message;
+        } else if (formError.status >= 400) {
+          message = SERVER_ERROR_MESSAGE;
+        } else {
+          message = UNKNOWN_ERROR_MESSAGE;
         }
+      } else if (formSuccess && formSuccess.data.message) {
+        message = formSuccess.data.message;
+      }
+    }
 
-        const classes = cx('Form-message', 'px2', className, {
-            'Form-message--visible': !!message,
-            'text-success': formSuccess != undefined,
-            'text-error': formError != undefined
-        });
+    const classes = cx("Form-message", "px2", className, {
+      "Form-message--visible": !!message,
+      "text-success": formSuccess != undefined,
+      "text-error": formError != undefined,
+    });
 
-        return (
-            <span className={classes}>{message}</span>
-        );
-    }
+    return <span className={classes}>{message}</span>;
+  }
 }
diff --git a/frontend/src/metabase/components/icons/ClockIcon.jsx b/frontend/src/metabase/components/icons/ClockIcon.jsx
index 1055736182e3ab5e908bec7d874d8f21ccc02abf..4da6fb820eb98c985fcb18bf4463f1efaf8ba36d 100644
--- a/frontend/src/metabase/components/icons/ClockIcon.jsx
+++ b/frontend/src/metabase/components/icons/ClockIcon.jsx
@@ -1,10 +1,42 @@
 import React from "react";
 
-const ClockIcon = ({ hour = 12, minute = 40, width = 20, height = 20, className }) =>
-    <svg width={width} height={height} className={className} viewBox="0 0 20 20" version="1.1" xmlns="http://www.w3.org/2000/svg">
-        <circle cx="10" cy="10" r="10" fill="currentColor"/>
-        <line x1="10" y1="10" x2="10" y2="5" stroke="white" strokeWidth={2} strokeLinecap="round" transform={`rotate(${hour % 12 / 12 * 360} 10 10)`} />
-        <line x1="10" y1="10" x2="10" y2="6" stroke="white" strokeWidth={2} strokeLinecap="round" transform={`rotate(${minute % 60 / 60 * 360} 10 10)`} />
-    </svg>
+const ClockIcon = ({
+  hour = 12,
+  minute = 40,
+  width = 20,
+  height = 20,
+  className,
+}) => (
+  <svg
+    width={width}
+    height={height}
+    className={className}
+    viewBox="0 0 20 20"
+    version="1.1"
+    xmlns="http://www.w3.org/2000/svg"
+  >
+    <circle cx="10" cy="10" r="10" fill="currentColor" />
+    <line
+      x1="10"
+      y1="10"
+      x2="10"
+      y2="5"
+      stroke="white"
+      strokeWidth={2}
+      strokeLinecap="round"
+      transform={`rotate(${(hour % 12) / 12 * 360} 10 10)`}
+    />
+    <line
+      x1="10"
+      y1="10"
+      x2="10"
+      y2="6"
+      stroke="white"
+      strokeWidth={2}
+      strokeLinecap="round"
+      transform={`rotate(${(minute % 60) / 60 * 360} 10 10)`}
+    />
+  </svg>
+);
 
 export default ClockIcon;
diff --git a/frontend/src/metabase/components/icons/CountdownIcon.jsx b/frontend/src/metabase/components/icons/CountdownIcon.jsx
index 918e2aa8f3422e41d95f5921b808de89dc06c957..8f1e62bde0050b9bac6f4e0850cc8173987079d1 100644
--- a/frontend/src/metabase/components/icons/CountdownIcon.jsx
+++ b/frontend/src/metabase/components/icons/CountdownIcon.jsx
@@ -1,8 +1,32 @@
 import React from "react";
 
-const CountdownIcon = ({ percent = 0.75, width = 20, height = 20, className }) =>
-    <svg width={width} height={height} className={className} viewBox="0 0 32 32" style={{ transform: "rotate(-" + (percent * 360 + 90) + "deg)", borderRadius: "50%" }}>
-        <circle r="16" cx="16" cy="16" fill="currentColor" stroke="currentColor" fillOpacity="0.5" strokeWidth="32" strokeDasharray={(percent * 100) + " 100"} />
-    </svg>
+const CountdownIcon = ({
+  percent = 0.75,
+  width = 20,
+  height = 20,
+  className,
+}) => (
+  <svg
+    width={width}
+    height={height}
+    className={className}
+    viewBox="0 0 32 32"
+    style={{
+      transform: "rotate(-" + (percent * 360 + 90) + "deg)",
+      borderRadius: "50%",
+    }}
+  >
+    <circle
+      r="16"
+      cx="16"
+      cy="16"
+      fill="currentColor"
+      stroke="currentColor"
+      fillOpacity="0.5"
+      strokeWidth="32"
+      strokeDasharray={percent * 100 + " 100"}
+    />
+  </svg>
+);
 
 export default CountdownIcon;
diff --git a/frontend/src/metabase/components/icons/FullscreenIcon.jsx b/frontend/src/metabase/components/icons/FullscreenIcon.jsx
index b7aab0e9fcd48c4525c81d2fcaae0a3f69974946..38e8eb9005ce11229c1774b2df5f8437a2844a4b 100644
--- a/frontend/src/metabase/components/icons/FullscreenIcon.jsx
+++ b/frontend/src/metabase/components/icons/FullscreenIcon.jsx
@@ -2,7 +2,8 @@ import React from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const FullscreenIcon = ({ isFullscreen, ...props }) =>
-    <Icon name={isFullscreen ? "contract" : "expand"} {...props} />
+const FullscreenIcon = ({ isFullscreen, ...props }) => (
+  <Icon name={isFullscreen ? "contract" : "expand"} {...props} />
+);
 
 export default FullscreenIcon;
diff --git a/frontend/src/metabase/components/icons/NightModeIcon.jsx b/frontend/src/metabase/components/icons/NightModeIcon.jsx
index dcca140f9e5c04d467b8889c9a74ab35cbf60112..b1ffbbb840d25de4ae5e1affefbffe644fff2ed8 100644
--- a/frontend/src/metabase/components/icons/NightModeIcon.jsx
+++ b/frontend/src/metabase/components/icons/NightModeIcon.jsx
@@ -5,12 +5,12 @@ import React from "react";
 import Icon from "metabase/components/Icon.jsx";
 
 type Props = {
-    // ...IconProps,
-    isNightMode: boolean
+  // ...IconProps,
+  isNightMode: boolean,
 };
 
 const NightModeIcon = ({ isNightMode, ...props }: Props) => (
-    <Icon name={isNightMode ? "sun" : "moon"} {...props} />
+  <Icon name={isNightMode ? "sun" : "moon"} {...props} />
 );
 
 export default NightModeIcon;
diff --git a/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx b/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx
index b193b9e445f1f24c267c37d987ae08ea5a757131..b76a90b0fe67ee2214341c159d4d810da7315eee 100644
--- a/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx
+++ b/frontend/src/metabase/containers/AddToDashSelectDashModal.jsx
@@ -1,94 +1,104 @@
 /* @flow  */
 
 import React, { Component } from "react";
-import {connect} from "react-redux";
+import { connect } from "react-redux";
 
-import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx';
-import Icon from 'metabase/components/Icon.jsx';
+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 SortableItemList from "metabase/components/SortableItemList.jsx";
 import * as Urls from "metabase/lib/urls";
 
-import * as dashboardsActions from 'metabase/dashboards/dashboards';
-import { getDashboardListing } from 'metabase/dashboards/selectors';
-import { t } from 'c-3po';
-import type { Dashboard } from 'metabase/meta/types/Dashboard'
-import type { Card } from 'metabase/meta/types/Card'
-const mapStateToProps = (state) => ({
-    dashboards: getDashboardListing(state)
+import * as dashboardsActions from "metabase/dashboards/dashboards";
+import { getDashboardListing } from "metabase/dashboards/selectors";
+import { t } from "c-3po";
+import type { Dashboard } from "metabase/meta/types/Dashboard";
+import type { Card } from "metabase/meta/types/Card";
+const mapStateToProps = state => ({
+  dashboards: getDashboardListing(state),
 });
 
 const mapDispatchToProps = {
-    fetchDashboards: dashboardsActions.fetchDashboards,
-    createDashboard: dashboardsActions.createDashboard
+  fetchDashboards: dashboardsActions.fetchDashboards,
+  createDashboard: dashboardsActions.createDashboard,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class AddToDashSelectDashModal extends Component {
-    state = {
-        shouldCreateDashboard: false
-    };
+  state = {
+    shouldCreateDashboard: false,
+  };
 
-    props: {
-        card: Card,
-        onClose: () => void,
-        onChangeLocation: (string) => void,
-        // via connect:
-        dashboards: Dashboard[],
-        fetchDashboards: () => any,
-        createDashboard: (Dashboard) => any
-    };
+  props: {
+    card: Card,
+    onClose: () => void,
+    onChangeLocation: string => void,
+    // via connect:
+    dashboards: Dashboard[],
+    fetchDashboards: () => any,
+    createDashboard: Dashboard => any,
+  };
 
-    componentWillMount() {
-        this.props.fetchDashboards();
-    }
+  componentWillMount() {
+    this.props.fetchDashboards();
+  }
 
-    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, {addCardWithId: this.props.card.id}));
-    }
+  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, { addCardWithId: this.props.card.id }),
+    );
+  };
 
-    createDashboard = async(newDashboard: Dashboard) => {
-        try {
-            const action = await this.props.createDashboard(newDashboard, {});
-            this.addToDashboard(action.payload);
-        } catch (e) {
-            console.log("createDashboard failed", e);
-        }
+  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.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 {
-            return (
-                <ModalContent
-                    id="AddToDashSelectDashModal"
-                    title={t`Add Question to Dashboard`}
-                    onClose={this.props.onClose}
-                >
-                <div className="flex flex-column">
-                    <div
-                        className="link flex-align-right px4 cursor-pointer"
-                        onClick={() => this.setState({ shouldCreateDashboard: true })}
-                    >
-                        <div
-                            className="mt1 flex align-center absolute"
-                            style={ { right: 40 } }
-                        >
-                            <Icon name="add" size={16} />
-                            <h3 className="ml1">{t`Add to new dashboard`}</h3>
-                        </div>
-                    </div>
-                    <SortableItemList
-                        items={this.props.dashboards}
-                        onClickItemFn={this.addToDashboard}
-                    />
-                </div>
-                </ModalContent>
-            );
-        }
+  render() {
+    if (this.props.dashboards === null) {
+      return <div />;
+    } else if (
+      this.props.dashboards.length === 0 ||
+      this.state.shouldCreateDashboard === true
+    ) {
+      return (
+        <CreateDashboardModal
+          createDashboardFn={this.createDashboard}
+          onClose={this.props.onClose}
+        />
+      );
+    } else {
+      return (
+        <ModalContent
+          id="AddToDashSelectDashModal"
+          title={t`Add Question to Dashboard`}
+          onClose={this.props.onClose}
+        >
+          <div className="flex flex-column">
+            <div
+              className="link flex-align-right px4 cursor-pointer"
+              onClick={() => this.setState({ shouldCreateDashboard: true })}
+            >
+              <div
+                className="mt1 flex align-center absolute"
+                style={{ right: 40 }}
+              >
+                <Icon name="add" size={16} />
+                <h3 className="ml1">{t`Add to new dashboard`}</h3>
+              </div>
+            </div>
+            <SortableItemList
+              items={this.props.dashboards}
+              onClickItemFn={this.addToDashboard}
+            />
+          </div>
+        </ModalContent>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/containers/EntitySearch.jsx b/frontend/src/metabase/containers/EntitySearch.jsx
index 527b17594ce40dc8b432f5690808fd4c7968aa0a..59e920df480e6bf10b9d7ae1033aaad217c1557d 100644
--- a/frontend/src/metabase/containers/EntitySearch.jsx
+++ b/frontend/src/metabase/containers/EntitySearch.jsx
@@ -1,9 +1,9 @@
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
+import React, { Component } from "react";
+import { connect } from "react-redux";
 import { push, replace } from "react-router-redux";
 import _ from "underscore";
 import cx from "classnames";
-import { t } from 'c-3po'
+import { t } from "c-3po";
 
 import SearchHeader from "metabase/components/SearchHeader";
 import DirectionalButton from "metabase/components/DirectionalButton";
@@ -16,505 +16,579 @@ import { KEYCODE_DOWN, KEYCODE_ENTER, KEYCODE_UP } from "metabase/lib/keyboard";
 import { LocationDescriptor } from "metabase/meta/types/index";
 import { parseHashOptions, updateQueryString } from "metabase/lib/browser";
 
-const PAGE_SIZE = 10
+const PAGE_SIZE = 10;
 
 const SEARCH_GROUPINGS = [
-    {
-        id: "name",
-        name: t`Name`,
-        icon: null,
-        // Name grouping is a no-op grouping so always put all results to same group with identifier `0`
-        groupBy: () => 0,
-        // Setting name to null hides the group header in SearchResultsGroup component
-        getGroupName: () => null
-    },
-    {
-        id: "table",
-        name: t`Table`,
-        icon: "table2",
-        groupBy: (entity) => entity.table.id,
-        getGroupName: (entity) => entity.table.display_name
-    },
-    {
-        id: "database",
-        name: t`Database`,
-        icon: "database",
-        groupBy: (entity) => entity.table.db.id,
-        getGroupName: (entity) => entity.table.db.name
-    },
-    {
-        id: "creator",
-        name: t`Creator`,
-        icon: "mine",
-        groupBy: (entity) => entity.creator.id,
-        getGroupName: (entity) => entity.creator.common_name
-    },
-]
-const DEFAULT_SEARCH_GROUPING = SEARCH_GROUPINGS[0]
+  {
+    id: "name",
+    name: t`Name`,
+    icon: null,
+    // Name grouping is a no-op grouping so always put all results to same group with identifier `0`
+    groupBy: () => 0,
+    // Setting name to null hides the group header in SearchResultsGroup component
+    getGroupName: () => null,
+  },
+  {
+    id: "table",
+    name: t`Table`,
+    icon: "table2",
+    groupBy: entity => entity.table.id,
+    getGroupName: entity => entity.table.display_name,
+  },
+  {
+    id: "database",
+    name: t`Database`,
+    icon: "database",
+    groupBy: entity => entity.table.db.id,
+    getGroupName: entity => entity.table.db.name,
+  },
+  {
+    id: "creator",
+    name: t`Creator`,
+    icon: "mine",
+    groupBy: entity => entity.creator.id,
+    getGroupName: entity => entity.creator.common_name,
+  },
+];
+const DEFAULT_SEARCH_GROUPING = SEARCH_GROUPINGS[0];
 
 type Props = {
-    title: string,
-    entities: any[], // Sorted list of entities like segments or metrics
-    getUrlForEntity: (any) => void,
-    backButtonUrl: ?string,
+  title: string,
+  entities: any[], // Sorted list of entities like segments or metrics
+  getUrlForEntity: any => void,
+  backButtonUrl: ?string,
 
-    onReplaceLocation: (LocationDescriptor) => void,
-    onChangeLocation: (LocationDescriptor) => void,
+  onReplaceLocation: LocationDescriptor => void,
+  onChangeLocation: LocationDescriptor => void,
 
-    location: LocationDescriptor // Injected by withRouter HOC
-}
+  location: LocationDescriptor, // Injected by withRouter HOC
+};
 
-@connect(null, { onReplaceLocation: replace, onChangeLocation: push  })
+@connect(null, { onReplaceLocation: replace, onChangeLocation: push })
 @withRouter
 export default class EntitySearch extends Component {
-    searchHeaderInput: ?HTMLButtonElement
-    props: Props
-
-    constructor(props) {
-        super(props);
-        this.state = {
-            filteredEntities: props.entities,
-            currentGrouping: DEFAULT_SEARCH_GROUPING,
-            searchText: ""
-        };
-    }
-
-    componentDidMount = () => {
-        this.parseQueryString()
-    }
-
-    componentWillReceiveProps = (nextProps) => {
-        this.applyFiltersForEntities(nextProps.entities)
-    }
-
-    parseQueryString = () => {
-        const options = parseHashOptions(this.props.location.search.substring(1))
-        if (Object.keys(options).length > 0) {
-            if (options.search) {
-                this.setSearchText(String(options.search))
-            }
-            if (options.grouping) {
-                const grouping = SEARCH_GROUPINGS.find((grouping) => grouping.id === options.grouping)
-                if (grouping) {
-                    this.setGrouping(grouping)
-                }
-            }
+  searchHeaderInput: ?HTMLButtonElement;
+  props: Props;
+
+  constructor(props) {
+    super(props);
+    this.state = {
+      filteredEntities: props.entities,
+      currentGrouping: DEFAULT_SEARCH_GROUPING,
+      searchText: "",
+    };
+  }
+
+  componentDidMount = () => {
+    this.parseQueryString();
+  };
+
+  componentWillReceiveProps = nextProps => {
+    this.applyFiltersForEntities(nextProps.entities);
+  };
+
+  parseQueryString = () => {
+    const options = parseHashOptions(this.props.location.search.substring(1));
+    if (Object.keys(options).length > 0) {
+      if (options.search) {
+        this.setSearchText(String(options.search));
+      }
+      if (options.grouping) {
+        const grouping = SEARCH_GROUPINGS.find(
+          grouping => grouping.id === options.grouping,
+        );
+        if (grouping) {
+          this.setGrouping(grouping);
         }
-    }
-
-    updateUrl = (queryOptionsUpdater) => {
-        const { onReplaceLocation, location } = this.props;
-        onReplaceLocation(updateQueryString(location, queryOptionsUpdater))
-
-    }
-
-    setSearchText = (searchText) => {
-        this.setState({ searchText }, this.applyFiltersAfterFilterChange)
-        this.updateUrl((currentOptions) => searchText !== ""
-            ? ({ ...currentOptions, search: searchText})
-            : _.omit(currentOptions, 'search')
-        )
-    }
-
-    resetSearchText = () => {
-        this.setSearchText("")
-        this.searchHeaderInput.focus()
-    }
-
-    applyFiltersAfterFilterChange = () => this.applyFiltersForEntities(this.props.entities)
-
-    applyFiltersForEntities = (entities) => {
-        const { searchText } = this.state;
-
-        if (searchText !== "") {
-            const filteredEntities = entities.filter(({ name, description }) =>
-                caseInsensitiveSearch(name, searchText)
-            )
-
-            this.setState({ filteredEntities })
-        }
-        else {
-            this.setState({ filteredEntities: entities })
-        }
-    }
-
-    setGrouping = (grouping) => {
-        this.setState({ currentGrouping: grouping })
-        this.updateUrl((currentOptions) => grouping !== DEFAULT_SEARCH_GROUPING
-            ? { ...currentOptions, grouping: grouping.id }
-            : _.omit(currentOptions, 'grouping')
-        )
-        this.searchHeaderInput.focus()
-    }
-
-    // Returns an array of groups based on current grouping. The groups are sorted by their name.
-    // Entities inside each group aren't separately sorted as EntitySearch expects that the `entities`
-    // is already in the desired order.
-    getGroups = () => {
-        const { currentGrouping, filteredEntities } = this.state;
-
-        return _.chain(filteredEntities)
-            .groupBy(currentGrouping.groupBy)
-            .pairs()
-            .map(([groupId, entitiesInGroup]) => ({
-                groupName: currentGrouping.getGroupName(entitiesInGroup[0]),
-                entitiesInGroup
-            }))
-            .sortBy(({ groupName }) => groupName !== null && groupName.toLowerCase())
-            .value()
-    }
-
-    render() {
-        const { title, backButtonUrl, getUrlForEntity, onChangeLocation } = this.props;
-        const { searchText, currentGrouping, filteredEntities } = this.state;
-
-        const hasUngroupedResults = currentGrouping === DEFAULT_SEARCH_GROUPING && filteredEntities.length > 0
-
-        return (
-            <div className="bg-slate-extra-light full Entity-search">
-                <div className="wrapper wrapper--small pt4 pb4">
-                    <div className="flex mb4 align-center" style={{ height: "50px" }}>
-                        <div className="Entity-search-back-button mr2" onClick={ () => backButtonUrl ? onChangeLocation(backButtonUrl) : window.history.back() }>
-                            <DirectionalButton direction="back" />
-                        </div>
-                        <div className="text-centered flex-full">
-                            <h2>{title}</h2>
-                        </div>
-                    </div>
-                    <div>
-                        <SearchGroupingOptions
-                            currentGrouping={currentGrouping}
-                            setGrouping={this.setGrouping}
-                        />
-                        <div
-                            className={cx("bg-white bordered", { "rounded": !hasUngroupedResults }, { "rounded-top": hasUngroupedResults })}
-                            style={{ padding: "5px 15px" }}
-                        >
-                            <SearchHeader
-                                searchText={searchText}
-                                setSearchText={this.setSearchText}
-                                autoFocus
-                                inputRef={el => this.searchHeaderInput = el}
-                                resetSearchText={this.resetSearchText}
-                            />
-                        </div>
-                        { filteredEntities.length > 0 &&
-                            <GroupedSearchResultsList
-                                groupingIcon={currentGrouping.icon}
-                                groups={this.getGroups()}
-                                getUrlForEntity={getUrlForEntity}
-                            />
-                        }
-                        { filteredEntities.length === 0 &&
-                            <div className="mt4">
-                                <EmptyState
-                                    message={
-                                        <div className="mt4">
-                                            <h3 className="text-grey-5">{t`No results found`}</h3>
-                                            <p className="text-grey-4">{t`Try adjusting your filter to find what you’re looking for.`}</p>
-                                        </div>
-                                    }
-                                    image="/app/img/empty_question"
-                                    imageHeight="213px"
-                                    imageClassName="mln2"
-                                    smallDescription
-                                />
-                            </div>
-                        }
-                    </div>
-                </div>
+      }
+    }
+  };
+
+  updateUrl = queryOptionsUpdater => {
+    const { onReplaceLocation, location } = this.props;
+    onReplaceLocation(updateQueryString(location, queryOptionsUpdater));
+  };
+
+  setSearchText = searchText => {
+    this.setState({ searchText }, this.applyFiltersAfterFilterChange);
+    this.updateUrl(
+      currentOptions =>
+        searchText !== ""
+          ? { ...currentOptions, search: searchText }
+          : _.omit(currentOptions, "search"),
+    );
+  };
+
+  resetSearchText = () => {
+    this.setSearchText("");
+    this.searchHeaderInput.focus();
+  };
+
+  applyFiltersAfterFilterChange = () =>
+    this.applyFiltersForEntities(this.props.entities);
+
+  applyFiltersForEntities = entities => {
+    const { searchText } = this.state;
+
+    if (searchText !== "") {
+      const filteredEntities = entities.filter(({ name, description }) =>
+        caseInsensitiveSearch(name, searchText),
+      );
+
+      this.setState({ filteredEntities });
+    } else {
+      this.setState({ filteredEntities: entities });
+    }
+  };
+
+  setGrouping = grouping => {
+    this.setState({ currentGrouping: grouping });
+    this.updateUrl(
+      currentOptions =>
+        grouping !== DEFAULT_SEARCH_GROUPING
+          ? { ...currentOptions, grouping: grouping.id }
+          : _.omit(currentOptions, "grouping"),
+    );
+    this.searchHeaderInput.focus();
+  };
+
+  // Returns an array of groups based on current grouping. The groups are sorted by their name.
+  // Entities inside each group aren't separately sorted as EntitySearch expects that the `entities`
+  // is already in the desired order.
+  getGroups = () => {
+    const { currentGrouping, filteredEntities } = this.state;
+
+    return _.chain(filteredEntities)
+      .groupBy(currentGrouping.groupBy)
+      .pairs()
+      .map(([groupId, entitiesInGroup]) => ({
+        groupName: currentGrouping.getGroupName(entitiesInGroup[0]),
+        entitiesInGroup,
+      }))
+      .sortBy(({ groupName }) => groupName !== null && groupName.toLowerCase())
+      .value();
+  };
+
+  render() {
+    const {
+      title,
+      backButtonUrl,
+      getUrlForEntity,
+      onChangeLocation,
+    } = this.props;
+    const { searchText, currentGrouping, filteredEntities } = this.state;
+
+    const hasUngroupedResults =
+      currentGrouping === DEFAULT_SEARCH_GROUPING &&
+      filteredEntities.length > 0;
+
+    return (
+      <div className="bg-slate-extra-light full Entity-search">
+        <div className="wrapper wrapper--small pt4 pb4">
+          <div className="flex mb4 align-center" style={{ height: "50px" }}>
+            <div
+              className="Entity-search-back-button mr2"
+              onClick={() =>
+                backButtonUrl
+                  ? onChangeLocation(backButtonUrl)
+                  : window.history.back()
+              }
+            >
+              <DirectionalButton direction="back" />
             </div>
-        )
-    }
-}
-
-export const SearchGroupingOptions = ({ currentGrouping, setGrouping }) =>
-    <div className="Entity-search-grouping-options">
-        <h3 className="mb3">{t`View by`}</h3>
-        <ul>
-            { SEARCH_GROUPINGS.map((groupingOption) =>
-                <SearchGroupingOption
-                    key={groupingOption.name}
-                    grouping={groupingOption}
-                    active={currentGrouping === groupingOption}
-                    setGrouping={setGrouping}
+            <div className="text-centered flex-full">
+              <h2>{title}</h2>
+            </div>
+          </div>
+          <div>
+            <SearchGroupingOptions
+              currentGrouping={currentGrouping}
+              setGrouping={this.setGrouping}
+            />
+            <div
+              className={cx(
+                "bg-white bordered",
+                { rounded: !hasUngroupedResults },
+                { "rounded-top": hasUngroupedResults },
+              )}
+              style={{ padding: "5px 15px" }}
+            >
+              <SearchHeader
+                searchText={searchText}
+                setSearchText={this.setSearchText}
+                autoFocus
+                inputRef={el => (this.searchHeaderInput = el)}
+                resetSearchText={this.resetSearchText}
+              />
+            </div>
+            {filteredEntities.length > 0 && (
+              <GroupedSearchResultsList
+                groupingIcon={currentGrouping.icon}
+                groups={this.getGroups()}
+                getUrlForEntity={getUrlForEntity}
+              />
+            )}
+            {filteredEntities.length === 0 && (
+              <div className="mt4">
+                <EmptyState
+                  message={
+                    <div className="mt4">
+                      <h3 className="text-grey-5">{t`No results found`}</h3>
+                      <p className="text-grey-4">{t`Try adjusting your filter to find what you’re looking for.`}</p>
+                    </div>
+                  }
+                  image="/app/img/empty_question"
+                  imageHeight="213px"
+                  imageClassName="mln2"
+                  smallDescription
                 />
+              </div>
             )}
-        </ul>
-    </div>
-
-export class SearchGroupingOption extends Component {
-    props: {
-        grouping: any,
-        active: boolean,
-        setGrouping: (any) => boolean
-    }
+          </div>
+        </div>
+      </div>
+    );
+  }
+}
 
-    onSetGrouping = () => {
-        this.props.setGrouping(this.props.grouping)
-    }
+export const SearchGroupingOptions = ({ currentGrouping, setGrouping }) => (
+  <div className="Entity-search-grouping-options">
+    <h3 className="mb3">{t`View by`}</h3>
+    <ul>
+      {SEARCH_GROUPINGS.map(groupingOption => (
+        <SearchGroupingOption
+          key={groupingOption.name}
+          grouping={groupingOption}
+          active={currentGrouping === groupingOption}
+          setGrouping={setGrouping}
+        />
+      ))}
+    </ul>
+  </div>
+);
 
-    render() {
-        const { grouping, active } = this.props;
-
-        return (
-            <li
-                className={cx(
-                    "my2 cursor-pointer text-uppercase text-small text-green-saturated-hover",
-                    {"text-grey-4": !active},
-                    {"text-green-saturated": active}
-                )}
-                onClick={this.onSetGrouping}
-            >
-                {grouping.name}
-            </li>
-        )
-    }
+export class SearchGroupingOption extends Component {
+  props: {
+    grouping: any,
+    active: boolean,
+    setGrouping: any => boolean,
+  };
+
+  onSetGrouping = () => {
+    this.props.setGrouping(this.props.grouping);
+  };
+
+  render() {
+    const { grouping, active } = this.props;
+
+    return (
+      <li
+        className={cx(
+          "my2 cursor-pointer text-uppercase text-small text-green-saturated-hover",
+          { "text-grey-4": !active },
+          { "text-green-saturated": active },
+        )}
+        onClick={this.onSetGrouping}
+      >
+        {grouping.name}
+      </li>
+    );
+  }
 }
 
 export class GroupedSearchResultsList extends Component {
-    props: {
-        groupingIcon: string,
-        groups: any,
-        getUrlForEntity: (any) => void,
-    }
-
-    state = {
-        highlightedItemIndex: 0,
-        // `currentPages` is used as a map-like structure for storing the current pagination page for each group.
-        // If a given group has no value in currentPages, then it is assumed to be in the first page (`0`).
-        currentPages: {}
-    }
-
-    componentDidMount() {
-        window.addEventListener("keydown", this.onKeyDown, true);
-    }
-
-    componentWillUnmount() {
-        window.removeEventListener("keydown", this.onKeyDown, true);
-    }
-
-    componentWillReceiveProps() {
-        this.setState({
-            highlightedItemIndex: 0,
-            currentPages: {}
-        })
-    }
-
-    /**
-     * Returns the count of currently visible entities for each result group.
-     */
-    getVisibleEntityCounts() {
-        const { groups } = this.props;
-        const { currentPages } = this.state
-        return groups.map((group, index) =>
-            Math.min(PAGE_SIZE, group.entitiesInGroup.length - (currentPages[index] || 0) * PAGE_SIZE)
-        )
-    }
-
-    onKeyDown = (e) => {
-        const { highlightedItemIndex } = this.state
-
-        if (e.keyCode === KEYCODE_UP) {
-            this.setState({ highlightedItemIndex: Math.max(0, highlightedItemIndex - 1) })
-            e.preventDefault();
-        } else if (e.keyCode === KEYCODE_DOWN) {
-            const visibleEntityCount = this.getVisibleEntityCounts().reduce((a, b) => a + b)
-            this.setState({ highlightedItemIndex: Math.min(highlightedItemIndex + 1, visibleEntityCount - 1) })
-            e.preventDefault();
-        }
-    }
-
-    /**
-     * Returns `{ groupIndex, itemIndex }` which describes that which item in which group is currently highlighted.
-     * Calculates it based on current visible entities (as pagination affects which entities are visible on given time)
-     * and the current highlight index that is modified with up and down arrow keys
-     */
-    getHighlightPosition() {
-        const { highlightedItemIndex } = this.state
-        const visibleEntityCounts = this.getVisibleEntityCounts()
-
-        let entitiesInPreviousGroups = 0
-        for (let groupIndex = 0; groupIndex < visibleEntityCounts.length; groupIndex++) {
-            const visibleEntityCount = visibleEntityCounts[groupIndex]
-            const indexInCurrentGroup = highlightedItemIndex - entitiesInPreviousGroups
-
-            if (indexInCurrentGroup <= visibleEntityCount - 1) {
-                return { groupIndex, itemIndex: indexInCurrentGroup }
-            }
-
-           entitiesInPreviousGroups += visibleEntityCount
-        }
-    }
-
-    /**
-     * Sets the current pagination page by finding the group that match the `entities` list of entities
-     */
-    setCurrentPage = (entities, page) => {
-        const { groups } = this.props;
-        const { currentPages } = this.state;
-        const groupIndex = groups.findIndex((group) => group.entitiesInGroup === entities)
-
-        this.setState({
-            highlightedItemIndex: 0,
-            currentPages: {
-                ...currentPages,
-                [groupIndex]: page
+  props: {
+    groupingIcon: string,
+    groups: any,
+    getUrlForEntity: any => void,
+  };
+
+  state = {
+    highlightedItemIndex: 0,
+    // `currentPages` is used as a map-like structure for storing the current pagination page for each group.
+    // If a given group has no value in currentPages, then it is assumed to be in the first page (`0`).
+    currentPages: {},
+  };
+
+  componentDidMount() {
+    window.addEventListener("keydown", this.onKeyDown, true);
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener("keydown", this.onKeyDown, true);
+  }
+
+  componentWillReceiveProps() {
+    this.setState({
+      highlightedItemIndex: 0,
+      currentPages: {},
+    });
+  }
+
+  /**
+   * Returns the count of currently visible entities for each result group.
+   */
+  getVisibleEntityCounts() {
+    const { groups } = this.props;
+    const { currentPages } = this.state;
+    return groups.map((group, index) =>
+      Math.min(
+        PAGE_SIZE,
+        group.entitiesInGroup.length - (currentPages[index] || 0) * PAGE_SIZE,
+      ),
+    );
+  }
+
+  onKeyDown = e => {
+    const { highlightedItemIndex } = this.state;
+
+    if (e.keyCode === KEYCODE_UP) {
+      this.setState({
+        highlightedItemIndex: Math.max(0, highlightedItemIndex - 1),
+      });
+      e.preventDefault();
+    } else if (e.keyCode === KEYCODE_DOWN) {
+      const visibleEntityCount = this.getVisibleEntityCounts().reduce(
+        (a, b) => a + b,
+      );
+      this.setState({
+        highlightedItemIndex: Math.min(
+          highlightedItemIndex + 1,
+          visibleEntityCount - 1,
+        ),
+      });
+      e.preventDefault();
+    }
+  };
+
+  /**
+   * Returns `{ groupIndex, itemIndex }` which describes that which item in which group is currently highlighted.
+   * Calculates it based on current visible entities (as pagination affects which entities are visible on given time)
+   * and the current highlight index that is modified with up and down arrow keys
+   */
+  getHighlightPosition() {
+    const { highlightedItemIndex } = this.state;
+    const visibleEntityCounts = this.getVisibleEntityCounts();
+
+    let entitiesInPreviousGroups = 0;
+    for (
+      let groupIndex = 0;
+      groupIndex < visibleEntityCounts.length;
+      groupIndex++
+    ) {
+      const visibleEntityCount = visibleEntityCounts[groupIndex];
+      const indexInCurrentGroup =
+        highlightedItemIndex - entitiesInPreviousGroups;
+
+      if (indexInCurrentGroup <= visibleEntityCount - 1) {
+        return { groupIndex, itemIndex: indexInCurrentGroup };
+      }
+
+      entitiesInPreviousGroups += visibleEntityCount;
+    }
+  }
+
+  /**
+   * Sets the current pagination page by finding the group that match the `entities` list of entities
+   */
+  setCurrentPage = (entities, page) => {
+    const { groups } = this.props;
+    const { currentPages } = this.state;
+    const groupIndex = groups.findIndex(
+      group => group.entitiesInGroup === entities,
+    );
+
+    this.setState({
+      highlightedItemIndex: 0,
+      currentPages: {
+        ...currentPages,
+        [groupIndex]: page,
+      },
+    });
+  };
+
+  render() {
+    const { groupingIcon, groups, getUrlForEntity } = this.props;
+    const { currentPages } = this.state;
+
+    const highlightPosition = this.getHighlightPosition(groups);
+
+    return (
+      <div className="full">
+        {groups.map(({ groupName, entitiesInGroup }, groupIndex) => (
+          <SearchResultsGroup
+            key={groupIndex}
+            groupName={groupName}
+            groupIcon={groupingIcon}
+            entities={entitiesInGroup}
+            getUrlForEntity={getUrlForEntity}
+            highlightItemAtIndex={
+              groupIndex === highlightPosition.groupIndex
+                ? highlightPosition.itemIndex
+                : undefined
             }
-        })
-    }
-
-    render() {
-        const { groupingIcon, groups, getUrlForEntity } = this.props;
-        const { currentPages } = this.state;
-
-        const highlightPosition = this.getHighlightPosition(groups)
-
-        return (
-            <div className="full">
-                {groups.map(({ groupName, entitiesInGroup }, groupIndex) =>
-                    <SearchResultsGroup
-                        key={groupIndex}
-                        groupName={groupName}
-                        groupIcon={groupingIcon}
-                        entities={entitiesInGroup}
-                        getUrlForEntity={getUrlForEntity}
-                        highlightItemAtIndex={groupIndex === highlightPosition.groupIndex ? highlightPosition.itemIndex : undefined}
-                        currentPage={currentPages[groupIndex] || 0}
-                        setCurrentPage={this.setCurrentPage}
-                    />
-                )}
-            </div>
-        )
-    }
+            currentPage={currentPages[groupIndex] || 0}
+            setCurrentPage={this.setCurrentPage}
+          />
+        ))}
+      </div>
+    );
+  }
 }
 
-export const SearchResultsGroup = ({ groupName, groupIcon, entities, getUrlForEntity, highlightItemAtIndex, currentPage, setCurrentPage }) =>
-    <div>
-        { groupName !== null &&
-            <div className="flex align-center bg-slate-almost-extra-light bordered mt3 px3 py2">
-                <Icon className="mr1" style={{color: "#BCC5CA"}} name={groupIcon}/>
-                <h4>{groupName}</h4>
-            </div>
-        }
-        <SearchResultsList
-            entities={entities}
-            getUrlForEntity={getUrlForEntity}
-            highlightItemAtIndex={highlightItemAtIndex}
-            currentPage={currentPage}
-            setCurrentPage={setCurrentPage}
-        />
-    </div>
-
+export const SearchResultsGroup = ({
+  groupName,
+  groupIcon,
+  entities,
+  getUrlForEntity,
+  highlightItemAtIndex,
+  currentPage,
+  setCurrentPage,
+}) => (
+  <div>
+    {groupName !== null && (
+      <div className="flex align-center bg-slate-almost-extra-light bordered mt3 px3 py2">
+        <Icon className="mr1" style={{ color: "#BCC5CA" }} name={groupIcon} />
+        <h4>{groupName}</h4>
+      </div>
+    )}
+    <SearchResultsList
+      entities={entities}
+      getUrlForEntity={getUrlForEntity}
+      highlightItemAtIndex={highlightItemAtIndex}
+      currentPage={currentPage}
+      setCurrentPage={setCurrentPage}
+    />
+  </div>
+);
 
 class SearchResultsList extends Component {
-    props: {
-        entities: any[],
-        getUrlForEntity: () => void,
-        highlightItemAtIndex?: number,
-        currentPage: number,
-        setCurrentPage: (entities, number) => void
-    }
-
-    state = {
-        page: 0
-    }
-
-    getPaginationSection = (start, end, entityCount) => {
-        const { entities, currentPage, setCurrentPage } = this.props
-
-        const currentEntitiesText = start === end ? `${start + 1}` : `${start + 1}-${end + 1}`
-        const isInBeginning = start === 0
-        const isInEnd = end + 1 >= entityCount
-
-        return (
-            <li className="py1 px3 flex justify-end align-center">
-                <span className="text-bold">{ currentEntitiesText }</span>&nbsp;{t`of`}&nbsp;<span
-                className="text-bold">{entityCount}</span>
-                <span
-                    className={cx(
-                        "mx1 flex align-center justify-center rounded",
-                        { "cursor-pointer bg-grey-2 text-white": !isInBeginning },
-                        { "bg-grey-0 text-grey-1": isInBeginning }
-                    )}
-                    style={{width: "22px", height: "22px"}}
-                    onClick={() => !isInBeginning && setCurrentPage(entities, currentPage - 1)}>
-                    <Icon name="chevronleft" size={14}/>
-                </span>
-                <span
-                    className={cx(
-                        "flex align-center justify-center rounded",
-                        { "cursor-pointer bg-grey-2 text-white": !isInEnd },
-                        { "bg-grey-0 text-grey-2": isInEnd }
-                    )}
-                    style={{width: "22px", height: "22px"}}
-                    onClick={() => !isInEnd && setCurrentPage(entities, currentPage + 1)}>
-                        <Icon name="chevronright" size={14}/>
-                </span>
-            </li>
-        )
-    }
-    render() {
-        const { currentPage, entities, getUrlForEntity, highlightItemAtIndex } = this.props
-
-        const showPagination = PAGE_SIZE < entities.length
-
-        let start = PAGE_SIZE * currentPage;
-        let end = Math.min(entities.length - 1, PAGE_SIZE * (currentPage + 1) - 1);
-        const entityCount = entities.length;
-
-        const entitiesInCurrentPage = entities.slice(start, end + 1)
-
-        return (
-            <ol className="Entity-search-results-list flex-full bg-white border-left border-right border-bottom rounded-bottom">
-                {entitiesInCurrentPage.map((entity, index) =>
-                    <SearchResultListItem key={index} entity={entity} getUrlForEntity={getUrlForEntity} highlight={ highlightItemAtIndex === index } />
-                )}
-                {showPagination && this.getPaginationSection(start, end, entityCount)}
-            </ol>
-        )
-    }
+  props: {
+    entities: any[],
+    getUrlForEntity: () => void,
+    highlightItemAtIndex?: number,
+    currentPage: number,
+    setCurrentPage: (entities, number) => void,
+  };
+
+  state = {
+    page: 0,
+  };
+
+  getPaginationSection = (start, end, entityCount) => {
+    const { entities, currentPage, setCurrentPage } = this.props;
+
+    const currentEntitiesText =
+      start === end ? `${start + 1}` : `${start + 1}-${end + 1}`;
+    const isInBeginning = start === 0;
+    const isInEnd = end + 1 >= entityCount;
+
+    return (
+      <li className="py1 px3 flex justify-end align-center">
+        <span className="text-bold">{currentEntitiesText}</span>&nbsp;{t`of`}&nbsp;<span className="text-bold">
+          {entityCount}
+        </span>
+        <span
+          className={cx(
+            "mx1 flex align-center justify-center rounded",
+            { "cursor-pointer bg-grey-2 text-white": !isInBeginning },
+            { "bg-grey-0 text-grey-1": isInBeginning },
+          )}
+          style={{ width: "22px", height: "22px" }}
+          onClick={() =>
+            !isInBeginning && setCurrentPage(entities, currentPage - 1)
+          }
+        >
+          <Icon name="chevronleft" size={14} />
+        </span>
+        <span
+          className={cx(
+            "flex align-center justify-center rounded",
+            { "cursor-pointer bg-grey-2 text-white": !isInEnd },
+            { "bg-grey-0 text-grey-2": isInEnd },
+          )}
+          style={{ width: "22px", height: "22px" }}
+          onClick={() => !isInEnd && setCurrentPage(entities, currentPage + 1)}
+        >
+          <Icon name="chevronright" size={14} />
+        </span>
+      </li>
+    );
+  };
+  render() {
+    const {
+      currentPage,
+      entities,
+      getUrlForEntity,
+      highlightItemAtIndex,
+    } = this.props;
+
+    const showPagination = PAGE_SIZE < entities.length;
+
+    let start = PAGE_SIZE * currentPage;
+    let end = Math.min(entities.length - 1, PAGE_SIZE * (currentPage + 1) - 1);
+    const entityCount = entities.length;
+
+    const entitiesInCurrentPage = entities.slice(start, end + 1);
+
+    return (
+      <ol className="Entity-search-results-list flex-full bg-white border-left border-right border-bottom rounded-bottom">
+        {entitiesInCurrentPage.map((entity, index) => (
+          <SearchResultListItem
+            key={index}
+            entity={entity}
+            getUrlForEntity={getUrlForEntity}
+            highlight={highlightItemAtIndex === index}
+          />
+        ))}
+        {showPagination && this.getPaginationSection(start, end, entityCount)}
+      </ol>
+    );
+  }
 }
 
 @connect(null, { onChangeLocation: push })
 export class SearchResultListItem extends Component {
-    props: {
-        entity: any,
-        getUrlForEntity: (any) => void,
-        highlight?: boolean,
-
-        onChangeLocation: (string) => void
-    }
-
-    componentDidMount() {
-        window.addEventListener("keydown", this.onKeyDown, true);
-    }
-    componentWillUnmount() {
-        window.removeEventListener("keydown", this.onKeyDown, true);
-    }
-    /**
-     * If the current search result entity is highlighted via arrow keys, then we want to
-     * let the press of Enter to navigate to that entity
-     */
-    onKeyDown = (e) => {
-        const { highlight, entity, getUrlForEntity, onChangeLocation } = this.props;
-        if (highlight && e.keyCode === KEYCODE_ENTER) {
-            onChangeLocation(getUrlForEntity(entity))
-        }
-    }
-
-    render() {
-        const { entity, highlight, getUrlForEntity } = this.props;
-
-        return (
-            <li>
-                <Link
-                className={cx("no-decoration flex py2 px3 cursor-pointer bg-slate-extra-light-hover border-bottom", { "bg-grey-0": highlight })}
-                to={getUrlForEntity(entity)}
-                >
-                    <h4 className="text-brand flex-full mr1"> { entity.name } </h4>
-                </Link>
-            </li>
-        )
-    }
+  props: {
+    entity: any,
+    getUrlForEntity: any => void,
+    highlight?: boolean,
+
+    onChangeLocation: string => void,
+  };
+
+  componentDidMount() {
+    window.addEventListener("keydown", this.onKeyDown, true);
+  }
+  componentWillUnmount() {
+    window.removeEventListener("keydown", this.onKeyDown, true);
+  }
+  /**
+   * If the current search result entity is highlighted via arrow keys, then we want to
+   * let the press of Enter to navigate to that entity
+   */
+  onKeyDown = e => {
+    const { highlight, entity, getUrlForEntity, onChangeLocation } = this.props;
+    if (highlight && e.keyCode === KEYCODE_ENTER) {
+      onChangeLocation(getUrlForEntity(entity));
+    }
+  };
+
+  render() {
+    const { entity, highlight, getUrlForEntity } = this.props;
+
+    return (
+      <li>
+        <Link
+          className={cx(
+            "no-decoration flex py2 px3 cursor-pointer bg-slate-extra-light-hover border-bottom",
+            { "bg-grey-0": highlight },
+          )}
+          to={getUrlForEntity(entity)}
+        >
+          <h4 className="text-brand flex-full mr1"> {entity.name} </h4>
+        </Link>
+      </li>
+    );
+  }
 }
diff --git a/frontend/src/metabase/containers/RemappedValue.jsx b/frontend/src/metabase/containers/RemappedValue.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..f7507d82d363f6614a6af410f1058a880670f273
--- /dev/null
+++ b/frontend/src/metabase/containers/RemappedValue.jsx
@@ -0,0 +1,76 @@
+import React from "react";
+
+import { formatValue } from "metabase/lib/formatting";
+
+import AutoLoadRemapped from "metabase/hoc/Remapped";
+
+const defaultRenderNormal = ({ value, column }) => (
+  <span className="text-bold">{value}</span>
+);
+
+const defaultRenderRemapped = ({
+  value,
+  displayValue,
+  column,
+  displayColumn,
+}) => (
+  <span>
+    <span className="text-bold">{displayValue}</span>
+    {/* Show the underlying ID for PK/FK */}
+    {column.isID() && <span style={{ opacity: 0.5 }}>{" - " + value}</span>}
+  </span>
+);
+
+const RemappedValueContent = ({
+  value,
+  column,
+  displayValue,
+  displayColumn,
+  renderNormal = defaultRenderNormal,
+  renderRemapped = defaultRenderRemapped,
+  ...props
+}) => {
+  if (column != null) {
+    value = formatValue(value, {
+      ...props,
+      column: column,
+      jsx: true,
+      remap: false,
+    });
+  }
+  if (displayColumn != null) {
+    displayValue = formatValue(displayValue, {
+      ...props,
+      column: displayColumn,
+      jsx: true,
+      remap: false,
+    });
+  }
+  if (displayValue != null) {
+    return renderRemapped({ value, displayValue, column, displayColumn });
+  } else {
+    return renderNormal({ value, column });
+  }
+};
+
+export const AutoLoadRemappedValue = AutoLoadRemapped(RemappedValueContent);
+
+export const FieldRemappedValue = props => (
+  <RemappedValueContent
+    {...props}
+    displayValue={props.column.remappedValue(props.value)}
+    displayColumn={props.column.remappedField()}
+  />
+);
+
+const RemappedValue = ({ autoLoad = true, ...props }) =>
+  autoLoad ? (
+    <AutoLoadRemappedValue {...props} />
+  ) : (
+    <FieldRemappedValue {...props} />
+  );
+
+export default RemappedValue;
+
+// test version doesn't use metabase/hoc/Remapped which requires a redux store
+export const TestRemappedValue = RemappedValueContent;
diff --git a/frontend/src/metabase/containers/SaveQuestionModal.css b/frontend/src/metabase/containers/SaveQuestionModal.css
index 0e5c6ff086c85562ad509cebc66f6e73d1a31ad4..b741ec7615cee2a9635d3193f4631c721d55cf9b 100644
--- a/frontend/src/metabase/containers/SaveQuestionModal.css
+++ b/frontend/src/metabase/containers/SaveQuestionModal.css
@@ -1,19 +1,19 @@
 .saveQuestionModalFields {
-    overflow: hidden;
+  overflow: hidden;
 }
 
 .saveQuestionModalFields-enter {
-    max-height: 0px;
+  max-height: 0px;
 }
 .saveQuestionModalFields-enter.saveQuestionModalFields-enter-active {
-    /* using 100% max-height breaks the transition */
-    max-height: 300px;
-    transition: max-height 500ms ease-out;
+  /* using 100% max-height breaks the transition */
+  max-height: 300px;
+  transition: max-height 500ms ease-out;
 }
 .saveQuestionModalFields-leave {
-    max-height: 300px;
+  max-height: 300px;
 }
 .saveQuestionModalFields-leave.saveQuestionModalFields-leave-active {
-    max-height: 0px;
-    transition: max-height 500ms ease-out;
-}
\ No newline at end of file
+  max-height: 0px;
+  transition: max-height 500ms ease-out;
+}
diff --git a/frontend/src/metabase/containers/SaveQuestionModal.jsx b/frontend/src/metabase/containers/SaveQuestionModal.jsx
index 3dd6bffdea50e59a823366559ab9152d5b8e990d..4489a3c7dc2a76225578b77226e0ae15390cfd22 100644
--- a/frontend/src/metabase/containers/SaveQuestionModal.jsx
+++ b/frontend/src/metabase/containers/SaveQuestionModal.jsx
@@ -11,247 +11,273 @@ import Button from "metabase/components/Button";
 import CollectionList from "metabase/questions/containers/CollectionList";
 
 import Query from "metabase/lib/query";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import "./SaveQuestionModal.css";
 import ButtonWithStatus from "metabase/components/ButtonWithStatus";
 
 export default class SaveQuestionModal extends Component {
+  constructor(props, context) {
+    super(props, context);
 
-    constructor(props, context) {
-        super(props, context);
+    const isStructured = Query.isStructured(props.card.dataset_query);
 
-        const isStructured = Query.isStructured(props.card.dataset_query);
+    this.state = {
+      error: null,
+      valid: false,
+      details: {
+        name:
+          props.card.name || isStructured
+            ? Query.generateQueryDescription(
+                props.tableMetadata,
+                props.card.dataset_query.query,
+              )
+            : "",
+        description: props.card.description || "",
+        collection_id: props.card.collection_id || null,
+        saveType: props.originalCard ? "overwrite" : "create",
+      },
+    };
+  }
 
-        this.state = {
-            error: null,
-            valid: false,
-            details: {
-                name: props.card.name || isStructured ? Query.generateQueryDescription(props.tableMetadata, props.card.dataset_query.query) : "",
-                description: props.card.description || '',
-                collection_id: props.card.collection_id || null,
-                saveType: props.originalCard ? "overwrite" : "create"
-            }
-        };
-    }
+  static propTypes = {
+    card: PropTypes.object.isRequired,
+    originalCard: PropTypes.object,
+    tableMetadata: PropTypes.object, // can't be required, sometimes null
+    createFn: PropTypes.func.isRequired,
+    saveFn: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    multiStep: PropTypes.bool,
+  };
 
-    static propTypes = {
-        card: PropTypes.object.isRequired,
-        originalCard: PropTypes.object,
-        tableMetadata: PropTypes.object, // can't be required, sometimes null
-        createFn: PropTypes.func.isRequired,
-        saveFn: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired,
-        multiStep: PropTypes.bool
-    }
+  componentDidMount() {
+    this.validateForm();
+  }
 
-    componentDidMount() {
-        this.validateForm();
-    }
-
-    componentDidUpdate() {
-        this.validateForm();
-    }
+  componentDidUpdate() {
+    this.validateForm();
+  }
 
-    validateForm() {
-        let { details } = this.state;
+  validateForm() {
+    let { details } = this.state;
 
-        let valid = true;
+    let valid = true;
 
-        // name is required for create
-        if (details.saveType === "create" && !details.name) {
-            valid = false;
-        }
-
-        if (this.state.valid !== valid) {
-            this.setState({ valid });
-        }
+    // name is required for create
+    if (details.saveType === "create" && !details.name) {
+      valid = false;
     }
 
-    onChange(fieldName, fieldValue) {
-        this.setState({ details: { ...this.state.details, [fieldName]: fieldValue ? fieldValue : null }});
+    if (this.state.valid !== valid) {
+      this.setState({ valid });
     }
+  }
+
+  onChange(fieldName, fieldValue) {
+    this.setState({
+      details: {
+        ...this.state.details,
+        [fieldName]: fieldValue ? fieldValue : null,
+      },
+    });
+  }
 
-    formSubmitted = async (e) => {
-        try {
-            if (e) {
-                e.preventDefault();
-            }
+  formSubmitted = async e => {
+    try {
+      if (e) {
+        e.preventDefault();
+      }
 
-            let { details } = this.state;
-            // TODO Atte Keinäenn 31/1/18 Refactor this
-            // I think that the primary change should be that
-            // SaveQuestionModal uses Question objects instead of directly modifying card objects –
-            // but that is something that doesn't need to be done first)
-            // question
-            //     .setDisplayName(details.name.trim())
-            //     .setDescription(details.description ? details.description.trim() : null)
-            //     .setCollectionId(details.collection_id)
-            let { card, originalCard, createFn, saveFn } = this.props;
+      let { details } = this.state;
+      // TODO Atte Keinäenn 31/1/18 Refactor this
+      // I think that the primary change should be that
+      // SaveQuestionModal uses Question objects instead of directly modifying card objects –
+      // but that is something that doesn't need to be done first)
+      // question
+      //     .setDisplayName(details.name.trim())
+      //     .setDescription(details.description ? details.description.trim() : null)
+      //     .setCollectionId(details.collection_id)
+      let { card, originalCard, createFn, saveFn } = this.props;
 
-            card = {
-                ...card,
-                name: details.saveType === "overwrite" ?
-                    originalCard.name :
-                    details.name.trim(),
-                // since description is optional, it can be null, so check for a description before trimming it
-                description: details.saveType === "overwrite" ?
-                    originalCard.description :
-                    details.description ? details.description.trim() : null,
-                collection_id: details.saveType === "overwrite" ?
-                    originalCard.collection_id :
-                    details.collection_id
-            };
+      card = {
+        ...card,
+        name:
+          details.saveType === "overwrite"
+            ? originalCard.name
+            : details.name.trim(),
+        // since description is optional, it can be null, so check for a description before trimming it
+        description:
+          details.saveType === "overwrite"
+            ? originalCard.description
+            : details.description ? details.description.trim() : null,
+        collection_id:
+          details.saveType === "overwrite"
+            ? originalCard.collection_id
+            : details.collection_id,
+      };
 
-            if (details.saveType === "create") {
-                await createFn(card);
-            } else if (details.saveType === "overwrite") {
-                card.id = this.props.originalCard.id;
-                await saveFn(card);
-            }
+      if (details.saveType === "create") {
+        await createFn(card);
+      } else if (details.saveType === "overwrite") {
+        card.id = this.props.originalCard.id;
+        await saveFn(card);
+      }
 
-            this.props.onClose();
-        } catch (error) {
-            if (error && !error.isCanceled) {
-                this.setState({ error: error });
-            }
+      this.props.onClose();
+    } catch (error) {
+      if (error && !error.isCanceled) {
+        this.setState({ error: error });
+      }
 
-            // Throw error for ButtonWithStatus
-            throw error;
-        }
+      // Throw error for ButtonWithStatus
+      throw error;
     }
+  };
 
-    render() {
-        let { error, details } = this.state;
-        var formError;
-        if (error) {
-            var errorMessage;
-            if (error.status === 500) {
-                errorMessage = t`Server error encountered`;
-            }
+  render() {
+    let { error, details } = this.state;
+    var formError;
+    if (error) {
+      var errorMessage;
+      if (error.status === 500) {
+        errorMessage = t`Server error encountered`;
+      }
 
-            if (error.data && error.data.message) {
-                errorMessage = error.data.message;
-            }
-            if (error.data && error.data.errors) {
-                errorMessage = Object.values(error.data.errors);
-            }
+      if (error.data && error.data.message) {
+        errorMessage = error.data.message;
+      }
+      if (error.data && error.data.errors) {
+        errorMessage = Object.values(error.data.errors);
+      }
 
-            // TODO: timeout display?
-            if (errorMessage) {
-                formError = (
-                    <span className="text-error px2">{errorMessage}</span>
-                );
-            }
-        }
+      // TODO: timeout display?
+      if (errorMessage) {
+        formError = <span className="text-error px2">{errorMessage}</span>;
+      }
+    }
 
-        var saveOrUpdate = null;
-        if (!this.props.card.id && this.props.originalCard) {
-            saveOrUpdate = (
+    var saveOrUpdate = null;
+    if (!this.props.card.id && this.props.originalCard) {
+      saveOrUpdate = (
+        <FormField
+          displayName={t`Replace or save as new?`}
+          fieldName="saveType"
+          errors={this.state.errors}
+        >
+          <Radio
+            value={this.state.details.saveType}
+            onChange={value => this.onChange("saveType", value)}
+            options={[
+              {
+                name: t`Replace original question, "${
+                  this.props.originalCard.name
+                }"`,
+                value: "overwrite",
+              },
+              { name: t`Save as new question`, value: "create" },
+            ]}
+            isVertical
+          />
+        </FormField>
+      );
+    }
+
+    let title = this.props.multiStep
+      ? t`First, save your question`
+      : t`Save question`;
+
+    return (
+      <ModalContent
+        id="SaveQuestionModal"
+        title={title}
+        footer={[
+          formError,
+          <Button onClick={this.props.onClose}>{t`Cancel`}</Button>,
+          <ButtonWithStatus
+            disabled={!this.state.valid}
+            onClickOperation={this.formSubmitted}
+          />,
+        ]}
+        onClose={this.props.onClose}
+      >
+        <form className="Form-inputs" onSubmit={this.formSubmitted}>
+          {saveOrUpdate}
+          <ReactCSSTransitionGroup
+            transitionName="saveQuestionModalFields"
+            transitionEnterTimeout={500}
+            transitionLeaveTimeout={500}
+          >
+            {details.saveType === "create" && (
+              <div
+                key="saveQuestionModalFields"
+                className="saveQuestionModalFields"
+              >
                 <FormField
-                    displayName={t`Replace or save as new?`}
-                    fieldName="saveType"
-                    errors={this.state.errors}
+                  displayName={t`Name`}
+                  fieldName="name"
+                  errors={this.state.errors}
                 >
-                    <Radio
-                        value={this.state.details.saveType}
-                        onChange={(value) => this.onChange("saveType", value)}
-                        options={[
-                            { name: t`Replace original question, "${this.props.originalCard.name}"`, value: "overwrite" },
-                            { name: t`Save as new question`, value: "create" },
-                        ]}
-                        isVertical
-                    />
+                  <input
+                    className="Form-input full"
+                    name="name"
+                    placeholder={t`What is the name of your card?`}
+                    value={this.state.details.name}
+                    onChange={e => this.onChange("name", e.target.value)}
+                    autoFocus
+                  />
                 </FormField>
-            );
-        }
-
-        let title = this.props.multiStep ? t`First, save your question` : t`Save question`;
-
-        return (
-            <ModalContent
-                id="SaveQuestionModal"
-                title={title}
-                footer={[
-                        formError,
-                        <Button onClick={this.props.onClose}>
-                            {t`Cancel`}
-                        </Button>,
-                        <ButtonWithStatus
-                            disabled={!this.state.valid}
-                            onClickOperation={this.formSubmitted}
-                        />
-                ]}
-                onClose={this.props.onClose}
-            >
-                <form className="Form-inputs" onSubmit={this.formSubmitted}>
-                    {saveOrUpdate}
-                    <ReactCSSTransitionGroup
-                        transitionName="saveQuestionModalFields"
-                        transitionEnterTimeout={500}
-                        transitionLeaveTimeout={500}
-                    >
-                        { details.saveType === "create" &&
-                            <div key="saveQuestionModalFields" className="saveQuestionModalFields">
-                                <FormField
-                                    displayName={t`Name`}
-                                    fieldName="name"
-                                    errors={this.state.errors}
-                                >
-                                    <input
-                                        className="Form-input full"
-                                        name="name" placeholder={t`What is the name of your card?`}
-                                        value={this.state.details.name}
-                                        onChange={(e) => this.onChange("name", e.target.value)}
-                                        autoFocus
-                                    />
-                                </FormField>
-                                <FormField
-                                    displayName={t`Description`}
-                                    fieldName="description"
-                                    errors={this.state.errors}
-                                >
-                                    <textarea
-                                        className="Form-input full"
-                                        name="description"
-                                        placeholder={t`It's optional but oh, so helpful`}
-                                        value={this.state.details.description}
-                                        onChange={(e) => this.onChange("description", e.target.value)}
-                                    />
-                                </FormField>
-                                <CollectionList writable>
-                                { (collections) => collections.length > 0 &&
-                                    <FormField
-                                        displayName={t`Which collection should this go in?`}
-                                        fieldName="collection_id"
-                                        errors={this.state.errors}
-                                    >
-                                        <Select
-                                            className="block"
-                                            value={this.state.details.collection_id}
-                                            onChange={e => this.onChange("collection_id", e.target.value)}
-                                        >
-                                            {[{ name: t`None`, id: null }]
-                                            .concat(collections)
-                                            .map((collection, index) =>
-                                                <Option
-                                                    key={index}
-                                                    value={collection.id}
-                                                    icon={collection.id != null ? "collection" : null}
-                                                    iconColor={collection.color}
-                                                    iconSize={18}
-                                                >
-                                                    {collection.name}
-                                                </Option>
-                                            )}
-                                        </Select>
-                                    </FormField>
+                <FormField
+                  displayName={t`Description`}
+                  fieldName="description"
+                  errors={this.state.errors}
+                >
+                  <textarea
+                    className="Form-input full"
+                    name="description"
+                    placeholder={t`It's optional but oh, so helpful`}
+                    value={this.state.details.description}
+                    onChange={e => this.onChange("description", e.target.value)}
+                  />
+                </FormField>
+                <CollectionList writable>
+                  {collections =>
+                    collections.length > 0 && (
+                      <FormField
+                        displayName={t`Which collection should this go in?`}
+                        fieldName="collection_id"
+                        errors={this.state.errors}
+                      >
+                        <Select
+                          className="block"
+                          value={this.state.details.collection_id}
+                          onChange={e =>
+                            this.onChange("collection_id", e.target.value)
+                          }
+                        >
+                          {[{ name: t`None`, id: null }]
+                            .concat(collections)
+                            .map((collection, index) => (
+                              <Option
+                                key={index}
+                                value={collection.id}
+                                icon={
+                                  collection.id != null ? "collection" : null
                                 }
-                                </CollectionList>
-                            </div>
-                        }
-                    </ReactCSSTransitionGroup>
-                </form>
-            </ModalContent>
-        );
-    }
+                                iconColor={collection.color}
+                                iconSize={18}
+                              >
+                                {collection.name}
+                              </Option>
+                            ))}
+                        </Select>
+                      </FormField>
+                    )
+                  }
+                </CollectionList>
+              </div>
+            )}
+          </ReactCSSTransitionGroup>
+        </form>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/containers/UndoListing.css b/frontend/src/metabase/containers/UndoListing.css
index 3be2dae68c297a134267fd45feac0c99259beeba..7d5b2c21f225dce3e804b1737d96b8491b426673 100644
--- a/frontend/src/metabase/containers/UndoListing.css
+++ b/frontend/src/metabase/containers/UndoListing.css
@@ -1,33 +1,33 @@
 :local(.listing) {
-    composes: m2 from "style";
-    composes: fixed left bottom from "style";
-    z-index: 99;
+  composes: m2 from "style";
+  composes: fixed left bottom from "style";
+  z-index: 99;
 }
 
 :local(.undo) {
-    composes: mt2 p2 from "style";
-    composes: bordered rounded shadowed from "style";
-    composes: bg-white from "style";
-    composes: relative from "style";
-    composes: flex align-center from "style";
-    width: 320px;
+  composes: mt2 p2 from "style";
+  composes: bordered rounded shadowed from "style";
+  composes: bg-white from "style";
+  composes: relative from "style";
+  composes: flex align-center from "style";
+  width: 320px;
 }
 
 :local(.actions) {
-    composes: flex align-center flex-align-right from "style";
+  composes: flex align-center flex-align-right from "style";
 }
 
 :local(.undoButton) {
-    composes: mx2 from "style";
-    composes: text-uppercase text-bold from "style";
-    color: var(--brand-color);
+  composes: mx2 from "style";
+  composes: text-uppercase text-bold from "style";
+  color: var(--brand-color);
 }
 :local(.dismissButton) {
-    composes: cursor-pointer from "style";
-    color: var(--grey-1);
+  composes: cursor-pointer from "style";
+  color: var(--grey-1);
 }
 :local(.dismissButton):hover {
-    color: var(--grey-3);
+  color: var(--grey-3);
 }
 
 .UndoListing-enter {
@@ -35,9 +35,9 @@
 .UndoListing-enter.UndoListing-enter-active {
 }
 .UndoListing-leave {
-    opacity: 1;
+  opacity: 1;
 }
 .UndoListing-leave.UndoListing-leave-active {
-    opacity: 0.01;
-    transition: opacity 300ms ease-in;
+  opacity: 0.01;
+  transition: opacity 300ms ease-in;
 }
diff --git a/frontend/src/metabase/containers/UndoListing.jsx b/frontend/src/metabase/containers/UndoListing.jsx
index 5749b023c56be5d9192b20950edd722101aad7f1..b6b1f22748ee80506d0daf8f3e36c7dea50ca70d 100644
--- a/frontend/src/metabase/containers/UndoListing.jsx
+++ b/frontend/src/metabase/containers/UndoListing.jsx
@@ -7,7 +7,7 @@ import S from "./UndoListing.css";
 
 import { dismissUndo, performUndo } from "metabase/redux/undo";
 import { getUndos } from "metabase/selectors/undo";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon";
 import BodyComponent from "metabase/components/BodyComponent";
 
@@ -15,49 +15,58 @@ import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 
 const mapStateToProps = (state, props) => {
   return {
-      undos: getUndos(state, props)
-  }
-}
+    undos: getUndos(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    dismissUndo,
-    performUndo
-}
+  dismissUndo,
+  performUndo,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @BodyComponent
 export default class UndoListing extends Component {
-    static propTypes = {
-        undos:          PropTypes.array.isRequired,
-        performUndo:    PropTypes.func.isRequired,
-        dismissUndo:    PropTypes.func.isRequired,
-    };
-
-    render() {
-        const { undos, performUndo, dismissUndo } = this.props;
-        return (
-            <ul className={S.listing}>
-                <ReactCSSTransitionGroup
-                    transitionName="UndoListing"
-                    transitionEnterTimeout={300}
-                    transitionLeaveTimeout={300}
-                >
-                { undos.map(undo =>
-                    <li key={undo._domId} className={S.undo}>
-                        <div className={S.message}>
-                            {typeof undo.message === "function" ? undo.message(undo) : undo.message}
-                        </div>
-
-                        { undo.actions &&
-                            <div className={S.actions}>
-                                <a className={S.undoButton} onClick={() => performUndo(undo.id)}>{t`Undo`}</a>
-                                <Icon className={S.dismissButton} name="close" onClick={() => dismissUndo(undo.id)} />
-                            </div>
-                        }
-                    </li>
-                )}
-                </ReactCSSTransitionGroup>
-            </ul>
-        );
-    }
+  static propTypes = {
+    undos: PropTypes.array.isRequired,
+    performUndo: PropTypes.func.isRequired,
+    dismissUndo: PropTypes.func.isRequired,
+  };
+
+  render() {
+    const { undos, performUndo, dismissUndo } = this.props;
+    return (
+      <ul className={S.listing}>
+        <ReactCSSTransitionGroup
+          transitionName="UndoListing"
+          transitionEnterTimeout={300}
+          transitionLeaveTimeout={300}
+        >
+          {undos.map(undo => (
+            <li key={undo._domId} className={S.undo}>
+              <div className={S.message}>
+                {typeof undo.message === "function"
+                  ? undo.message(undo)
+                  : undo.message}
+              </div>
+
+              {undo.actions && (
+                <div className={S.actions}>
+                  <a
+                    className={S.undoButton}
+                    onClick={() => performUndo(undo.id)}
+                  >{t`Undo`}</a>
+                  <Icon
+                    className={S.dismissButton}
+                    name="close"
+                    onClick={() => dismissUndo(undo.id)}
+                  />
+                </div>
+              )}
+            </li>
+          ))}
+        </ReactCSSTransitionGroup>
+      </ul>
+    );
+  }
 }
diff --git a/frontend/src/metabase/css/admin.css b/frontend/src/metabase/css/admin.css
index 76f797f32d5f686f289212b6e2b7a9c5f7273c93..70eb69974f88e05bc9d64d42271bd80888a5494a 100644
--- a/frontend/src/metabase/css/admin.css
+++ b/frontend/src/metabase/css/admin.css
@@ -1,221 +1,220 @@
 :root {
-    --admin-nav-bg-color: #8091AB;
-    --admin-nav-bg-color-tint: #9AA7BC;
-    --admin-nav-item-text-color: rgba(255, 255, 255, 0.63);
-    --admin-nav-item-text-active-color: #fff;
-    --page-header-padding: 2.375rem;
+  --admin-nav-bg-color: #8091ab;
+  --admin-nav-bg-color-tint: #9aa7bc;
+  --admin-nav-item-text-color: rgba(255, 255, 255, 0.63);
+  --admin-nav-item-text-active-color: #fff;
+  --page-header-padding: 2.375rem;
 }
 
 .AdminNav {
-    background: var(--admin-nav-bg-color);
-    color: #fff;
-    font-size: 0.85rem;
+  background: var(--admin-nav-bg-color);
+  color: #fff;
+  font-size: 0.85rem;
 }
 
 .AdminNav .NavItem {
-    color: var(--admin-nav-item-text-color);
+  color: var(--admin-nav-item-text-color);
 }
 
 .AdminNav .NavItem:hover,
 .AdminNav .NavItem.is--selected {
-    color: var(--admin-nav-item-text-active-color);
+  color: var(--admin-nav-item-text-active-color);
 }
 
 /* TODO: this feels itchy. should refactor .NavItem.is--selected to be less cascadey */
 .AdminNav .NavItem:hover:after,
 .AdminNav .NavItem.is--selected:after {
-    display: none;
+  display: none;
 }
 
-
 .AdminNav .NavDropdown.open .NavDropdown-button,
 .AdminNav .NavDropdown .NavDropdown-content-layer {
-    background-color: var(--admin-nav-bg-color-tint);
+  background-color: var(--admin-nav-bg-color-tint);
 }
 
 .AdminNav .Dropdown-item:hover {
-    background-color: var(--admin-nav-bg-color);
+  background-color: var(--admin-nav-bg-color);
 }
 
 /* utility to get a simple common hover state for admin items */
 .HoverItem:hover,
 .AdminHoverItem:hover {
-    background-color: #F3F8FD;
-    transition: background .2s linear;
+  background-color: #f3f8fd;
+  transition: background 0.2s linear;
 }
 
 .AdminNav .Dropdown-chevron {
-    color: #fff;
+  color: #fff;
 }
 
 .Actions {
-    background-color: rgba(243,243,243,0.46);
-    border: 1px solid #E0E0E0;
-    padding: 2em;
+  background-color: rgba(243, 243, 243, 0.46);
+  border: 1px solid #e0e0e0;
+  padding: 2em;
 }
 
 .Actions-group {
-    margin-bottom: 2em;
+  margin-bottom: 2em;
 }
 
 .Actions-group:last-child {
-    margin-bottom: 0;
+  margin-bottom: 0;
 }
 
 .Actions-groupLabel {
-    font-size: 1em;
-    margin-bottom: 1em;
+  font-size: 1em;
+  margin-bottom: 1em;
 }
 
 .ContentTable {
-    width: 100%;
-    border-collapse: collapse;
-    border-spacing: 0;
-    text-align: left;
+  width: 100%;
+  border-collapse: collapse;
+  border-spacing: 0;
+  text-align: left;
 }
 
 .ContentTable thead {
-    border-bottom: 1px solid #D8D8D8;
+  border-bottom: 1px solid #d8d8d8;
 }
 
 .AdminBadge {
-    background-color: #A989C5;
-    border-radius: 4px;
-    color: #fff;
-    padding: 0.25em;
+  background-color: #a989c5;
+  border-radius: 4px;
+  color: #fff;
+  padding: 0.25em;
 }
 .PageHeader {
-    padding-top: var(--page-header-padding);
-    padding-bottom: var(--page-header-padding);
+  padding-top: var(--page-header-padding);
+  padding-bottom: var(--page-header-padding);
 }
 
 .PageTitle {
-    margin: 0;
+  margin: 0;
 }
 
 .Table-actions {
-    text-align: right;
+  text-align: right;
 }
 
 .ContentTable .Table-actions {
-    opacity: 0;
+  opacity: 0;
 }
 
 .ContentTable td,
 .ContentTable th {
-    padding: 1em;
+  padding: 1em;
 }
 
 /* TODO: remove this and apply AdminHoverItem to content rows */
 .ContentTable tbody tr:hover {
-    background-color: rgba(74, 144, 226, 0.04);
+  background-color: rgba(74, 144, 226, 0.04);
 }
 
 .ContentTable tr:hover .Table-actions {
-    opacity: 1;
-    transition: opacity .2s linear;
+  opacity: 1;
+  transition: opacity 0.2s linear;
 }
 
 .AdminList {
-    background-color: #F9FBFC;
-    border: var(--border-size) var(--border-style) var(--border-color);
-    border-radius: var(--default-border-radius);
-    width: 266px;
-    box-shadow: inset -1px -1px 3px rgba(0,0,0,0.05);
-    padding-bottom: 0.75em;
+  background-color: #f9fbfc;
+  border: var(--border-size) var(--border-style) var(--border-color);
+  border-radius: var(--default-border-radius);
+  width: 266px;
+  box-shadow: inset -1px -1px 3px rgba(0, 0, 0, 0.05);
+  padding-bottom: 0.75em;
 }
 
 .AdminList-search {
-    position: relative;
+  position: relative;
 }
 
 .AdminList-search .Icon {
-    position: absolute;
-    margin-top: auto;
-    margin-bottom: auto;
-    top: 0;
-    bottom: 0;
-    margin: auto;
-    margin-left: 1em;
-    color: #C0C0C0;
+  position: absolute;
+  margin-top: auto;
+  margin-bottom: auto;
+  top: 0;
+  bottom: 0;
+  margin: auto;
+  margin-left: 1em;
+  color: #c0c0c0;
 }
 
 .AdminList-search .AdminInput {
-    padding: 0.5em;
-    padding-left: 2em;
-    font-size: 18px;
-    width: 100%;
-    border-top-left-radius: var(--default-border-radius);
-    border-top-right-radius: var(--default-border-radius);
-    border-bottom-color: var(--border-color);
+  padding: 0.5em;
+  padding-left: 2em;
+  font-size: 18px;
+  width: 100%;
+  border-top-left-radius: var(--default-border-radius);
+  border-top-right-radius: var(--default-border-radius);
+  border-bottom-color: var(--border-color);
 }
 
 .AdminList-item {
-    padding: 0.75em 1em 0.75em 1em;
-    border: var(--border-size) var(--border-style) transparent;
-    border-radius: var(--default-border-radius);
-    margin-bottom: 0.25em;
+  padding: 0.75em 1em 0.75em 1em;
+  border: var(--border-size) var(--border-style) transparent;
+  border-radius: var(--default-border-radius);
+  margin-bottom: 0.25em;
 }
 
 .AdminList-item.selected {
-    color: var(--brand-color);
+  color: var(--brand-color);
 }
 
 .AdminList-item.selected,
 .AdminList-item:hover {
-    background-color: white;
-    border-color: var(--border-color);
-    margin-left: -0.5em;
-    margin-right: -0.5em;
-    padding-left: 1.5em;
-    padding-right: 1.5em;
-    box-shadow: 0 1px 2px rgba(0,0,0,0.1);
+  background-color: white;
+  border-color: var(--border-color);
+  margin-left: -0.5em;
+  margin-right: -0.5em;
+  padding-left: 1.5em;
+  padding-right: 1.5em;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.1);
 }
 
 .AdminList-section {
-    margin-top: 1em;
-    padding: 0.5em 1em 0.5em 1em;
-    text-transform: uppercase;
-    color: color(var(--base-grey) shade(20%));
-    font-weight: 700;
-    font-size: smaller;
+  margin-top: 1em;
+  padding: 0.5em 1em 0.5em 1em;
+  text-transform: uppercase;
+  color: color(var(--base-grey) shade(20%));
+  font-weight: 700;
+  font-size: smaller;
 }
 
 .AdminList-item .ProgressBar {
-    opacity: 0.2;
+  opacity: 0.2;
 }
 
 .AdminList-item.selected .ProgressBar {
-    opacity: 1.0;
+  opacity: 1;
 }
 
 .AdminInput {
-    color: var(--default-font-color);
-    padding: var(--padding-1);
-    background-color: #FCFCFC;
-    border: 1px solid transparent;
+  color: var(--default-font-color);
+  padding: var(--padding-1);
+  background-color: #fcfcfc;
+  border: 1px solid transparent;
 }
 .AdminInput:focus {
-    border-color: var(--brand-color);
-    box-shadow: none;
-    outline: 0;
+  border-color: var(--brand-color);
+  box-shadow: none;
+  outline: 0;
 }
 
 .AdminSelect {
-    display: inline-block;
-    padding: 0.6em;
-    border: 1px solid var(--border-color);
-    border-radius: var(--default-border-radius);
-    font-size: 14px;
-    font-weight: 700;
-    min-width: 90px;
+  display: inline-block;
+  padding: 0.6em;
+  border: 1px solid var(--border-color);
+  border-radius: var(--default-border-radius);
+  font-size: 14px;
+  font-weight: 700;
+  min-width: 90px;
 }
 
 .AdminSelectBorderless {
-    display: inline-block;
-    font-size: 14px;
-    font-weight: 700;
-    margin-bottom: 3px;
+  display: inline-block;
+  font-size: 14px;
+  font-weight: 700;
+  margin-bottom: 3px;
 }
 
 .AdminSelect--borderless {
@@ -228,97 +227,96 @@
   margin-left: 0;
 }
 
-
 .MetadataTable-title {
-    background-color: #FCFCFC;
+  background-color: #fcfcfc;
 }
 
 .TableEditor-table-name {
-    font-size: 24px;
+  font-size: 24px;
 }
 
 .TableEditor-field-name {
-    font-size: 16px;
+  font-size: 16px;
 }
 
 .TableEditor-table-description,
 .TableEditor-field-description {
-    font-size: 14px;
+  font-size: 14px;
 }
 
 .TableEditor-field-visibility {
-    /*color: var(--orange-color);*/
+  /*color: var(--orange-color);*/
 }
 
 .TableEditor-field-visibility .ColumnarSelector-row:hover {
-    background-color: var(--brand-color) !important;
-    color: white !important;
+  background-color: var(--brand-color) !important;
+  color: white !important;
 }
 
 .TableEditor-field-type {
-    /*color: var(--purple-color);*/
+  /*color: var(--purple-color);*/
 }
 
 .TableEditor-field-type .ColumnarSelector-row:hover {
-    background-color: var(--brand-color) !important;
-    color: white !important;
+  background-color: var(--brand-color) !important;
+  color: white !important;
 }
 
 .TableEditor-field-special-type,
 .TableEditor-field-target {
-    margin-top: 3px;
-    /*color: var(--green-color);*/
+  margin-top: 3px;
+  /*color: var(--green-color);*/
 }
 
 .TableEditor-field-special-type .ColumnarSelector-row:hover,
 .TableEditor-field-target .ColumnarSelector-row:hover {
-    background-color: var(--brand-color) !important;
-    color: white !important;
+  background-color: var(--brand-color) !important;
+  color: white !important;
 }
 
 .ProgressBar {
-    position: relative;
-    border: 1px solid #6F7A8B;
+  position: relative;
+  border: 1px solid #6f7a8b;
 
-    width: 55px;
-    height: 10px;
-    border-radius: 99px;
+  width: 55px;
+  height: 10px;
+  border-radius: 99px;
 }
 .ProgressBar--mini {
-    width: 17px;
-    height: 8px;
-    border-radius: 2px;
+  width: 17px;
+  height: 8px;
+  border-radius: 2px;
 }
 .ProgressBar-progress {
-    background-color: #6F7A8B;
-    position: absolute;
-    height: 100%;
-    top: 0px;
-    left: 0px;
-    border-radius: inherit;
-    border-top-left-radius: 0px;
-    border-bottom-left-radius: 0px;
+  background-color: #6f7a8b;
+  position: absolute;
+  height: 100%;
+  top: 0px;
+  left: 0px;
+  border-radius: inherit;
+  border-top-left-radius: 0px;
+  border-bottom-left-radius: 0px;
 }
 
 .SaveStatus {
-    line-height: 1;
+  line-height: 1;
 }
 
 .SaveStatus:last-child {
-    border-right: none !important;
+  border-right: none !important;
 }
 
 .SettingsInput {
-    width: 400px;
+  width: 400px;
 }
 
 .SettingsPassword {
-    width: 200px;
+  width: 200px;
 }
 
 .UserActionsSelect {
-    padding-top: 1em;
-    min-width: 180px;
+  padding-top: 1em;
+  min-width: 180px;
 }
 
 .AdminTable {
@@ -332,7 +330,7 @@
 .AdminTable th {
   text-transform: uppercase;
   color: color(var(--base-grey) shade(40%));
-  padding:  var(--padding-1);
+  padding: var(--padding-1);
   font-weight: normal;
 }
 
diff --git a/frontend/src/metabase/css/card.css b/frontend/src/metabase/css/card.css
index ef7e4ca968e78668391c3aaa0d2eaaef706b479e..efd7c5faa0340ab500a4d64105eb03b2d278631d 100644
--- a/frontend/src/metabase/css/card.css
+++ b/frontend/src/metabase/css/card.css
@@ -17,7 +17,7 @@
 }
 
 .Card-title {
-  color: #3F3A3A;
+  color: #3f3a3a;
   font-size: 0.8em;
 }
 
@@ -31,9 +31,9 @@
 }
 
 @media screen and (--breakpoint-min-md) {
-    .Card-title {
-        font-size: 0.8em;
-    }
+  .Card-title {
+    font-size: 0.8em;
+  }
 }
 
 .Card--errored {
@@ -41,7 +41,7 @@
 }
 
 .Card-scalarValue {
-    overflow: hidden;
+  overflow: hidden;
 }
 
 @media screen and (--breakpoint-min-lg) {
@@ -72,7 +72,7 @@
 
 .CardSettings {
   border-top: 1px solid #ddd;
-  box-shadow: inset 0 1px 1px rgba(0, 0, 0, .12);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.12);
 }
 
 .CardSettings-label {
@@ -90,9 +90,9 @@
 }
 
 .CardSettings-colorBlock:last-child {
-    margin-right: 0;
+  margin-right: 0;
 }
 
 .Card--scalar {
-    padding: 1em;
+  padding: 1em;
 }
diff --git a/frontend/src/metabase/css/components/buttons.css b/frontend/src/metabase/css/components/buttons.css
index e7d358f8462e9ad3591edb15422bfb64ea5f93ba..ae78a3fa7487554a0f37e657f19aff5c94de9d6f 100644
--- a/frontend/src/metabase/css/components/buttons.css
+++ b/frontend/src/metabase/css/components/buttons.css
@@ -3,12 +3,12 @@
   --default-button-border-color: #b9b9b9;
   --default-button-background-color: #fff;
 
-  --primary-button-border-color: #509EE3;
-  --primary-button-bg-color: #509EE3;
-  --warning-button-border-color: #EF8C8C;
-  --warning-button-bg-color: #EF8C8C;
-  --danger-button-bg-color: #EF8C8C;
-  --selected-button-bg-color: #F4F6F8;
+  --primary-button-border-color: #509ee3;
+  --primary-button-bg-color: #509ee3;
+  --warning-button-border-color: #ef8c8c;
+  --warning-button-bg-color: #ef8c8c;
+  --danger-button-bg-color: #ef8c8c;
+  --selected-button-bg-color: #f4f6f8;
 
   --success-button-color: var(--success-color);
 }
@@ -18,7 +18,7 @@
   box-sizing: border-box;
   text-decoration: none;
   padding: 0.5rem 0.75rem;
-  background: #FBFCFD;
+  background: #fbfcfd;
   border: 1px solid #ddd;
   color: var(--default-font-color);
   cursor: pointer;
@@ -32,38 +32,38 @@
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .Button {
-        padding: 0.75rem 1rem;
-    }
+  .Button {
+    padding: 0.75rem 1rem;
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .Button {
-        padding: 1rem 1.5rem;
-    }
+  .Button {
+    padding: 1rem 1.5rem;
+  }
 }
 
 .Button:hover {
-  transition: border .3s linear;
+  transition: border 0.3s linear;
 }
 
 .Button--small {
-    padding: 0.45rem 1rem;
-    font-size: 0.6rem;
+  padding: 0.45rem 1rem;
+  font-size: 0.6rem;
 }
 
 .Button--medium {
-    padding: 0.5rem 1rem;
-    font-size: 0.8rem;
+  padding: 0.5rem 1rem;
+  font-size: 0.8rem;
 }
 
 .Button--large {
-    padding: 0.8rem 1.25rem;
-    font-size: 1rem;
+  padding: 0.8rem 1.25rem;
+  font-size: 1rem;
 }
 
 .Button-normal {
-    font-weight: normal;
+  font-weight: normal;
 }
 
 .Button--primary {
@@ -95,21 +95,21 @@
 }
 
 .Button--white {
-    background-color: white;
-    color: color(var(--base-grey) shade(30%));
-    border-color: color(var(--base-grey) shade(30%));
+  background-color: white;
+  color: color(var(--base-grey) shade(30%));
+  border-color: color(var(--base-grey) shade(30%));
 }
 
 .Button--purple {
-    color: white;
-    background-color: #A989C5;
-    border: 1px solid #A989C5;
+  color: white;
+  background-color: #a989c5;
+  border: 1px solid #a989c5;
 }
 
 .Button--purple:hover {
-    color: white;
-    background-color: #885AB1;
-    border-color: #885AB1;
+  color: white;
+  background-color: #885ab1;
+  border-color: #885ab1;
 }
 
 .Button--borderless {
@@ -122,10 +122,10 @@
 }
 
 .Button--onlyIcon {
-    border: none;
-    background: transparent;
-    color: var(--default-font-color);
-    padding: 0;
+  border: none;
+  background: transparent;
+  color: var(--default-font-color);
+  padding: 0;
 }
 
 .Button-group {
@@ -156,16 +156,16 @@
 }
 
 .Button-group--blue {
-  border-color: rgb(194,216,242);
+  border-color: rgb(194, 216, 242);
 }
 
 .Button-group--blue .Button {
-  color: rgb(147,155,178);
+  color: rgb(147, 155, 178);
 }
 
 .Button-group--blue .Button--active {
-  background-color: rgb(227,238,250);
-  color: rgb(74,144,226);
+  background-color: rgb(227, 238, 250);
+  color: rgb(74, 144, 226);
 }
 
 .Button-group--brand {
@@ -175,7 +175,7 @@
 .Button-group--brand .Button {
   border-color: white;
   color: var(--brand-color);
-  background-color: #E5E5E5;
+  background-color: #e5e5e5;
 }
 
 .Button-group--brand .Button--active {
@@ -190,74 +190,73 @@
 
 .Button--selected,
 .Button--selected:hover {
-  box-shadow: inset 0 1px 1px rgba(0, 0, 0, .12);
+  box-shadow: inset 0 1px 1px rgba(0, 0, 0, 0.12);
   background-color: var(--selected-button-bg-color);
 }
 
 .Button--danger {
-    background-color: var(--danger-button-bg-color);
-    border-color: var(--danger-button-bg-color);
-    color: #fff;
+  background-color: var(--danger-button-bg-color);
+  border-color: var(--danger-button-bg-color);
+  color: #fff;
 }
 
 .Button--danger:hover {
-    color: white;
-    background-color: color(var(--danger-button-bg-color) shade(10%));
-    border-color: color(var(--danger-button-bg-color) shade(10%));
+  color: white;
+  background-color: color(var(--danger-button-bg-color) shade(10%));
+  border-color: color(var(--danger-button-bg-color) shade(10%));
 }
 
 .Button--success {
-    background-color: var(--success-button-color);
-    border-color: var(--success-button-color);
-    color: #fff;
+  background-color: var(--success-button-color);
+  border-color: var(--success-button-color);
+  color: #fff;
 }
-.Button--success:hover
-{
-    color: #fff;
+.Button--success:hover {
+  color: #fff;
 }
 
 .Button--success-new {
-    border-color: var(--success-button-color);
-    color: var(--success-button-color);
-    font-weight: bold;
+  border-color: var(--success-button-color);
+  color: var(--success-button-color);
+  font-weight: bold;
 }
 
 /* toggle button */
 .Button-toggle {
-    color: var(--grey-text-color);
-    display: flex;
-    line-height: 1;
-    border: 1px solid #ddd;
-    border-radius: 40px;
-    width: 3rem;
-    transition: background .2s linear .2s, border .2s linear .2s;
+  color: var(--grey-text-color);
+  display: flex;
+  line-height: 1;
+  border: 1px solid #ddd;
+  border-radius: 40px;
+  width: 3rem;
+  transition: background 0.2s linear 0.2s, border 0.2s linear 0.2s;
 }
 
 .Button-toggleIndicator {
-    margin-left: 0;
-    display: flex;
-    align-items: center;
-    justify-content: center;
-    padding: 0.25rem;
-    border: 1px solid #ddd;
-    border-radius: 99px;
-    box-shadow: 0 1px 3px rgba(0, 0, 0, .02);
-    transition: margin .3s linear;
-    background-color: #fff;
+  margin-left: 0;
+  display: flex;
+  align-items: center;
+  justify-content: center;
+  padding: 0.25rem;
+  border: 1px solid #ddd;
+  border-radius: 99px;
+  box-shadow: 0 1px 3px rgba(0, 0, 0, 0.02);
+  transition: margin 0.3s linear;
+  background-color: #fff;
 }
 
 .Button-toggle.Button--toggled .Button-toggleIndicator {
-    margin-left: 50%;
-    transition: margin .3s linear;
+  margin-left: 50%;
+  transition: margin 0.3s linear;
 }
 
 .Button-toggle.Button--toggled {
-    color: var(--brand-color);
-    background-color: var(--brand-color);
-    border-color: var(--brand-color);
-    transition: background .2s linear .2s, border .2s linear .2s;
+  color: var(--brand-color);
+  background-color: var(--brand-color);
+  border-color: var(--brand-color);
+  transition: background 0.2s linear 0.2s, border 0.2s linear 0.2s;
 }
 
 .Button--withIcon {
-    line-height: 1;
+  line-height: 1;
 }
diff --git a/frontend/src/metabase/css/components/dropdown.css b/frontend/src/metabase/css/components/dropdown.css
index bf376ac434f443dd31a5fb684110ab38bf76e7d4..826aa5dd1498115c0d4c38d2e409e0ca9645515d 100644
--- a/frontend/src/metabase/css/components/dropdown.css
+++ b/frontend/src/metabase/css/components/dropdown.css
@@ -17,7 +17,7 @@
   border: 1px solid var(--dropdown-border-color);
   background-color: #fff;
   border-radius: 4px;
-  box-shadow: 0 0 2px rgba(0, 0, 0, .12);
+  box-shadow: 0 0 2px rgba(0, 0, 0, 0.12);
   background-clip: padding-box;
   padding-top: 1em;
   padding-bottom: 1em;
@@ -30,36 +30,36 @@
   border-left: 5px solid transparent;
   border-right: 5px solid transparent;
   border-right: 5px solid red;
-  content: '';
+  content: "";
   display: block;
 }
 
 /* switching from home rolled to BS logic for dropdowns so we still have both classes */
 .Dropdown.open .Dropdown-content,
-.Dropdown--showing.Dropdown-content{
+.Dropdown--showing.Dropdown-content {
   opacity: 1;
   pointer-events: all;
-  transition: opacity .3s linear, margin .2s linear;
+  transition: opacity 0.3s linear, margin 0.2s linear;
   margin-top: 0;
 }
 
 .Dropdown-item {
-    padding-top: 1rem;
-    padding-bottom: 1rem;
-    padding-left: 2rem;
-    padding-right: 2rem;
-    line-height: 1;
+  padding-top: 1rem;
+  padding-bottom: 1rem;
+  padding-left: 2rem;
+  padding-right: 2rem;
+  line-height: 1;
 }
 
 .Dropdown .Dropdown-item .link:hover {
-    text-decoration: none;
+  text-decoration: none;
 }
 
 .Dropdown-item:hover {
-    color: #fff;
-    background-color: var(--brand-color);
+  color: #fff;
+  background-color: var(--brand-color);
 }
 
 .Dropdown .Dropdown-item:hover {
-    text-decoration: none;
+  text-decoration: none;
 }
diff --git a/frontend/src/metabase/css/components/form.css b/frontend/src/metabase/css/components/form.css
index 8f7af661405ec7107efdab6bf72a9ece385b0649..9881938d2bb7862cedf6d8941cb209184d8b81c1 100644
--- a/frontend/src/metabase/css/components/form.css
+++ b/frontend/src/metabase/css/components/form.css
@@ -1,17 +1,17 @@
 :root {
-    --form-padding: 1em;
-    --form-input-placeholder-color: #C0C0C0;
+  --form-padding: 1em;
+  --form-input-placeholder-color: #c0c0c0;
 
-    --form-input-size: 1.0rem;
-    --form-input-size-medium: 1.25rem;
-    --form-input-size-large: 1.571rem;
+  --form-input-size: 1rem;
+  --form-input-size-medium: 1.25rem;
+  --form-input-size-large: 1.571rem;
 
-    --form-label-color: #949494;
-    --form-offset: 2.4rem;
+  --form-label-color: #949494;
+  --form-offset: 2.4rem;
 }
 
 .Form-new {
-    padding-top: 2rem;
+  padding-top: 2rem;
 }
 
 /* TODO: combine this and the scoped version */
@@ -22,203 +22,212 @@
 }
 
 .Form-field {
-    position: relative;
-    color: #6C6C6C;
-    margin-bottom: 1.5rem;
+  position: relative;
+  color: #6c6c6c;
+  margin-bottom: 1.5rem;
 }
 
 /* TODO: remove this scoping once we've converted non admin forms */
 /* form labels inherit the color of the parent, allowing for easy error changes */
 .Form-field .Form-label {
-    display: block;
-    font-size: 0.85em;
-    font-weight: 700;
-    color: currentColor;
+  display: block;
+  font-size: 0.85em;
+  font-weight: 700;
+  color: currentColor;
 }
 
 .Form-field.Form--fieldError {
-    color: var(--error-color);
+  color: var(--error-color);
 }
 
 .Form-input {
-    padding-top: 0.6rem;
-    padding-bottom: 0.6rem;
-    font-family: var(--default-font-family);
-    line-height: 1;
-    border: none;
-    background-color: transparent;
-    transition: color .3s linear;
+  padding-top: 0.6rem;
+  padding-bottom: 0.6rem;
+  font-family: var(--default-font-family);
+  line-height: 1;
+  border: none;
+  background-color: transparent;
+  transition: color 0.3s linear;
 }
 
 .Form-message {
-    opacity: 0;
-    transition: none;
+  opacity: 0;
+  transition: none;
 }
 
 .Form-message.Form-message--visible {
-    opacity: 1;
-    transition: opacity 500ms linear;
+  opacity: 1;
+  transition: opacity 500ms linear;
 }
 
 /* form-input font sizes */
-.Form-input { font-size: var(--form-input-size); }
+.Form-input {
+  font-size: var(--form-input-size);
+}
 
 @media screen and (--breakpoint-min-md) {
-    .Form-input { font-size: var(--form-input-size-medium); }
+  .Form-input {
+    font-size: var(--form-input-size-medium);
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .Form-input { font-size: var(--form-input-size-large); }
+  .Form-input {
+    font-size: var(--form-input-size-large);
+  }
 }
 
 .Form-input:focus {
-    outline: none;
+  outline: none;
 }
 
 .Form-offset {
-    padding-left: var(--form-offset);
+  padding-left: var(--form-offset);
 }
 
 .Form-charm {
-    position: absolute;
-    display: block;
-    bottom: 0;
-    left: 0;
-    width: 0.15em;
-    height: 3em;
-    background-color: #ddd;
-    box-sizing: border-box;
-    opacity: 0;
-    transition: background-color .3s linear;
-    transition: opacity .3s linear;
+  position: absolute;
+  display: block;
+  bottom: 0;
+  left: 0;
+  width: 0.15em;
+  height: 3em;
+  background-color: #ddd;
+  box-sizing: border-box;
+  opacity: 0;
+  transition: background-color 0.3s linear;
+  transition: opacity 0.3s linear;
 }
 
-
 .Form-field.Form--fieldError .Form-charm {
-    background-color: var(--error-color);
-    opacity: 1;
+  background-color: var(--error-color);
+  opacity: 1;
 }
 
 .Form-input:focus + .Form-charm {
-    background-color: var(--brand-color);
-    opacity: 1;
+  background-color: var(--brand-color);
+  opacity: 1;
 }
 
 .Form-field:hover .Form-input {
-    color: #ddd;
-    background: rgba(0, 0, 0, 0.02);
+  color: #ddd;
+  background: rgba(0, 0, 0, 0.02);
 }
 
 /* ewww */
 .Form-field:hover .Form-input.ng-dirty {
-    color: #222;
-    background-color: #fff;
+  color: #222;
+  background-color: #fff;
 }
 
 .Form-field:hover .Form-charm {
-    opacity: 1;
+  opacity: 1;
 }
 
 .Form-field:hover .Form-input:focus {
-    transition: color .3s linear;
-    color: #444;
-    background-color: transparent;
-    transition: background .3s linear;
+  transition: color 0.3s linear;
+  color: #444;
+  background-color: transparent;
+  transition: background 0.3s linear;
 }
 
 .Form-radio {
-    display: none;
+  display: none;
 }
 
 .Form-radio + label {
-    cursor: pointer;
-    display: inline-block;
-    flex: 0 0 auto;
-    position: relative;
-    margin-right: var(--margin-1);
-    width: 8px;
-    height: 8px;
-    border: 2px solid white;
-    box-shadow: 0 0 0 2px var(--grey-3);
-    border-radius: 8px;
+  cursor: pointer;
+  display: inline-block;
+  flex: 0 0 auto;
+  position: relative;
+  margin-right: var(--margin-1);
+  width: 8px;
+  height: 8px;
+  border: 2px solid white;
+  box-shadow: 0 0 0 2px var(--grey-3);
+  border-radius: 8px;
 }
 
 .Form-radio:checked + label {
-    box-shadow: 0 0 0 2px var(--brand-color);
-    background-color: var(--brand-color);
+  box-shadow: 0 0 0 2px var(--brand-color);
+  background-color: var(--brand-color);
 }
 
 /* TODO: replace instances of Form-group with Form-field */
 .Form-group {
   padding: var(--form-padding);
-  transition: opacity .3s linear;
+  transition: opacity 0.3s linear;
 }
 
 .Form-groupDisabled {
   opacity: 0.2;
   pointer-events: none;
-  transition: opacity .3s linear;
+  transition: opacity 0.3s linear;
 }
 
 .Form-actions {
-    padding-left: var(--form-offset);
-    padding-bottom: var(--form-offset);
-    display: flex;
-    align-items: center;
+  padding-left: var(--form-offset);
+  padding-bottom: var(--form-offset);
+  display: flex;
+  align-items: center;
 }
 
 .FormTitleSeparator {
-    position: relative;
-    border-bottom: 1px solid #e8e8e8;
+  position: relative;
+  border-bottom: 1px solid #e8e8e8;
 }
 
-::-webkit-input-placeholder { /* WebKit browsers */
-    color: var(--form-input-placeholder-color);
+::-webkit-input-placeholder {
+  /* WebKit browsers */
+  color: var(--form-input-placeholder-color);
 }
-:-moz-placeholder { /* Mozilla Firefox 4 to 18 */
-   color:    var(--form-input-placeholder-color);
-   opacity:  1;
+:-moz-placeholder {
+  /* Mozilla Firefox 4 to 18 */
+  color: var(--form-input-placeholder-color);
+  opacity: 1;
 }
-::-moz-placeholder { /* Mozilla Firefox 19+ */
-   color:    var(--form-input-placeholder-color);
-   opacity:  1;
+::-moz-placeholder {
+  /* Mozilla Firefox 19+ */
+  color: var(--form-input-placeholder-color);
+  opacity: 1;
 }
-:-ms-input-placeholder { /* Internet Explorer 10+ */
-   color:    var(--form-input-placeholder-color);
+:-ms-input-placeholder {
+  /* Internet Explorer 10+ */
+  color: var(--form-input-placeholder-color);
 }
 
 .NewForm .Form-label {
-    text-transform: uppercase;
-    color: color(var(--base-grey) shade(30%));
-    margin-bottom: 0.5em;
+  text-transform: uppercase;
+  color: color(var(--base-grey) shade(30%));
+  margin-bottom: 0.5em;
 }
 
 .NewForm .Form-input {
-    font-size: 16px;
-    color: var(--default-font-color);
-    padding: 0.5em;
-    background-color: #FCFCFC;
-    border: 1px solid #EAEAEA;
-    border-radius: 4px;
+  font-size: 16px;
+  color: var(--default-font-color);
+  padding: 0.5em;
+  background-color: #fcfcfc;
+  border: 1px solid #eaeaea;
+  border-radius: 4px;
 }
 .NewForm .Form-input:focus {
-    border-color: var(--brand-color);
-    box-shadow: none;
-    outline: 0;
+  border-color: var(--brand-color);
+  box-shadow: none;
+  outline: 0;
 }
 
 .NewForm .Form-header {
-    padding: var(--padding-4);
+  padding: var(--padding-4);
 }
 
 .NewForm .Form-inputs {
-    padding-left: var(--padding-4);
-    padding-right: var(--padding-4);
+  padding-left: var(--padding-4);
+  padding-right: var(--padding-4);
 }
 
 .NewForm .Form-actions {
-    padding-bottom: 1.2rem;
-    padding-top: 1.2rem;
-    padding-left: var(--padding-4);
-    padding-right: var(--padding-4);
+  padding-bottom: 1.2rem;
+  padding-top: 1.2rem;
+  padding-left: var(--padding-4);
+  padding-right: var(--padding-4);
 }
diff --git a/frontend/src/metabase/css/components/header.css b/frontend/src/metabase/css/components/header.css
index d835233c7bc189a992b830b026604c8cc428ef13..ed096260547d6e12a19d90133152a4cf2ad0612e 100644
--- a/frontend/src/metabase/css/components/header.css
+++ b/frontend/src/metabase/css/components/header.css
@@ -1,68 +1,67 @@
-
 .Header-title {
-    width: 455px;
+  width: 455px;
 }
 
 .Header-title-name {
-    font-size: 1.24em;
-    color: var(--grey-text-color);
+  font-size: 1.24em;
+  color: var(--grey-text-color);
 }
 
 .Header-attribution {
-    display: none; /* disabled */
-    color: #ADADAD;
-    margin-bottom: 0.5em;
+  display: none; /* disabled */
+  color: #adadad;
+  margin-bottom: 0.5em;
 }
 
 .Header-buttonSection {
-    padding-right: 1em;
-    margin-right: 1em;
-    border-right: 1px solid rgba(0,0,0,0.2);
+  padding-right: 1em;
+  margin-right: 1em;
+  border-right: 1px solid rgba(0, 0, 0, 0.2);
 }
 
 .Header-buttonSection:last-child {
-    padding-right: 0;
-    margin-right: 0;
-    border-right: none;
+  padding-right: 0;
+  margin-right: 0;
+  border-right: none;
 }
 
 .Header-button {
-    margin-right: 1.5em;
+  margin-right: 1.5em;
 }
 
 .Header-button:last-child {
-    margin-right: 0;
+  margin-right: 0;
 }
 
 .EditHeader {
-    background-color: #6CAFED;
+  background-color: #6cafed;
 }
 
 .EditHeader.EditHeader--admin {
-    background-color: var(--admin-nav-bg-color-tint);
+  background-color: var(--admin-nav-bg-color-tint);
 }
 
 .EditHeader-title {
-    font-weight: 700;
-    color: white;
+  font-weight: 700;
+  color: white;
 }
 
 .EditHeader-subtitle {
-    color: rgba(255,255,255,0.5);
+  color: rgba(255, 255, 255, 0.5);
 }
 
 .EditHeader .Button {
-    color: white;
-    border: none;
-    font-size: 1em;
-    text-transform: capitalize;
-    background-color: rgba(255,255,255,0.1);
-    margin-left: 0.75em;
+  color: white;
+  border: none;
+  font-size: 1em;
+  text-transform: capitalize;
+  background-color: rgba(255, 255, 255, 0.1);
+  margin-left: 0.75em;
 }
 
 .EditHeader .Button--primary {
-    background-color: white;
-    color: var(--brand-color);
+  background-color: white;
+  color: var(--brand-color);
 }
 
 .EditHeader.EditHeader--admin .Button--primary {
@@ -71,8 +70,8 @@
 }
 
 .EditHeader .Button:hover {
-    color: white;
-    background-color: var(--brand-color);
+  color: white;
+  background-color: var(--brand-color);
 }
 
 .EditHeader.EditHeader--admin .Button:hover {
diff --git a/frontend/src/metabase/css/components/icons.css b/frontend/src/metabase/css/components/icons.css
index 681a26826dedc4b64e14b42096bd34742a6aba07..77b7a4419f24e226dd160364264cab2263f29ab8 100644
--- a/frontend/src/metabase/css/components/icons.css
+++ b/frontend/src/metabase/css/components/icons.css
@@ -1,5 +1,5 @@
 :root {
-  --icon-color: #BFC4D1;
+  --icon-color: #bfc4d1;
 }
 
 .IconWrapper {
@@ -7,36 +7,35 @@
 }
 
 .Logo .Icon {
-    width: 33px;
-    height: 42.5px;
+  width: 33px;
+  height: 42.5px;
 }
 
 @keyframes icon-pulse {
-    0% {
-        box-shadow: 0 0 5px rgba(80,158,227, 1.0);
-    }
-    50% {
-        box-shadow: 0 0 5px rgba(80,158,227, 0.25);
-    }
-    100% {
-        box-shadow: 0 0 5px rgba(80,158,227, 1.0);
-    }
+  0% {
+    box-shadow: 0 0 5px rgba(80, 158, 227, 1);
+  }
+  50% {
+    box-shadow: 0 0 5px rgba(80, 158, 227, 0.25);
+  }
+  100% {
+    box-shadow: 0 0 5px rgba(80, 158, 227, 1);
+  }
 }
 
-
 .Icon--pulse {
-    border-radius: 99px;
-    box-shadow: 0 0 5px #509EE3;
-    padding: 0.75em;
-    animation-name: icon-pulse;
-    animation-duration: 2s;
-    animation-iteration-count: infinite;
-    animation-timing-function: linear;
+  border-radius: 99px;
+  box-shadow: 0 0 5px #509ee3;
+  padding: 0.75em;
+  animation-name: icon-pulse;
+  animation-duration: 2s;
+  animation-iteration-count: infinite;
+  animation-timing-function: linear;
 }
 
 @media screen and (--breakpoint-min-md) {
-    .Logo .Icon {
-        width: 66px;
-        height: 85px;
-    }
+  .Logo .Icon {
+    width: 66px;
+    height: 85px;
+  }
 }
diff --git a/frontend/src/metabase/css/components/list.css b/frontend/src/metabase/css/components/list.css
index bae3f575e1de873b91326c693e231c6ef5c29cd4..32fb3866fd98bffd67a8e3b991726762c4fe51a6 100644
--- a/frontend/src/metabase/css/components/list.css
+++ b/frontend/src/metabase/css/components/list.css
@@ -1,4 +1,3 @@
-
 .List {
   padding: var(--padding-1);
 }
@@ -9,25 +8,25 @@
 }
 
 .List-item .Icon {
-    color: var(--slate-light-color);
+  color: var(--slate-light-color);
 }
 
 .List-section-header {
-    color: var(--default-font-color);
-    border: 2px solid transparent; /* so that spacing matches .List-item */
+  color: var(--default-font-color);
+  border: 2px solid transparent; /* so that spacing matches .List-item */
 }
 
 /* these crazy rules are needed to get currentColor to propagate to the right elements in the right states */
 .List-section--togglable .List-section-header:hover,
 .List-section--togglable .List-section-header:hover .Icon,
 .List-section--togglable .List-section-header:hover .List-section-title,
-.List-section--expanded      .List-section-header,
-.List-section--expanded      .List-section-header .List-section-icon .Icon {
-    color: currentColor;
+.List-section--expanded .List-section-header,
+.List-section--expanded .List-section-header .List-section-icon .Icon {
+  color: currentColor;
 }
 
 .List-section--expanded .List-section-header .List-section-title {
-    color: var(--default-font-color);
+  color: var(--default-font-color);
 }
 
 .List-section-title {
@@ -36,33 +35,33 @@
 }
 
 .List-item {
-    display: flex;
-    border-radius: 4px;
-    margin-top: 2px;
-    margin-bottom: 2px;
+  display: flex;
+  border-radius: 4px;
+  margin-top: 2px;
+  margin-bottom: 2px;
 }
 
 .List-item--disabled .List-item-title {
-    color: var(--grey-3);
+  color: var(--grey-3);
 }
 
 .List-item:not(.List-item--disabled):hover,
 .List-item--selected {
-    background-color: currentColor;
-    border-color: rgba(0,0,0,0.2);
-    /*color: white;*/
+  background-color: currentColor;
+  border-color: rgba(0, 0, 0, 0.2);
+  /*color: white;*/
 }
 
 .List-item-title {
-    color: var(--default-font-color);
+  color: var(--default-font-color);
 }
 
 .List-item:not(.List-item--disabled):hover .List-item-title,
 .List-item--selected .List-item-title {
-    color: white;
+  color: white;
 }
 
 .List-item:not(.List-item--disabled):hover .Icon,
 .List-item--selected .Icon {
-    color: white !important;
+  color: white !important;
 }
diff --git a/frontend/src/metabase/css/components/modal.css b/frontend/src/metabase/css/components/modal.css
index f0ed2022c449a8b32a77d0a5f280dc7783bbd6bd..a2b4898c199828b0938a210b2a218ad02b8a8aab 100644
--- a/frontend/src/metabase/css/components/modal.css
+++ b/frontend/src/metabase/css/components/modal.css
@@ -10,7 +10,7 @@
 .Modal {
   margin: auto;
   width: 640px;
-  box-shadow: 0 0 6px rgba(0, 0, 0, .12);
+  box-shadow: 0 0 6px rgba(0, 0, 0, 0.12);
   max-height: 90%;
   overflow-y: auto;
 }
@@ -23,9 +23,15 @@
   margin: 0;
 }
 
-.Modal.Modal--small   { width: 480px; } /* TODO - why is this one px? */
-.Modal.Modal--medium  { width: 65%; }
-.Modal.Modal--wide    { width: 85%; }
+.Modal.Modal--small {
+  width: 480px;
+} /* TODO - why is this one px? */
+.Modal.Modal--medium {
+  width: 65%;
+}
+.Modal.Modal--wide {
+  width: 85%;
+}
 
 .Modal.Modal--tall {
   min-height: 85%;
@@ -69,7 +75,6 @@
   background-color: var(--modal-background-color-transition);
 }
 
-
 /* modal */
 
 .Modal-backdrop.Modal-appear .Modal,
diff --git a/frontend/src/metabase/css/components/select.css b/frontend/src/metabase/css/components/select.css
index 370e8e547f980ce53628afbfa077981371f7076b..bc2fbc53446b5442db2beb47b215f2442dcfbbac 100644
--- a/frontend/src/metabase/css/components/select.css
+++ b/frontend/src/metabase/css/components/select.css
@@ -1,5 +1,5 @@
 :root {
-  --select-arrow-bg-color: #CACACA;
+  --select-arrow-bg-color: #cacaca;
   --select-border-color: #d9d9d9;
   --select-bg-color: #fff;
   --select-text-color: #777;
@@ -15,7 +15,7 @@
 /* custom arrows */
 .Select:before,
 .Select:after {
-  content: '';
+  content: "";
   position: absolute;
   top: 50%;
   right: 1em;
@@ -26,18 +26,18 @@
 
 /* arrow pointing up */
 .Select:before {
-  margin-top: -.25rem;
-  border-left: .3rem solid transparent;
-  border-right: .3rem solid transparent;
-  border-bottom: .3rem solid var(--select-arrow-bg-color);
+  margin-top: -0.25rem;
+  border-left: 0.3rem solid transparent;
+  border-right: 0.3rem solid transparent;
+  border-bottom: 0.3rem solid var(--select-arrow-bg-color);
 }
 
 /* arrow pointing down */
 .Select:after {
-  margin-top:  .2rem;
-  border-left: .3rem solid transparent;
-  border-right: .3rem solid transparent;
-  border-top: .3rem solid var(--select-arrow-bg-color);
+  margin-top: 0.2rem;
+  border-left: 0.3rem solid transparent;
+  border-right: 0.3rem solid transparent;
+  border-top: 0.3rem solid var(--select-arrow-bg-color);
 }
 
 .Select select {
@@ -53,46 +53,46 @@
 
   border-radius: var(--select-border-radius);
   -webkit-appearance: none;
-     -moz-appearance: none;
+  -moz-appearance: none;
 
-   box-shadow: 0 1px 2px rgba(0, 0, 0, .12);
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.12);
 }
 
 .Select--blue select {
-    color: rgb(78,146,223);
-    border-color: rgb(195,216,241);
-    background-color: rgb(227,238,249);
+  color: rgb(78, 146, 223);
+  border-color: rgb(195, 216, 241);
+  background-color: rgb(227, 238, 249);
 }
 .Select--blue:after {
-    border-top: .3rem solid rgb(78,146,223);
+  border-top: 0.3rem solid rgb(78, 146, 223);
 }
 .Select--blue:before {
-    border-bottom: .3rem solid rgb(78,146,223);
+  border-bottom: 0.3rem solid rgb(78, 146, 223);
 }
 
 .Select--purple select {
-    color: rgb(168,138,195);
-    border-color: rgb(203,186,219);
-    background-color: rgb(231,223,239);
+  color: rgb(168, 138, 195);
+  border-color: rgb(203, 186, 219);
+  background-color: rgb(231, 223, 239);
 }
 .Select--purple:after {
-    border-top: .3rem solid rgb(168,138,195);
+  border-top: 0.3rem solid rgb(168, 138, 195);
 }
 .Select--purple:before {
-    border-bottom: .3rem solid rgb(168,138,195);
+  border-bottom: 0.3rem solid rgb(168, 138, 195);
 }
 
 .Select--small select {
-    padding: 0.25rem 1.5rem 0.25rem 0.5rem;
-    font-size: 0.7em;
-    line-height: 1.5em;
+  padding: 0.25rem 1.5rem 0.25rem 0.5rem;
+  font-size: 0.7em;
+  line-height: 1.5em;
 }
 .Select--small:after {
-    margin-top: -.1rem;
-    right: 0.5em;
+  margin-top: -0.1rem;
+  right: 0.5em;
 }
 .Select--small:before {
-    border-bottom: none;
+  border-bottom: none;
 }
 
 .Select select:focus {
diff --git a/frontend/src/metabase/css/components/table.css b/frontend/src/metabase/css/components/table.css
index d8876e21970c07db916c4c39434c8e88d46b9cb7..d995ae0b45f6d77b6f564838ca78c9368166ff59 100644
--- a/frontend/src/metabase/css/components/table.css
+++ b/frontend/src/metabase/css/components/table.css
@@ -1,6 +1,6 @@
 :root {
-  --table-border-color: rgba(213, 213, 213, .3);
-  --table-alt-bg-color: rgba(0, 0, 0, .02);
+  --table-border-color: rgba(213, 213, 213, 0.3);
+  --table-alt-bg-color: rgba(0, 0, 0, 0.02);
 
   --entity-image-small-size: 24px;
   --entity-image-large-size: 64px;
@@ -11,7 +11,9 @@
   which follows the suggested rendering and defaults to center, whereas
   chrome and others do not
 */
-th { text-align: left; }
+th {
+  text-align: left;
+}
 
 .Table {
   /* standard table reset */
diff --git a/frontend/src/metabase/css/containers/entity_search.css b/frontend/src/metabase/css/containers/entity_search.css
index ac8bf588aae4d1ddd87d61ec22b919bd81281230..af097cd0d875e309fbe0e66129a27af78ccd8433 100644
--- a/frontend/src/metabase/css/containers/entity_search.css
+++ b/frontend/src/metabase/css/containers/entity_search.css
@@ -1,34 +1,33 @@
 @media screen and (--breakpoint-min-md) {
-    .Entity-search-back-button {
-        position: absolute;
-        margin-left: -150px;
-    }
+  .Entity-search-back-button {
+    position: absolute;
+    margin-left: -150px;
+  }
 
-    .Entity-search-grouping-options {
-        position: absolute;
-        margin-left: -150px;
-        margin-top: 22px;
-    }
+  .Entity-search-grouping-options {
+    position: absolute;
+    margin-left: -150px;
+    margin-top: 22px;
+  }
 }
 
-
 @media screen and (--breakpoint-max-md) {
-    .Entity-search-grouping-options {
-        display: flex;
-        align-items: center;
-    }
-    .Entity-search-grouping-options > h3 {
-        margin-bottom: 0;
-        margin-right: 20px;
-    }
-    .Entity-search-grouping-options > ul {
-        display: flex;
-    }
-    .Entity-search-grouping-options > ul > li {
-        margin-right: 10px;
-    }
+  .Entity-search-grouping-options {
+    display: flex;
+    align-items: center;
+  }
+  .Entity-search-grouping-options > h3 {
+    margin-bottom: 0;
+    margin-right: 20px;
+  }
+  .Entity-search-grouping-options > ul {
+    display: flex;
+  }
+  .Entity-search-grouping-options > ul > li {
+    margin-right: 10px;
+  }
 }
 
 .Entity-search input {
-    width: 100%;
-}
\ No newline at end of file
+  width: 100%;
+}
diff --git a/frontend/src/metabase/css/core/arrow.css b/frontend/src/metabase/css/core/arrow.css
index facacae3f7276eea3ccd21dceb0033b205831263..f39e075313474487e7ceb1f68ce9d7ea78104857 100644
--- a/frontend/src/metabase/css/core/arrow.css
+++ b/frontend/src/metabase/css/core/arrow.css
@@ -1,21 +1,20 @@
-
 /* TODO: based on popover.css, combine them? */
 /* TODO: other arrow directions */
 
 .arrow-right {
-    position: relative; /* TODO: should it be up to the consumer to set a non-static positioning? */
+  position: relative; /* TODO: should it be up to the consumer to set a non-static positioning? */
 }
 
 /* shared arrow styles */
 .arrow-right:before,
 .arrow-right:after {
-	position: absolute;
-	content: '';
-	display: block;
-	border-left: 10px solid transparent;
-	border-right: 10px solid transparent;
-	border-top: 10px solid transparent;
-	border-bottom: 10px solid transparent;
+  position: absolute;
+  content: "";
+  display: block;
+  border-left: 10px solid transparent;
+  border-right: 10px solid transparent;
+  border-top: 10px solid transparent;
+  border-bottom: 10px solid transparent;
 }
 
 /* create a slightly larger arrow on the right for border purposes */
@@ -31,7 +30,8 @@
 }
 
 /* move our arrows to the center */
-.arrow-right:before, .arrow-right:after {
+.arrow-right:before,
+.arrow-right:after {
   top: 50%;
   margin-top: -10px;
 }
diff --git a/frontend/src/metabase/css/core/base.css b/frontend/src/metabase/css/core/base.css
index 2f631379bce964e98052ce8289e24aea15522d72..5f34dd5bab0b31cc42a47c2020451956b79a3609 100644
--- a/frontend/src/metabase/css/core/base.css
+++ b/frontend/src/metabase/css/core/base.css
@@ -5,23 +5,23 @@
 }
 
 html {
-    height: 100%; /* ensure the entire page will fill the window */
-    width: 100%;
+  height: 100%; /* ensure the entire page will fill the window */
+  width: 100%;
 }
 
 body {
-    font-family: var(--default-font-family), sans-serif;
-    font-size: var(--default-font-size);
-    font-weight: 400;
-    font-style: normal;
-    color: var(--default-font-color);
-    margin: 0;
-    height: 100%; /* ensure the entire page will fill the window */
-    display: flex;
-    flex-direction: column;
-
-    -webkit-font-smoothing: antialiased;
-    -moz-osx-font-smoothing: grayscale;
+  font-family: var(--default-font-family), sans-serif;
+  font-size: var(--default-font-size);
+  font-weight: 400;
+  font-style: normal;
+  color: var(--default-font-color);
+  margin: 0;
+  height: 100%; /* ensure the entire page will fill the window */
+  display: flex;
+  flex-direction: column;
+
+  -webkit-font-smoothing: antialiased;
+  -moz-osx-font-smoothing: grayscale;
 }
 
 /*
@@ -31,41 +31,44 @@ body {
 */
 ul,
 ol {
-    padding: 0;
-    margin: 0;
-    list-style-type: none;
+  padding: 0;
+  margin: 0;
+  list-style-type: none;
 }
 
 /* reset button element */
 button {
-    font-size: 100%;
-    -webkit-appearance: none;
-    border: 0;
-    padding: 0;
-    margin: 0;
-    outline: none;
+  font-size: 100%;
+  -webkit-appearance: none;
+  border: 0;
+  padding: 0;
+  margin: 0;
+  outline: none;
 }
 
 a {
-    color: inherit;
-    cursor: pointer;
+  color: inherit;
+  cursor: pointer;
 }
 
 input,
 textarea {
-    font-family: var(--default-font-family), "Helvetica Neue", Helvetica, sans-serif;
+  font-family: var(--default-font-family), "Helvetica Neue", Helvetica,
+    sans-serif;
 }
 
 .pointer-events-none {
   pointer-events: none;
 }
 
-.disabled, :local(.disabled) {
+.disabled,
+:local(.disabled) {
   pointer-events: none;
   opacity: 0.4;
 }
 
-.faded, :local(.faded) {
+.faded,
+:local(.faded) {
   opacity: 0.4;
 }
 
@@ -73,17 +76,22 @@ textarea {
   background-color: #f9fbfc;
 }
 
-.circle { border-radius: 99px; }
+.circle {
+  border-radius: 99px;
+}
 
 .undefined {
-    border: 1px solid red !important;
+  border: 1px solid red !important;
 }
 
 @keyframes spin {
-  100% { transform: rotate(360deg); }
+  100% {
+    transform: rotate(360deg);
+  }
 }
 
 @keyframes spin-reverse {
-  100% { transform: rotate(-360deg); }
+  100% {
+    transform: rotate(-360deg);
+  }
 }
-
diff --git a/frontend/src/metabase/css/core/bordered.css b/frontend/src/metabase/css/core/bordered.css
index 60e29c01f764bf236e9b7b7dcc0acdf47face6a4..fa653208337eee4e75c861ffdf9422a24c74b455 100644
--- a/frontend/src/metabase/css/core/bordered.css
+++ b/frontend/src/metabase/css/core/bordered.css
@@ -2,14 +2,16 @@
   --border-size: 1px;
   --border-size-med: 2px;
   --border-style: solid;
-  --border-color: #F0F0F0;
+  --border-color: #f0f0f0;
 }
 
-.bordered, :local(.bordered) {
+.bordered,
+:local(.bordered) {
   border: var(--border-size) var(--border-style) var(--border-color);
 }
 
-.border-bottom, :local(.border-bottom) {
+.border-bottom,
+:local(.border-bottom) {
   border-bottom: var(--border-size) var(--border-style) var(--border-color);
 }
 
@@ -18,7 +20,8 @@
   border-bottom: none;
 }
 
-.border-top, :local(.border-top) {
+.border-top,
+:local(.border-top) {
   border-top: var(--border-size) var(--border-style) var(--border-color);
 }
 
@@ -52,44 +55,49 @@
 }
 
 .border-light {
-    border-color: rgba(255,255,255,0.2) !important;
+  border-color: rgba(255, 255, 255, 0.2) !important;
 }
 
 .border-dark,
 .border-dark-hover:hover {
-    border-color: rgba(0,0,0,0.2) !important;
+  border-color: rgba(0, 0, 0, 0.2) !important;
 }
 
 .border-grey-1 {
-    border-color: var(--grey-1) !important;
+  border-color: var(--grey-1) !important;
+}
+.border-grey-2 {
+  border-color: var(--grey-2) !important;
 }
 
 .border-green {
-    border-color: var(--green-color) !important;
+  border-color: var(--green-color) !important;
 }
 
 .border-purple {
-    border-color: var(--purple-color) !important;
+  border-color: var(--purple-color) !important;
 }
 
-.border-error, :local(.border-error) {
-    border-color: var(--error-color) !important;
+.border-error,
+:local(.border-error) {
+  border-color: var(--error-color) !important;
 }
 
 .border-gold {
-    border-color: var(--gold-color) !important;
+  border-color: var(--gold-color) !important;
 }
 
 .border-success {
-    border-color: var(--success-color) !important;
+  border-color: var(--success-color) !important;
 }
 
-.border-brand, :local(.border-brand) {
-    border-color: var(--brand-color) !important;
+.border-brand,
+:local(.border-brand) {
+  border-color: var(--brand-color) !important;
 }
 
 .border-brand-hover:hover {
-    border-color: var(--brand-color);
+  border-color: var(--brand-color);
 }
 
 .border-hover:hover {
@@ -99,7 +107,8 @@
 /* BORDERLESS IS THE DEFAULT */
 /* ONLY USE IF needing to override an existing border! */
 /* ensure there is no border via important */
-.borderless, :local(.borderless) {
+.borderless,
+:local(.borderless) {
   border: none !important;
 }
 
@@ -108,5 +117,5 @@
 }
 
 .border-med {
-    border-width: var(--border-size-med);
+  border-width: var(--border-size-med);
 }
diff --git a/frontend/src/metabase/css/core/box_sizing.css b/frontend/src/metabase/css/core/box_sizing.css
index 0d39ed63ae5a61f13afeacd2bf99281c9d92eba8..5bb401fbcc0e93f4d29a7985ccc9d3273eb7e097 100644
--- a/frontend/src/metabase/css/core/box_sizing.css
+++ b/frontend/src/metabase/css/core/box_sizing.css
@@ -15,10 +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
+  box-sizing: border-box;
+}
diff --git a/frontend/src/metabase/css/core/clearfix.css b/frontend/src/metabase/css/core/clearfix.css
index 31655cc145d9577f7209d8a30a6709c050739571..46ede0656bcaf2cd966ed6e582257517c5d805da 100644
--- a/frontend/src/metabase/css/core/clearfix.css
+++ b/frontend/src/metabase/css/core/clearfix.css
@@ -11,12 +11,12 @@
  */
 .clearfix:before,
 .clearfix:after {
-    content: " "; /* 1 */
-    display: table; /* 2 */
+  content: " "; /* 1 */
+  display: table; /* 2 */
 }
 
 .clearfix:after {
-    clear: both;
+  clear: both;
 }
 
 /**
@@ -24,5 +24,5 @@
  * Include this rule to trigger hasLayout and contain floats.
  */
 .clearfix {
-    *zoom: 1;
+  *zoom: 1;
 }
diff --git a/frontend/src/metabase/css/core/colors.css b/frontend/src/metabase/css/core/colors.css
index b8afe3c68e74d4582edc096a351e0eba6ee431b2..a8aa1c24dfed918a35952e29283de7eb6d4cee13 100644
--- a/frontend/src/metabase/css/core/colors.css
+++ b/frontend/src/metabase/css/core/colors.css
@@ -1,9 +1,10 @@
 :root {
-  --brand-color: #509EE3;
-  --brand-light-color: #CDE3F8;
-  --brand-saturated-color: #2D86D4;
+  --brand-color: #509ee3;
+  --brand-light-color: #cde3f8;
+  --brand-saturated-color: #2d86d4;
 
-  --base-grey: #f8f8f8;
+  --base-grey: #f8f9fa;
+  --grey-5percent: color(var(--base-grey) shade(5%));
   --grey-1: color(var(--base-grey) shade(10%));
   --grey-2: color(var(--base-grey) shade(20%));
   --grey-3: color(var(--base-grey) shade(30%));
@@ -11,69 +12,84 @@
   --grey-5: color(var(--base-grey) shade(50%));
 
   --grey-text-color: #797979;
-  --alt-color: #F5F7F9;
-  --alt-bg-color: #F4F6F8;
-
-  --success-color: #9CC177;
-  --headsup-color: #F5A623;
-  --warning-color: #E35050;
-
-  --gold-color: #F9D45C;
-  --orange-color: #F9A354;
-  --purple-color: #A989C5;
-  --green-color: #9CC177;
-  --green-saturated-color: #84BB4C;
-  --dark-color: #4C545B;
-  --error-color: #EF8C8C;
-  --slate-color: #9BA5B1;
-  --slate-light-color: #DFE8EA;
-  --slate-almost-extra-light-color: #EDF2F5;
-  --slate-extra-light-color: #F9FBFC;
+  --alt-color: #f5f7f9;
+  --alt-bg-color: #f4f6f8;
+
+  --success-color: #9cc177;
+  --headsup-color: #f5a623;
+  --warning-color: #e35050;
+
+  --gold-color: #f9d45c;
+  --orange-color: #f9a354;
+  --purple-color: #a989c5;
+  --green-color: #9cc177;
+  --green-saturated-color: #84bb4c;
+  --dark-color: #4c545b;
+  --error-color: #ef8c8c;
+  --slate-color: #9ba5b1;
+  --slate-light-color: #dfe8ea;
+  --slate-almost-extra-light-color: #edf2f5;
+  --slate-extra-light-color: #f9fbfc;
 }
 
-.text-default, :local(.text-default) {
-    color: var(--default-font-color);
+.text-default,
+:local(.text-default) {
+  color: var(--default-font-color);
 }
 
 .text-default-hover:hover {
-    color: var(--default-font-color);
+  color: var(--default-font-color);
 }
 
-.text-danger { color: #EEA5A5; }
+.text-danger {
+  color: #eea5a5;
+}
 
 /* brand */
-.text-brand, :local(.text-brand),
-.text-brand-hover:hover, :local(.text-brand-hover):hover {
-    color: var(--brand-color);
+.text-brand,
+:local(.text-brand),
+.text-brand-hover:hover,
+:local(.text-brand-hover):hover {
+  color: var(--brand-color);
 }
 
 .text-brand-darken,
 .text-brand-darken-hover:hover {
-    color: color(var(--brand-color) shade(20%));
+  color: color(var(--brand-color) shade(20%));
 }
 
-.text-brand-light, :local(.text-brand-light),
-.text-brand-light-hover:hover, :local(.text-brand-light-hover):hover {
-    color: var(--brand-light-color);
+.text-brand-light,
+:local(.text-brand-light),
+.text-brand-light-hover:hover,
+:local(.text-brand-light-hover):hover {
+  color: var(--brand-light-color);
 }
 
 .bg-brand,
 .bg-brand-hover:hover,
-.bg-brand-active:active { background-color: var(--brand-color); }
+.bg-brand-active:active {
+  background-color: var(--brand-color);
+}
 
 @media screen and (--breakpoint-min-md) {
-  .md-bg-brand { background-color: var(--brand-color) !important; }
+  .md-bg-brand {
+    background-color: var(--brand-color) !important;
+  }
 }
 
-
 /* success */
 
-.text-success { color: var(--success-color); }
-.bg-success { background-color: var(--success-color); }
+.text-success {
+  color: var(--success-color);
+}
+.bg-success {
+  background-color: var(--success-color);
+}
 
 /* error */
 
-.text-error, :local(.text-error),
+.text-error,
+:local(.text-error),
 .text-error-hover:hover {
   color: var(--error-color);
 }
@@ -83,7 +99,7 @@
   background-color: var(--error-color);
 }
 .bg-error-input {
-  background-color: #FCE8E8
+  background-color: #fce8e8;
 }
 
 /* warning */
@@ -99,103 +115,174 @@
 /* favorite */
 .text-gold,
 .text-gold-hover:hover {
-    color: var(--gold-color);
+  color: var(--gold-color);
 }
 
 .text-purple,
 .text-purple-hover:hover {
-    color: var(--purple-color);
+  color: var(--purple-color);
 }
 
 .text-green,
 .text-green-hover:hover {
-    color: var(--green-color);
+  color: var(--green-color);
 }
 
 .text-green-saturated,
 .text-green-saturated-hover:hover {
-    color: var(--green-saturated-color);
+  color: var(--green-saturated-color);
 }
 
 .text-orange,
 .text-orange-hover:hover {
-    color: var(--orange-color);
+  color: var(--orange-color);
 }
 
-.text-slate { color: var(--slate-color); }
-.text-slate-light { color: var(--slate-light-color); }
-.text-slate-extra-light { background-color: var(--slate-extra-light-color); }
+.text-slate {
+  color: var(--slate-color);
+}
+.text-slate-light {
+  color: var(--slate-light-color);
+}
+.text-slate-extra-light {
+  background-color: var(--slate-extra-light-color);
+}
 
-.bg-gold { background-color: var(--gold-color); }
-.bg-purple { background-color: var(--purple-color); }
-.bg-green { background-color: var(--green-color); }
+.bg-gold {
+  background-color: var(--gold-color);
+}
+
+.bg-purple,
+.bg-purple-hover:hover {
+  background-color: var(--purple-color);
+}
+
+.bg-green {
+  background-color: var(--green-color);
+}
 
 /* alt */
-.bg-alt, .bg-alt-hover:hover { background-color: var(--alt-color); }
+.bg-alt,
+.bg-alt-hover:hover {
+  background-color: var(--alt-color);
+}
 
 /* grey */
-.text-grey-1, :local(.text-grey-1),
-.text-grey-1-hover:hover { color: var(--grey-1) }
+.text-grey-1,
+:local(.text-grey-1),
+.text-grey-1-hover:hover {
+  color: var(--grey-1);
+}
 
-.text-grey-2, :local(.text-grey-2),
-.text-grey-2-hover:hover { color: var(--grey-2) }
+.text-grey-2,
+:local(.text-grey-2),
+.text-grey-2-hover:hover {
+  color: var(--grey-2);
+}
 
-.text-grey-3, :local(.text-grey-3),
-.text-grey-3-hover:hover { color: var(--grey-3) }
+.text-grey-3,
+:local(.text-grey-3),
+.text-grey-3-hover:hover {
+  color: var(--grey-3);
+}
 
 .text-grey-4,
-.text-grey-4-hover:hover { color: var(--grey-4) }
+.text-grey-4-hover:hover {
+  color: var(--grey-4);
+}
 
 .text-grey-5,
-.text-grey-5-hover:hover { color: var(--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-grey-0-hover:hover {
+  background-color: var(--base-grey);
+}
+.bg-grey-05 {
+  background-color: var(--grey-5percent);
+}
+.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); }
-.bg-slate-almost-extra-light { background-color: var(--slate-almost-extra-light-color);}
-.bg-slate-extra-light { background-color: var(--slate-extra-light-color); }
-.bg-slate-extra-light-hover:hover { background-color: var(--slate-extra-light-color); }
+.bg-slate {
+  background-color: var(--slate-color);
+}
+.bg-slate-light {
+  background-color: var(--slate-light-color);
+}
+.bg-slate-almost-extra-light {
+  background-color: var(--slate-almost-extra-light-color);
+}
+.bg-slate-extra-light {
+  background-color: var(--slate-extra-light-color);
+}
+.bg-slate-extra-light-hover:hover {
+  background-color: var(--slate-extra-light-color);
+}
 
-.text-dark, :local(.text-dark) {
-    color: var(--dark-color);
+.text-dark,
+:local(.text-dark) {
+  color: var(--dark-color);
 }
 
 /* white  - move to bottom for specificity since its often used on hovers, etc */
-.text-white, :local(.text-white),
-.text-white-hover:hover { color: #fff; }
+.text-white,
+:local(.text-white),
+.text-white-hover:hover {
+  color: #fff;
+}
 
 @media screen and (--breakpoint-min-md) {
-  .md-text-white { color: #fff; }
+  .md-text-white {
+    color: #fff;
+  }
 }
 
 /* common pattern, background brand, text white when hovering or selected */
 .brand-hover:hover {
-    color: #fff;
-    background-color: var(--brand-color);
+  color: #fff;
+  background-color: var(--brand-color);
 }
 .brand-hover:hover * {
-    color: #fff;
+  color: #fff;
 }
 
-.bg-white, :local(.bg-white) { background-color: #fff; }
+.bg-white,
+:local(.bg-white) {
+  background-color: #fff;
+}
 
-.bg-light-blue { background-color: #F5FAFC; }
+.bg-light-blue {
+  background-color: #f5fafc;
+}
 
 .bg-light-blue-hover:hover {
-  background-color: #E4F0FA;
+  background-color: #e4f0fa;
 }
 
 .text-light-blue,
 .text-light-blue-hover:hover {
-  color: #CFE4F5
+  color: #cfe4f5;
+}
+.text-slate {
+  color: #606e7b;
 }
-.text-slate { color: #606E7B; }
 
-.bg-transparent { background-color: transparent }
+.bg-transparent {
+  background-color: transparent;
+}
diff --git a/frontend/src/metabase/css/core/cursor.css b/frontend/src/metabase/css/core/cursor.css
index 2de366c0f853a38828613c1e03978ef96e503597..3e50861fcc9a21c05b25cdf0302d29056a034939 100644
--- a/frontend/src/metabase/css/core/cursor.css
+++ b/frontend/src/metabase/css/core/cursor.css
@@ -1,7 +1,9 @@
-.cursor-pointer, :local(.cursor-pointer) {
-    cursor: pointer;
+.cursor-pointer,
+:local(.cursor-pointer) {
+  cursor: pointer;
 }
 
-.cursor-default, :local(.cursor-default) {
-    cursor: default;
+.cursor-default,
+:local(.cursor-default) {
+  cursor: default;
 }
diff --git a/frontend/src/metabase/css/core/flex.css b/frontend/src/metabase/css/core/flex.css
index 600321610734f0805c3244d8fa0544178565edd9..91ea33a7143d0c048fee1f5e7b41ffe5f75f233a 100644
--- a/frontend/src/metabase/css/core/flex.css
+++ b/frontend/src/metabase/css/core/flex.css
@@ -1,19 +1,23 @@
 /* provide flex utilities in lieu of float based layouts */
 
-.flex, :local(.flex) {
-    display: flex;
+.flex,
+:local(.flex) {
+  display: flex;
 }
 
-.flex-full, :local(.flex-full) {
-    flex: 1;
+.flex-full,
+:local(.flex-full) {
+  flex: 1;
 }
 
-.flex-half, :local(.flex-half) {
-    flex: 0.5;
+.flex-half,
+:local(.flex-half) {
+  flex: 0.5;
 }
 
-.flex-no-shrink, :local(.flex-no-shrink) {
-    flex-shrink: 0;
+.flex-no-shrink,
+:local(.flex-no-shrink) {
+  flex-shrink: 0;
 }
 
 /* The behavior of how `flex: <flex-grow>` sets flex-basis is inconsistent across
@@ -28,138 +32,169 @@
  *  a desired behavior that the element grows with its contents.
 */
 .flex-basis-auto {
-    flex-basis: auto;
+  flex-basis: auto;
 }
 
 .shrink-below-content-size {
-    /* W3C spec says:
+  /* 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;
+  min-width: 0;
 }
 
-.align-center, :local(.align-center) {
-    align-items: center;
+.align-center,
+:local(.align-center) {
+  align-items: center;
 }
 
-.align-baseline, :local(.align-baseline) {
-    align-items: baseline;
+.align-baseline,
+:local(.align-baseline) {
+  align-items: baseline;
 }
 
-.justify-center, :local(.justify-center) {
-    justify-content: center;
+.justify-center,
+:local(.justify-center) {
+  justify-content: center;
 }
 
 .justify-between {
-    justify-content: space-between;
+  justify-content: space-between;
 }
 
 .justify-end {
-    justify-content: flex-end;
+  justify-content: flex-end;
 }
 
 .align-start {
-    align-items: flex-start;
+  align-items: flex-start;
 }
 
 .align-end {
-    align-items: flex-end;
+  align-items: flex-end;
 }
 
-.align-self-end, :local(.align-self-end) {
-    align-self: flex-end;
+.align-self-end,
+:local(.align-self-end) {
+  align-self: flex-end;
 }
 
-.flex-align-right, :local(.flex-align-right) {
-    margin-left: auto;
+.flex-align-right,
+:local(.flex-align-right) {
+  margin-left: auto;
 }
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-flex-align-right { margin-left: auto; }
+  .sm-flex-align-right {
+    margin-left: auto;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-flex-align-right { margin-left: auto; }
+  .md-flex-align-right {
+    margin-left: auto;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .lg-flex-align-right { margin-left: auto; }
+  .lg-flex-align-right {
+    margin-left: auto;
+  }
 }
 
-.layout-centered, :local(.layout-centered) {
+.layout-centered,
+:local(.layout-centered) {
   align-items: center;
   justify-content: center;
 }
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-layout-centered {
-        align-items: center;
-        justify-content: center;
-    }
+  .sm-layout-centered {
+    align-items: center;
+    justify-content: center;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-layout-centered {
-        align-items: center;
-        justify-content: center;
-    }
+  .md-layout-centered {
+    align-items: center;
+    justify-content: center;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .lg-layout-centered {
-        align-items: center;
-        justify-content: center;
-    }
+  .lg-layout-centered {
+    align-items: center;
+    justify-content: center;
+  }
 }
 
-.flex-column { flex-direction: column; }
+.flex-column {
+  flex-direction: column;
+}
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-flex-column { flex-direction: column; }
+  .sm-flex-column {
+    flex-direction: column;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-flex-column { flex-direction: column; }
+  .md-flex-column {
+    flex-direction: column;
+  }
 }
 
-.flex-row, :local(.flex-row) {
+.flex-row,
+:local(.flex-row) {
   flex-direction: row;
 }
 
 .flex-wrap {
-    flex-wrap: wrap;
+  flex-wrap: wrap;
 }
 
-.flex-reverse { flex-direction: row-reverse; }
+.flex-reverse {
+  flex-direction: row-reverse;
+}
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-flex-reverse { flex-direction: row-reverse; }
+  .sm-flex-reverse {
+    flex-direction: row-reverse;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-flex-reverse { flex-direction: row-reverse; }
+  .md-flex-reverse {
+    flex-direction: row-reverse;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .lg-flex-reverse { flex-direction: row-reverse; }
+  .lg-flex-reverse {
+    flex-direction: row-reverse;
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .xl-flex-reverse { flex-direction: row-reverse; }
+  .xl-flex-reverse {
+    flex-direction: row-reverse;
+  }
 }
 
 .no-flex {
-    flex: 0 1 0%;
+  flex: 0 1 0%;
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-no-flex { flex: 0 !important; }
+  .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
+  max-width: 100%;
+}
diff --git a/frontend/src/metabase/css/core/float.css b/frontend/src/metabase/css/core/float.css
index 176f709c7340ea624016a16ed5b05eba71d0e4de..9e52aedcc53cb185baff18056a002c069ca7e45c 100644
--- a/frontend/src/metabase/css/core/float.css
+++ b/frontend/src/metabase/css/core/float.css
@@ -1,2 +1,8 @@
-.float-left, :local(.float-left)   { float: left; }
-.float-right, :local(.float-right) { float: right; }
+.float-left,
+:local(.float-left) {
+  float: left;
+}
+.float-right,
+:local(.float-right) {
+  float: right;
+}
diff --git a/frontend/src/metabase/css/core/grid.css b/frontend/src/metabase/css/core/grid.css
index dea70d422fdff7506ac610ce7d390aaed2dd6bc2..52e9b3c0b087b2aa26e149069aeb5c8ca1b7ec45 100644
--- a/frontend/src/metabase/css/core/grid.css
+++ b/frontend/src/metabase/css/core/grid.css
@@ -1,5 +1,4 @@
 :root {
-
 }
 
 .Grid {
@@ -15,7 +14,6 @@
   flex: 1;
 }
 
-
 .Grid--flexCells > .Grid-cell {
   display: flex;
 }
@@ -245,29 +243,29 @@
 }
 
 .Grid-cell.Cell--1of3 {
-    flex: 0 0 33.3333%;
+  flex: 0 0 33.3333%;
 }
 
 @media screen and (--breakpoint-min-sm) {
-    .Grid-cell.sm-Cell--1of3 {
-        flex: 0 0 33.3333%;
-    }
+  .Grid-cell.sm-Cell--1of3 {
+    flex: 0 0 33.3333%;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .Grid-cell.md-Cell--1of3 {
-        flex: 0 0 33.3333%;
-    }
+  .Grid-cell.md-Cell--1of3 {
+    flex: 0 0 33.3333%;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .Grid-cell.lg-Cell--1of3 {
-        flex: 0 0 33.3333%;
-    }
+  .Grid-cell.lg-Cell--1of3 {
+    flex: 0 0 33.3333%;
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .Grid-cell.xl-Cell--1of3 {
-        flex: 0 0 33.3333%;
-    }
+  .Grid-cell.xl-Cell--1of3 {
+    flex: 0 0 33.3333%;
+  }
 }
diff --git a/frontend/src/metabase/css/core/headings.css b/frontend/src/metabase/css/core/headings.css
index 4bebedff55d9dfad4ed0a55170de8cd3630762b7..164c6c5dc3e5b9e0783b05624b92a65d3adbbdc7 100644
--- a/frontend/src/metabase/css/core/headings.css
+++ b/frontend/src/metabase/css/core/headings.css
@@ -2,47 +2,101 @@
   --default-header-margin: 0;
 }
 
-h1, .h1,
-h2, .h2,
-h3, .h3,
-h4, .h4,
-h5, .h5,
-h6, .h6 {
+h1,
+.h1,
+h2,
+.h2,
+h3,
+.h3,
+h4,
+.h4,
+h5,
+.h5,
+h6,
+.h6 {
   font-weight: 700;
   margin-top: var(--default-header-margin);
   margin-bottom: var(--default-header-margin);
 }
 
-.h1 { font-size: 2em; }
-.h2 { font-size: 1.5em; }
-.h3 { font-size: 1.17em; }
-.h4 { font-size: 1.12em; }
-.h5 { font-size: .83em; }
-.h6 { font-size: .75em; }
+.h1 {
+  font-size: 2em;
+}
+.h2 {
+  font-size: 1.5em;
+}
+.h3 {
+  font-size: 1.17em;
+}
+.h4 {
+  font-size: 1.12em;
+}
+.h5 {
+  font-size: 0.83em;
+}
+.h6 {
+  font-size: 0.75em;
+}
 
 @media screen and (--breakpoint-min-sm) {
-  .sm-h1 { font-size: 2em; }
-  .sm-h2 { font-size: 1.5em; }
-  .sm-h3 { font-size: 1.17em; }
-  .sm-h4 { font-size: 1.12em; }
-  .sm-h5 { font-size: .83em; }
-  .sm-h6 { font-size: .75em; }
+  .sm-h1 {
+    font-size: 2em;
+  }
+  .sm-h2 {
+    font-size: 1.5em;
+  }
+  .sm-h3 {
+    font-size: 1.17em;
+  }
+  .sm-h4 {
+    font-size: 1.12em;
+  }
+  .sm-h5 {
+    font-size: 0.83em;
+  }
+  .sm-h6 {
+    font-size: 0.75em;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-  .md-h1 { font-size: 2em; }
-  .md-h2 { font-size: 1.5em; }
-  .md-h3 { font-size: 1.17em; }
-  .md-h4 { font-size: 1.12em; }
-  .md-h5 { font-size: .83em; }
-  .md-h6 { font-size: .75em; }
+  .md-h1 {
+    font-size: 2em;
+  }
+  .md-h2 {
+    font-size: 1.5em;
+  }
+  .md-h3 {
+    font-size: 1.17em;
+  }
+  .md-h4 {
+    font-size: 1.12em;
+  }
+  .md-h5 {
+    font-size: 0.83em;
+  }
+  .md-h6 {
+    font-size: 0.75em;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-  .lg-h1 { font-size: 2em; }
-  .lg-h2 { font-size: 1.5em; }
-  .lg-h3 { font-size: 1.17em; }
-  .lg-h4 { font-size: 1.12em; }
-  .lg-h5 { font-size: .83em; }
-  .lg-h6 { font-size: .75em; }
+  .lg-h1 {
+    font-size: 2em;
+  }
+  .lg-h2 {
+    font-size: 1.5em;
+  }
+  .lg-h3 {
+    font-size: 1.17em;
+  }
+  .lg-h4 {
+    font-size: 1.12em;
+  }
+  .lg-h5 {
+    font-size: 0.83em;
+  }
+  .lg-h6 {
+    font-size: 0.75em;
+  }
 }
diff --git a/frontend/src/metabase/css/core/hide.css b/frontend/src/metabase/css/core/hide.css
index 58ac6c1cdc5073c497ca5c53e55113b86fd78ace..ca7ba491495acff600ffbe02443e08e266ac936b 100644
--- a/frontend/src/metabase/css/core/hide.css
+++ b/frontend/src/metabase/css/core/hide.css
@@ -1,52 +1,80 @@
-.hide { display: none !important; }
-.show { display: inherit; }
+.hide {
+  display: none !important;
+}
+.show {
+  display: inherit;
+}
 
-.hidden { visibility: hidden; }
+.hidden {
+  visibility: hidden;
+}
 
 .sm-show,
 .md-show,
 .lg-show,
-.xl-show { display: none; }
+.xl-show {
+  display: none;
+}
 
 /* extra-small */
 
 @media screen and (--breakpoint-min-xs) {
-  .xs-hide { display: none !important; }
+  .xs-hide {
+    display: none !important;
+  }
 }
 @media screen and (--breakpoint-min-xs) {
-  .xs-show { display: inherit !important; }
+  .xs-show {
+    display: inherit !important;
+  }
 }
 
 /* small */
 
 @media screen and (--breakpoint-min-sm) {
-  .sm-hide { display: none !important; }
+  .sm-hide {
+    display: none !important;
+  }
 }
 @media screen and (--breakpoint-min-sm) {
-  .sm-show { display: inherit !important; }
+  .sm-show {
+    display: inherit !important;
+  }
 }
 
 /* medium */
 
 @media screen and (--breakpoint-min-md) {
-  .md-hide { display: none !important; }
+  .md-hide {
+    display: none !important;
+  }
 }
 @media screen and (--breakpoint-min-md) {
-  .md-show { display: inherit !important; }
+  .md-show {
+    display: inherit !important;
+  }
 }
 /* large */
 
 @media screen and (--breakpoint-min-lg) {
-  .lg-hide { display: none !important; }
+  .lg-hide {
+    display: none !important;
+  }
 }
 @media screen and (--breakpoint-min-lg) {
-  .lg-show { display: inherit !important; }
+  .lg-show {
+    display: inherit !important;
+  }
 }
 
 /* xl */
-@media screen and (--breakpoint-min-xl) h{
-  .xl-hide { display: none !important; }
+@media screen and (--breakpoint-min-xl) h {
+  .xl-hide {
+    display: none !important;
+  }
 }
 @media screen and (--breakpoint-min-xl) {
-  .xl-show { display: inherit !important; }
+  .xl-show {
+    display: inherit !important;
+  }
 }
diff --git a/frontend/src/metabase/css/core/hover.css b/frontend/src/metabase/css/core/hover.css
index 20dfa4219af9c970e282fd641cb2199b31a095ca..2d48da6c4f9d38739e44a05559988423a6a5849a 100644
--- a/frontend/src/metabase/css/core/hover.css
+++ b/frontend/src/metabase/css/core/hover.css
@@ -3,15 +3,23 @@
   hide and show a child element using display
 */
 .hover-parent.hover--display .hover-child,
-.hover-parent:hover.hover--display .hover-child--hidden { display: none; }
+.hover-parent:hover.hover--display .hover-child--hidden {
+  display: none;
+}
 
-.hover-parent:hover.hover--display .hover-child { display: block; }
+.hover-parent:hover.hover--display .hover-child {
+  display: block;
+}
 
 /*
   visibility
   hide and show a child element using visibility
 */
 .hover-parent.hover--visibility .hover-child,
-.hover-parent:hover.hover--visibility .hover-child--hidden { visibility: hidden; }
+.hover-parent:hover.hover--visibility .hover-child--hidden {
+  visibility: hidden;
+}
 
-.hover-parent:hover.hover--visibility .hover-child { visibility: visible; }
+.hover-parent:hover.hover--visibility .hover-child {
+  visibility: visible;
+}
diff --git a/frontend/src/metabase/css/core/index.css b/frontend/src/metabase/css/core/index.css
index 212ede6b6f4518561bb0be9aa489ad6fafad8e7d..72fef96f02b2dada5a3147212e82bcb6d5a2ffc3 100644
--- a/frontend/src/metabase/css/core/index.css
+++ b/frontend/src/metabase/css/core/index.css
@@ -1,24 +1,24 @@
-@import './arrow.css';
-@import './base.css';
-@import './bordered.css';
-@import './box_sizing.css';
-@import './breakpoints.css';
-@import './clearfix.css';
-@import './colors.css';
-@import './cursor.css';
-@import './flex.css';
-@import './float.css';
-@import './grid.css';
-@import './headings.css';
-@import './hide.css';
-@import './hover.css';
-@import './inputs.css';
-@import './layout.css';
-@import './link.css';
-@import './overflow.css';
-@import './rounded.css';
-@import './scroll.css';
-@import './shadow.css';
-@import './spacing.css';
-@import './text.css';
-@import './transitions.css';
+@import "./arrow.css";
+@import "./base.css";
+@import "./bordered.css";
+@import "./box_sizing.css";
+@import "./breakpoints.css";
+@import "./clearfix.css";
+@import "./colors.css";
+@import "./cursor.css";
+@import "./flex.css";
+@import "./float.css";
+@import "./grid.css";
+@import "./headings.css";
+@import "./hide.css";
+@import "./hover.css";
+@import "./inputs.css";
+@import "./layout.css";
+@import "./link.css";
+@import "./overflow.css";
+@import "./rounded.css";
+@import "./scroll.css";
+@import "./shadow.css";
+@import "./spacing.css";
+@import "./text.css";
+@import "./transitions.css";
diff --git a/frontend/src/metabase/css/core/inputs.css b/frontend/src/metabase/css/core/inputs.css
index 6d03467c34408d71bedc11a816f776a3cf1d90a8..db6c5b3b1939a16c96c1b5d12236b5455b043c17 100644
--- a/frontend/src/metabase/css/core/inputs.css
+++ b/frontend/src/metabase/css/core/inputs.css
@@ -1,16 +1,17 @@
 :root {
   --input-border-color: #d9d9d9;
-  --input-border-active-color: #4E82C0;
+  --input-border-active-color: #4e82c0;
   --input-border-radius: 4px;
 }
 
-.input, :local(.input) {
+.input,
+:local(.input) {
   color: var(--dark-color);
   font-size: 1.12em;
   padding: 0.75rem 0.75rem;
   border: 1px solid var(--input-border-color);
   border-radius: var(--input-border-radius);
-  transition: border .3s linear;
+  transition: border 0.3s linear;
 }
 
 /* React doesn't receive events from IE11:s input clear button so don't show it */
@@ -25,10 +26,11 @@
 }
 
 .input--focus,
-.input:focus, :local(.input):focus {
+.input:focus,
+:local(.input):focus {
   outline: none;
   border: 1px solid var(--input-border-active-color);
-  transition: border .3s linear;
+  transition: border 0.3s linear;
   color: #222;
 }
 
@@ -41,7 +43,7 @@
 }
 
 .input:disabled {
-  opacity: .5;
+  opacity: 0.5;
   cursor: not-allowed;
 }
 
@@ -53,4 +55,3 @@
 .input[type="search"] {
   -webkit-appearance: none;
 }
-
diff --git a/frontend/src/metabase/css/core/layout.css b/frontend/src/metabase/css/core/layout.css
index d5a68a934806232f4d24d661f872086ddbbcdfd4..e23e95c44994ea4c691830169af7ed73966b90e0 100644
--- a/frontend/src/metabase/css/core/layout.css
+++ b/frontend/src/metabase/css/core/layout.css
@@ -1,99 +1,144 @@
 :root {
-    --sm-width: 752px;
-    --md-width: 940px;
-    --lg-width: 1140px;
-    --xl-width: 1540px;
+  --sm-width: 752px;
+  --md-width: 940px;
+  --lg-width: 1140px;
+  --xl-width: 1540px;
 }
 
-.wrapper, :local(.wrapper) {
+.wrapper,
+:local(.wrapper) {
   width: 100%;
   margin: 0 auto;
 }
 
 @media screen and (--breakpoint-min-sm) {
-    .wrapper, :local(.wrapper) {
-        padding-left: 2em;
-        padding-right: 2em;
-    }
+  .wrapper,
+  :local(.wrapper) {
+    padding-left: 2em;
+    padding-right: 2em;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .wrapper, :local(.wrapper) {
-        padding-left: 3em;
-        padding-right: 3em;
-    }
+  .wrapper,
+  :local(.wrapper) {
+    padding-left: 3em;
+    padding-right: 3em;
+  }
 }
 
 /* set height full relative to the parent */
-.full-height, :local(.full-height) { height: 100%; }
+.full-height,
+:local(.full-height) {
+  height: 100%;
+}
 
 /* set height to that of the viewport */
-.viewport-height { height: 100vh; }
+.viewport-height {
+  height: 100vh;
+}
 
 /* display utilities */
 .block,
-:local(.block)        { display: block; }
+:local(.block) {
+  display: block;
+}
 
 @media screen and (--breakpoint-min-lg) {
-.lg-block { display: block; }
+  .lg-block {
+    display: block;
+  }
 }
 
 .inline,
-:local(.inline)       { display: inline; }
+:local(.inline) {
+  display: inline;
+}
 
 .inline-block,
-:local(.inline-block) { display: inline-block; }
+:local(.inline-block) {
+  display: inline-block;
+}
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-inline-block { display: inline-block; }
+  .sm-inline-block {
+    display: inline-block;
+  }
 }
 
-.table { display: table; }
+.table {
+  display: table;
+}
 
-.full, :local(.full) { width: 100%; }
-.half { width: 50%; }
+.full,
+:local(.full) {
+  width: 100%;
+}
+.half {
+  width: 50%;
+}
 
 /* position utilities */
-.fixed, :local(.fixed) {
+.fixed,
+:local(.fixed) {
   position: fixed;
 }
 
-.relative, :local(.relative) { position: relative; }
-.absolute, :local(.absolute) { position: absolute; }
+.relative,
+:local(.relative) {
+  position: relative;
+}
+.absolute,
+:local(.absolute) {
+  position: absolute;
+}
 
-.top, :local(.top)       { top: 0; }
-.right, :local(.right)   { right: 0; }
-.bottom, :local(.bottom) { bottom: 0; }
-.left, :local(.left)     { left: 0; }
+.top,
+:local(.top) {
+  top: 0;
+}
+.right,
+:local(.right) {
+  right: 0;
+}
+.bottom,
+:local(.bottom) {
+  bottom: 0;
+}
+.left,
+:local(.left) {
+  left: 0;
+}
 
 @media screen and (--breakpoint-min-md) {
-    .wrapper.wrapper--trim,
-    :local(.wrapper.wrapper--trim) {
-        max-width: var(--md-width);
-    }
+  .wrapper.wrapper--trim,
+  :local(.wrapper.wrapper--trim) {
+    max-width: var(--md-width);
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .wrapper.wrapper--small,
-    :local(.wrapper.wrapper--small) {
-        max-width: var(--sm-width);
-    }
+  .wrapper.wrapper--small,
+  :local(.wrapper.wrapper--small) {
+    max-width: var(--sm-width);
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
   .wrapper.lg-wrapper--trim {
-        max-width: var(--lg-width);
+    max-width: var(--lg-width);
   }
 }
 
 @media screen and (--breakpoint-min-xl) {
   .wrapper.lg-wrapper--trim {
-        max-width: var(--xl-width);
+    max-width: var(--xl-width);
   }
 }
 
 /* fully fit the parent element - use as a base for app-y pages like QB or settings */
-.spread, :local(.spread) {
+.spread,
+:local(.spread) {
   position: absolute;
   top: 0;
   left: 0;
diff --git a/frontend/src/metabase/css/core/link.css b/frontend/src/metabase/css/core/link.css
index 7618593680c342f625c93a7d2f242def97b707ce..e3e272244fc98bebe0873e8c93ee4d8d7c8ba34e 100644
--- a/frontend/src/metabase/css/core/link.css
+++ b/frontend/src/metabase/css/core/link.css
@@ -1,9 +1,10 @@
 :root {
-  --default-link-color: #4A90E2;
+  --default-link-color: #4a90e2;
 }
 
-.no-decoration, :local(.no-decoration) {
-    text-decoration: none;
+.no-decoration,
+:local(.no-decoration) {
+  text-decoration: none;
 }
 
 .link {
diff --git a/frontend/src/metabase/css/core/overflow.css b/frontend/src/metabase/css/core/overflow.css
index 66c551d2ab30f976a6832321810c8c4de48cd239..5bc078735264ad1cfdd5a352bbc29a10e080c8f4 100644
--- a/frontend/src/metabase/css/core/overflow.css
+++ b/frontend/src/metabase/css/core/overflow.css
@@ -1,7 +1,7 @@
 .overflow-auto {
-    overflow: auto;
+  overflow: auto;
 }
 
 .overflow-hidden {
-    overflow: hidden;
-}
\ No newline at end of file
+  overflow: hidden;
+}
diff --git a/frontend/src/metabase/css/core/rounded.css b/frontend/src/metabase/css/core/rounded.css
index 3d27f218a99693f0b7ac25c85682c36f9b647dff..8e0454f727082450b36ae2841195188c1252ea15 100644
--- a/frontend/src/metabase/css/core/rounded.css
+++ b/frontend/src/metabase/css/core/rounded.css
@@ -2,27 +2,28 @@
   --default-border-radius: 4px;
 }
 
-.rounded, :local(.rounded) {
+.rounded,
+:local(.rounded) {
   border-radius: var(--default-border-radius);
 }
 
 .rounded-top {
-  border-top-left-radius:  var(--default-border-radius);
+  border-top-left-radius: var(--default-border-radius);
   border-top-right-radius: var(--default-border-radius);
 }
 
 .rounded-bottom {
-  border-bottom-left-radius:  var(--default-border-radius);
+  border-bottom-left-radius: var(--default-border-radius);
   border-bottom-right-radius: var(--default-border-radius);
 }
 
 .rounded-left {
-  border-top-left-radius:    var(--default-border-radius);
+  border-top-left-radius: var(--default-border-radius);
   border-bottom-left-radius: var(--default-border-radius);
 }
 
 .rounded-right {
-  border-top-right-radius:    var(--default-border-radius);
+  border-top-right-radius: var(--default-border-radius);
   border-bottom-right-radius: var(--default-border-radius);
 }
 
diff --git a/frontend/src/metabase/css/core/scroll.css b/frontend/src/metabase/css/core/scroll.css
index 0199f45feca49af15b563d0e20c7f67513025688..dee6d79171864284c498c76baf37f16b89c7f0b1 100644
--- a/frontend/src/metabase/css/core/scroll.css
+++ b/frontend/src/metabase/css/core/scroll.css
@@ -1,73 +1,78 @@
-.scroll-y, :local(.scroll-y) { overflow-y: auto; }
-.scroll-x { overflow-x: auto; }
+.scroll-y,
+:local(.scroll-y) {
+  overflow-y: auto;
+}
+.scroll-x {
+  overflow-x: auto;
+}
 
 .scroll-show::-webkit-scrollbar {
-    width: 15px;
-    min-height: 10px;
+  width: 15px;
+  min-height: 10px;
 }
 
 .scroll-show--hover::-webkit-scrollbar {
-    display: none;
+  display: none;
 }
 .scroll-show--hover:hover::-webkit-scrollbar {
-    display: inherit;
+  display: inherit;
 }
 
 .scroll-show::-webkit-scrollbar-thumb {
-    border: 4px solid transparent;
-    border-radius: 7px;
-    background-clip: padding-box;
-    background-color: #c2c2c2;
+  border: 4px solid transparent;
+  border-radius: 7px;
+  background-clip: padding-box;
+  background-color: #c2c2c2;
 }
 
 .scroll-show::-webkit-scrollbar-button {
-    width: 0;
-    height: 0;
-    display: none;
+  width: 0;
+  height: 0;
+  display: none;
 }
 .scroll-show::-webkit-scrollbar-corner {
-    background-color: transparent;
+  background-color: transparent;
 }
 
 .scroll-show:hover::-webkit-scrollbar-thumb {
-    background-color: #7d7d7d;
+  background-color: #7d7d7d;
 }
 .scroll-show::-webkit-scrollbar-thumb:horizontal:hover,
 .scroll-show::-webkit-scrollbar-thumb:vertical:hover {
-    background-color: #7d7d7d;
+  background-color: #7d7d7d;
 }
 .scroll-show::-webkit-scrollbar-thumb:horizontal:active,
 .scroll-show::-webkit-scrollbar-thumb:vertical:active {
-    background-color: #7d7d7d;
+  background-color: #7d7d7d;
 }
 
 /* scroll light */
 .scroll-show.scroll--light::-webkit-scrollbar-thumb {
-    border-radius: 0;
-    background-color: #CFE4F5;
+  border-radius: 0;
+  background-color: #cfe4f5;
 }
 
 .scroll-show.scroll--light::-webkit-scrollbar-thumb:horizontal:hover,
 .scroll-show.scroll--light::-webkit-scrollbar-thumb:vertical:hover,
 .scroll-show.scroll--light::-webkit-scrollbar-thumb:horizontal:active,
 .scroll-show.scroll--light::-webkit-scrollbar-thumb:vertical:active {
-    background-color: #C7D9E4;
+  background-color: #c7d9e4;
 }
 
 .scroll-hide {
-    -ms-overflow-style: none;  /* IE 10+ */
-    overflow: -moz-scrollbars-none;  /* Firefox */
+  -ms-overflow-style: none; /* IE 10+ */
+  overflow: -moz-scrollbars-none; /* Firefox */
 }
 .scroll-hide::-webkit-scrollbar {
-    display: none; /* Safari and Chrome */
+  display: none; /* Safari and Chrome */
 }
 
 .scroll-hide-all,
 .scroll-hide-all * {
-    -ms-overflow-style: none;  /* IE 10+ */
-    overflow: -moz-scrollbars-none;  /* Firefox */
+  -ms-overflow-style: none; /* IE 10+ */
+  overflow: -moz-scrollbars-none; /* Firefox */
 }
 .scroll-hide-all::-webkit-scrollbar,
 .scroll-hide-all *::-webkit-scrollbar {
-    display: none; /* Safari and Chrome */
+  display: none; /* Safari and Chrome */
 }
diff --git a/frontend/src/metabase/css/core/shadow.css b/frontend/src/metabase/css/core/shadow.css
index 961a72bc7d2238ed8a0b307e7685c8044689e99c..23f04090f49652791d347d65d6e3d3e489d69af4 100644
--- a/frontend/src/metabase/css/core/shadow.css
+++ b/frontend/src/metabase/css/core/shadow.css
@@ -1,8 +1,9 @@
 :root {
-    --shadow-color: rgba(0, 0, 0, .08);
-    --shadow-hover-color: rgba(0, 0, 0, .12);
+  --shadow-color: rgba(0, 0, 0, 0.08);
+  --shadow-hover-color: rgba(0, 0, 0, 0.12);
 }
-.shadowed, :local(.shadowed) {
+.shadowed,
+:local(.shadowed) {
   box-shadow: 0 2px 2px var(--shadow-color);
 }
 
diff --git a/frontend/src/metabase/css/core/spacing.css b/frontend/src/metabase/css/core/spacing.css
index c6eb796c11fd993deec7054d69f03a45a1ec245a..83ee93691e9d133e0a3a229bb2bc97777ed3f651 100644
--- a/frontend/src/metabase/css/core/spacing.css
+++ b/frontend/src/metabase/css/core/spacing.css
@@ -10,874 +10,1444 @@
   --margin-4: 2rem;
 }
 
-.ml-auto, :local(.ml-auto) { margin-left: auto; }
-.mr-auto, :local(.mr-auto) { margin-right: auto; }
-.mt-auto { margin-top: auto; }
+.ml-auto,
+:local(.ml-auto) {
+  margin-left: auto;
+}
+.mr-auto,
+:local(.mr-auto) {
+  margin-right: auto;
+}
+.mt-auto {
+  margin-top: auto;
+}
 
 /* padding */
 
 /* 0 */
-.p0, :local(.p0)   { padding: 0; }
-.pt0, :local(.pt0) { padding-top: 0; }
-.pb0, :local(.pb0) { padding-bottom: 0; }
-.pl0, :local(.pl0) { padding-left: 0; }
-.pr0, :local(.pr0) { padding-right: 0; }
+.p0,
+:local(.p0) {
+  padding: 0;
+}
+.pt0,
+:local(.pt0) {
+  padding-top: 0;
+}
+.pb0,
+:local(.pb0) {
+  padding-bottom: 0;
+}
+.pl0,
+:local(.pl0) {
+  padding-left: 0;
+}
+.pr0,
+:local(.pr0) {
+  padding-right: 0;
+}
 
 /* 1 */
-.p1, :local(.p1) {
+.p1,
+:local(.p1) {
   padding: var(--padding-1);
 }
 
-.px1, :local(.px1) {
-  padding-left:  var(--padding-1);
+.px1,
+:local(.px1) {
+  padding-left: var(--padding-1);
   padding-right: var(--padding-1);
 }
 
-.py1, :local(.py1) {
-  padding-top:    var(--padding-1);
+.py1,
+:local(.py1) {
+  padding-top: var(--padding-1);
   padding-bottom: var(--padding-1);
 }
 
-.pt1, :local(.pt1) { padding-top:    var(--padding-1); }
-.pb1, :local(.pb1) { padding-bottom: var(--padding-1); }
-.pl1, :local(.pl1) { padding-left:   var(--padding-1); }
-.pr1, :local(.pr1) { padding-right:  var(--padding-1); }
+.pt1,
+:local(.pt1) {
+  padding-top: var(--padding-1);
+}
+.pb1,
+:local(.pb1) {
+  padding-bottom: var(--padding-1);
+}
+.pl1,
+:local(.pl1) {
+  padding-left: var(--padding-1);
+}
+.pr1,
+:local(.pr1) {
+  padding-right: var(--padding-1);
+}
 
 /* 2 */
 
-.p2, :local(.p2) { padding: var(--padding-2); }
+.p2,
+:local(.p2) {
+  padding: var(--padding-2);
+}
 
-.px2, :local(.px2) {
-  padding-left:  var(--padding-2);
+.px2,
+:local(.px2) {
+  padding-left: var(--padding-2);
   padding-right: var(--padding-2);
 }
 
-.py2, :local(.py2) {
-  padding-top:    var(--padding-2);
+.py2,
+:local(.py2) {
+  padding-top: var(--padding-2);
   padding-bottom: var(--padding-2);
 }
 
-.pt2, :local(.pt2) { padding-top:    var(--padding-2); }
-.pb2, :local(.pb2) { padding-bottom: var(--padding-2); }
-.pl2, :local(.pl2) { padding-left:   var(--padding-2); }
-.pr2, :local(.pr2) { padding-right:  var(--padding-2); }
+.pt2,
+:local(.pt2) {
+  padding-top: var(--padding-2);
+}
+.pb2,
+:local(.pb2) {
+  padding-bottom: var(--padding-2);
+}
+.pl2,
+:local(.pl2) {
+  padding-left: var(--padding-2);
+}
+.pr2,
+:local(.pr2) {
+  padding-right: var(--padding-2);
+}
 
 /* 3 */
 
-.p3, :local(.p3) { padding: var(--padding-3); }
+.p3,
+:local(.p3) {
+  padding: var(--padding-3);
+}
 
-.px3, :local(.px3) {
-  padding-left:  var(--padding-3);
+.px3,
+:local(.px3) {
+  padding-left: var(--padding-3);
   padding-right: var(--padding-3);
 }
 
-.py3, :local(.py3) {
-  padding-top:    var(--padding-3);
+.py3,
+:local(.py3) {
+  padding-top: var(--padding-3);
   padding-bottom: var(--padding-3);
 }
 
-.pt3, :local(.pt3) { padding-top:    var(--padding-3); }
-.pb3, :local(.pb3) { padding-bottom: var(--padding-3); }
-.pl3, :local(.pl3) { padding-left:   var(--padding-3); }
-.pr3, :local(.pr3) { padding-right:  var(--padding-3); }
-
+.pt3,
+:local(.pt3) {
+  padding-top: var(--padding-3);
+}
+.pb3,
+:local(.pb3) {
+  padding-bottom: var(--padding-3);
+}
+.pl3,
+:local(.pl3) {
+  padding-left: var(--padding-3);
+}
+.pr3,
+:local(.pr3) {
+  padding-right: var(--padding-3);
+}
 
 /* 4 */
 
-.p4, :local(.p4) { padding: var(--padding-4); }
+.p4,
+:local(.p4) {
+  padding: var(--padding-4);
+}
 
-.px4, :local(.px4) {
-  padding-left:  var(--padding-4);
+.px4,
+:local(.px4) {
+  padding-left: var(--padding-4);
   padding-right: var(--padding-4);
 }
 
-.py4, :local(.py4) {
-  padding-top:    var(--padding-4);
+.py4,
+:local(.py4) {
+  padding-top: var(--padding-4);
   padding-bottom: var(--padding-4);
 }
 
-.pt4, :local(.pt4) { padding-top:    var(--padding-4); }
-.pb4, :local(.pb4) { padding-bottom: var(--padding-4); }
-.pl4, :local(.pl4) { padding-left:   var(--padding-4); }
-.pr4, :local(.pr4) { padding-right:  var(--padding-4); }
-
+.pt4,
+:local(.pt4) {
+  padding-top: var(--padding-4);
+}
+.pb4,
+:local(.pb4) {
+  padding-bottom: var(--padding-4);
+}
+.pl4,
+:local(.pl4) {
+  padding-left: var(--padding-4);
+}
+.pr4,
+:local(.pr4) {
+  padding-right: var(--padding-4);
+}
 
 /* margin */
 
- /* 0 */
-.m0, :local(.m0)  { margin: 0; }
-.mt0, :local(.mt0) { margin-top: 0; }
-.mb0, :local(.mb0) { margin-bottom: 0; }
-.ml0, :local(.ml0) { margin-left: 0; }
-.mr0, :local(.mr0) { margin-right: 0; }
+/* 0 */
+.m0,
+:local(.m0) {
+  margin: 0;
+}
+.mt0,
+:local(.mt0) {
+  margin-top: 0;
+}
+.mb0,
+:local(.mb0) {
+  margin-bottom: 0;
+}
+.ml0,
+:local(.ml0) {
+  margin-left: 0;
+}
+.mr0,
+:local(.mr0) {
+  margin-right: 0;
+}
 
 /* 1 */
-.m1, :local(.m1) { margin: var(--margin-1); }
+.m1,
+:local(.m1) {
+  margin: var(--margin-1);
+}
 
-.mx1, :local(.mx1) {
-  margin-left:  var(--margin-1);
+.mx1,
+:local(.mx1) {
+  margin-left: var(--margin-1);
   margin-right: var(--margin-1);
 }
 
-.my1, :local(.my1) {
-  margin-top:    var(--margin-1);
+.my1,
+:local(.my1) {
+  margin-top: var(--margin-1);
   margin-bottom: var(--margin-1);
 }
 
-.mt1, :local(.mt1) { margin-top:    var(--margin-1); }
-.mb1, :local(.mb1) { margin-bottom: var(--margin-1); }
-.ml1, :local(.ml1) { margin-left:   var(--margin-1); }
-.mr1, :local(.mr1) { margin-right:  var(--margin-1); }
+.mt1,
+:local(.mt1) {
+  margin-top: var(--margin-1);
+}
+.mb1,
+:local(.mb1) {
+  margin-bottom: var(--margin-1);
+}
+.ml1,
+:local(.ml1) {
+  margin-left: var(--margin-1);
+}
+.mr1,
+:local(.mr1) {
+  margin-right: var(--margin-1);
+}
 
 /* 2 */
 
-.m2, :local(.m2) { margin: var(--margin-2); }
+.m2,
+:local(.m2) {
+  margin: var(--margin-2);
+}
 
-.mx2, :local(.mx2) {
-  margin-left:  var(--margin-2);
+.mx2,
+:local(.mx2) {
+  margin-left: var(--margin-2);
   margin-right: var(--margin-2);
 }
 
-.my2, :local(.my2) {
-  margin-top:    var(--margin-2);
+.my2,
+:local(.my2) {
+  margin-top: var(--margin-2);
   margin-bottom: var(--margin-2);
 }
 
-.mt2, :local(.mt2) { margin-top:    var(--margin-2); }
-.mb2, :local(.mb2) { margin-bottom: var(--margin-2); }
-.ml2, :local(.ml2) { margin-left:   var(--margin-2); }
-.mr2, :local(.mr2) { margin-right:  var(--margin-2); }
+.mt2,
+:local(.mt2) {
+  margin-top: var(--margin-2);
+}
+.mb2,
+:local(.mb2) {
+  margin-bottom: var(--margin-2);
+}
+.ml2,
+:local(.ml2) {
+  margin-left: var(--margin-2);
+}
+.mr2,
+:local(.mr2) {
+  margin-right: var(--margin-2);
+}
 
 /* 3 */
 
-.m3, :local(.m3) { margin: var(--margin-3); }
+.m3,
+:local(.m3) {
+  margin: var(--margin-3);
+}
 
-.mx3, :local(.mx3) {
-  margin-left:  var(--margin-3);
+.mx3,
+:local(.mx3) {
+  margin-left: var(--margin-3);
   margin-right: var(--margin-3);
 }
 
-.my3, :local(.my3) {
-  margin-top:    var(--padding-3);
+.my3,
+:local(.my3) {
+  margin-top: var(--padding-3);
   margin-bottom: var(--padding-3);
 }
 
-.mt3, :local(.mt3) { margin-top:    var(--margin-3); }
-.mb3, :local(.mb3) { margin-bottom: var(--margin-3); }
-.ml3, :local(.ml3) { margin-left:   var(--margin-3); }
-.mr3, :local(.mr3) { margin-right:  var(--margin-3); }
+.mt3,
+:local(.mt3) {
+  margin-top: var(--margin-3);
+}
+.mb3,
+:local(.mb3) {
+  margin-bottom: var(--margin-3);
+}
+.ml3,
+:local(.ml3) {
+  margin-left: var(--margin-3);
+}
+.mr3,
+:local(.mr3) {
+  margin-right: var(--margin-3);
+}
 
 /* 4 */
 
-.m4, :local(.m4) { margin: var(--margin-4); }
+.m4,
+:local(.m4) {
+  margin: var(--margin-4);
+}
 
-.mx4, :local(.mx4) {
-  margin-left:  var(--margin-4);
+.mx4,
+:local(.mx4) {
+  margin-left: var(--margin-4);
   margin-right: var(--margin-4);
 }
 
-.my4, :local(.my4) {
-  margin-top:    var(--margin-4);
+.my4,
+:local(.my4) {
+  margin-top: var(--margin-4);
   margin-bottom: var(--margin-4);
 }
 
-.mt4, :local(.mt4) { margin-top:    var(--margin-4); }
-.mb4, :local(.mb4) { margin-bottom: var(--margin-4); }
-.ml4, :local(.ml4) { margin-left:   var(--margin-4); }
-.mr4, :local(.mr4) { margin-right:  var(--margin-4); }
+.mt4,
+:local(.mt4) {
+  margin-top: var(--margin-4);
+}
+.mb4,
+:local(.mb4) {
+  margin-bottom: var(--margin-4);
+}
+.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) }
+.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 */
 
 @media screen and (--breakpoint-min-sm) {
-    /* padding */
-
-    /* 0 */
-    .sm-p0  { padding:        0; }
-    .sm-pt0 { padding-top:    0; }
-    .sm-pb0 { padding-bottom: 0; }
-    .sm-pl0 { padding-left:   0; }
-    .sm-pr0 { padding-right:  0; }
-
-    /* 1 */
-    .sm-p1 { padding: var(--padding-1); }
-
-    .sm-px1 {
-      padding-left:  var(--padding-1);
-      padding-right: var(--padding-1);
-    }
-
-    .sm-py1 {
-      padding-top:    var(--padding-1);
-      padding-bottom: var(--padding-1);
-    }
-
-    .sm-pt1 { padding-top:    var(--padding-1); }
-    .sm-pb1 { padding-bottom: var(--padding-1); }
-    .sm-pl1 { padding-left:   var(--padding-1); }
-    .sm-pr1 { padding-right:  var(--padding-1); }
-
-    /* 2 */
-
-    .sm-p2 { padding: var(--padding-2); }
-
-    .sm-px2 {
-      padding-left:  var(--padding-2);
-      padding-right: var(--padding-2);
-    }
-
-    .sm-py2 {
-      padding-top:    var(--padding-2);
-      padding-bottom: var(--padding-2);
-    }
-
-    .sm-pt2 { padding-top:    var(--padding-2); }
-    .sm-pb2 { padding-bottom: var(--padding-2); }
-    .sm-pl2 { padding-left:   var(--padding-2); }
-    .sm-pr2 { padding-right:  var(--padding-2); }
-
-    /* 3 */
-
-    .sm-p3 { padding: var(--padding-3); }
-
-    .sm-px3 {
-      padding-left:  var(--padding-3);
-      padding-right: var(--padding-3);
-    }
-
-    .sm-py3 {
-      padding-top:    var(--padding-3);
-      padding-bottom: var(--padding-3);
-    }
-
-    .sm-pt3 { padding-top:    var(--padding-3); }
-    .sm-pb3 { padding-bottom: var(--padding-3); }
-    .sm-pl3 { padding-left:   var(--padding-3); }
-    .sm-pr3 { padding-right:  var(--padding-3); }
-
-
-    /* 4 */
-
-    .sm-p4 { padding: var(--padding-4); }
-
-    .sm-px4 {
-      padding-left:  var(--padding-4);
-      padding-right: var(--padding-4);
-    }
-
-    .sm-py4 {
-      padding-top:    var(--padding-4);
-      padding-bottom: var(--padding-4);
-    }
-
-    .sm-pt4 { padding-top:    var(--padding-4); }
-    .sm-pb4 { padding-bottom: var(--padding-4); }
-    .sm-pl4 { padding-left:   var(--padding-4); }
-    .sm-pr4 { padding-right:  var(--padding-4); }
-
-
-    /* margin */
-
-     /* 0 */
-    .sm-m0  { margin:        0; }
-    .sm-mt0 { margin-top:    0; }
-    .sm-mb0 { margin-bottom: 0; }
-    .sm-ml0 { margin-left:   0; }
-    .sm-mr0 { margin-right:  0; }
-
-    /* 1 */
-    .sm-m1 { margin: var(--margin-1); }
-
-    .sm-mx1 {
-      margin-left:  var(--margin-1);
-      margin-right: var(--margin-1);
-    }
-
-    .sm-my1 {
-      margin-top:    var(--margin-1);
-      margin-bottom: var(--margin-1);
-    }
-
-    .sm-mt1 { margin-top:    var(--margin-1); }
-    .sm-mb1 { margin-bottom: var(--margin-1); }
-    .sm-ml1 { margin-left:   var(--margin-1); }
-    .sm-mr1 { margin-right:  var(--margin-1); }
-
-    /* 2 */
-
-    .sm-m2 { margin: var(--margin-2); }
-
-    .sm-mx2 {
-      margin-left:  var(--margin-2);
-      margin-right: var(--margin-2);
-    }
-
-    .sm-my2 {
-      margin-top:    var(--margin-2);
-      margin-bottom: var(--margin-2);
-    }
-
-    .sm-mt2 { margin-top:    var(--margin-2); }
-    .sm-mb2 { margin-bottom: var(--margin-2); }
-    .sm-ml2 { margin-left:   var(--margin-2); }
-    .sm-mr2 { margin-right:  var(--margin-2); }
-
-    /* 3 */
-
-    .sm-m3 { margin: var(--margin-3); }
-
-    .sm-mx3 {
-      margin-left:  var(--margin-3);
-      margin-right: var(--margin-3);
-    }
-
-    .sm-my3 {
-      margin-top:    var(--padding-3);
-      margin-bottom: var(--padding-3);
-    }
-
-    .sm-mt3 { margin-top:    var(--margin-3); }
-    .sm-mb3 { margin-bottom: var(--margin-3); }
-    .sm-ml3 { margin-left:   var(--margin-3); }
-    .sm-mr3 { margin-right:  var(--margin-3); }
-
-    /* 4 */
-
-    .sm-m4 { margin: var(--margin-4); }
-
-    .sm-mx4 {
-      margin-left:  var(--margin-4);
-      margin-right: var(--margin-4);
-    }
-
-    .sm-my4 {
-      margin-top:    var(--margin-4);
-      margin-bottom: var(--margin-4);
-    }
-
-    .sm-mt4 { margin-top:    var(--margin-4); }
-    .sm-mb4 { margin-bottom: var(--margin-4); }
-    .sm-ml4 { margin-left:   var(--margin-4); }
-    .sm-mr4 { margin-right:  var(--margin-4); }
+  /* padding */
+
+  /* 0 */
+  .sm-p0 {
+    padding: 0;
+  }
+  .sm-pt0 {
+    padding-top: 0;
+  }
+  .sm-pb0 {
+    padding-bottom: 0;
+  }
+  .sm-pl0 {
+    padding-left: 0;
+  }
+  .sm-pr0 {
+    padding-right: 0;
+  }
+
+  /* 1 */
+  .sm-p1 {
+    padding: var(--padding-1);
+  }
+
+  .sm-px1 {
+    padding-left: var(--padding-1);
+    padding-right: var(--padding-1);
+  }
+
+  .sm-py1 {
+    padding-top: var(--padding-1);
+    padding-bottom: var(--padding-1);
+  }
+
+  .sm-pt1 {
+    padding-top: var(--padding-1);
+  }
+  .sm-pb1 {
+    padding-bottom: var(--padding-1);
+  }
+  .sm-pl1 {
+    padding-left: var(--padding-1);
+  }
+  .sm-pr1 {
+    padding-right: var(--padding-1);
+  }
+
+  /* 2 */
+
+  .sm-p2 {
+    padding: var(--padding-2);
+  }
+
+  .sm-px2 {
+    padding-left: var(--padding-2);
+    padding-right: var(--padding-2);
+  }
+
+  .sm-py2 {
+    padding-top: var(--padding-2);
+    padding-bottom: var(--padding-2);
+  }
+
+  .sm-pt2 {
+    padding-top: var(--padding-2);
+  }
+  .sm-pb2 {
+    padding-bottom: var(--padding-2);
+  }
+  .sm-pl2 {
+    padding-left: var(--padding-2);
+  }
+  .sm-pr2 {
+    padding-right: var(--padding-2);
+  }
+
+  /* 3 */
+
+  .sm-p3 {
+    padding: var(--padding-3);
+  }
+
+  .sm-px3 {
+    padding-left: var(--padding-3);
+    padding-right: var(--padding-3);
+  }
+
+  .sm-py3 {
+    padding-top: var(--padding-3);
+    padding-bottom: var(--padding-3);
+  }
+
+  .sm-pt3 {
+    padding-top: var(--padding-3);
+  }
+  .sm-pb3 {
+    padding-bottom: var(--padding-3);
+  }
+  .sm-pl3 {
+    padding-left: var(--padding-3);
+  }
+  .sm-pr3 {
+    padding-right: var(--padding-3);
+  }
+
+  /* 4 */
+
+  .sm-p4 {
+    padding: var(--padding-4);
+  }
+
+  .sm-px4 {
+    padding-left: var(--padding-4);
+    padding-right: var(--padding-4);
+  }
+
+  .sm-py4 {
+    padding-top: var(--padding-4);
+    padding-bottom: var(--padding-4);
+  }
+
+  .sm-pt4 {
+    padding-top: var(--padding-4);
+  }
+  .sm-pb4 {
+    padding-bottom: var(--padding-4);
+  }
+  .sm-pl4 {
+    padding-left: var(--padding-4);
+  }
+  .sm-pr4 {
+    padding-right: var(--padding-4);
+  }
+
+  /* margin */
+
+  /* 0 */
+  .sm-m0 {
+    margin: 0;
+  }
+  .sm-mt0 {
+    margin-top: 0;
+  }
+  .sm-mb0 {
+    margin-bottom: 0;
+  }
+  .sm-ml0 {
+    margin-left: 0;
+  }
+  .sm-mr0 {
+    margin-right: 0;
+  }
+
+  /* 1 */
+  .sm-m1 {
+    margin: var(--margin-1);
+  }
+
+  .sm-mx1 {
+    margin-left: var(--margin-1);
+    margin-right: var(--margin-1);
+  }
+
+  .sm-my1 {
+    margin-top: var(--margin-1);
+    margin-bottom: var(--margin-1);
+  }
+
+  .sm-mt1 {
+    margin-top: var(--margin-1);
+  }
+  .sm-mb1 {
+    margin-bottom: var(--margin-1);
+  }
+  .sm-ml1 {
+    margin-left: var(--margin-1);
+  }
+  .sm-mr1 {
+    margin-right: var(--margin-1);
+  }
+
+  /* 2 */
+
+  .sm-m2 {
+    margin: var(--margin-2);
+  }
+
+  .sm-mx2 {
+    margin-left: var(--margin-2);
+    margin-right: var(--margin-2);
+  }
+
+  .sm-my2 {
+    margin-top: var(--margin-2);
+    margin-bottom: var(--margin-2);
+  }
+
+  .sm-mt2 {
+    margin-top: var(--margin-2);
+  }
+  .sm-mb2 {
+    margin-bottom: var(--margin-2);
+  }
+  .sm-ml2 {
+    margin-left: var(--margin-2);
+  }
+  .sm-mr2 {
+    margin-right: var(--margin-2);
+  }
+
+  /* 3 */
+
+  .sm-m3 {
+    margin: var(--margin-3);
+  }
+
+  .sm-mx3 {
+    margin-left: var(--margin-3);
+    margin-right: var(--margin-3);
+  }
+
+  .sm-my3 {
+    margin-top: var(--padding-3);
+    margin-bottom: var(--padding-3);
+  }
+
+  .sm-mt3 {
+    margin-top: var(--margin-3);
+  }
+  .sm-mb3 {
+    margin-bottom: var(--margin-3);
+  }
+  .sm-ml3 {
+    margin-left: var(--margin-3);
+  }
+  .sm-mr3 {
+    margin-right: var(--margin-3);
+  }
+
+  /* 4 */
+
+  .sm-m4 {
+    margin: var(--margin-4);
+  }
+
+  .sm-mx4 {
+    margin-left: var(--margin-4);
+    margin-right: var(--margin-4);
+  }
+
+  .sm-my4 {
+    margin-top: var(--margin-4);
+    margin-bottom: var(--margin-4);
+  }
+
+  .sm-mt4 {
+    margin-top: var(--margin-4);
+  }
+  .sm-mb4 {
+    margin-bottom: var(--margin-4);
+  }
+  .sm-ml4 {
+    margin-left: var(--margin-4);
+  }
+  .sm-mr4 {
+    margin-right: var(--margin-4);
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    /* padding */
-
-    /* 0 */
-    .md-p0  { padding:        0; }
-    .md-pt0 { padding-top:    0; }
-    .md-pb0 { padding-bottom: 0; }
-    .md-pl0 { padding-left:   0; }
-    .md-pr0 { padding-right:  0; }
-
-    /* 1 */
-    .md-p1 { padding: var(--padding-1); }
-
-    .md-px1 {
-      padding-left:  var(--padding-1);
-      padding-right: var(--padding-1);
-    }
-
-    .md-py1 {
-      padding-top:    var(--padding-1);
-      padding-bottom: var(--padding-1);
-    }
-
-    .md-pt1 { padding-top:    var(--padding-1); }
-    .md-pb1 { padding-bottom: var(--padding-1); }
-    .md-pl1 { padding-left:   var(--padding-1); }
-    .md-pr1 { padding-right:  var(--padding-1); }
-
-    /* 2 */
-
-    .md-p2 { padding: var(--padding-2); }
-
-    .md-px2 {
-      padding-left:  var(--padding-2);
-      padding-right: var(--padding-2);
-    }
-
-    .md-py2 {
-      padding-top:    var(--padding-2);
-      padding-bottom: var(--padding-2);
-    }
-
-    .md-pt2 { padding-top:    var(--padding-2); }
-    .md-pb2 { padding-bottom: var(--padding-2); }
-    .md-pl2 { padding-left:   var(--padding-2); }
-    .md-pr2 { padding-right:  var(--padding-2); }
-
-    /* 3 */
-
-    .md-p3 { padding: var(--padding-3); }
-
-    .md-px3 {
-      padding-left:  var(--padding-3);
-      padding-right: var(--padding-3);
-    }
-
-    .md-py3 {
-      padding-top:    var(--padding-3);
-      padding-bottom: var(--padding-3);
-    }
-
-    .md-pt3 { padding-top:    var(--padding-3); }
-    .md-pb3 { padding-bottom: var(--padding-3); }
-    .md-pl3 { padding-left:   var(--padding-3); }
-    .md-pr3 { padding-right:  var(--padding-3); }
-
-
-    /* 4 */
-
-    .md-p4 { padding: var(--padding-4); }
-
-    .md-px4 {
-      padding-left:  var(--padding-4);
-      padding-right: var(--padding-4);
-    }
-
-    .md-py4 {
-      padding-top:    var(--padding-4);
-      padding-bottom: var(--padding-4);
-    }
-
-    .md-pt4 { padding-top:    var(--padding-4); }
-    .md-pb4 { padding-bottom: var(--padding-4); }
-    .md-pl4 { padding-left:   var(--padding-4); }
-    .md-pr4 { padding-right:  var(--padding-4); }
-
-
-    /* margin */
-
-     /* 0 */
-    .md-m0  { margin:        0; }
-    .md-mt0 { margin-top:    0; }
-    .md-mb0 { margin-bottom: 0; }
-    .md-ml0 { margin-left:   0; }
-    .md-mr0 { margin-right:  0; }
-
-    /* 1 */
-    .md-m1 { margin: var(--margin-1); }
-
-    .md-mx1 {
-      margin-left:  var(--margin-1);
-      margin-right: var(--margin-1);
-    }
-
-    .md-my1 {
-      margin-top:    var(--margin-1);
-      margin-bottom: var(--margin-1);
-    }
-
-    .md-mt1 { margin-top:    var(--margin-1); }
-    .md-mb1 { margin-bottom: var(--margin-1); }
-    .md-ml1 { margin-left:   var(--margin-1); }
-    .md-mr1 { margin-right:  var(--margin-1); }
-
-    /* 2 */
-
-    .md-m2 { margin: var(--margin-2); }
-
-    .md-mx2 {
-      margin-left:  var(--margin-2);
-      margin-right: var(--margin-2);
-    }
-
-    .md-my2 {
-      margin-top:    var(--margin-2);
-      margin-bottom: var(--margin-2);
-    }
-
-    .md-mt2 { margin-top:    var(--margin-2); }
-    .md-mb2 { margin-bottom: var(--margin-2); }
-    .md-ml2 { margin-left:   var(--margin-2); }
-    .md-mr2 { margin-right:  var(--margin-2); }
-
-    /* 3 */
-
-    .md-m3 { margin: var(--margin-3); }
-
-    .md-mx3 {
-      margin-left:  var(--margin-3);
-      margin-right: var(--margin-3);
-    }
-
-    .md-my3 {
-      margin-top:    var(--padding-3);
-      margin-bottom: var(--padding-3);
-    }
-
-    .md-mt3 { margin-top:    var(--margin-3); }
-    .md-mb3 { margin-bottom: var(--margin-3); }
-    .md-ml3 { margin-left:   var(--margin-3); }
-    .md-mr3 { margin-right:  var(--margin-3); }
-
-    /* 4 */
-
-    .md-m4 { margin: var(--margin-4); }
-
-    .md-mx4 {
-      margin-left:  var(--margin-4);
-      margin-right: var(--margin-4);
-    }
-
-    .md-my4 {
-      margin-top:    var(--margin-4);
-      margin-bottom: var(--margin-4);
-    }
-
-    .md-mt4 { margin-top:    var(--margin-4); }
-    .md-mb4 { margin-bottom: var(--margin-4); }
-    .md-ml4 { margin-left:   var(--margin-4); }
-    .md-mr4 { margin-right:  var(--margin-4); }
+  /* padding */
+
+  /* 0 */
+  .md-p0 {
+    padding: 0;
+  }
+  .md-pt0 {
+    padding-top: 0;
+  }
+  .md-pb0 {
+    padding-bottom: 0;
+  }
+  .md-pl0 {
+    padding-left: 0;
+  }
+  .md-pr0 {
+    padding-right: 0;
+  }
+
+  /* 1 */
+  .md-p1 {
+    padding: var(--padding-1);
+  }
+
+  .md-px1 {
+    padding-left: var(--padding-1);
+    padding-right: var(--padding-1);
+  }
+
+  .md-py1 {
+    padding-top: var(--padding-1);
+    padding-bottom: var(--padding-1);
+  }
+
+  .md-pt1 {
+    padding-top: var(--padding-1);
+  }
+  .md-pb1 {
+    padding-bottom: var(--padding-1);
+  }
+  .md-pl1 {
+    padding-left: var(--padding-1);
+  }
+  .md-pr1 {
+    padding-right: var(--padding-1);
+  }
+
+  /* 2 */
+
+  .md-p2 {
+    padding: var(--padding-2);
+  }
+
+  .md-px2 {
+    padding-left: var(--padding-2);
+    padding-right: var(--padding-2);
+  }
+
+  .md-py2 {
+    padding-top: var(--padding-2);
+    padding-bottom: var(--padding-2);
+  }
+
+  .md-pt2 {
+    padding-top: var(--padding-2);
+  }
+  .md-pb2 {
+    padding-bottom: var(--padding-2);
+  }
+  .md-pl2 {
+    padding-left: var(--padding-2);
+  }
+  .md-pr2 {
+    padding-right: var(--padding-2);
+  }
+
+  /* 3 */
+
+  .md-p3 {
+    padding: var(--padding-3);
+  }
+
+  .md-px3 {
+    padding-left: var(--padding-3);
+    padding-right: var(--padding-3);
+  }
+
+  .md-py3 {
+    padding-top: var(--padding-3);
+    padding-bottom: var(--padding-3);
+  }
+
+  .md-pt3 {
+    padding-top: var(--padding-3);
+  }
+  .md-pb3 {
+    padding-bottom: var(--padding-3);
+  }
+  .md-pl3 {
+    padding-left: var(--padding-3);
+  }
+  .md-pr3 {
+    padding-right: var(--padding-3);
+  }
+
+  /* 4 */
+
+  .md-p4 {
+    padding: var(--padding-4);
+  }
+
+  .md-px4 {
+    padding-left: var(--padding-4);
+    padding-right: var(--padding-4);
+  }
+
+  .md-py4 {
+    padding-top: var(--padding-4);
+    padding-bottom: var(--padding-4);
+  }
+
+  .md-pt4 {
+    padding-top: var(--padding-4);
+  }
+  .md-pb4 {
+    padding-bottom: var(--padding-4);
+  }
+  .md-pl4 {
+    padding-left: var(--padding-4);
+  }
+  .md-pr4 {
+    padding-right: var(--padding-4);
+  }
+
+  /* margin */
+
+  /* 0 */
+  .md-m0 {
+    margin: 0;
+  }
+  .md-mt0 {
+    margin-top: 0;
+  }
+  .md-mb0 {
+    margin-bottom: 0;
+  }
+  .md-ml0 {
+    margin-left: 0;
+  }
+  .md-mr0 {
+    margin-right: 0;
+  }
+
+  /* 1 */
+  .md-m1 {
+    margin: var(--margin-1);
+  }
+
+  .md-mx1 {
+    margin-left: var(--margin-1);
+    margin-right: var(--margin-1);
+  }
+
+  .md-my1 {
+    margin-top: var(--margin-1);
+    margin-bottom: var(--margin-1);
+  }
+
+  .md-mt1 {
+    margin-top: var(--margin-1);
+  }
+  .md-mb1 {
+    margin-bottom: var(--margin-1);
+  }
+  .md-ml1 {
+    margin-left: var(--margin-1);
+  }
+  .md-mr1 {
+    margin-right: var(--margin-1);
+  }
+
+  /* 2 */
+
+  .md-m2 {
+    margin: var(--margin-2);
+  }
+
+  .md-mx2 {
+    margin-left: var(--margin-2);
+    margin-right: var(--margin-2);
+  }
+
+  .md-my2 {
+    margin-top: var(--margin-2);
+    margin-bottom: var(--margin-2);
+  }
+
+  .md-mt2 {
+    margin-top: var(--margin-2);
+  }
+  .md-mb2 {
+    margin-bottom: var(--margin-2);
+  }
+  .md-ml2 {
+    margin-left: var(--margin-2);
+  }
+  .md-mr2 {
+    margin-right: var(--margin-2);
+  }
+
+  /* 3 */
+
+  .md-m3 {
+    margin: var(--margin-3);
+  }
+
+  .md-mx3 {
+    margin-left: var(--margin-3);
+    margin-right: var(--margin-3);
+  }
+
+  .md-my3 {
+    margin-top: var(--padding-3);
+    margin-bottom: var(--padding-3);
+  }
+
+  .md-mt3 {
+    margin-top: var(--margin-3);
+  }
+  .md-mb3 {
+    margin-bottom: var(--margin-3);
+  }
+  .md-ml3 {
+    margin-left: var(--margin-3);
+  }
+  .md-mr3 {
+    margin-right: var(--margin-3);
+  }
+
+  /* 4 */
+
+  .md-m4 {
+    margin: var(--margin-4);
+  }
+
+  .md-mx4 {
+    margin-left: var(--margin-4);
+    margin-right: var(--margin-4);
+  }
+
+  .md-my4 {
+    margin-top: var(--margin-4);
+    margin-bottom: var(--margin-4);
+  }
+
+  .md-mt4 {
+    margin-top: var(--margin-4);
+  }
+  .md-mb4 {
+    margin-bottom: var(--margin-4);
+  }
+  .md-ml4 {
+    margin-left: var(--margin-4);
+  }
+  .md-mr4 {
+    margin-right: var(--margin-4);
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    /* padding */
-
-    /* 0 */
-    .lg-p0  { padding:        0; }
-    .lg-pt0 { padding-top:    0; }
-    .lg-pb0 { padding-bottom: 0; }
-    .lg-pl0 { padding-left:   0; }
-    .lg-pr0 { padding-right:  0; }
-
-    /* 1 */
-    .lg-p1 { padding: var(--padding-1); }
-
-    .lg-px1 {
-      padding-left:  var(--padding-1);
-      padding-right: var(--padding-1);
-    }
-
-    .lg-py1 {
-      padding-top:    var(--padding-1);
-      padding-bottom: var(--padding-1);
-    }
-
-    .lg-pt1 { padding-top:    var(--padding-1); }
-    .lg-pb1 { padding-bottom: var(--padding-1); }
-    .lg-pl1 { padding-left:   var(--padding-1); }
-    .lg-pr1 { padding-right:  var(--padding-1); }
-
-    /* 2 */
-
-    .lg-p2 { padding: var(--padding-2); }
-
-    .lg-px2 {
-      padding-left:  var(--padding-2);
-      padding-right: var(--padding-2);
-    }
-
-    .lg-py2 {
-      padding-top:    var(--padding-2);
-      padding-bottom: var(--padding-2);
-    }
-
-    .lg-pt2 { padding-top:    var(--padding-2); }
-    .lg-pb2 { padding-bottom: var(--padding-2); }
-    .lg-pl2 { padding-left:   var(--padding-2); }
-    .lg-pr2 { padding-right:  var(--padding-2); }
-
-    /* 3 */
-
-    .lg-p3 { padding: var(--padding-3); }
-
-    .lg-px3 {
-      padding-left:  var(--padding-3);
-      padding-right: var(--padding-3);
-    }
-
-    .lg-py3 {
-      padding-top:    var(--padding-3);
-      padding-bottom: var(--padding-3);
-    }
-
-    .lg-pt3 { padding-top:    var(--padding-3); }
-    .lg-pb3 { padding-bottom: var(--padding-3); }
-    .lg-pl3 { padding-left:   var(--padding-3); }
-    .lg-pr3 { padding-right:  var(--padding-3); }
-
-
-    /* 4 */
-
-    .lg-p4 { padding: var(--padding-4); }
-
-    .lg-px4 {
-      padding-left:  var(--padding-4);
-      padding-right: var(--padding-4);
-    }
-
-    .lg-py4 {
-      padding-top:    var(--padding-4);
-      padding-bottom: var(--padding-4);
-    }
-
-    .lg-pt4 { padding-top:    var(--padding-4); }
-    .lg-pb4 { padding-bottom: var(--padding-4); }
-    .lg-pl4 { padding-left:   var(--padding-4); }
-    .lg-pr4 { padding-right:  var(--padding-4); }
-
-
-    /* margin */
-
-     /* 0 */
-    .lg-m0  { margin:        0; }
-    .lg-mt0 { margin-top:    0; }
-    .lg-mb0 { margin-bottom: 0; }
-    .lg-ml0 { margin-left:   0; }
-    .lg-mr0 { margin-right:  0; }
-
-    /* 1 */
-    .lg-m1 { margin: var(--margin-1); }
-
-    .lg-mx1 {
-      margin-left:  var(--margin-1);
-      margin-right: var(--margin-1);
-    }
-
-    .lg-my1 {
-      margin-top:    var(--margin-1);
-      margin-bottom: var(--margin-1);
-    }
-
-    .lg-mt1 { margin-top:    var(--margin-1); }
-    .lg-mb1 { margin-bottom: var(--margin-1); }
-    .lg-ml1 { margin-left:   var(--margin-1); }
-    .lg-mr1 { margin-right:  var(--margin-1); }
-
-    /* 2 */
-
-    .lg-m2 { margin: var(--margin-2); }
-
-    .lg-mx2 {
-      margin-left:  var(--margin-2);
-      margin-right: var(--margin-2);
-    }
-
-    .lg-my2 {
-      margin-top:    var(--margin-2);
-      margin-bottom: var(--margin-2);
-    }
-
-    .lg-mt2 { margin-top:    var(--margin-2); }
-    .lg-mb2 { margin-bottom: var(--margin-2); }
-    .lg-ml2 { margin-left:   var(--margin-2); }
-    .lg-mr2 { margin-right:  var(--margin-2); }
-
-    /* 3 */
-
-    .lg-m3 { margin: var(--margin-3); }
-
-    .lg-mx3 {
-      margin-left:  var(--margin-3);
-      margin-right: var(--margin-3);
-    }
-
-    .lg-my3 {
-      margin-top:    var(--padding-3);
-      margin-bottom: var(--padding-3);
-    }
-
-    .lg-mt3 { margin-top:    var(--margin-3); }
-    .lg-mb3 { margin-bottom: var(--margin-3); }
-    .lg-ml3 { margin-left:   var(--margin-3); }
-    .lg-mr3 { margin-right:  var(--margin-3); }
-
-    /* 4 */
-
-    .lg-m4 { margin: var(--margin-4); }
-
-    .lg-mx4 {
-      margin-left:  var(--margin-4);
-      margin-right: var(--margin-4);
-    }
-
-    .lg-my4 {
-      margin-top:    var(--margin-4);
-      margin-bottom: var(--margin-4);
-    }
-
-    .lg-mt4 { margin-top:    var(--margin-4); }
-    .lg-mb4 { margin-bottom: var(--margin-4); }
-    .lg-ml4 { margin-left:   var(--margin-4); }
-    .lg-mr4 { margin-right:  var(--margin-4); }
+  /* padding */
+
+  /* 0 */
+  .lg-p0 {
+    padding: 0;
+  }
+  .lg-pt0 {
+    padding-top: 0;
+  }
+  .lg-pb0 {
+    padding-bottom: 0;
+  }
+  .lg-pl0 {
+    padding-left: 0;
+  }
+  .lg-pr0 {
+    padding-right: 0;
+  }
+
+  /* 1 */
+  .lg-p1 {
+    padding: var(--padding-1);
+  }
+
+  .lg-px1 {
+    padding-left: var(--padding-1);
+    padding-right: var(--padding-1);
+  }
+
+  .lg-py1 {
+    padding-top: var(--padding-1);
+    padding-bottom: var(--padding-1);
+  }
+
+  .lg-pt1 {
+    padding-top: var(--padding-1);
+  }
+  .lg-pb1 {
+    padding-bottom: var(--padding-1);
+  }
+  .lg-pl1 {
+    padding-left: var(--padding-1);
+  }
+  .lg-pr1 {
+    padding-right: var(--padding-1);
+  }
+
+  /* 2 */
+
+  .lg-p2 {
+    padding: var(--padding-2);
+  }
+
+  .lg-px2 {
+    padding-left: var(--padding-2);
+    padding-right: var(--padding-2);
+  }
+
+  .lg-py2 {
+    padding-top: var(--padding-2);
+    padding-bottom: var(--padding-2);
+  }
+
+  .lg-pt2 {
+    padding-top: var(--padding-2);
+  }
+  .lg-pb2 {
+    padding-bottom: var(--padding-2);
+  }
+  .lg-pl2 {
+    padding-left: var(--padding-2);
+  }
+  .lg-pr2 {
+    padding-right: var(--padding-2);
+  }
+
+  /* 3 */
+
+  .lg-p3 {
+    padding: var(--padding-3);
+  }
+
+  .lg-px3 {
+    padding-left: var(--padding-3);
+    padding-right: var(--padding-3);
+  }
+
+  .lg-py3 {
+    padding-top: var(--padding-3);
+    padding-bottom: var(--padding-3);
+  }
+
+  .lg-pt3 {
+    padding-top: var(--padding-3);
+  }
+  .lg-pb3 {
+    padding-bottom: var(--padding-3);
+  }
+  .lg-pl3 {
+    padding-left: var(--padding-3);
+  }
+  .lg-pr3 {
+    padding-right: var(--padding-3);
+  }
+
+  /* 4 */
+
+  .lg-p4 {
+    padding: var(--padding-4);
+  }
+
+  .lg-px4 {
+    padding-left: var(--padding-4);
+    padding-right: var(--padding-4);
+  }
+
+  .lg-py4 {
+    padding-top: var(--padding-4);
+    padding-bottom: var(--padding-4);
+  }
+
+  .lg-pt4 {
+    padding-top: var(--padding-4);
+  }
+  .lg-pb4 {
+    padding-bottom: var(--padding-4);
+  }
+  .lg-pl4 {
+    padding-left: var(--padding-4);
+  }
+  .lg-pr4 {
+    padding-right: var(--padding-4);
+  }
+
+  /* margin */
+
+  /* 0 */
+  .lg-m0 {
+    margin: 0;
+  }
+  .lg-mt0 {
+    margin-top: 0;
+  }
+  .lg-mb0 {
+    margin-bottom: 0;
+  }
+  .lg-ml0 {
+    margin-left: 0;
+  }
+  .lg-mr0 {
+    margin-right: 0;
+  }
+
+  /* 1 */
+  .lg-m1 {
+    margin: var(--margin-1);
+  }
+
+  .lg-mx1 {
+    margin-left: var(--margin-1);
+    margin-right: var(--margin-1);
+  }
+
+  .lg-my1 {
+    margin-top: var(--margin-1);
+    margin-bottom: var(--margin-1);
+  }
+
+  .lg-mt1 {
+    margin-top: var(--margin-1);
+  }
+  .lg-mb1 {
+    margin-bottom: var(--margin-1);
+  }
+  .lg-ml1 {
+    margin-left: var(--margin-1);
+  }
+  .lg-mr1 {
+    margin-right: var(--margin-1);
+  }
+
+  /* 2 */
+
+  .lg-m2 {
+    margin: var(--margin-2);
+  }
+
+  .lg-mx2 {
+    margin-left: var(--margin-2);
+    margin-right: var(--margin-2);
+  }
+
+  .lg-my2 {
+    margin-top: var(--margin-2);
+    margin-bottom: var(--margin-2);
+  }
+
+  .lg-mt2 {
+    margin-top: var(--margin-2);
+  }
+  .lg-mb2 {
+    margin-bottom: var(--margin-2);
+  }
+  .lg-ml2 {
+    margin-left: var(--margin-2);
+  }
+  .lg-mr2 {
+    margin-right: var(--margin-2);
+  }
+
+  /* 3 */
+
+  .lg-m3 {
+    margin: var(--margin-3);
+  }
+
+  .lg-mx3 {
+    margin-left: var(--margin-3);
+    margin-right: var(--margin-3);
+  }
+
+  .lg-my3 {
+    margin-top: var(--padding-3);
+    margin-bottom: var(--padding-3);
+  }
+
+  .lg-mt3 {
+    margin-top: var(--margin-3);
+  }
+  .lg-mb3 {
+    margin-bottom: var(--margin-3);
+  }
+  .lg-ml3 {
+    margin-left: var(--margin-3);
+  }
+  .lg-mr3 {
+    margin-right: var(--margin-3);
+  }
+
+  /* 4 */
+
+  .lg-m4 {
+    margin: var(--margin-4);
+  }
+
+  .lg-mx4 {
+    margin-left: var(--margin-4);
+    margin-right: var(--margin-4);
+  }
+
+  .lg-my4 {
+    margin-top: var(--margin-4);
+    margin-bottom: var(--margin-4);
+  }
+
+  .lg-mt4 {
+    margin-top: var(--margin-4);
+  }
+  .lg-mb4 {
+    margin-bottom: var(--margin-4);
+  }
+  .lg-ml4 {
+    margin-left: var(--margin-4);
+  }
+  .lg-mr4 {
+    margin-right: var(--margin-4);
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    /* padding */
-
-    /* 0 */
-    .xl-p0  { padding:        0; }
-    .xl-pt0 { padding-top:    0; }
-    .xl-pb0 { padding-bottom: 0; }
-    .xl-pl0 { padding-left:   0; }
-    .xl-pr0 { padding-right:  0; }
-
-    /* 1 */
-    .xl-p1 { padding: var(--padding-1); }
-
-    .xl-px1 {
-      padding-left:  var(--padding-1);
-      padding-right: var(--padding-1);
-    }
-
-    .xl-py1 {
-      padding-top:    var(--padding-1);
-      padding-bottom: var(--padding-1);
-    }
-
-    .xl-pt1 { padding-top:    var(--padding-1); }
-    .xl-pb1 { padding-bottom: var(--padding-1); }
-    .xl-pl1 { padding-left:   var(--padding-1); }
-    .xl-pr1 { padding-right:  var(--padding-1); }
-
-    /* 2 */
-
-    .xl-p2 { padding: var(--padding-2); }
-
-    .xl-px2 {
-      padding-left:  var(--padding-2);
-      padding-right: var(--padding-2);
-    }
-
-    .xl-py2 {
-      padding-top:    var(--padding-2);
-      padding-bottom: var(--padding-2);
-    }
-
-    .xl-pt2 { padding-top:    var(--padding-2); }
-    .xl-pb2 { padding-bottom: var(--padding-2); }
-    .xl-pl2 { padding-left:   var(--padding-2); }
-    .xl-pr2 { padding-right:  var(--padding-2); }
-
-    /* 3 */
-
-    .xl-p3 { padding: var(--padding-3); }
-
-    .xl-px3 {
-      padding-left:  var(--padding-3);
-      padding-right: var(--padding-3);
-    }
-
-    .xl-py3 {
-      padding-top:    var(--padding-3);
-      padding-bottom: var(--padding-3);
-    }
-
-    .xl-pt3 { padding-top:    var(--padding-3); }
-    .xl-pb3 { padding-bottom: var(--padding-3); }
-    .xl-pl3 { padding-left:   var(--padding-3); }
-    .xl-pr3 { padding-right:  var(--padding-3); }
-
-
-    /* 4 */
-
-    .xl-p4 { padding: var(--padding-4); }
-
-    .xl-px4 {
-      padding-left:  var(--padding-4);
-      padding-right: var(--padding-4);
-    }
-
-    .xl-py4 {
-      padding-top:    var(--padding-4);
-      padding-bottom: var(--padding-4);
-    }
-
-    .xl-pt4 { padding-top:    var(--padding-4); }
-    .xl-pb4 { padding-bottom: var(--padding-4); }
-    .xl-pl4 { padding-left:   var(--padding-4); }
-    .xl-pr4 { padding-right:  var(--padding-4); }
-
-
-    /* margin */
-
-     /* 0 */
-    .xl-m0  { margin:        0; }
-    .xl-mt0 { margin-top:    0; }
-    .xl-mb0 { margin-bottom: 0; }
-    .xl-ml0 { margin-left:   0; }
-    .xl-mr0 { margin-right:  0; }
-
-    /* 1 */
-    .xl-m1 { margin: var(--margin-1); }
-
-    .xl-mx1 {
-      margin-left:  var(--margin-1);
-      margin-right: var(--margin-1);
-    }
-
-    .xl-my1 {
-      margin-top:    var(--margin-1);
-      margin-bottom: var(--margin-1);
-    }
-
-    .xl-mt1 { margin-top:    var(--margin-1); }
-    .xl-mb1 { margin-bottom: var(--margin-1); }
-    .xl-ml1 { margin-left:   var(--margin-1); }
-    .xl-mr1 { margin-right:  var(--margin-1); }
-
-    /* 2 */
-
-    .xl-m2 { margin: var(--margin-2); }
-
-    .xl-mx2 {
-      margin-left:  var(--margin-2);
-      margin-right: var(--margin-2);
-    }
-
-    .xl-my2 {
-      margin-top:    var(--margin-2);
-      margin-bottom: var(--margin-2);
-    }
-
-    .xl-mt2 { margin-top:    var(--margin-2); }
-    .xl-mb2 { margin-bottom: var(--margin-2); }
-    .xl-ml2 { margin-left:   var(--margin-2); }
-    .xl-mr2 { margin-right:  var(--margin-2); }
-
-    /* 3 */
-
-    .xl-m3 { margin: var(--margin-3); }
-
-    .xl-mx3 {
-      margin-left:  var(--margin-3);
-      margin-right: var(--margin-3);
-    }
-
-    .xl-my3 {
-      margin-top:    var(--padding-3);
-      margin-bottom: var(--padding-3);
-    }
-
-    .xl-mt3 { margin-top:    var(--margin-3); }
-    .xl-mb3 { margin-bottom: var(--margin-3); }
-    .xl-ml3 { margin-left:   var(--margin-3); }
-    .xl-mr3 { margin-right:  var(--margin-3); }
-
-    /* 4 */
-
-    .xl-m4 { margin: var(--margin-4); }
-
-    .xl-mx4 {
-      margin-left:  var(--margin-4);
-      margin-right: var(--margin-4);
-    }
-
-    .xl-my4 {
-      margin-top:    var(--margin-4);
-      margin-bottom: var(--margin-4);
-    }
-
-    .xl-mt4 { margin-top:    var(--margin-4); }
-    .xl-mb4 { margin-bottom: var(--margin-4); }
-    .xl-ml4 { margin-left:   var(--margin-4); }
-    .xl-mr4 { margin-right:  var(--margin-4); }
+  /* padding */
+
+  /* 0 */
+  .xl-p0 {
+    padding: 0;
+  }
+  .xl-pt0 {
+    padding-top: 0;
+  }
+  .xl-pb0 {
+    padding-bottom: 0;
+  }
+  .xl-pl0 {
+    padding-left: 0;
+  }
+  .xl-pr0 {
+    padding-right: 0;
+  }
+
+  /* 1 */
+  .xl-p1 {
+    padding: var(--padding-1);
+  }
+
+  .xl-px1 {
+    padding-left: var(--padding-1);
+    padding-right: var(--padding-1);
+  }
+
+  .xl-py1 {
+    padding-top: var(--padding-1);
+    padding-bottom: var(--padding-1);
+  }
+
+  .xl-pt1 {
+    padding-top: var(--padding-1);
+  }
+  .xl-pb1 {
+    padding-bottom: var(--padding-1);
+  }
+  .xl-pl1 {
+    padding-left: var(--padding-1);
+  }
+  .xl-pr1 {
+    padding-right: var(--padding-1);
+  }
+
+  /* 2 */
+
+  .xl-p2 {
+    padding: var(--padding-2);
+  }
+
+  .xl-px2 {
+    padding-left: var(--padding-2);
+    padding-right: var(--padding-2);
+  }
+
+  .xl-py2 {
+    padding-top: var(--padding-2);
+    padding-bottom: var(--padding-2);
+  }
+
+  .xl-pt2 {
+    padding-top: var(--padding-2);
+  }
+  .xl-pb2 {
+    padding-bottom: var(--padding-2);
+  }
+  .xl-pl2 {
+    padding-left: var(--padding-2);
+  }
+  .xl-pr2 {
+    padding-right: var(--padding-2);
+  }
+
+  /* 3 */
+
+  .xl-p3 {
+    padding: var(--padding-3);
+  }
+
+  .xl-px3 {
+    padding-left: var(--padding-3);
+    padding-right: var(--padding-3);
+  }
+
+  .xl-py3 {
+    padding-top: var(--padding-3);
+    padding-bottom: var(--padding-3);
+  }
+
+  .xl-pt3 {
+    padding-top: var(--padding-3);
+  }
+  .xl-pb3 {
+    padding-bottom: var(--padding-3);
+  }
+  .xl-pl3 {
+    padding-left: var(--padding-3);
+  }
+  .xl-pr3 {
+    padding-right: var(--padding-3);
+  }
+
+  /* 4 */
+
+  .xl-p4 {
+    padding: var(--padding-4);
+  }
+
+  .xl-px4 {
+    padding-left: var(--padding-4);
+    padding-right: var(--padding-4);
+  }
+
+  .xl-py4 {
+    padding-top: var(--padding-4);
+    padding-bottom: var(--padding-4);
+  }
+
+  .xl-pt4 {
+    padding-top: var(--padding-4);
+  }
+  .xl-pb4 {
+    padding-bottom: var(--padding-4);
+  }
+  .xl-pl4 {
+    padding-left: var(--padding-4);
+  }
+  .xl-pr4 {
+    padding-right: var(--padding-4);
+  }
+
+  /* margin */
+
+  /* 0 */
+  .xl-m0 {
+    margin: 0;
+  }
+  .xl-mt0 {
+    margin-top: 0;
+  }
+  .xl-mb0 {
+    margin-bottom: 0;
+  }
+  .xl-ml0 {
+    margin-left: 0;
+  }
+  .xl-mr0 {
+    margin-right: 0;
+  }
+
+  /* 1 */
+  .xl-m1 {
+    margin: var(--margin-1);
+  }
+
+  .xl-mx1 {
+    margin-left: var(--margin-1);
+    margin-right: var(--margin-1);
+  }
+
+  .xl-my1 {
+    margin-top: var(--margin-1);
+    margin-bottom: var(--margin-1);
+  }
+
+  .xl-mt1 {
+    margin-top: var(--margin-1);
+  }
+  .xl-mb1 {
+    margin-bottom: var(--margin-1);
+  }
+  .xl-ml1 {
+    margin-left: var(--margin-1);
+  }
+  .xl-mr1 {
+    margin-right: var(--margin-1);
+  }
+
+  /* 2 */
+
+  .xl-m2 {
+    margin: var(--margin-2);
+  }
+
+  .xl-mx2 {
+    margin-left: var(--margin-2);
+    margin-right: var(--margin-2);
+  }
+
+  .xl-my2 {
+    margin-top: var(--margin-2);
+    margin-bottom: var(--margin-2);
+  }
+
+  .xl-mt2 {
+    margin-top: var(--margin-2);
+  }
+  .xl-mb2 {
+    margin-bottom: var(--margin-2);
+  }
+  .xl-ml2 {
+    margin-left: var(--margin-2);
+  }
+  .xl-mr2 {
+    margin-right: var(--margin-2);
+  }
+
+  /* 3 */
+
+  .xl-m3 {
+    margin: var(--margin-3);
+  }
+
+  .xl-mx3 {
+    margin-left: var(--margin-3);
+    margin-right: var(--margin-3);
+  }
+
+  .xl-my3 {
+    margin-top: var(--padding-3);
+    margin-bottom: var(--padding-3);
+  }
+
+  .xl-mt3 {
+    margin-top: var(--margin-3);
+  }
+  .xl-mb3 {
+    margin-bottom: var(--margin-3);
+  }
+  .xl-ml3 {
+    margin-left: var(--margin-3);
+  }
+  .xl-mr3 {
+    margin-right: var(--margin-3);
+  }
+
+  /* 4 */
+
+  .xl-m4 {
+    margin: var(--margin-4);
+  }
+
+  .xl-mx4 {
+    margin-left: var(--margin-4);
+    margin-right: var(--margin-4);
+  }
+
+  .xl-my4 {
+    margin-top: var(--margin-4);
+    margin-bottom: var(--margin-4);
+  }
+
+  .xl-mt4 {
+    margin-top: var(--margin-4);
+  }
+  .xl-mb4 {
+    margin-bottom: var(--margin-4);
+  }
+  .xl-ml4 {
+    margin-left: var(--margin-4);
+  }
+  .xl-mr4 {
+    margin-right: var(--margin-4);
+  }
 }
diff --git a/frontend/src/metabase/css/core/text.css b/frontend/src/metabase/css/core/text.css
index ff86cebb0336cc93ebd4ad042b2cc2f4b872f81f..2cdd2b7eb1c1a088c7e05535068758840b008428 100644
--- a/frontend/src/metabase/css/core/text.css
+++ b/frontend/src/metabase/css/core/text.css
@@ -1,137 +1,186 @@
 :root {
-  --body-text-color: #8E9BA9;
+  --body-text-color: #8e9ba9;
   --70-percent-black: #444444;
 }
 
 /* center */
-.text-centered, :local(.text-centered) { text-align: center; }
+.text-centered,
+:local(.text-centered) {
+  text-align: center;
+}
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-text-centered { text-align: center; }
+  .sm-text-centered {
+    text-align: center;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-text-centered { text-align: center; }
+  .md-text-centered {
+    text-align: center;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .lg-text-centered { text-align: center; }
+  .lg-text-centered {
+    text-align: center;
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .xl-text-centered { text-align: center; }
+  .xl-text-centered {
+    text-align: center;
+  }
 }
 
 /* left */
 
-.text-left, :local(.text-left) { text-align: left; }
+.text-left,
+:local(.text-left) {
+  text-align: left;
+}
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-text-left { text-align: left; }
+  .sm-text-left {
+    text-align: left;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-text-left { text-align: left; }
+  .md-text-left {
+    text-align: left;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .lg-text-left { text-align: left; }
+  .lg-text-left {
+    text-align: left;
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .xl-text-left { text-align: left; }
+  .xl-text-left {
+    text-align: left;
+  }
 }
 
 /* right */
 
-.text-right, :local(.text-right) { text-align: right; }
+.text-right,
+:local(.text-right) {
+  text-align: right;
+}
 
 @media screen and (--breakpoint-min-sm) {
-    .sm-text-right { text-align: right; }
+  .sm-text-right {
+    text-align: right;
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-    .md-text-right { text-align: right; }
+  .md-text-right {
+    text-align: right;
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-    .lg-text-right { text-align: right; }
+  .lg-text-right {
+    text-align: right;
+  }
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .xl-text-right { text-align: right; }
+  .xl-text-right {
+    text-align: right;
+  }
 }
 
-.text-uppercase, :local(.text-uppercase) {
+.text-uppercase,
+:local(.text-uppercase) {
   text-transform: uppercase;
   letter-spacing: 0.06em;
 }
-.text-lowercase { text-transform: lowercase; }
+.text-lowercase {
+  text-transform: lowercase;
+}
 
-.text-capitalize { text-transform: capitalize; }
+.text-capitalize {
+  text-transform: capitalize;
+}
 
 /* text weight */
-.text-light  { font-weight: 300; }
-.text-normal { font-weight: 400; }
-.text-bold, :local(.text-bold)   { font-weight: 700; }
+.text-light {
+  font-weight: 300;
+}
+.text-normal {
+  font-weight: 400;
+}
+.text-bold,
+:local(.text-bold) {
+  font-weight: 700;
+}
 
 /* text style */
 
-.text-italic { font-style: italic; }
+.text-italic {
+  font-style: italic;
+}
 
 /* larger text size used for descriptions  */
-.text-body, :local(.text-body) {
+.text-body,
+:local(.text-body) {
   font-size: 1.286em;
   line-height: 1.457em;
   color: var(--body-text-color); /* TODO - is this bad? */
 }
 
-.text-paragraph, :local(.text-paragraph) {
+.text-paragraph,
+:local(.text-paragraph) {
   font-size: 1.143em;
   line-height: 1.5em;
 }
 
 .text-small {
-    font-size: 0.875em;
+  font-size: 0.875em;
 }
 
 .text-smaller {
-    font-size: 0.8em;
+  font-size: 0.8em;
 }
 
 .text-current {
-    color: currentColor;
+  color: currentColor;
 }
 
 .text-underline {
-    text-decoration: underline;
+  text-decoration: underline;
 }
 
 .text-underline-hover:hover {
-    text-decoration: underline;
+  text-decoration: underline;
 }
 
 .text-ellipsis {
-    text-overflow: ellipsis;
+  text-overflow: ellipsis;
 }
 
 .text-nowrap {
-    white-space: nowrap;
+  white-space: nowrap;
 }
 
 .text-code {
-    font-family: monospace;
-    color: #8691AC;
-    background-color: #E9F2F5;
-    border-radius: 2px;
-    padding: 0.2em 0.4em;
-    line-height: 1.4em;
-    white-space: pre;
+  font-family: monospace;
+  color: #8691ac;
+  background-color: #e9f2f5;
+  border-radius: 2px;
+  padding: 0.2em 0.4em;
+  line-height: 1.4em;
+  white-space: pre;
 }
 
 .text-monospace,
 :local(.text-monospace) {
-    font-family: Monaco, monospace;
+  font-family: Monaco, monospace;
 }
 
 .text-pre-wrap {
@@ -139,5 +188,5 @@
 }
 
 .text-measure {
-    max-width: 620px;
+  max-width: 620px;
 }
diff --git a/frontend/src/metabase/css/core/transitions.css b/frontend/src/metabase/css/core/transitions.css
index 77c7f40f648e8d13be14f7c9790cf86d57bfbe26..34b69a7ce78475f0cda15b815d45ef39ce728c2b 100644
--- a/frontend/src/metabase/css/core/transitions.css
+++ b/frontend/src/metabase/css/core/transitions.css
@@ -1,16 +1,18 @@
-.transition-color, :local(.transition-color) {
-  transition: color .3s linear;
+.transition-color,
+:local(.transition-color) {
+  transition: color 0.3s linear;
 }
-.transition-background, :local(.transition-background) {
-	transition: background .2s linear;
+.transition-background,
+:local(.transition-background) {
+  transition: background 0.2s linear;
 }
 .transition-shadow {
-  transition: box-shadow .2s linear;
+  transition: box-shadow 0.2s linear;
 }
 .transition-all {
-  transition: all .2s linear;
+  transition: all 0.2s linear;
 }
 
 .transition-border {
-  transition: border .3s linear;
+  transition: border 0.3s linear;
 }
diff --git a/frontend/src/metabase/css/dashboard.css b/frontend/src/metabase/css/dashboard.css
index 8dbc014cddfaca4b77452c0805d543a3f94ca434..98d17531dcf69b8752bd0a8f1f44f8a691f1b92a 100644
--- a/frontend/src/metabase/css/dashboard.css
+++ b/frontend/src/metabase/css/dashboard.css
@@ -2,7 +2,7 @@
   --night-mode-color: rgba(255, 255, 255, 0.86);
 }
 .Dashboard {
-    background-color: #f9fbfc;
+  background-color: #f9fbfc;
 }
 
 .DashboardHeader {
@@ -11,24 +11,30 @@
 }
 
 .Dash-wrapper {
-    width: 100%;
+  width: 100%;
 }
 
 @media screen and (--breakpoint-min-sm) {
-   .Dash-wrapper { max-width: var(--sm-width); }
+  .Dash-wrapper {
+    max-width: var(--sm-width);
+  }
 }
 
 @media screen and (--breakpoint-min-md) {
-   .Dash-wrapper { max-width: var(--md-width); }
+  .Dash-wrapper {
+    max-width: var(--md-width);
+  }
 }
 
 @media screen and (--breakpoint-min-lg) {
-   .Dash-wrapper { max-width: var(--lg-width); }
+  .Dash-wrapper {
+    max-width: var(--lg-width);
+  }
 }
 
 /* Fullscreen mode */
 .Dashboard.Dashboard--fullscreen .Header-button {
-  color: #D2DBE4;
+  color: #d2dbe4;
 }
 
 .Dashboard.Dashboard--fullscreen .DashboardHeader {
@@ -50,7 +56,7 @@
 
 .Dashboard.Dashboard--night .Header-button,
 .Dashboard.Dashboard--night .Header-button svg {
-    color: rgba(151, 151, 151, 0.3);
+  color: rgba(151, 151, 151, 0.3);
 }
 
 .Dashboard.Dashboard--fullscreen .fullscreen-normal-text {
@@ -104,29 +110,29 @@
 }
 
 .DashCard .Card.Card--slow {
-    border-color: var(--gold-color);
+  border-color: var(--gold-color);
 }
 
 .Dash--editing .DashCard .Card {
-    pointer-events: none;
-    transition: border .3s, background-color .3s;
+  pointer-events: none;
+  transition: border 0.3s, background-color 0.3s;
 }
 .Dash--editing.Dash--editingParameter .DashCard .Card {
-    pointer-events: auto;
+  pointer-events: auto;
 }
 
 @keyframes fade-out-yellow {
-    from {
-        background-color: rgba(255, 250, 243, 1.0);
-    }
-    to {
-        background-color: rgba(255, 255, 255, 1.0);
-    }
+  from {
+    background-color: rgba(255, 250, 243, 1);
+  }
+  to {
+    background-color: rgba(255, 255, 255, 1);
+  }
 }
 
 .Dash--editing .DashCard .Card.Card--recent {
-    animation-duration: 30s;
-    animation-name: fade-out-yellow;
+  animation-duration: 30s;
+  animation-name: fade-out-yellow;
 }
 
 .Dash--editing .DashCard:hover .Card .Card-heading {
@@ -159,32 +165,32 @@
 }
 
 .Dash--editing .DashCard .Card {
-    user-select: none;
+  user-select: none;
 }
 
 .DashCard .Card {
-    box-shadow: 0px 1px 3px rgba(0,0,0,0.08);
+  box-shadow: 0px 1px 3px rgba(0, 0, 0, 0.08);
 }
 
 .Dash--editing .DashCard.dragging .Card {
-    box-shadow: 3px 3px 8px rgba(0,0,0,0.1);
+  box-shadow: 3px 3px 8px rgba(0, 0, 0, 0.1);
 }
 
 .Dash--editing .DashCard.dragging,
 .Dash--editing .DashCard.resizing {
-    z-index: 2;
+  z-index: 2;
 }
 
 .Dash--editing .DashCard.dragging .Card,
 .Dash--editing .DashCard.resizing .Card {
-    background-color: #E5F1FB !important;
-    border: 1px solid var(--brand-color);
+  background-color: #e5f1fb !important;
+  border: 1px solid var(--brand-color);
 }
 
 .DashCard .DashCard-actions {
   pointer-events: none;
   opacity: 0;
-  transition: opacity .3s linear;
+  transition: opacity 0.3s linear;
 }
 
 .DashCard .DashCard-actions-persistent {
@@ -198,42 +204,42 @@
 }
 
 .Dash--editing .DashCard .Visualization-slow-spinner {
-    position: absolute;
-    right: -2px;
-    top: -2px;
+  position: absolute;
+  right: -2px;
+  top: -2px;
 }
 .Dash--editing .DashCard:hover .Visualization-slow-spinner {
-    opacity: 0;
-    transition: opacity .15s linear;
+  opacity: 0;
+  transition: opacity 0.15s linear;
 }
 
 .Dash--editing .DashCard.dragging .DashCard-actions,
 .Dash--editing .DashCard.resizing .DashCard-actions {
-    opacity: 0;
-    transition: opacity .3s linear;
+  opacity: 0;
+  transition: opacity 0.3s linear;
 }
 
 .Dash--editing .DashCard {
-    transition: transform .3s;
+  transition: transform 0.3s;
 }
 
 .Dash--editing .DashCard.dragging,
 .Dash--editing .DashCard.resizing {
-    transition: transform 0s;
+  transition: transform 0s;
 }
 
 .Dash--editing .DashCard {
-    cursor: move;
+  cursor: move;
 }
 
 .Dash--editing .DashCard .react-resizable-handle {
-    position: absolute;
-    width: 40px;
-    height: 40px;
-    bottom: 0;
-    right: 0;
-    cursor: nwse-resize;
-    z-index: 1; /* ensure the handle is above the card contents */
+  position: absolute;
+  width: 40px;
+  height: 40px;
+  bottom: 0;
+  right: 0;
+  cursor: nwse-resize;
+  z-index: 1; /* ensure the handle is above the card contents */
 }
 
 .Dash--editing .DashCard .react-resizable-handle:after {
@@ -246,7 +252,7 @@
   border-bottom: 2px solid color(var(--base-grey) shade(20%));
   border-right: 2px solid color(var(--base-grey) shade(20%));
   border-bottom-right-radius: 2px;
-  transition: opacity .2s;
+  transition: opacity 0.2s;
   opacity: 0.01;
 }
 
@@ -264,18 +270,18 @@
 }
 
 .Dash--editing .react-grid-placeholder {
-    z-index: 0;
-    background-color: #F2F2F2;
-    transition: all 0.15s linear;
+  z-index: 0;
+  background-color: #f2f2f2;
+  transition: all 0.15s linear;
 }
 
 .Dash--editing .Card-title {
-    pointer-events: none;
+  pointer-events: none;
 }
 
 /* ensure action buttons do not respond to events when dragging */
 .Dash--editing.Dash--dragging .DashCard-actions {
-    pointer-events: none !important;
+  pointer-events: none !important;
 }
 
 .Modal.AddSeriesModal {
@@ -296,9 +302,13 @@
   accomodate for viewing distance on TVs etc
 */
 @media screen and (min-width: 1280px) {
-  .Dashboard.Dashboard--fullscreen { font-size: 1.2em; }
+  .Dashboard.Dashboard--fullscreen {
+    font-size: 1.2em;
+  }
   /* keep the dashboard header title from being overwhelmingly large */
-  .Dashboard.Dashboard--fullscreen .Header-title-name { font-size: 1em; }
+  .Dashboard.Dashboard--fullscreen .Header-title-name {
+    font-size: 1em;
+  }
 
   .Dashboard.Dashboard--fullscreen .fullscreen-text-small .LegendItem {
     font-size: 1em;
@@ -306,40 +316,43 @@
 }
 
 @media screen and (min-width: 1540px) {
-  .Dashboard.Dashboard--fullscreen { font-size: 1.4em; }
+  .Dashboard.Dashboard--fullscreen {
+    font-size: 1.4em;
+  }
 }
 
-
 /* what for to print the dashboards */
 @media print {
-    header,
-    nav {
-        display: none;
-    }
-    .DashCard .Card {
-      box-shadow: none;
-      border-color: #a1a1a1;
-    }
-    /* improve label contrast */
-    .dc-chart .axis .tick text,
-    .dc-chart .x-axis-label,
-    .dc-chart .y-axis-label {
-      fill: #222222;
-    }
+  header,
+  nav {
+    display: none;
+  }
+  .DashCard .Card {
+    box-shadow: none;
+    border-color: #a1a1a1;
+  }
+  /* improve label contrast */
+  .dc-chart .axis .tick text,
+  .dc-chart .x-axis-label,
+  .dc-chart .y-axis-label {
+    fill: #222222;
+  }
 }
 
 @media print and (orientation: portrait) {
-    html {
-        width: 8.5in;
-    }
+  html {
+    width: 8.5in;
+  }
 }
 @media print and (orientation: landscape) {
-    html {
-        width: 11in;
-    }
+  html {
+    width: 11in;
+  }
 }
 
-@page { margin: 1cm; }
+@page {
+  margin: 1cm;
+}
 
 /* when in night mode goal lines should be more visible */
 .Dashboard--night .goal .line {
diff --git a/frontend/src/metabase/css/home.css b/frontend/src/metabase/css/home.css
index cbfb395bf09180447a2baacf169f0516a0e9c371..a083c96cac42dac73b0cc5cb1e5835a8f6d231c7 100644
--- a/frontend/src/metabase/css/home.css
+++ b/frontend/src/metabase/css/home.css
@@ -1,13 +1,11 @@
 .Nav {
-    z-index: 4;
+  z-index: 4;
 }
 
-
 .NavItem.NavItem--selected {
   background-color: rgba(0, 0, 0, 0.2);
 }
 
-
 .NavItem > .Icon {
   padding-left: 1em;
   padding-right: 1em;
@@ -17,68 +15,68 @@
 
 @media screen and (--breakpoint-min-sm) {
   .NavItem {
-      border-radius: 8px;
+    border-radius: 8px;
   }
   .NavItem:hover,
   .NavItem.NavItem--selected {
-      background-color: rgba(255, 255, 255, 0.08);
+    background-color: rgba(255, 255, 255, 0.08);
   }
 }
 
 .NavNewQuestion {
-    box-shadow: 0px 2px 2px 0px rgba(77, 136, 189, 0.69);
+  box-shadow: 0px 2px 2px 0px rgba(77, 136, 189, 0.69);
 }
 .NavNewQuestion:hover {
-    box-shadow: 0px 3px 2px 2px rgba(77, 136, 189, 0.75);
-    color: #3875AC;
+  box-shadow: 0px 3px 2px 2px rgba(77, 136, 189, 0.75);
+  color: #3875ac;
 }
 
 .Greeting {
-    padding-top: 2rem;
-    padding-bottom: 3rem;
+  padding-top: 2rem;
+  padding-bottom: 3rem;
 }
 
 @media screen and (--breakpoint-min-xl) {
-    .Greeting {
-        padding-top: 6em;
-        padding-bottom: 6em;
-    }
+  .Greeting {
+    padding-top: 6em;
+    padding-bottom: 6em;
+  }
 }
 
 .bullet {
-    position: relative;
-    margin-left: 1.2em;
+  position: relative;
+  margin-left: 1.2em;
 }
 .bullet:before {
-    content: "\2022";
-    color: #6FB0EB;
-    position: absolute;
-    top: 0;
-    margin-top: 16px;
-    left: -0.85em;
+  content: "\2022";
+  color: #6fb0eb;
+  position: absolute;
+  top: 0;
+  margin-top: 16px;
+  left: -0.85em;
 }
 
 .NavDropdown {
-    position: relative;
+  position: relative;
 }
 .NavDropdown.open {
-    z-index: 100;
+  z-index: 100;
 }
 .NavDropdown .NavDropdown-content {
-    display: none;
+  display: none;
 }
 .NavDropdown.open .NavDropdown-content {
-    display: inherit;
+  display: inherit;
 }
 .NavDropdown .NavDropdown-button {
-    position: relative;
-    border-radius: 8px;
+  position: relative;
+  border-radius: 8px;
 }
 .NavDropdown .NavDropdown-content {
-    position: absolute;
-    border-radius: 4px;
-    top: 38px;
-    min-width: 200px;
+  position: absolute;
+  border-radius: 4px;
+  top: 38px;
+  min-width: 200px;
 }
 
 .NavDropdown .NavDropdown-content.NavDropdown-content--dashboards {
@@ -87,110 +85,116 @@
 
 .NavDropdown .NavDropdown-button:before,
 .NavDropdown .NavDropdown-content:before {
-    content:"";
-    position: absolute;
-    top: 0;
-    left: 0;
-    width: 100%;
-    height: 100%;
-    box-shadow: 0 0 4px rgba(0, 0, 0, .12);
-    background-clip: padding-box;
+  content: "";
+  position: absolute;
+  top: 0;
+  left: 0;
+  width: 100%;
+  height: 100%;
+  box-shadow: 0 0 4px rgba(0, 0, 0, 0.12);
+  background-clip: padding-box;
 }
 
 .NavDropdown .NavDropdown-content:before {
-    z-index: -2;
-    border-radius: 4px;
+  z-index: -2;
+  border-radius: 4px;
 }
 .NavDropdown .NavDropdown-button:before {
-    z-index: -1;
-    opacity: 0;
-    border-radius: 8px;
+  z-index: -1;
+  opacity: 0;
+  border-radius: 8px;
 }
 .NavDropdown.open .NavDropdown-button:before {
-    opacity: 1;
+  opacity: 1;
 }
 .NavDropdown .NavDropdown-content-layer {
-    position: relative;
-    z-index: 1;
-    overflow: hidden;
+  position: relative;
+  z-index: 1;
+  overflow: hidden;
 }
 .NavDropdown .NavDropdown-button-layer {
-    position: relative;
-    z-index: 2;
+  position: relative;
+  z-index: 2;
 }
 
 .NavDropdown.open .NavDropdown-button,
 .NavDropdown .NavDropdown-content-layer {
-    background-color: #6FB0EB;
+  background-color: #6fb0eb;
 }
 
 .NavDropdown .NavDropdown-content-layer {
-    padding-top: 10px;
-    border-radius: 4px;
+  padding-top: 10px;
+  border-radius: 4px;
 }
 
 .NavDropdown .DashboardList {
-    min-width: 332px;
+  min-width: 332px;
 }
 
 .QuestionCircle {
-    display: inline-block;
-    font-size: 3.25rem;
-    width: 73px;
-    height: 73px;
-    border-radius: 99px;
-    border: 3px solid currentcolor;
-    text-align: center;
+  display: inline-block;
+  font-size: 3.25rem;
+  width: 73px;
+  height: 73px;
+  border-radius: 99px;
+  border: 3px solid currentcolor;
+  text-align: center;
 }
 
 .IconCircle {
-    line-height: 0;
-    padding: var(--padding-1);
-    border-radius: 99px;
-    border: 1px solid currentcolor;
+  line-height: 0;
+  padding: var(--padding-1);
+  border-radius: 99px;
+  border: 1px solid currentcolor;
 }
 
 @keyframes pop {
-    0% { transform: scale(0.75) }
-   75% { transform: scale(1.0625) }
-  100% { transform: scale(1) }
+  0% {
+    transform: scale(0.75);
+  }
+  75% {
+    transform: scale(1.0625);
+  }
+  100% {
+    transform: scale(1);
+  }
 }
 
 .animate-pop {
-    animation-name: popin;
-    animation-duration: .15s;
-    animation-timing-function: ease-out;
+  animation-name: popin;
+  animation-duration: 0.15s;
+  animation-timing-function: ease-out;
 }
 
 .AdminLink {
-    opacity: 0.435;
+  opacity: 0.435;
 }
 
 .AdminLink:hover {
-    opacity: 1;
+  opacity: 1;
 }
 
 .break-word {
-    word-wrap: break-word;
+  word-wrap: break-word;
 }
 
 .tooltip {
-    position: absolute;
-    background-color: #fff;
-    border-radius: 2px;
-    box-shadow: 1px 1px 1px rgba(0, 0, 0, .12);
-    color: #ddd;
+  position: absolute;
+  background-color: #fff;
+  border-radius: 2px;
+  box-shadow: 1px 1px 1px rgba(0, 0, 0, 0.12);
+  color: #ddd;
 }
 
 .TableDescription {
-    max-width: 42rem;
-    line-height: 1.4;
+  max-width: 42rem;
+  line-height: 1.4;
 }
 
 .Layout-sidebar {
   min-height: 100vh;
   width: 346px;
-  background-color: #F9FBFC;
+  background-color: #f9fbfc;
   border-left: 2px solid var(--border-color);
 }
 .Layout-mainColumn {
@@ -200,10 +204,10 @@
 }
 
 .Sidebar-header {
-    font-size: 13px;
-    letter-spacing: 0.5px;
-    line-height: 1;
-    text-transform: uppercase;
+  font-size: 13px;
+  letter-spacing: 0.5px;
+  line-height: 1;
+  text-transform: uppercase;
 }
 
 @media screen and (--breakpoint-min-md) {
@@ -213,11 +217,15 @@
 }
 
 /* there are 5 mav items on mobile, so distribute the items evenly */
-.Nav ul li { flex: 0 20%; }
+.Nav ul li {
+  flex: 0 20%;
+}
 
 /* on larger screens, things should just flow naturally */
 @media screen and (--breakpoint-min-md) {
-  .Nav ul li { flex: unset; }
+  .Nav ul li {
+    flex: unset;
+  }
 }
 
 /* the logo nav item needs a little bit of additional padding so that it
@@ -236,4 +244,3 @@
     padding-bottom: 0;
   }
 }
-
diff --git a/frontend/src/metabase/css/index.css b/frontend/src/metabase/css/index.css
index 940fbff597ca7ce42fc846324d2bd5f4d81a8e55..b8fffdf6275b9b4dff57cecad962d81e5519d97d 100644
--- a/frontend/src/metabase/css/index.css
+++ b/frontend/src/metabase/css/index.css
@@ -1,26 +1,26 @@
-@import './vendor.css';
+@import "./vendor.css";
 
-@import './core/index.css';
+@import "./core/index.css";
 
-@import './components/buttons.css';
-@import './components/dropdown.css';
-@import './components/form.css';
-@import './components/header.css';
-@import './components/icons.css';
-@import './components/list.css';
-@import './components/modal.css';
-@import './components/select.css';
-@import './components/table.css';
+@import "./components/buttons.css";
+@import "./components/dropdown.css";
+@import "./components/form.css";
+@import "./components/header.css";
+@import "./components/icons.css";
+@import "./components/list.css";
+@import "./components/modal.css";
+@import "./components/select.css";
+@import "./components/table.css";
 
-@import './containers/entity_search.css';
+@import "./containers/entity_search.css";
 
-@import './admin.css';
-@import './card.css';
-@import './dashboard.css';
-@import './home.css';
-@import './login.css';
-@import './pulse.css';
-@import './query_builder.css';
-@import './setup.css';
-@import './tutorial.css';
-@import './xray.css';
+@import "./admin.css";
+@import "./card.css";
+@import "./dashboard.css";
+@import "./home.css";
+@import "./login.css";
+@import "./pulse.css";
+@import "./query_builder.css";
+@import "./setup.css";
+@import "./tutorial.css";
+@import "./xray.css";
diff --git a/frontend/src/metabase/css/login.css b/frontend/src/metabase/css/login.css
index 10ab282d7f3c6fda41025543e650a13d046ef467..e30bee15dda3293b65e6ce227bf9435a8dc3e28a 100644
--- a/frontend/src/metabase/css/login.css
+++ b/frontend/src/metabase/css/login.css
@@ -1,148 +1,147 @@
 /* ensure our form doesn't get too wide */
 .Login-wrapper {
-    max-width: 1240px;
-    margin: 0 auto;
+  max-width: 1240px;
+  margin: 0 auto;
 }
 
 /* the login content should always sit on top of the illustration */
 .Login-content {
-    position: relative;
+  position: relative;
 }
 
 .Login-header {
-    color: #6A6A6A;
+  color: #6a6a6a;
 }
 
 .brand-scene {
-    overflow: hidden;
-    height: 180px;
+  overflow: hidden;
+  height: 180px;
 }
 
 .brand-boat-container {
-    position: absolute;
-    bottom: 0;
-    z-index: 6;
-    -webkit-animation: boat_trip 200s linear infinite;
-    margin-bottom: 0.5em;
+  position: absolute;
+  bottom: 0;
+  z-index: 6;
+  -webkit-animation: boat_trip 200s linear infinite;
+  margin-bottom: 0.5em;
 }
 
 .brand-boat {
-    transform-origin: 50% bottom;
-    animation: boat_rock 2s ease-in-out infinite;
-    animation-direction: alternate;
+  transform-origin: 50% bottom;
+  animation: boat_rock 2s ease-in-out infinite;
+  animation-direction: alternate;
 }
 
 @keyframes boat_trip {
-    0% {
-        margin-left: -2%;
-    }
-    10% {
-        margin-left: 5%;
-        transform: translate3d(0, 0, 2%);
-    }
-    100% {
-        margin-left: 120%;
-    }
+  0% {
+    margin-left: -2%;
+  }
+  10% {
+    margin-left: 5%;
+    transform: translate3d(0, 0, 2%);
+  }
+  100% {
+    margin-left: 120%;
+  }
 }
 
 @keyframes boat_lost {
-    0% {
-        margin-left: 40%;
-        transform: rotateY(0deg);
-    }
-    45% {
-        margin-left: 60%;
-        transform: rotateY(0deg);
-    }
-    50% {
-        margin-left: 60%;
-        transform: rotateY(180deg);
-    }
-    95% {
-        margin-left: 40%;
-        transform: rotateY(180deg);
-    }
-    100% {
-        margin-left: 40%;
-        transform: rotateY(0deg);
-    }
+  0% {
+    margin-left: 40%;
+    transform: rotateY(0deg);
+  }
+  45% {
+    margin-left: 60%;
+    transform: rotateY(0deg);
+  }
+  50% {
+    margin-left: 60%;
+    transform: rotateY(180deg);
+  }
+  95% {
+    margin-left: 40%;
+    transform: rotateY(180deg);
+  }
+  100% {
+    margin-left: 40%;
+    transform: rotateY(0deg);
+  }
 }
 
 @keyframes boat_rock {
-    from {
-        transform: rotate(-10deg);
-    }
-    to {
-        transform: rotate(10deg);
-    }
+  from {
+    transform: rotate(-10deg);
+  }
+  to {
+    transform: rotate(10deg);
+  }
 }
 
 .brand-illustration {
-    height: 180px;
-    position: absolute;
-    bottom: 15px;
-    z-index: 5;
-    margin: 0 auto;
-    display: flex;
+  height: 180px;
+  position: absolute;
+  bottom: 15px;
+  z-index: 5;
+  margin: 0 auto;
+  display: flex;
 }
 
 .brand-bridge {
-    margin-left: -140px;
+  margin-left: -140px;
 }
 
 .brand-mountain-1 {
-    position: relative;
-    z-index: 50;
+  position: relative;
+  z-index: 50;
 }
 
 .NotFoundScene .brand-bridge,
 .NotFoundScene .brand-mountain-1,
 .NotFoundScene .brand-mountain-1,
 .NotFoundScene .brand-illustration {
-    display: none;
+  display: none;
 }
 
 .NotFoundScene .brand-boat-container {
-    animation: boat_lost 30s linear infinite;
+  animation: boat_lost 30s linear infinite;
 }
 
 /* flip the second mountain around */
 .brand-mountain-2 {
-    -moz-transform: scaleX(-1);
-    -webkit-transform: scaleX(-1);
-    -o-transform: scaleX(-1);
-    transform: scaleX(-1);
-    -ms-filter: fliph; /*IE*/
-    filter: fliph; /*IE*/
-    margin-left: -170px;
+  -moz-transform: scaleX(-1);
+  -webkit-transform: scaleX(-1);
+  -o-transform: scaleX(-1);
+  transform: scaleX(-1);
+  -ms-filter: fliph; /*IE*/
+  filter: fliph; /*IE*/
+  margin-left: -170px;
 }
 
-
 .SuccessGroup {
-    display: flex;
-    flex-direction: column;
-    align-items: center;
-    color: var(--success-color);
-    padding: 4em;
+  display: flex;
+  flex-direction: column;
+  align-items: center;
+  color: var(--success-color);
+  padding: 4em;
 }
 
 .SuccessMark {
-    display: flex;
-    padding: 1em;
-    border: 3px solid var(--success-color);
-    border-radius: 99px;
-    color: var(--success-color);
-    line-height: 1;
+  display: flex;
+  padding: 1em;
+  border: 3px solid var(--success-color);
+  border-radius: 99px;
+  color: var(--success-color);
+  line-height: 1;
 }
 
 .SuccessText {
-    font-weight: bold;
-    margin-top: 1em;
-    text-align: center;
+  font-weight: bold;
+  margin-top: 1em;
+  text-align: center;
 }
 
 .ForgotForm,
 .SuccessGroup {
-    position: relative;
-    z-index: 10;
+  position: relative;
+  z-index: 10;
 }
diff --git a/frontend/src/metabase/css/pulse.css b/frontend/src/metabase/css/pulse.css
index 8c028d35da7d965bc36913caf0e5ceadae9bdd34..29bd27b47ac9575f3cf260fb2b1964bffd72927f 100644
--- a/frontend/src/metabase/css/pulse.css
+++ b/frontend/src/metabase/css/pulse.css
@@ -1,21 +1,21 @@
 .PulseEdit-header,
 .PulseEdit-footer {
-    width: 100%;
-    margin: 0 auto;
-    padding-left: 180px;
-    padding-right: 180px;
+  width: 100%;
+  margin: 0 auto;
+  padding-left: 180px;
+  padding-right: 180px;
 }
 
 .PulseEdit-content {
-    max-width: 550px;
-    margin-left: 180px;
+  max-width: 550px;
+  margin-left: 180px;
 }
 
 .PulseButton {
-  color: rgb(121,130,127);
+  color: rgb(121, 130, 127);
   font-weight: 700;
   border-width: 2px;
-  border-color: rgb(222,228,226);
+  border-color: rgb(222, 228, 226);
 }
 
 .PulseEdit .input,
@@ -24,10 +24,9 @@
 .PulseEdit .border-row-divider,
 .PulseEdit .AdminSelect {
   border-width: 2px;
-  border-color: rgb(222,228,226);
+  border-color: rgb(222, 228, 226);
 }
 
-
 .PulseEdit .AdminSelect {
   padding: 1em;
 }
@@ -35,7 +34,7 @@
 .PulseEdit .input:focus,
 .PulseEdit .input--focus {
   border-width: 2px;
-  border-color: rgb(97,167,229) !important;
+  border-color: rgb(97, 167, 229) !important;
 }
 
 .PulseListItem button {
@@ -43,7 +42,7 @@
 }
 
 .bg-grey-0 {
-  background-color: rgb(252,252,253);
+  background-color: rgb(252, 252, 253);
 }
 
 .PulseEditButton {
@@ -60,27 +59,27 @@
 }
 
 .PulseListItem.PulseListItem--focused {
-  border-color: #509EE3;
-  box-shadow: 0 0 3px #509EE3;
+  border-color: #509ee3;
+  box-shadow: 0 0 3px #509ee3;
 }
 
 .DangerZone:hover {
   border-color: var(--error-color);
-  transition: border .3s ease-in;
+  transition: border 0.3s ease-in;
 }
 
 .DangerZone .Button--danger {
-    opacity: 0.4;
-    background: #FBFCFD;
-    border: 1px solid #ddd;
-    color: #444;
+  opacity: 0.4;
+  background: #fbfcfd;
+  border: 1px solid #ddd;
+  color: #444;
 }
 
 .DangerZone:hover .Button--danger {
-    opacity: 1;
-    background-color: var(--danger-button-bg-color);
-    border-color: var(--danger-button-bg-color);
-    color: #fff;
+  opacity: 1;
+  background-color: var(--danger-button-bg-color);
+  border-color: var(--danger-button-bg-color);
+  color: #fff;
 }
 
 .Modal.WhatsAPulseModal {
diff --git a/frontend/src/metabase/css/query_builder.css b/frontend/src/metabase/css/query_builder.css
index b0da440114f5ef14bb5fbe403b1cf9c89a005e82..10c9c21b905f3363846feada194d3d17cd5c47a4 100644
--- a/frontend/src/metabase/css/query_builder.css
+++ b/frontend/src/metabase/css/query_builder.css
@@ -1,34 +1,33 @@
 :root {
-    --selection-color: #ccdff6;
+  --selection-color: #ccdff6;
 }
 
 #react_qb_viz {
-    flex-grow: 1;
+  flex-grow: 1;
 }
 
 /* @layout */
 .QueryBuilder {
-    transition: margin-right 0.35s;
- }
+  transition: margin-right 0.35s;
+}
 
 .QueryBuilder--showSideDrawer {
-    margin-right: 300px;
+  margin-right: 300px;
 }
 
 .QueryHeader-details {
-    display: flex;
-    align-items: center;
+  display: flex;
+  align-items: center;
 }
 
-
 .QueryHeader-section {
-    padding-right: 1em;
-    margin-right: 1em;
-    border-right: 1px solid rgba(0,0,0,0.2);
+  padding-right: 1em;
+  margin-right: 1em;
+  border-right: 1px solid rgba(0, 0, 0, 0.2);
 }
 
 .QueryHeader-section:last-child {
-    border-right: none;
+  border-right: none;
 }
 
 /*
@@ -47,26 +46,26 @@
 
 /* a section of the graphical query itself */
 .Query-section {
-    display: flex;
-    align-items: center;
+  display: flex;
+  align-items: center;
 }
 
 .Query-section.Query-section--right {
-    justify-content: flex-end;
+  justify-content: flex-end;
 }
 
 .QueryName {
-    font-weight: 200;
-    margin-top: 0;
-    margin-bottom: 0;
-    font-size: 1.2rem;
+  font-weight: 200;
+  margin-top: 0;
+  margin-bottom: 0;
+  font-size: 1.2rem;
 }
 
 .Query-label {
-    text-transform: uppercase;
-    font-size: 10px;
-    font-weight: 700;
-    color: color(var(--base-grey) shade(30%));
+  text-transform: uppercase;
+  font-size: 10px;
+  font-weight: 700;
+  color: color(var(--base-grey) shade(30%));
 }
 
 .Query-filters {
@@ -74,37 +73,37 @@
 }
 
 .Query-filterList {
-    display: flex;
-    overflow-y: hidden;
-    white-space: nowrap;
-    min-height: 55px;
+  display: flex;
+  overflow-y: hidden;
+  white-space: nowrap;
+  min-height: 55px;
 }
 
 .Query-filter {
-    display: flex;
-    flex-shrink: 0;
-    font-size: 0.75em;
-    border: 2px solid transparent;
-    border-radius: var(--default-border-radius);
+  display: flex;
+  flex-shrink: 0;
+  font-size: 0.75em;
+  border: 2px solid transparent;
+  border-radius: var(--default-border-radius);
 }
 
 .Query-filter.selected {
-    border-color: var(--purple-color);
+  border-color: var(--purple-color);
 }
 
 .Filter-section {
-    display: flex;
-    align-items: center;
-    flex-shrink: 0;
+  display: flex;
+  align-items: center;
+  flex-shrink: 0;
 }
 
 .Query-filter .input {
-    border-radius: 0;
-    border: none;
-    font-size: inherit;
-    background-color: transparent;
-    width: 150px;
-    padding: 0;
+  border-radius: 0;
+  border: none;
+  font-size: inherit;
+  background-color: transparent;
+  width: 150px;
+  padding: 0;
 }
 
 .TooltipFilterList .Query-filter {
@@ -124,146 +123,146 @@
     @selectionmodule
 */
 .SelectionModule {
-    color: var(--brand-color);
+  color: var(--brand-color);
 }
 
 .SelectionList {
-    padding-top: 5px;
-    overflow-y: auto;
-    max-height: 340px;
+  padding-top: 5px;
+  overflow-y: auto;
+  max-height: 340px;
 }
 
 .SelectionItems {
-    max-width: 320px;
+  max-width: 320px;
 }
 
 .SelectionItems.SelectionItems--open {
-    opacity: 1;
-    transition: opacity .3s linear;
-    pointer-events: all;
+  opacity: 1;
+  transition: opacity 0.3s linear;
+  pointer-events: all;
 }
 
 .SelectionItems.SelectionItems--expanded {
-    max-height: inherit;
+  max-height: inherit;
 }
 
 .SelectionItem {
-    display: flex;
-    align-items: center;
-    cursor: pointer;
-    padding: 0.75rem 1.5rem 0.75rem 0.75rem;
-    background-color: #fff;
+  display: flex;
+  align-items: center;
+  cursor: pointer;
+  padding: 0.75rem 1.5rem 0.75rem 0.75rem;
+  background-color: #fff;
 }
 
 .SelectionItem:hover {
-    background-color: currentColor;
+  background-color: currentColor;
 }
 
 .SelectionItem .Icon {
-    margin-left: 0.5rem;
-    margin-right: 0.75rem;
-    color: currentcolor;
+  margin-left: 0.5rem;
+  margin-right: 0.75rem;
+  color: currentcolor;
 }
 
 .SelectionItem .Icon-check {
-    opacity: 0;
+  opacity: 0;
 }
 
 .SelectionItem .Icon-chevrondown {
-    opacity: 1;
- }
+  opacity: 1;
+}
 
 .SelectionItem:hover .Icon {
-    color: #fff !important;
+  color: #fff !important;
 }
 
 .SelectionItem:hover .SelectionModule-display {
-    color: #fff;
+  color: #fff;
 }
 
 .SelectionItem:hover .SelectionModule-description {
-    color: #fff;
+  color: #fff;
 }
 
 .SelectionItem.SelectionItem--selected .Icon-check {
-    opacity: 1;
+  opacity: 1;
 }
 
 .SelectionModule-display {
-    color: currentColor;
-    margin-bottom: 0.25em;
+  color: currentColor;
+  margin-bottom: 0.25em;
 }
 
 .SelectionModule-description {
-    color: color(var(--base-grey) shade(40%));
-    font-size: 0.8rem;
+  color: color(var(--base-grey) shade(40%));
+  font-size: 0.8rem;
 }
 
 .Visualization {
-    transition: background .3s linear;
+  transition: background 0.3s linear;
 }
 
 .Visualization.Visualization--loading {
-    transition: background .3s linear;
+  transition: background 0.3s linear;
 }
 
 .Visualization.Visualization--error {
-    justify-content: center;
+  justify-content: center;
 }
 
 .Visualization--scalar {
-    justify-content: center;
-    font-size: 8rem;
-    font-weight: 200;
+  justify-content: center;
+  font-size: 8rem;
+  font-weight: 200;
 }
 
 .Loading {
-    background-color: rgba(255, 255, 255, 0.82);
+  background-color: rgba(255, 255, 255, 0.82);
 }
 
 /* query errors */
 .QueryError {
-    flex-direction: column;
-    justify-content: center;
-    max-width: 500px;
-    margin-left: auto;
-    margin-right: auto;
+  flex-direction: column;
+  justify-content: center;
+  max-width: 500px;
+  margin-left: auto;
+  margin-right: auto;
 }
 
 .QueryError-iconWrapper {
-    padding: 2em;
-    margin-bottom: 2em;
-    border: 4px solid var(--error-color);
-    border-radius: 99px;
+  padding: 2em;
+  margin-bottom: 2em;
+  border: 4px solid var(--error-color);
+  border-radius: 99px;
 }
 
 .QueryError-image {
-    background-repeat: no-repeat;
-    margin-bottom: 1rem;
+  background-repeat: no-repeat;
+  margin-bottom: 1rem;
 }
 
 .QueryError-image--noRows {
   width: 120px;
   height: 120px;
-  background-image: url('../app/assets/img/no_results.svg');
+  background-image: url("../app/assets/img/no_results.svg");
 }
 
 .QueryError-image--queryError {
   width: 120px;
   height: 120px;
-  background-image: url('../app/assets/img/no_understand.svg');
+  background-image: url("../app/assets/img/no_understand.svg");
 }
 
 .QueryError-image--serverError {
   width: 120px;
   height: 148px;
-  background-image: url('../app/assets/img/blown_up.svg');
+  background-image: url("../app/assets/img/blown_up.svg");
 }
 
 .QueryError-image--timeout {
   width: 120px;
   height: 120px;
-  background-image: url('../app/assets/img/stopwatch.svg');
+  background-image: url("../app/assets/img/stopwatch.svg");
 }
 
 .QueryError-message {
@@ -301,152 +300,150 @@
 }
 
 .QueryError2 {
-    padding-top: 4rem;
-    margin-left: auto;
-    margin-right: auto;
+  padding-top: 4rem;
+  margin-left: auto;
+  margin-right: auto;
 }
 
 .QueryError2-details {
-    max-width: 500px;
+  max-width: 500px;
 }
 
 .QueryError2-detailBody {
-    background-color: #f8f8f8;
-    max-height: 15rem;
-    overflow: auto;
+  background-color: #f8f8f8;
+  max-height: 15rem;
+  overflow: auto;
 }
 
-
 /* GUI BUILDER */
 
 .GuiBuilder {
-    position: relative;
-    display: flex;
-    flex-direction: column;
-    font-size: 0.9em;
-    z-index: 2;
-    background-color: #fff;
+  position: relative;
+  display: flex;
+  flex-direction: column;
+  font-size: 0.9em;
+  z-index: 2;
+  background-color: #fff;
 
-    border: 1px solid #e0e0e0;
+  border: 1px solid #e0e0e0;
 }
 
 /* for medium breakpoint only expand if data reference is not shown */
 @media screen and (--breakpoint-min-md) {
-    .GuiBuilder {
-        font-size: 1.0em;
-    }
+  .GuiBuilder {
+    font-size: 1em;
+  }
 }
 
 /* un-expanded (default) */
 .GuiBuilder-row {
-    border-bottom: 1px solid #e0e0e0;
+  border-bottom: 1px solid #e0e0e0;
 }
 .GuiBuilder-row:last-child {
-    border-bottom-color: transparent;
+  border-bottom-color: transparent;
 }
 .GuiBuilder-data {
-    border-right: 1px solid #e0e0e0;
+  border-right: 1px solid #e0e0e0;
 }
 .GuiBuilder-filtered-by {
-    border-right: 1px solid transparent;
+  border-right: 1px solid transparent;
 }
 .GuiBuilder-view {
-    border-right: 1px solid #e0e0e0;
+  border-right: 1px solid #e0e0e0;
 }
 .GuiBuilder-sort-limit {
-    border-left: 1px solid #e0e0e0;
+  border-left: 1px solid #e0e0e0;
 }
 
 /* expanded */
 .GuiBuilder.GuiBuilder--expand {
-    flex-direction: row;
+  flex-direction: row;
 }
 .GuiBuilder.GuiBuilder--expand .GuiBuilder-row:last-child {
-    border-right-color: transparent;
-    border-bottom-color: #e0e0e0;
+  border-right-color: transparent;
+  border-bottom-color: #e0e0e0;
 }
 .GuiBuilder.GuiBuilder--expand .GuiBuilder-filtered-by {
-    border-right-color: #e0e0e0;
+  border-right-color: #e0e0e0;
 }
 
-
 .GuiBuilder-section {
-    position: relative;
-    min-height: 55px;
-    min-width: 100px;
+  position: relative;
+  min-height: 55px;
+  min-width: 100px;
 }
 
 .GuiBuilder-section-label {
-    background-color: white;
-    position: absolute;
-    top: -7px;
-    left: 10px;
-    padding-left: 10px;
-    padding-right: 10px;
+  background-color: white;
+  position: absolute;
+  top: -7px;
+  left: 10px;
+  padding-left: 10px;
+  padding-right: 10px;
 }
 
 .QueryOption {
-    font-weight: 700;
+  font-weight: 700;
 }
 
 .QueryOption:hover {
-    cursor: pointer;
+  cursor: pointer;
 }
 
 /* @transitions */
 
 .AddToDashSuccess {
-    max-width: 260px;
-    text-align: center;
+  max-width: 260px;
+  text-align: center;
 }
 
 /* DATA SECTION */
 
 .GuiBuilder-data {
-    z-index: 1; /* moved the arrow thingy above the filter outline */
+  z-index: 1; /* moved the arrow thingy above the filter outline */
 }
 
 /* FILTER BY SECTION */
 
 .Filter-section-field,
 .Filter-section-operator {
-    color: var(--purple-color);
+  color: var(--purple-color);
 }
 
 .Filter-section-field .QueryOption {
-    color: var(--purple-color);
+  color: var(--purple-color);
 }
 .Filter-section-operator .QueryOption {
-    color: var(--purple-color);
-    text-transform: lowercase;
+  color: var(--purple-color);
+  text-transform: lowercase;
 }
 .Filter-section-value .QueryOption {
-    color: white;
-    background-color: var(--purple-color);
-    border: 1px solid color(var(--purple-color) shade(30%));
-    border-radius: 6px;
-    padding: 0.5em;
-    padding-top: 0.3em;
-    padding-bottom: 0.3em;
-    margin-bottom: 0.2em;
+  color: white;
+  background-color: var(--purple-color);
+  border: 1px solid color(var(--purple-color) shade(30%));
+  border-radius: 6px;
+  padding: 0.5em;
+  padding-top: 0.3em;
+  padding-bottom: 0.3em;
+  margin-bottom: 0.2em;
 }
 
 .Filter-section-value {
-    padding-right: 0.5em;
-    padding-bottom: 0.25em;
+  padding-right: 0.5em;
+  padding-bottom: 0.25em;
 }
 
 .Filter-section-sort-field.selected .QueryOption,
 .Filter-section-sort-direction.selected .QueryOption {
-    color: inherit;
+  color: inherit;
 }
 
 .FilterPopover .ColumnarSelector-row--selected,
 .FilterPopover .PopoverHeader-item.selected {
-    color: var(--purple-color) !important;
+  color: var(--purple-color) !important;
 }
 .FilterPopover .ColumnarSelector-row:hover {
-    background-color: var(--purple-color) !important;
+  background-color: var(--purple-color) !important;
 }
 
 /* VIEW SECTION */
@@ -454,149 +451,151 @@
 .View-section-aggregation,
 .View-section-aggregation-target,
 .View-section-breakout {
-    color: var(--green-color);
+  color: var(--green-color);
 }
 
 .View-section-aggregation.selected .QueryOption,
 .View-section-aggregation-target.selected .QueryOption,
 .View-section-breakout.selected .QueryOption {
-    color: var(--green-color);
+  color: var(--green-color);
 }
 
 /* SORT/LIMIT SECTION */
 
 .GuiBuilder-sort-limit {
-    min-width: 0px;
+  min-width: 0px;
 }
 
 .EllipsisButton {
-    font-size: 3em;
-    position: relative;
-    top: -0.8rem;
+  font-size: 3em;
+  position: relative;
+  top: -0.8rem;
 }
 
 /* NATIVE */
 
 .NativeQueryEditor .GuiBuilder-data {
-    border-right: none;
+  border-right: none;
 }
 
 /* VISUALIZATION SETTINGS */
 
 .VisualizationSettings .GuiBuilder-section {
-    border-right: none !important;
+  border-right: none !important;
 }
 
 .ChartType-button {
-    width: 38px;
-    height: 38px;
-    border-radius: 38px;
-    background-color: white;
-    border: 1px solid #ccdff6;
+  width: 38px;
+  height: 38px;
+  border-radius: 38px;
+  background-color: white;
+  border: 1px solid #ccdff6;
 }
 
 .ChartType-popover {
-    min-width: 15em !important;
+  min-width: 15em !important;
 }
 
 .ChartType--selected {
-    color: white;
-    background-color: rgb(74, 144, 226);
+  color: white;
+  background-color: rgb(74, 144, 226);
 }
 
 .ChartType--notSensible {
-    opacity: 0.5;
+  opacity: 0.5;
 }
 
 .ColorWell {
-    width: 18px;
-    height: 18px;
-    margin: 3px;
-    margin-right: 0.3rem;
+  width: 18px;
+  height: 18px;
+  margin: 3px;
+  margin-right: 0.3rem;
 }
 
 .RunButton {
-    z-index: 1;
-    opacity: 1;
-    box-shadow: 0 1px 2px rgba(0, 0, 0, .22);
-    transition: transform 0.5s, opacity 0.5s;
-    min-width: 8em;
-    position: relative;
+  z-index: 1;
+  opacity: 1;
+  box-shadow: 0 1px 2px rgba(0, 0, 0, 0.22);
+  transition: transform 0.5s, opacity 0.5s;
+  min-width: 8em;
+  position: relative;
 }
 
 .RunButton.RunButton--hidden {
-    transform: translateY(-65px);
-    opacity: 0;
+  transform: translateY(-65px);
+  opacity: 0;
 }
 
 /* DATA REFERENCE */
 
 .SideDrawer {
-    z-index: -1;
-    position: absolute;
-    top: 0;
-    right: 0;
-    width: 300px;
-    height: 100%;
-    background-color: var(--slate-extra-light-color);
-    overflow: hidden;
+  z-index: -1;
+  position: absolute;
+  top: 0;
+  right: 0;
+  width: 300px;
+  height: 100%;
+  background-color: var(--slate-extra-light-color);
+  overflow: hidden;
 }
 
 .SideDrawer--show {
-    z-index: 0;
+  z-index: 0;
 }
 
 .DataReference-container {
-    width: 300px;
+  width: 300px;
 }
 
 .DataReference h1 {
-    font-size: 20pt;
+  font-size: 20pt;
 }
 
 .DataReference-paneCount {
-    padding-right: 0.6em;
+  padding-right: 0.6em;
 }
 
 /* object detail */
 .ObjectDetail {
-    width: 100%;
-    margin: 0 auto;
-    margin-bottom: 2rem;
-    border: 1px solid #DEDEDE;
+  width: 100%;
+  margin: 0 auto;
+  margin-bottom: 2rem;
+  border: 1px solid #dedede;
 }
 
 @media screen and (--breakpoint-min-xl) {
   /* prevent the object detail from getting too wide on large screens */
-  .ObjectDetail { max-width: 1140px; }
+  .ObjectDetail {
+    max-width: 1140px;
+  }
 }
 
 .ObjectDetail-headingGroup {
-    border-bottom: 1px solid #DEDEDE;
+  border-bottom: 1px solid #dedede;
 }
 
 .ObjectDetail-infoMain {
-    border-right: 1px solid #DEDEDE;
-    margin-left: 2.4rem;
-    font-size: 1rem;
+  border-right: 1px solid #dedede;
+  margin-left: 2.4rem;
+  font-size: 1rem;
 }
 
 .ObjectJSON {
-    max-height: 200px;
-    overflow: scroll;
-    padding: 1em;
-    background-color: #F8F8F8;
-    border: 1px solid #dedede;
-    border-radius: 2px;
+  max-height: 200px;
+  overflow: scroll;
+  padding: 1em;
+  background-color: #f8f8f8;
+  border: 1px solid #dedede;
+  border-radius: 2px;
 }
 
 .PopoverBody.AddToDashboard {
-    min-width: 25em;
+  min-width: 25em;
 }
 
 .FieldList-grouping-trigger {
-    display: flex;
-    visibility: hidden;
+  display: flex;
+  visibility: hidden;
 }
 
 .List-item--segment .Icon,
@@ -611,13 +610,13 @@
 
 .List-item:not(.List-item--disabled):hover .FieldList-grouping-trigger,
 .List-item--selected .FieldList-grouping-trigger {
-    visibility: visible;
-    border-left: 2px solid rgba(0,0,0,0.1);
-    color: rgba(255,255,255,0.5);
+  visibility: visible;
+  border-left: 2px solid rgba(0, 0, 0, 0.1);
+  color: rgba(255, 255, 255, 0.5);
 }
 
 .QuestionTooltipTarget {
-  color: rgb(225,225,225);
+  color: rgb(225, 225, 225);
   display: inline-block;
   border: 2px solid currentColor;
   border-radius: 99px;
@@ -638,7 +637,6 @@
   font-weight: bold;
 }
 
-
 .FilterRemove-field {
   border-radius: 99px;
   opacity: 0;
@@ -649,7 +647,7 @@
   justify-content: center;
   background-color: var(--purple-color);
   border: 1px solid var(--purple-color);
-  transition: opacity .3s ease-out;
+  transition: opacity 0.3s ease-out;
 }
 
 .FilterInput:hover .FilterRemove-field {
@@ -669,27 +667,27 @@
   white-space: pre-wrap;
   white-space: -moz-pre-wrap;
   white-space: -o-pre-wrap;
-  background-color: #F9FBFC;
-  border: 1px solid #D5DBE3;
+  background-color: #f9fbfc;
+  border: 1px solid #d5dbe3;
   border-radius: 4px;
 }
 
 .ParameterValuePickerNoPopover input {
-    font-size: 16px;
-    color: var(--default-font-color);
-    border: none;
+  font-size: 16px;
+  color: var(--default-font-color);
+  border: none;
 }
 
 .ParameterValuePickerNoPopover--selected input {
-    font-weight: bold;
-    color: var(--brand-color);
+  font-weight: bold;
+  color: var(--brand-color);
 }
 
 .ParameterValuePickerNoPopover input:focus {
-    outline: none;
-    color: var(--default-font-color);
+  outline: none;
+  color: var(--default-font-color);
 }
 
 .ParameterValuePickerNoPopover input::-webkit-input-placeholder {
-    color: var(--grey-4);
+  color: var(--grey-4);
 }
diff --git a/frontend/src/metabase/css/setup.css b/frontend/src/metabase/css/setup.css
index 081f625d85a784442619ecc12568c41fd61b3855..c18df9720cda0589695040a0771091070f1c197e 100644
--- a/frontend/src/metabase/css/setup.css
+++ b/frontend/src/metabase/css/setup.css
@@ -1,88 +1,88 @@
 :root {
-    --indicator-size: 3.000em; /* ~ 42 px */
-    --indicator-border-radius: 99px;
-    --setup-border-color: #D7D7D7;
+  --indicator-size: 3em; /* ~ 42 px */
+  --indicator-border-radius: 99px;
+  --setup-border-color: #d7d7d7;
 }
 
 .SetupSteps {
-    margin-top: 4.000rem;
+  margin-top: 4rem;
 }
 
 .SetupNav {
-    border-bottom: 1px solid #f5f5f5;
+  border-bottom: 1px solid #f5f5f5;
 }
 
 .Setup-brandWordMark {
-    font-size: 1.688rem;
+  font-size: 1.688rem;
 }
 
 .SetupStep {
-    margin-bottom: 1.714rem;
-    border: 1px solid var(--setup-border-color);
-    flex: 1;
+  margin-bottom: 1.714rem;
+  border: 1px solid var(--setup-border-color);
+  flex: 1;
 }
 
 .SetupStep.SetupStep--active {
-    color: var(--brand-color);
+  color: var(--brand-color);
 }
 
 .SetupStep.SetupStep--completed {
-    color: var(--success-color);
+  color: var(--success-color);
 }
 
 .SetupStep.SetupStep--todo {
-    color: var(--brand-color);
-    background-color: #EDF2F8;
-    border-style: dashed;
+  color: var(--brand-color);
+  background-color: #edf2f8;
+  border-style: dashed;
 }
 
 .SetupStep-indicator {
-    left: calc((var(--indicator-size) / 2) * -1);
-    width: var(--indicator-size);
-    height: var(--indicator-size);
-    border-radius: var(--indicator-border-radius);
-    border-color: color(var(--base-grey) shade(20%));
-    font-weight: bold;
-    line-height: 1;
-    background-color: #fff;
-    margin-top: -3px;
+  left: calc((var(--indicator-size) / 2) * -1);
+  width: var(--indicator-size);
+  height: var(--indicator-size);
+  border-radius: var(--indicator-border-radius);
+  border-color: color(var(--base-grey) shade(20%));
+  font-weight: bold;
+  line-height: 1;
+  background-color: #fff;
+  margin-top: -3px;
 }
 
 .SetupStep-check {
-    color: #fff;
-    display: none;
+  color: #fff;
+  display: none;
 }
 
 .SetupStep-title {
-    color: currentColor; /* use the color of the parent to power the header text */
+  color: currentColor; /* use the color of the parent to power the header text */
 }
 
-.SetupStep.SetupStep--active  .SetupStep-indicator {
-    color: var(--brand-color);
+.SetupStep.SetupStep--active .SetupStep-indicator {
+  color: var(--brand-color);
 }
 
 .SetupStep.SetupStep--completed .SetupStep-indicator {
-    border-color: #9CC177;
-    background-color: #C8E1B0;
+  border-color: #9cc177;
+  background-color: #c8e1b0;
 }
 
 .SetupStep.SetupStep--completed .SetupStep-check {
-    display: block;
+  display: block;
 }
 
 .SetupStep.SetupStep--completed .SetupStep-number {
-    display: none;
+  display: none;
 }
 
 .SetupCompleted {
-    text-align: center;
+  text-align: center;
 }
 
 .SetupCompleted .SetupStep-title {
-    font-size: 2rem;
-    line-height: 2rem;
+  font-size: 2rem;
+  line-height: 2rem;
 }
 
 .SetupHelp {
-    color: var(--body-text-color);
+  color: var(--body-text-color);
 }
diff --git a/frontend/src/metabase/css/vendor.css b/frontend/src/metabase/css/vendor.css
index 683d146355997d36209c9d394393e889d3f15a1e..927e26c2b47de190cc7bcfeda660cc7f4fec9660 100644
--- a/frontend/src/metabase/css/vendor.css
+++ b/frontend/src/metabase/css/vendor.css
@@ -1,5 +1,5 @@
 /* d3 */
-@import 'dc/dc.css';
+@import "dc/dc.css";
 
 /* z-index utils */
-@import 'z-index/z-index.css';
+@import "z-index/z-index.css";
diff --git a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx
index 33f692a241a9b33c7fa3fb740c89038305fef049..b693b408b92e96f9d678e2958092f166ada81fca 100644
--- a/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx
+++ b/frontend/src/metabase/dashboard/components/AddSeriesModal.jsx
@@ -17,250 +17,338 @@ import cx from "classnames";
 import { getIn } from "icepick";
 
 function getQueryColumns(card, databases) {
-    let dbId = card.dataset_query.database;
-    if (card.dataset_query.type !== "query") {
-        return null;
-    }
-    let query = card.dataset_query.query;
-    let table = databases && databases[dbId] && databases[dbId].tables_lookup[query.source_table];
-    if (!table) {
-        return null;
-    }
-    return Query.getQueryColumns(table, query);
+  let dbId = card.dataset_query.database;
+  if (card.dataset_query.type !== "query") {
+    return null;
+  }
+  let query = card.dataset_query.query;
+  let table =
+    databases &&
+    databases[dbId] &&
+    databases[dbId].tables_lookup[query.source_table];
+  if (!table) {
+    return null;
+  }
+  return Query.getQueryColumns(table, query);
 }
 
 export default class AddSeriesModal extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            searchValue: "",
-            error: null,
-            series: props.dashcard.series || [],
-            badCards: {}
-        };
-
-        _.bindAll(this, "onSearchChange", "onSearchFocus", "onDone", "filteredCards", "onRemoveSeries")
-    }
-
-    static propTypes = {
-        dashcard: PropTypes.object.isRequired,
-        cards: PropTypes.array,
-        dashcardData: PropTypes.object.isRequired,
-        fetchCards: PropTypes.func.isRequired,
-        fetchCardData: PropTypes.func.isRequired,
-        fetchDatabaseMetadata: PropTypes.func.isRequired,
-        setDashCardAttributes: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired
+    this.state = {
+      searchValue: "",
+      error: null,
+      series: props.dashcard.series || [],
+      badCards: {},
     };
-    static defaultProps = {};
 
-    async componentDidMount() {
-        try {
-            await this.props.fetchCards();
-            await Promise.all(_.uniq(this.props.cards.map(c => c.database_id)).map(db_id =>
-                this.props.fetchDatabaseMetadata(db_id)
-            ));
-        } catch (error) {
-            console.error(error);
-            this.setState({ error });
-        }
-    }
+    _.bindAll(
+      this,
+      "onSearchChange",
+      "onSearchFocus",
+      "onDone",
+      "filteredCards",
+      "onRemoveSeries",
+    );
+  }
 
-    onSearchFocus() {
-        MetabaseAnalytics.trackEvent("Dashboard", "Edit Series Modal", "search");
-    }
+  static propTypes = {
+    dashcard: PropTypes.object.isRequired,
+    cards: PropTypes.array,
+    dashcardData: PropTypes.object.isRequired,
+    fetchCards: PropTypes.func.isRequired,
+    fetchCardData: PropTypes.func.isRequired,
+    fetchDatabaseMetadata: PropTypes.func.isRequired,
+    setDashCardAttributes: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
+  static defaultProps = {};
 
-    onSearchChange(e) {
-        this.setState({ searchValue: e.target.value.toLowerCase() });
+  async componentDidMount() {
+    try {
+      await this.props.fetchCards();
+      await Promise.all(
+        _.uniq(this.props.cards.map(c => c.database_id)).map(db_id =>
+          this.props.fetchDatabaseMetadata(db_id),
+        ),
+      );
+    } catch (error) {
+      console.error(error);
+      this.setState({ error });
     }
+  }
 
-    async onCardChange(card, e) {
-        const { dashcard, dashcardData } = this.props;
-        let { CardVisualization } = getVisualizationRaw([{ card: dashcard.card }]);
-        try {
-            if (e.target.checked) {
-                if (getIn(dashcardData, [dashcard.id, card.id]) === undefined) {
-                    this.setState({ state: "loading" });
-                    await this.props.fetchCardData(card, dashcard, { reload: false, clear: true });
-                }
-                let sourceDataset = getIn(this.props.dashcardData, [dashcard.id, dashcard.card.id]);
-                let seriesDataset = getIn(this.props.dashcardData, [dashcard.id, card.id]);
-                if (CardVisualization.seriesAreCompatible(
-                    { card: dashcard.card, data: sourceDataset.data },
-                    { card: card, data: seriesDataset.data }
-                )) {
-                    this.setState({
-                        state: null,
-                        series: this.state.series.concat(card)
-                    });
-
-                    MetabaseAnalytics.trackEvent("Dashboard", "Add Series", card.display+", success");
-                } else {
-                    this.setState({
-                        state: "incompatible",
-                        badCards: { ...this.state.badCards, [card.id]: true }
-                    });
-                    setTimeout(() => this.setState({ state: null }), 2000);
+  onSearchFocus() {
+    MetabaseAnalytics.trackEvent("Dashboard", "Edit Series Modal", "search");
+  }
 
-                    MetabaseAnalytics.trackEvent("Dashboard", "Add Series", card.dataset_query.type+", "+card.display+", fail");
-                }
-            } else {
-                this.setState({ series: this.state.series.filter(c => c.id !== card.id) });
+  onSearchChange(e) {
+    this.setState({ searchValue: e.target.value.toLowerCase() });
+  }
 
-                MetabaseAnalytics.trackEvent("Dashboard", "Remove Series");
-            }
-        } catch (e) {
-            console.error("onCardChange", e);
-            this.setState({
-                state: "incompatible",
-                badCards: { ...this.state.badCards, [card.id]: true }
-            });
-            setTimeout(() => this.setState({ state: null }), 2000);
+  async onCardChange(card, e) {
+    const { dashcard, dashcardData } = this.props;
+    let { CardVisualization } = getVisualizationRaw([{ card: dashcard.card }]);
+    try {
+      if (e.target.checked) {
+        if (getIn(dashcardData, [dashcard.id, card.id]) === undefined) {
+          this.setState({ state: "loading" });
+          await this.props.fetchCardData(card, dashcard, {
+            reload: false,
+            clear: true,
+          });
         }
-    }
+        let sourceDataset = getIn(this.props.dashcardData, [
+          dashcard.id,
+          dashcard.card.id,
+        ]);
+        let seriesDataset = getIn(this.props.dashcardData, [
+          dashcard.id,
+          card.id,
+        ]);
+        if (
+          CardVisualization.seriesAreCompatible(
+            { card: dashcard.card, data: sourceDataset.data },
+            { card: card, data: seriesDataset.data },
+          )
+        ) {
+          this.setState({
+            state: null,
+            series: this.state.series.concat(card),
+          });
 
-    onRemoveSeries(card) {
-        this.setState({ series: this.state.series.filter(c => c.id !== card.id) });
-        MetabaseAnalytics.trackEvent("Dashboard", "Remove Series");
-    }
+          MetabaseAnalytics.trackEvent(
+            "Dashboard",
+            "Add Series",
+            card.display + ", success",
+          );
+        } else {
+          this.setState({
+            state: "incompatible",
+            badCards: { ...this.state.badCards, [card.id]: true },
+          });
+          setTimeout(() => this.setState({ state: null }), 2000);
 
-    onDone() {
-        this.props.setDashCardAttributes({
-            id: this.props.dashcard.id,
-            attributes: { series: this.state.series }
+          MetabaseAnalytics.trackEvent(
+            "Dashboard",
+            "Add Series",
+            card.dataset_query.type + ", " + card.display + ", fail",
+          );
+        }
+      } else {
+        this.setState({
+          series: this.state.series.filter(c => c.id !== card.id),
         });
-        this.props.onClose();
-        MetabaseAnalytics.trackEvent("Dashboard", "Edit Series Modal", "done");
-    }
 
-    filteredCards() {
-        const { cards, dashcard, databases, dashcardData } = this.props;
-        const { searchValue } = this.state;
+        MetabaseAnalytics.trackEvent("Dashboard", "Remove Series");
+      }
+    } catch (e) {
+      console.error("onCardChange", e);
+      this.setState({
+        state: "incompatible",
+        badCards: { ...this.state.badCards, [card.id]: true },
+      });
+      setTimeout(() => this.setState({ state: null }), 2000);
+    }
+  }
 
-        const initialSeries = {
-            card: dashcard.card,
-            data: getIn(dashcardData, [dashcard.id, dashcard.card.id, "data"])
-        };
+  onRemoveSeries(card) {
+    this.setState({ series: this.state.series.filter(c => c.id !== card.id) });
+    MetabaseAnalytics.trackEvent("Dashboard", "Remove Series");
+  }
 
-        let { CardVisualization } = getVisualizationRaw([{ card: dashcard.card }]);
+  onDone() {
+    this.props.setDashCardAttributes({
+      id: this.props.dashcard.id,
+      attributes: { series: this.state.series },
+    });
+    this.props.onClose();
+    MetabaseAnalytics.trackEvent("Dashboard", "Edit Series Modal", "done");
+  }
 
-        return cards.filter(card => {
-            try {
-                // filter out the card itself
-                if (card.id === dashcard.card.id) {
-                    return false;
-                }
-                if (card.dataset_query.type === "query") {
-                    if (!CardVisualization.seriesAreCompatible(initialSeries,
-                        { card: card, data: { cols: getQueryColumns(card, databases), rows: [] } }
-                    )) {
-                        return false;
-                    }
-                }
-                // search
-                if (searchValue && card.name.toLowerCase().indexOf(searchValue) < 0) {
-                    return false;
-                }
-                return true;
-            } catch (e) {
-                console.warn(e);
-                return false;
-            }
-        });
-    }
+  filteredCards() {
+    const { cards, dashcard, databases, dashcardData } = this.props;
+    const { searchValue } = this.state;
 
-    render() {
-        const { dashcard, dashcardData, cards } = this.props;
+    const initialSeries = {
+      card: dashcard.card,
+      data: getIn(dashcardData, [dashcard.id, dashcard.card.id, "data"]),
+    };
 
-        let error = this.state.error;
+    let { CardVisualization } = getVisualizationRaw([{ card: dashcard.card }]);
 
-        let filteredCards;
-        if (!error && cards) {
-            filteredCards = this.filteredCards();
-            if (filteredCards.length === 0) {
-                error = new Error("Whoops, no compatible questions match your search.");
-            }
-            // SQL cards at the bottom
-            filteredCards.sort((a, b) => {
-                if (a.dataset_query.type !== "query") {
-                    return 1;
-                } else if (b.dataset_query.type !== "query") {
-                    return -1;
-                } else {
-                    return 0;
-                }
+    return cards.filter(card => {
+      try {
+        // filter out the card itself
+        if (card.id === dashcard.card.id) {
+          return false;
+        }
+        if (card.dataset_query.type === "query") {
+          if (
+            !CardVisualization.seriesAreCompatible(initialSeries, {
+              card: card,
+              data: { cols: getQueryColumns(card, databases), rows: [] },
             })
+          ) {
+            return false;
+          }
+        }
+        // search
+        if (searchValue && card.name.toLowerCase().indexOf(searchValue) < 0) {
+          return false;
         }
+        return true;
+      } catch (e) {
+        console.warn(e);
+        return false;
+      }
+    });
+  }
 
-        let badCards = this.state.badCards;
+  render() {
+    const { dashcard, dashcardData, cards } = this.props;
 
-        let enabledCards = {};
-        for (let c of this.state.series) {
-            enabledCards[c.id] = true;
+    let error = this.state.error;
+
+    let filteredCards;
+    if (!error && cards) {
+      filteredCards = this.filteredCards();
+      if (filteredCards.length === 0) {
+        error = new Error("Whoops, no compatible questions match your search.");
+      }
+      // SQL cards at the bottom
+      filteredCards.sort((a, b) => {
+        if (a.dataset_query.type !== "query") {
+          return 1;
+        } else if (b.dataset_query.type !== "query") {
+          return -1;
+        } else {
+          return 0;
         }
+      });
+    }
 
-        let series = [dashcard.card].concat(this.state.series).map(card => ({
-            card: card,
-            data: getIn(dashcardData, [dashcard.id, card.id, "data"])
-        })).filter(s => !!s.data);
+    let badCards = this.state.badCards;
 
-        return (
-            <div className="spread flex">
-                <div className="flex flex-column flex-full">
-                    <div className="flex-no-shrink h3 pl4 pt4 pb2 text-bold">Edit data</div>
-                    <div className="flex-full ml2 mr1 relative">
-                        <Visualization
-                            className="spread"
-                            rawSeries={series}
-                            showTitle
-                            isDashboard
-                            isMultiseries
-                            onRemoveSeries={this.onRemoveSeries}
-                        />
-                        { this.state.state &&
-                            <div className="spred flex layout-centered" style={{ backgroundColor: "rgba(255,255,255,0.80)" }}>
-                                { this.state.state === "loading" ?
-                                    <div className="h3 rounded bordered p3 bg-white shadowed">Applying Question</div>
-                                : this.state.state === "incompatible" ?
-                                    <div className="h3 rounded bordered p3 bg-error border-error text-white">That question isn't compatible</div>
-                                : null }
-                            </div>
-                        }
-                    </div>
-                    <div className="flex-no-shrink pl4 pb4 pt1">
-                        <button className="Button Button--primary" onClick={this.onDone}>Done</button>
-                        <button data-metabase-event={"Dashboard;Edit Series Modal;cancel"} className="Button ml2" onClick={this.props.onClose}>Cancel</button>
-                    </div>
-                </div>
-                <div className="border-left flex flex-column" style={{width: 370, backgroundColor: "#F8FAFA", borderColor: "#DBE1DF" }}>
-                    <div className="flex-no-shrink border-bottom flex flex-row align-center" style={{ borderColor: "#DBE1DF" }}>
-                        <Icon className="ml2" name="search" size={16} />
-                        <input className="h4 input full pl1" style={{ border: "none", backgroundColor: "transparent" }} type="search" placeholder="Search for a question" onFocus={this.onSearchFocus} onChange={this.onSearchChange}/>
-                    </div>
-                    <LoadingAndErrorWrapper className="flex flex-full" loading={!filteredCards} error={error} noBackground>
-                    { () =>
-                        <ul className="flex-full scroll-y scroll-show pr1">
-                        {filteredCards.map(card =>
-                            <li key={card.id} className={cx("my1 pl2 py1 flex align-center", { disabled: badCards[card.id] })}>
-                                <span className="px1 flex-no-shrink">
-                                    <CheckBox checked={enabledCards[card.id]} onChange={this.onCardChange.bind(this, card)}/>
-                                </span>
-                                <span className="px1">
-                                    {card.name}
-                                </span>
-                                { card.dataset_query.type !== "query" &&
-                                    <Tooltip tooltip="We're not sure if this question is compatible">
-                                        <Icon className="px1 flex-align-right text-grey-2 text-grey-4-hover cursor-pointer flex-no-shrink" name="warning" size={20} />
-                                    </Tooltip>
-                                }
-                            </li>
-                        )}
-                        </ul>
-                    }
-                    </LoadingAndErrorWrapper>
-                </div>
-            </div>
-        );
+    let enabledCards = {};
+    for (let c of this.state.series) {
+      enabledCards[c.id] = true;
     }
+
+    let series = [dashcard.card]
+      .concat(this.state.series)
+      .map(card => ({
+        card: card,
+        data: getIn(dashcardData, [dashcard.id, card.id, "data"]),
+      }))
+      .filter(s => !!s.data);
+
+    return (
+      <div className="spread flex">
+        <div className="flex flex-column flex-full">
+          <div className="flex-no-shrink h3 pl4 pt4 pb2 text-bold">
+            Edit data
+          </div>
+          <div className="flex-full ml2 mr1 relative">
+            <Visualization
+              className="spread"
+              rawSeries={series}
+              showTitle
+              isDashboard
+              isMultiseries
+              onRemoveSeries={this.onRemoveSeries}
+            />
+            {this.state.state && (
+              <div
+                className="spred flex layout-centered"
+                style={{ backgroundColor: "rgba(255,255,255,0.80)" }}
+              >
+                {this.state.state === "loading" ? (
+                  <div className="h3 rounded bordered p3 bg-white shadowed">
+                    Applying Question
+                  </div>
+                ) : this.state.state === "incompatible" ? (
+                  <div className="h3 rounded bordered p3 bg-error border-error text-white">
+                    That question isn't compatible
+                  </div>
+                ) : null}
+              </div>
+            )}
+          </div>
+          <div className="flex-no-shrink pl4 pb4 pt1">
+            <button className="Button Button--primary" onClick={this.onDone}>
+              Done
+            </button>
+            <button
+              data-metabase-event={"Dashboard;Edit Series Modal;cancel"}
+              className="Button ml2"
+              onClick={this.props.onClose}
+            >
+              Cancel
+            </button>
+          </div>
+        </div>
+        <div
+          className="border-left flex flex-column"
+          style={{
+            width: 370,
+            backgroundColor: "#F8FAFA",
+            borderColor: "#DBE1DF",
+          }}
+        >
+          <div
+            className="flex-no-shrink border-bottom flex flex-row align-center"
+            style={{ borderColor: "#DBE1DF" }}
+          >
+            <Icon className="ml2" name="search" size={16} />
+            <input
+              className="h4 input full pl1"
+              style={{ border: "none", backgroundColor: "transparent" }}
+              type="search"
+              placeholder="Search for a question"
+              onFocus={this.onSearchFocus}
+              onChange={this.onSearchChange}
+            />
+          </div>
+          <LoadingAndErrorWrapper
+            className="flex flex-full"
+            loading={!filteredCards}
+            error={error}
+            noBackground
+          >
+            {() => (
+              <ul className="flex-full scroll-y scroll-show pr1">
+                {filteredCards.map(card => (
+                  <li
+                    key={card.id}
+                    className={cx("my1 pl2 py1 flex align-center", {
+                      disabled: badCards[card.id],
+                    })}
+                  >
+                    <span className="px1 flex-no-shrink">
+                      <CheckBox
+                        checked={enabledCards[card.id]}
+                        onChange={this.onCardChange.bind(this, card)}
+                      />
+                    </span>
+                    <span className="px1">{card.name}</span>
+                    {card.dataset_query.type !== "query" && (
+                      <Tooltip tooltip="We're not sure if this question is compatible">
+                        <Icon
+                          className="px1 flex-align-right text-grey-2 text-grey-4-hover cursor-pointer flex-no-shrink"
+                          name="warning"
+                          size={20}
+                        />
+                      </Tooltip>
+                    )}
+                  </li>
+                ))}
+              </ul>
+            )}
+          </LoadingAndErrorWrapper>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx b/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx
index 0ab0ea01ddc693a8d45b094189192f875c624201..2d2e285d031e261ccf608ba3246f340423c4544c 100644
--- a/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx
+++ b/frontend/src/metabase/dashboard/components/AddToDashSelectQuestionModal.jsx
@@ -4,49 +4,51 @@ import PropTypes from "prop-types";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import AddToDashboard from "metabase/questions/containers/AddToDashboard.jsx";
 
-
 export default class AddToDashSelectQuestionModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            error: null
-        };
-    }
-
-    static propTypes = {
-        dashboard: PropTypes.object.isRequired,
-        cards: PropTypes.array,
+  constructor(props, context) {
+    super(props, context);
 
-        fetchCards: PropTypes.func.isRequired,
-        addCardToDashboard: PropTypes.func.isRequired,
-        onEditingChange: PropTypes.func.isRequired,
-
-        onClose: PropTypes.func.isRequired
+    this.state = {
+      error: null,
     };
+  }
 
-    async componentDidMount() {
-        try {
-            await this.props.fetchCards();
-        } catch (error) {
-            console.error(error);
-            this.setState({ error });
-        }
-    }
+  static propTypes = {
+    dashboard: PropTypes.object.isRequired,
+    cards: PropTypes.array,
 
-    onAdd(card) {
-        this.props.addCardToDashboard({ dashId: this.props.dashboard.id, cardId: card.id });
-        this.props.onEditingChange(true);
-        this.props.onClose();
-        MetabaseAnalytics.trackEvent("Dashboard", "Add Card");
-    }
+    fetchCards: PropTypes.func.isRequired,
+    addCardToDashboard: PropTypes.func.isRequired,
+    onEditingChange: PropTypes.func.isRequired,
+
+    onClose: PropTypes.func.isRequired,
+  };
 
-    render() {
-        return (
-            <AddToDashboard
-                onAdd={(card) => this.onAdd(card)}
-                onClose={this.props.onClose}
-            />
-        )
+  async componentDidMount() {
+    try {
+      await this.props.fetchCards();
+    } catch (error) {
+      console.error(error);
+      this.setState({ error });
     }
+  }
+
+  onAdd(card) {
+    this.props.addCardToDashboard({
+      dashId: this.props.dashboard.id,
+      cardId: card.id,
+    });
+    this.props.onEditingChange(true);
+    this.props.onClose();
+    MetabaseAnalytics.trackEvent("Dashboard", "Add Card");
+  }
+
+  render() {
+    return (
+      <AddToDashboard
+        onAdd={card => this.onAdd(card)}
+        onClose={this.props.onClose}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx b/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx
index 78922e6ebbd97eb75cba0fefdab9f05999c27f29..978ce608bf69c1aeb8cd616a73ef75061310676a 100644
--- a/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx
+++ b/frontend/src/metabase/dashboard/components/ArchiveDashboardModal.jsx
@@ -4,61 +4,65 @@ import PropTypes from "prop-types";
 import ModalContent from "metabase/components/ModalContent.jsx";
 
 export default class ArchiveDashboardModal extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            error: null
-        };
-    }
+    this.state = {
+      error: null,
+    };
+  }
 
-    static propTypes = {
-        dashboard: PropTypes.object.isRequired,
+  static propTypes = {
+    dashboard: PropTypes.object.isRequired,
 
-        onClose: PropTypes.func,
-        onArchive: PropTypes.func
-    };
+    onClose: PropTypes.func,
+    onArchive: PropTypes.func,
+  };
 
-    async archiveDashboard() {
-        try {
-            this.props.onArchive(this.props.dashboard);
-        } catch (error) {
-            this.setState({ error });
-        }
+  async archiveDashboard() {
+    try {
+      this.props.onArchive(this.props.dashboard);
+    } catch (error) {
+      this.setState({ error });
     }
+  }
 
-    render() {
-        var formError;
-        if (this.state.error) {
-            var errorMessage = "Server error encountered";
-            if (this.state.error.data &&
-                this.state.error.data.message) {
-                errorMessage = this.state.error.data.message;
-            } else {
-                errorMessage = this.state.error.message;
-            }
+  render() {
+    var formError;
+    if (this.state.error) {
+      var errorMessage = "Server error encountered";
+      if (this.state.error.data && this.state.error.data.message) {
+        errorMessage = this.state.error.data.message;
+      } else {
+        errorMessage = this.state.error.message;
+      }
 
-            // TODO: timeout display?
-            formError = (
-                <span className="text-error px2">{errorMessage}</span>
-            );
-        }
+      // TODO: timeout display?
+      formError = <span className="text-error px2">{errorMessage}</span>;
+    }
 
-        return (
-            <ModalContent
-                title="Archive Dashboard"
-                onClose={this.props.onClose}
-            >
-                <div className="Form-inputs mb4">
-                    <p>Are you sure you want to do this?</p>
-                </div>
+    return (
+      <ModalContent title="Archive Dashboard" onClose={this.props.onClose}>
+        <div className="Form-inputs mb4">
+          <p>Are you sure you want to do this?</p>
+        </div>
 
-                <div className="Form-actions">
-                    <button className="Button Button--danger" onClick={() => this.archiveDashboard()}>Yes</button>
-                    <button className="Button Button--primary ml1" onClick={this.props.onClose}>No</button>
-                    {formError}
-                </div>
-            </ModalContent>
-        );
-    }
+        <div className="Form-actions">
+          <button
+            className="Button Button--danger"
+            onClick={() => this.archiveDashboard()}
+          >
+            Yes
+          </button>
+          <button
+            className="Button Button--primary ml1"
+            onClick={this.props.onClose}
+          >
+            No
+          </button>
+          {formError}
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/DashCard.jsx b/frontend/src/metabase/dashboard/components/DashCard.jsx
index af45d5b4fbf893563636522cde0addf30cc259b0..992be8f22d2086e03684c4758ca93a33e30ce8c6 100644
--- a/frontend/src/metabase/dashboard/components/DashCard.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCard.jsx
@@ -3,7 +3,10 @@ import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 
 import visualizations, { getVisualizationRaw } from "metabase/visualizations";
-import Visualization, { ERROR_MESSAGE_GENERIC, ERROR_MESSAGE_PERMISSION } from "metabase/visualizations/components/Visualization.jsx";
+import Visualization, {
+  ERROR_MESSAGE_GENERIC,
+  ERROR_MESSAGE_PERMISSION,
+} from "metabase/visualizations/components/Visualization.jsx";
 
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ChartSettings from "metabase/visualizations/components/ChartSettings.jsx";
@@ -23,170 +26,235 @@ const DATASET_USUALLY_FAST_THRESHOLD = 15 * 1000;
 const HEADER_ICON_SIZE = 16;
 
 const HEADER_ACTION_STYLE = {
-    padding: 4
+  padding: 4,
 };
 
 export default class DashCard extends Component {
-    static propTypes = {
-        dashcard: PropTypes.object.isRequired,
-        dashcardData: PropTypes.object.isRequired,
-        slowCards: PropTypes.object.isRequired,
-        parameterValues: PropTypes.object.isRequired,
-        markNewCardSeen: PropTypes.func.isRequired,
-        fetchCardData: PropTypes.func.isRequired,
-        navigateToNewCardFromDashboard: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    dashcard: PropTypes.object.isRequired,
+    dashcardData: PropTypes.object.isRequired,
+    slowCards: PropTypes.object.isRequired,
+    parameterValues: PropTypes.object.isRequired,
+    markNewCardSeen: PropTypes.func.isRequired,
+    fetchCardData: PropTypes.func.isRequired,
+    navigateToNewCardFromDashboard: PropTypes.func.isRequired,
+  };
 
-    async componentDidMount() {
-        const { dashcard, markNewCardSeen } = this.props;
+  async componentDidMount() {
+    const { dashcard, markNewCardSeen } = this.props;
 
-        // HACK: way to scroll to a newly added card
-        if (dashcard.justAdded) {
-            ReactDOM.findDOMNode(this).scrollIntoView();
-            markNewCardSeen(dashcard.id);
-        }
+    // HACK: way to scroll to a newly added card
+    if (dashcard.justAdded) {
+      ReactDOM.findDOMNode(this).scrollIntoView();
+      markNewCardSeen(dashcard.id);
     }
+  }
 
-    componentWillUnmount() {
-        window.clearInterval(this.visibilityTimer);
-    }
+  componentWillUnmount() {
+    window.clearInterval(this.visibilityTimer);
+  }
+
+  render() {
+    const {
+      dashcard,
+      dashcardData,
+      slowCards,
+      isEditing,
+      isEditingParameter,
+      onAddSeries,
+      onRemove,
+      navigateToNewCardFromDashboard,
+      metadata,
+    } = this.props;
+
+    const mainCard = {
+      ...dashcard.card,
+      visualization_settings: {
+        ...dashcard.card.visualization_settings,
+        ...dashcard.visualization_settings,
+      },
+    };
+    const cards = [mainCard].concat(dashcard.series || []);
+    const series = cards.map(card => ({
+      ...getIn(dashcardData, [dashcard.id, card.id]),
+      card: card,
+      isSlow: slowCards[card.id],
+      isUsuallyFast:
+        card.query_average_duration &&
+        card.query_average_duration < DATASET_USUALLY_FAST_THRESHOLD,
+    }));
+
+    const loading = !(series.length > 0 && _.every(series, s => s.data));
+    const expectedDuration = Math.max(
+      ...series.map(s => s.card.query_average_duration || 0),
+    );
+    const usuallyFast = _.every(series, s => s.isUsuallyFast);
+    const isSlow =
+      loading &&
+      _.some(series, s => s.isSlow) &&
+      (usuallyFast ? "usually-fast" : "usually-slow");
+    const errors = series.map(s => s.error).filter(e => e);
 
-    render() {
-        const {
-            dashcard,
-            dashcardData,
-            slowCards,
-            isEditing,
-            isEditingParameter,
-            onAddSeries,
-            onRemove,
-            navigateToNewCardFromDashboard,
-            metadata
-        } = this.props;
-
-        const mainCard = {
-            ...dashcard.card,
-            visualization_settings: { ...dashcard.card.visualization_settings, ...dashcard.visualization_settings }
-        };
-        const cards = [mainCard].concat(dashcard.series || []);
-        const series = cards
-            .map(card => ({
-                ...getIn(dashcardData, [dashcard.id, card.id]),
-                card: card,
-                isSlow: slowCards[card.id],
-                isUsuallyFast: card.query_average_duration && (card.query_average_duration < DATASET_USUALLY_FAST_THRESHOLD)
-            }));
-
-        const loading = !(series.length > 0 && _.every(series, (s) => s.data));
-        const expectedDuration = Math.max(...series.map((s) => s.card.query_average_duration || 0));
-        const usuallyFast = _.every(series, (s) => s.isUsuallyFast);
-        const isSlow = loading && _.some(series, (s) => s.isSlow) && (usuallyFast ? "usually-fast" : "usually-slow");
-        const errors = series.map(s => s.error).filter(e => e);
-
-        let errorMessage, errorIcon;
-        if (_.any(errors, e => e && e.status === 403)) {
-            errorMessage = ERROR_MESSAGE_PERMISSION;
-            errorIcon = "key";
-        } else if (errors.length > 0) {
-            if (IS_EMBED_PREVIEW) {
-                errorMessage = errors[0] && errors[0].data || ERROR_MESSAGE_GENERIC;
-            } else {
-                errorMessage = ERROR_MESSAGE_GENERIC;
-            }
-            errorIcon = "warning";
-        }
-
-        return (
-            <div
-                className={cx("Card bordered rounded flex flex-column hover-parent hover--visibility", {
-                    "Card--recent": dashcard.isAdded,
-                    "Card--slow": isSlow === "usually-slow"
-                })}
-            >
-                <Visualization
-                    className="flex-full"
-                    error={errorMessage}
-                    errorIcon={errorIcon}
-                    isSlow={isSlow}
-                    expectedDuration={expectedDuration}
-                    rawSeries={series}
-                    showTitle
-                    isDashboard
-                    isEditing={isEditing}
-                    gridSize={this.props.isMobile ? undefined : { width: dashcard.sizeX, height: dashcard.sizeY }}
-                    actionButtons={isEditing && !isEditingParameter ?
-                        <DashCardActionButtons
-                            series={series}
-                            onRemove={onRemove}
-                            onAddSeries={onAddSeries}
-                            onReplaceAllVisualizationSettings={this.props.onReplaceAllVisualizationSettings}
-                        /> : undefined
-                    }
-                    onUpdateVisualizationSettings={this.props.onUpdateVisualizationSettings}
-                    replacementContent={isEditingParameter && <DashCardParameterMapper dashcard={dashcard} />}
-                    metadata={metadata}
-                    onChangeCardAndRun={ navigateToNewCardFromDashboard ? ({ nextCard, previousCard }) => {
-                        // navigateToNewCardFromDashboard needs `dashcard` for applying active filters to the query
-                        navigateToNewCardFromDashboard({ nextCard, previousCard, dashcard })
-                    } : null}
-                />
-            </div>
-        );
+    let errorMessage, errorIcon;
+    if (_.any(errors, e => e && e.status === 403)) {
+      errorMessage = ERROR_MESSAGE_PERMISSION;
+      errorIcon = "key";
+    } else if (errors.length > 0) {
+      if (IS_EMBED_PREVIEW) {
+        errorMessage = (errors[0] && errors[0].data) || ERROR_MESSAGE_GENERIC;
+      } else {
+        errorMessage = ERROR_MESSAGE_GENERIC;
+      }
+      errorIcon = "warning";
     }
+
+    return (
+      <div
+        className={cx(
+          "Card bordered rounded flex flex-column hover-parent hover--visibility",
+          {
+            "Card--recent": dashcard.isAdded,
+            "Card--slow": isSlow === "usually-slow",
+          },
+        )}
+      >
+        <Visualization
+          className="flex-full"
+          error={errorMessage}
+          errorIcon={errorIcon}
+          isSlow={isSlow}
+          expectedDuration={expectedDuration}
+          rawSeries={series}
+          showTitle
+          isDashboard
+          isEditing={isEditing}
+          gridSize={
+            this.props.isMobile
+              ? undefined
+              : { width: dashcard.sizeX, height: dashcard.sizeY }
+          }
+          actionButtons={
+            isEditing && !isEditingParameter ? (
+              <DashCardActionButtons
+                series={series}
+                onRemove={onRemove}
+                onAddSeries={onAddSeries}
+                onReplaceAllVisualizationSettings={
+                  this.props.onReplaceAllVisualizationSettings
+                }
+              />
+            ) : (
+              undefined
+            )
+          }
+          onUpdateVisualizationSettings={
+            this.props.onUpdateVisualizationSettings
+          }
+          replacementContent={
+            isEditingParameter && (
+              <DashCardParameterMapper dashcard={dashcard} />
+            )
+          }
+          metadata={metadata}
+          onChangeCardAndRun={
+            navigateToNewCardFromDashboard
+              ? ({ nextCard, previousCard }) => {
+                  // navigateToNewCardFromDashboard needs `dashcard` for applying active filters to the query
+                  navigateToNewCardFromDashboard({
+                    nextCard,
+                    previousCard,
+                    dashcard,
+                  });
+                }
+              : null
+          }
+        />
+      </div>
+    );
+  }
 }
 
-const DashCardActionButtons = ({ series, onRemove, onAddSeries, onReplaceAllVisualizationSettings }) =>
-    <span className="DashCard-actions flex align-center" style={{ lineHeight: 1 }}>
-        { getVisualizationRaw(series).CardVisualization.supportsSeries &&
-            <AddSeriesButton series={series} onAddSeries={onAddSeries} />
-        }
-        { onReplaceAllVisualizationSettings && !getVisualizationRaw(series).CardVisualization.disableSettingsConfig &&
-            <ChartSettingsButton series={series} onReplaceAllVisualizationSettings={onReplaceAllVisualizationSettings} />
-        }
-        <RemoveButton onRemove={onRemove} />
-    </span>
+const DashCardActionButtons = ({
+  series,
+  onRemove,
+  onAddSeries,
+  onReplaceAllVisualizationSettings,
+}) => (
+  <span
+    className="DashCard-actions flex align-center"
+    style={{ lineHeight: 1 }}
+  >
+    {getVisualizationRaw(series).CardVisualization.supportsSeries && (
+      <AddSeriesButton series={series} onAddSeries={onAddSeries} />
+    )}
+    {onReplaceAllVisualizationSettings &&
+      !getVisualizationRaw(series).CardVisualization.disableSettingsConfig && (
+        <ChartSettingsButton
+          series={series}
+          onReplaceAllVisualizationSettings={onReplaceAllVisualizationSettings}
+        />
+      )}
+    <RemoveButton onRemove={onRemove} />
+  </span>
+);
+
+const ChartSettingsButton = ({ series, onReplaceAllVisualizationSettings }) => (
+  <ModalWithTrigger
+    wide
+    tall
+    triggerElement={
+      <Icon name="gear" size={HEADER_ICON_SIZE} style={HEADER_ACTION_STYLE} />
+    }
+    triggerClasses="text-grey-2 text-grey-4-hover cursor-pointer flex align-center flex-no-shrink mr1"
+  >
+    <ChartSettings
+      series={series}
+      onChange={onReplaceAllVisualizationSettings}
+      isDashboard
+    />
+  </ModalWithTrigger>
+);
 
-const ChartSettingsButton = ({ series, onReplaceAllVisualizationSettings }) =>
-    <ModalWithTrigger
-        wide tall
-        triggerElement={<Icon name="gear" size={HEADER_ICON_SIZE} style={HEADER_ACTION_STYLE} />}
-        triggerClasses="text-grey-2 text-grey-4-hover cursor-pointer flex align-center flex-no-shrink mr1"
-    >
-        <ChartSettings
-            series={series}
-            onChange={onReplaceAllVisualizationSettings}
-            isDashboard
+const RemoveButton = ({ onRemove }) => (
+  <a
+    className="text-grey-2 text-grey-4-hover "
+    data-metabase-event="Dashboard;Remove Card Modal"
+    onClick={onRemove}
+    style={HEADER_ACTION_STYLE}
+  >
+    <Icon name="close" size={HEADER_ICON_SIZE} />
+  </a>
+);
+
+const AddSeriesButton = ({ series, onAddSeries }) => (
+  <a
+    data-metabase-event={"Dashboard;Edit Series Modal;open"}
+    className="text-grey-2 text-grey-4-hover cursor-pointer h3 flex-no-shrink relative mr1"
+    onClick={onAddSeries}
+    style={HEADER_ACTION_STYLE}
+  >
+    <span className="flex align-center">
+      <span className="flex">
+        <Icon
+          className="absolute"
+          name="add"
+          style={{ top: 0, left: 0 }}
+          size={HEADER_ICON_SIZE / 2}
         />
-    </ModalWithTrigger>
-
-const RemoveButton = ({ onRemove }) =>
-    <a className="text-grey-2 text-grey-4-hover " data-metabase-event="Dashboard;Remove Card Modal" onClick={onRemove} style={HEADER_ACTION_STYLE}>
-        <Icon name="close" size={HEADER_ICON_SIZE} />
-    </a>
-
-const AddSeriesButton = ({ series, onAddSeries }) =>
-    <a
-        data-metabase-event={"Dashboard;Edit Series Modal;open"}
-        className="text-grey-2 text-grey-4-hover cursor-pointer h3 flex-no-shrink relative mr1"
-        onClick={onAddSeries}
-        style={HEADER_ACTION_STYLE}
-    >
-        <span className="flex align-center">
-            <span className="flex">
-                <Icon className="absolute" name="add" style={{ top: 0, left: 0 }} size={HEADER_ICON_SIZE / 2} />
-                <Icon name={getSeriesIconName(series)} size={HEADER_ICON_SIZE} />
-            </span>
-            <span className="flex-no-shrink text-bold">
-                &nbsp;{ series.length > 1 ? "Edit" : "Add" }
-            </span>
-        </span>
-    </a>
+        <Icon name={getSeriesIconName(series)} size={HEADER_ICON_SIZE} />
+      </span>
+      <span className="flex-no-shrink text-bold">
+        &nbsp;{series.length > 1 ? "Edit" : "Add"}
+      </span>
+    </span>
+  </a>
+);
 
 function getSeriesIconName(series) {
-    try {
-        let display = series[0].card.display;
-        return visualizations.get(display === "scalar" ? "bar" : display).iconName;
-    } catch (e) {
-        return "bar";
-    }
+  try {
+    let display = series[0].card.display;
+    return visualizations.get(display === "scalar" ? "bar" : display).iconName;
+  } catch (e) {
+    return "bar";
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx b/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx
index da2ed9a73a588d01603a021508435fd5f479b027..0ddcf72b7e9a250843dbe65217b9b8b6a3c99d20 100644
--- a/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx
+++ b/frontend/src/metabase/dashboard/components/DashCardParameterMapper.jsx
@@ -2,18 +2,34 @@ import React from "react";
 
 import DashCardCardParameterMapper from "../containers/DashCardCardParameterMapper.jsx";
 
-const DashCardParameterMapper = ({ dashcard }) =>
-    <div className="relative flex-full flex flex-column layout-centered">
-        { dashcard.series && dashcard.series.length > 0 &&
-            <div className="mx4 my1 p1 rounded" style={{ backgroundColor: "#F5F5F5", color: "#8691AC", marginTop: -10 }}>
-                Make sure to make a selection for each series, or the filter won't work on this card.
-            </div>
-        }
-        <div className="flex mx4 z1" style={{ justifyContent: "space-around" }}>
-            {[dashcard.card].concat(dashcard.series || []).map(card =>
-                <DashCardCardParameterMapper key={`${dashcard.id},${card.id}`} dashcard={dashcard} card={card} />
-            )}
+const DashCardParameterMapper = ({ dashcard }) => (
+  <div className="relative flex-full flex flex-column layout-centered">
+    {dashcard.series &&
+      dashcard.series.length > 0 && (
+        <div
+          className="mx4 my1 p1 rounded"
+          style={{
+            backgroundColor: "#F5F5F5",
+            color: "#8691AC",
+            marginTop: -10,
+          }}
+        >
+          Make sure to make a selection for each series, or the filter won't
+          work on this card.
         </div>
+      )}
+    <div className="flex mx4 z1" style={{ justifyContent: "space-around" }}>
+      {[dashcard.card]
+        .concat(dashcard.series || [])
+        .map(card => (
+          <DashCardCardParameterMapper
+            key={`${dashcard.id},${card.id}`}
+            dashcard={dashcard}
+            card={card}
+          />
+        ))}
     </div>
+  </div>
+);
 
 export default DashCardParameterMapper;
diff --git a/frontend/src/metabase/dashboard/components/Dashboard.jsx b/frontend/src/metabase/dashboard/components/Dashboard.jsx
index 2a4fdd29cb889098547ac1b302017b2a9ab2e0e0..91a07bc3b747542a4fb118d4d1a69ac6a3a5494d 100644
--- a/frontend/src/metabase/dashboard/components/Dashboard.jsx
+++ b/frontend/src/metabase/dashboard/components/Dashboard.jsx
@@ -14,225 +14,287 @@ import DashboardControls from "../hoc/DashboardControls";
 import _ from "underscore";
 import cx from "classnames";
 
-import type { LocationDescriptor, ApiError, QueryParams } from "metabase/meta/types"
-
-import type { Card, CardId, VisualizationSettings } from "metabase/meta/types/Card";
-import type { DashboardWithCards, DashboardId, DashCardId } from "metabase/meta/types/Dashboard";
+import type {
+  LocationDescriptor,
+  ApiError,
+  QueryParams,
+} from "metabase/meta/types";
+
+import type {
+  Card,
+  CardId,
+  VisualizationSettings,
+} from "metabase/meta/types/Card";
+import type {
+  DashboardWithCards,
+  DashboardId,
+  DashCardId,
+} from "metabase/meta/types/Dashboard";
 import type { Revision, RevisionId } from "metabase/meta/types/Revision";
-import type { Parameter, ParameterId, ParameterValues, ParameterOption } from "metabase/meta/types/Parameter";
+import type {
+  Parameter,
+  ParameterId,
+  ParameterValues,
+  ParameterOption,
+} from "metabase/meta/types/Parameter";
 
 type Props = {
-    location:               LocationDescriptor,
-
-    dashboardId:            DashboardId,
-    dashboard:              DashboardWithCards,
-    cards:                  Card[],
-    revisions:              { [key: string]: Revision[] },
-
-    isAdmin:                boolean,
-    isEditable:             boolean,
-    isEditing:              boolean,
-    isEditingParameter:     boolean,
-
-    parameters:             Parameter[],
-    parameterValues:        ParameterValues,
-
-    addCardOnLoad:          DashboardId,
-
-    initialize:                 () => Promise<void>,
-    addCardToDashboard:         ({ dashId: DashCardId, cardId: CardId }) => void,
-    addTextDashCardToDashboard: ({ dashId: DashCardId }) => void,
-    archiveDashboard:           (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,
-
-    refreshPeriod:          number,
-    refreshElapsed:         number,
-    isFullscreen:           boolean,
-    isNightMode:            boolean,
-
-    onRefreshPeriodChange:  (?number) => void,
-    onNightModeChange:      (boolean) => void,
-    onFullscreenChange:     (boolean) => void,
-
-    loadDashboardParams:    () => void,
-
-    onReplaceAllDashCardVisualizationSettings: (dashcardId: DashCardId, settings: VisualizationSettings) => void,
-    onUpdateDashCardVisualizationSettings: (dashcardId: DashCardId, settings: VisualizationSettings) => void,
-
-    onChangeLocation:       (string) => void,
-    setErrorPage:           (error: ApiError) => void,
-}
+  location: LocationDescriptor,
+
+  dashboardId: DashboardId,
+  dashboard: DashboardWithCards,
+  cards: Card[],
+  revisions: { [key: string]: Revision[] },
+
+  isAdmin: boolean,
+  isEditable: boolean,
+  isEditing: boolean,
+  isEditingParameter: boolean,
+
+  parameters: Parameter[],
+  parameterValues: ParameterValues,
+
+  addCardOnLoad: DashboardId,
+
+  initialize: () => Promise<void>,
+  addCardToDashboard: ({ dashId: DashCardId, cardId: CardId }) => void,
+  addTextDashCardToDashboard: ({ dashId: DashCardId }) => void,
+  archiveDashboard: (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: boolean,
+    clear: boolean,
+  }) => 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,
+
+  refreshPeriod: number,
+  refreshElapsed: number,
+  isFullscreen: boolean,
+  isNightMode: boolean,
+
+  onRefreshPeriodChange: (?number) => void,
+  onNightModeChange: boolean => void,
+  onFullscreenChange: boolean => 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
-}
+  error: ?ApiError,
+};
 
 @DashboardControls
 export default class Dashboard extends Component {
-    props: Props;
-    state: State = {
-        error: null,
-    };
-
-    static propTypes = {
-        isEditable: PropTypes.bool,
-        isEditing: PropTypes.bool.isRequired,
-        isEditingParameter: PropTypes.bool.isRequired,
-
-        dashboard: PropTypes.object,
-        cards: PropTypes.array,
-        parameters: PropTypes.array,
-
-        addCardToDashboard: PropTypes.func.isRequired,
-        archiveDashboard: PropTypes.func.isRequired,
-        fetchCards: PropTypes.func.isRequired,
-        fetchDashboard: PropTypes.func.isRequired,
-        fetchRevisions: PropTypes.func.isRequired,
-        revertToRevision: PropTypes.func.isRequired,
-        saveDashboardAndCards: PropTypes.func.isRequired,
-        setDashboardAttributes: PropTypes.func.isRequired,
-        setEditingDashboard: PropTypes.func.isRequired,
-
-        onUpdateDashCardVisualizationSettings: PropTypes.func.isRequired,
-        onReplaceAllDashCardVisualizationSettings: PropTypes.func.isRequired,
-
-        onChangeLocation: PropTypes.func.isRequired,
-    };
-
-    static defaultProps = {
-        isEditable: true
-    };
-
-    componentDidMount() {
-        this.loadDashboard(this.props.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 });
-        }
-    }
-
-    async loadDashboard(dashboardId: DashboardId) {
-        this.props.initialize();
-
-        this.props.loadDashboardParams();
-        const { addCardOnLoad, fetchDashboard, fetchCards, addCardToDashboard, setErrorPage, location } = this.props;
-
-        try {
-            await fetchDashboard(dashboardId, location.query);
-            if (addCardOnLoad != null) {
-                // we have to load our cards before we can add one
-                await fetchCards();
-                this.setEditing(true);
-                addCardToDashboard({ dashId: dashboardId, cardId: addCardOnLoad });
-            }
-        } catch (error) {
-            if (error.status === 404) {
-                setErrorPage({ ...error, context: "dashboard" });
-            } else {
-                console.error(error);
-                this.setState({ error });
-            }
-        }
+  props: Props;
+  state: State = {
+    error: null,
+  };
+
+  static propTypes = {
+    isEditable: PropTypes.bool,
+    isEditing: PropTypes.bool.isRequired,
+    isEditingParameter: PropTypes.bool.isRequired,
+
+    dashboard: PropTypes.object,
+    cards: PropTypes.array,
+    parameters: PropTypes.array,
+
+    addCardToDashboard: PropTypes.func.isRequired,
+    archiveDashboard: PropTypes.func.isRequired,
+    fetchCards: PropTypes.func.isRequired,
+    fetchDashboard: PropTypes.func.isRequired,
+    fetchRevisions: PropTypes.func.isRequired,
+    revertToRevision: PropTypes.func.isRequired,
+    saveDashboardAndCards: PropTypes.func.isRequired,
+    setDashboardAttributes: PropTypes.func.isRequired,
+    setEditingDashboard: PropTypes.func.isRequired,
+
+    onUpdateDashCardVisualizationSettings: PropTypes.func.isRequired,
+    onReplaceAllDashCardVisualizationSettings: PropTypes.func.isRequired,
+
+    onChangeLocation: PropTypes.func.isRequired,
+  };
+
+  static defaultProps = {
+    isEditable: true,
+  };
+
+  componentDidMount() {
+    this.loadDashboard(this.props.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 });
     }
-
-    setEditing = (isEditing: boolean) => {
-        this.props.onRefreshPeriodChange(null);
-        this.props.setEditingDashboard(isEditing);
+  }
+
+  async loadDashboard(dashboardId: DashboardId) {
+    this.props.initialize();
+
+    this.props.loadDashboardParams();
+    const {
+      addCardOnLoad,
+      fetchDashboard,
+      fetchCards,
+      addCardToDashboard,
+      setErrorPage,
+      location,
+    } = this.props;
+
+    try {
+      await fetchDashboard(dashboardId, location.query);
+      if (addCardOnLoad != null) {
+        // we have to load our cards before we can add one
+        await fetchCards();
+        this.setEditing(true);
+        addCardToDashboard({ dashId: dashboardId, cardId: addCardOnLoad });
+      }
+    } catch (error) {
+      if (error.status === 404) {
+        setErrorPage({ ...error, context: "dashboard" });
+      } else {
+        console.error(error);
+        this.setState({ error });
+      }
     }
-
-    setDashboardAttribute = (attribute: string, value: any) => {
-        this.props.setDashboardAttributes({
-            id: this.props.dashboard.id,
-            attributes: { [attribute]: value }
-        });
+  }
+
+  setEditing = (isEditing: boolean) => {
+    this.props.onRefreshPeriodChange(null);
+    this.props.setEditingDashboard(isEditing);
+  };
+
+  setDashboardAttribute = (attribute: string, value: any) => {
+    this.props.setDashboardAttributes({
+      id: this.props.dashboard.id,
+      attributes: { [attribute]: value },
+    });
+  };
+
+  render() {
+    let {
+      dashboard,
+      isEditing,
+      editingParameter,
+      parameters,
+      parameterValues,
+      location,
+      isFullscreen,
+      isNightMode,
+    } = this.props;
+    let { error } = this.state;
+    isNightMode = isNightMode && isFullscreen;
+
+    let parametersWidget;
+    if (parameters && parameters.length > 0) {
+      parametersWidget = (
+        <Parameters
+          syncQueryString
+          isEditing={isEditing}
+          isFullscreen={isFullscreen}
+          isNightMode={isNightMode}
+          parameters={parameters.map(p => ({
+            ...p,
+            value: parameterValues[p.id],
+          }))}
+          query={location.query}
+          editingParameter={editingParameter}
+          setEditingParameter={this.props.setEditingParameter}
+          setParameterName={this.props.setParameterName}
+          setParameterDefaultValue={this.props.setParameterDefaultValue}
+          removeParameter={this.props.removeParameter}
+          setParameterValue={this.props.setParameterValue}
+        />
+      );
     }
 
-    render() {
-        let { dashboard, isEditing, editingParameter, parameters, parameterValues, location, isFullscreen, isNightMode } = this.props;
-        let { error } = this.state;
-        isNightMode = isNightMode && isFullscreen;
-
-        let parametersWidget;
-        if (parameters && parameters.length > 0) {
-            parametersWidget = (
-                <Parameters
-                    syncQueryString
-
-                    isEditing={isEditing}
-                    isFullscreen={isFullscreen}
-                    isNightMode={isNightMode}
-
-                    parameters={parameters.map(p => ({ ...p, value: parameterValues[p.id] }))}
-                    query={location.query}
-
-                    editingParameter={editingParameter}
-                    setEditingParameter={this.props.setEditingParameter}
-
-                    setParameterName={this.props.setParameterName}
-                    setParameterDefaultValue={this.props.setParameterDefaultValue}
-                    removeParameter={this.props.removeParameter}
-                    setParameterValue={this.props.setParameterValue}
+    return (
+      <LoadingAndErrorWrapper
+        className={cx("Dashboard flex-full pb4", {
+          "Dashboard--fullscreen": isFullscreen,
+          "Dashboard--night": isNightMode,
+        })}
+        loading={!dashboard}
+        error={error}
+      >
+        {() => (
+          <div className="full" style={{ overflowX: "hidden" }}>
+            <header className="DashboardHeader relative z2">
+              <DashboardHeader
+                {...this.props}
+                onEditingChange={this.setEditing}
+                setDashboardAttribute={this.setDashboardAttribute}
+                addParameter={this.props.addParameter}
+                parametersWidget={parametersWidget}
+              />
+            </header>
+            {!isFullscreen &&
+              parametersWidget && (
+                <div className="wrapper flex flex-column align-start mt2 relative z2">
+                  {parametersWidget}
+                </div>
+              )}
+            <div className="wrapper">
+              {dashboard.ordered_cards.length === 0 ? (
+                <div className="absolute z1 top bottom left right flex flex-column layout-centered">
+                  <span className="QuestionCircle">?</span>
+                  <div className="text-normal mt3 mb1">
+                    This dashboard is looking empty.
+                  </div>
+                  <div className="text-normal text-grey-2">
+                    Add a question to start making it useful!
+                  </div>
+                </div>
+              ) : (
+                <DashboardGrid
+                  {...this.props}
+                  onEditingChange={this.setEditing}
                 />
-            );
-        }
-
-        return (
-            <LoadingAndErrorWrapper className={cx("Dashboard flex-full pb4", { "Dashboard--fullscreen": isFullscreen, "Dashboard--night": isNightMode})} loading={!dashboard} error={error}>
-                {() =>
-                    <div className="full" style={{ overflowX: "hidden" }}>
-                        <header className="DashboardHeader relative z2">
-                            <DashboardHeader
-                                {...this.props}
-                                onEditingChange={this.setEditing}
-                                setDashboardAttribute={this.setDashboardAttribute}
-                                addParameter={this.props.addParameter}
-                                parametersWidget={parametersWidget}
-                            />
-                        </header>
-                        {!isFullscreen && parametersWidget &&
-                        <div className="wrapper flex flex-column align-start mt2 relative z2">
-                            {parametersWidget}
-                        </div>
-                        }
-                        <div className="wrapper">
-
-                            { dashboard.ordered_cards.length === 0 ?
-                                <div className="absolute z1 top bottom left right flex flex-column layout-centered">
-                                    <span className="QuestionCircle">?</span>
-                                    <div className="text-normal mt3 mb1">This dashboard is looking empty.</div>
-                                    <div className="text-normal text-grey-2">Add a question to start making it useful!</div>
-                                </div>
-                                :
-                                <DashboardGrid
-                                    {...this.props}
-                                    onEditingChange={this.setEditing}
-                                />
-                            }
-                        </div>
-                    </div>
-                }
-            </LoadingAndErrorWrapper>
-        );
-    }
-}
\ No newline at end of file
+              )}
+            </div>
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
+}
diff --git a/frontend/src/metabase/dashboard/components/DashboardActions.jsx b/frontend/src/metabase/dashboard/components/DashboardActions.jsx
index f2d194ee1f9775b855d927fa2fc97779cd7f2ead..68ab04ab611e6d9fd0dca6f0ac707792aa6e84ec 100644
--- a/frontend/src/metabase/dashboard/components/DashboardActions.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardActions.jsx
@@ -5,73 +5,64 @@ 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 = [];
+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 && !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 && 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>
-        );
-    }
+  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;
+  return buttons;
 };
diff --git a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
index ce36abfd94c59470b0421a9600e69f82b3313f37..2ecc0ddae2206fa990d0720c438c563585c4699f 100644
--- a/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardGrid.jsx
@@ -13,10 +13,10 @@ import { getVisualizationRaw } from "metabase/visualizations";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 import {
-    GRID_WIDTH,
-    GRID_ASPECT_RATIO,
-    GRID_MARGIN,
-    DEFAULT_CARD_SIZE
+  GRID_WIDTH,
+  GRID_ASPECT_RATIO,
+  GRID_MARGIN,
+  DEFAULT_CARD_SIZE,
 } from "metabase/lib/dashboard_grid";
 
 import _ from "underscore";
@@ -26,234 +26,289 @@ const MOBILE_ASPECT_RATIO = 3 / 2;
 
 @ExplicitSize
 export default class DashboardGrid extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            layout: this.getLayout(props),
-            dashcards: this.getSortedDashcards(props),
-            removeModalDashCard: null,
-            addSeriesModalDashCard: null,
-            isDragging: false
-        };
-
-        _.bindAll(this, "onDashCardMouseDown");
-    }
+    this.state = {
+      layout: this.getLayout(props),
+      dashcards: this.getSortedDashcards(props),
+      removeModalDashCard: null,
+      addSeriesModalDashCard: null,
+      isDragging: false,
+    };
 
-    static propTypes = {
-        isEditing: PropTypes.bool.isRequired,
-        isEditingParameter: PropTypes.bool.isRequired,
-        dashboard: PropTypes.object.isRequired,
-        parameterValues: PropTypes.object.isRequired,
-        cards: PropTypes.array,
+    _.bindAll(this, "onDashCardMouseDown");
+  }
 
-        setDashCardAttributes: PropTypes.func.isRequired,
-        removeCardFromDashboard: PropTypes.func.isRequired,
-        markNewCardSeen: PropTypes.func.isRequired,
-        fetchCardData: PropTypes.func.isRequired,
+  static propTypes = {
+    isEditing: PropTypes.bool.isRequired,
+    isEditingParameter: PropTypes.bool.isRequired,
+    dashboard: PropTypes.object.isRequired,
+    parameterValues: PropTypes.object.isRequired,
+    cards: PropTypes.array,
 
-        onUpdateDashCardVisualizationSettings: PropTypes.func.isRequired,
-        onReplaceAllDashCardVisualizationSettings: PropTypes.func.isRequired,
+    setDashCardAttributes: PropTypes.func.isRequired,
+    removeCardFromDashboard: PropTypes.func.isRequired,
+    markNewCardSeen: PropTypes.func.isRequired,
+    fetchCardData: PropTypes.func.isRequired,
 
-        onChangeLocation: PropTypes.func.isRequired
-    };
+    onUpdateDashCardVisualizationSettings: PropTypes.func.isRequired,
+    onReplaceAllDashCardVisualizationSettings: PropTypes.func.isRequired,
 
-    static defaultProps = {
-        width: 0,
-        isEditing: false,
-        isEditingParameter: false
-    };
+    onChangeLocation: PropTypes.func.isRequired,
+  };
 
-    componentWillReceiveProps(nextProps) {
-        this.setState({
-            dashcards: this.getSortedDashcards(nextProps),
-            layout: this.getLayout(nextProps)
-        });
-    }
+  static defaultProps = {
+    width: 0,
+    isEditing: false,
+    isEditingParameter: false,
+  };
 
-    onLayoutChange(layout) {
-        var changes = layout.filter(newLayout => !_.isEqual(newLayout, this.getLayoutForDashCard(newLayout.dashcard)));
-        for (var change of changes) {
-            this.props.setDashCardAttributes({
-                id: change.dashcard.id,
-                attributes: { col: change.x, row: change.y, sizeX: change.w, sizeY: change.h }
-            });
-        }
+  componentWillReceiveProps(nextProps) {
+    this.setState({
+      dashcards: this.getSortedDashcards(nextProps),
+      layout: this.getLayout(nextProps),
+    });
+  }
 
-        if (changes && changes.length > 0) {
-            MetabaseAnalytics.trackEvent("Dashboard", "Layout Changed");
-        }
+  onLayoutChange(layout) {
+    var changes = layout.filter(
+      newLayout =>
+        !_.isEqual(newLayout, this.getLayoutForDashCard(newLayout.dashcard)),
+    );
+    for (var change of changes) {
+      this.props.setDashCardAttributes({
+        id: change.dashcard.id,
+        attributes: {
+          col: change.x,
+          row: change.y,
+          sizeX: change.w,
+          sizeY: change.h,
+        },
+      });
     }
 
-    getSortedDashcards(props) {
-        return props.dashboard && props.dashboard.ordered_cards.sort((a, b) => {
-            if (a.row < b.row) { return -1; }
-            if (a.row > b.row) { return  1; }
-            if (a.col < b.col) { return -1; }
-            if (a.col > b.col) { return  1; }
-            return 0;
-        });
+    if (changes && changes.length > 0) {
+      MetabaseAnalytics.trackEvent("Dashboard", "Layout Changed");
     }
+  }
 
-    getLayoutForDashCard(dashcard) {
-        let { CardVisualization } = getVisualizationRaw([{ card: dashcard.card }]);
-        let initialSize = DEFAULT_CARD_SIZE;
-        let minSize = CardVisualization.minSize || DEFAULT_CARD_SIZE;
-        return ({
-            i: String(dashcard.id),
-            x: dashcard.col || 0,
-            y: dashcard.row || 0,
-            w: dashcard.sizeX || initialSize.width,
-            h: dashcard.sizeY || initialSize.height,
-            dashcard: dashcard,
-            minSize: minSize
-        });
-    }
+  getSortedDashcards(props) {
+    return (
+      props.dashboard &&
+      props.dashboard.ordered_cards.sort((a, b) => {
+        if (a.row < b.row) {
+          return -1;
+        }
+        if (a.row > b.row) {
+          return 1;
+        }
+        if (a.col < b.col) {
+          return -1;
+        }
+        if (a.col > b.col) {
+          return 1;
+        }
+        return 0;
+      })
+    );
+  }
 
-    getLayout(props) {
-        return props.dashboard.ordered_cards.map(this.getLayoutForDashCard);
-    }
+  getLayoutForDashCard(dashcard) {
+    let { CardVisualization } = getVisualizationRaw([{ card: dashcard.card }]);
+    let initialSize = DEFAULT_CARD_SIZE;
+    let minSize = CardVisualization.minSize || DEFAULT_CARD_SIZE;
+    return {
+      i: String(dashcard.id),
+      x: dashcard.col || 0,
+      y: dashcard.row || 0,
+      w: dashcard.sizeX || initialSize.width,
+      h: dashcard.sizeY || initialSize.height,
+      dashcard: dashcard,
+      minSize: minSize,
+    };
+  }
 
-    renderRemoveModal() {
-        // can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
-        let isOpen = this.state.removeModalDashCard != null;
-        return (
-            <Modal isOpen={isOpen}>
-                { isOpen && <RemoveFromDashboardModal
-                    dashcard={this.state.removeModalDashCard}
-                    dashboard={this.props.dashboard}
-                    removeCardFromDashboard={this.props.removeCardFromDashboard}
-                    onClose={() => this.setState({ removeModalDashCard: null })}
-                /> }
-            </Modal>
-        );
-    }
+  getLayout(props) {
+    return props.dashboard.ordered_cards.map(this.getLayoutForDashCard);
+  }
 
-    renderAddSeriesModal() {
-        // can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
-        let isOpen = this.state.addSeriesModalDashCard != null;
-        return (
-            <Modal className="Modal AddSeriesModal" isOpen={isOpen}>
-                { isOpen && <AddSeriesModal
-                    dashcard={this.state.addSeriesModalDashCard}
-                    dashboard={this.props.dashboard}
-                    cards={this.props.cards}
-                    dashcardData={this.props.dashcardData}
-                    databases={this.props.databases}
-                    fetchCards={this.props.fetchCards}
-                    fetchCardData={this.props.fetchCardData}
-                    fetchDatabaseMetadata={this.props.fetchDatabaseMetadata}
-                    removeCardFromDashboard={this.props.removeCardFromDashboard}
-                    setDashCardAttributes={this.props.setDashCardAttributes}
-                    onClose={() => this.setState({ addSeriesModalDashCard: null })}
-                /> }
-            </Modal>
-        );
-    }
+  renderRemoveModal() {
+    // can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
+    let isOpen = this.state.removeModalDashCard != null;
+    return (
+      <Modal isOpen={isOpen}>
+        {isOpen && (
+          <RemoveFromDashboardModal
+            dashcard={this.state.removeModalDashCard}
+            dashboard={this.props.dashboard}
+            removeCardFromDashboard={this.props.removeCardFromDashboard}
+            onClose={() => this.setState({ removeModalDashCard: null })}
+          />
+        )}
+      </Modal>
+    );
+  }
 
-    // we need to track whether or not we're dragging so we can disable pointer events on action buttons :-/
-    onDrag() {
-        if (!this.state.isDragging) {
-            this.setState({ isDragging: true });
-        }
-    }
-    onDragStop() {
-        this.setState({ isDragging: false });
-    }
+  renderAddSeriesModal() {
+    // can't use PopoverWithTrigger due to strange interaction with ReactGridLayout
+    let isOpen = this.state.addSeriesModalDashCard != null;
+    return (
+      <Modal className="Modal AddSeriesModal" isOpen={isOpen}>
+        {isOpen && (
+          <AddSeriesModal
+            dashcard={this.state.addSeriesModalDashCard}
+            dashboard={this.props.dashboard}
+            cards={this.props.cards}
+            dashcardData={this.props.dashcardData}
+            databases={this.props.databases}
+            fetchCards={this.props.fetchCards}
+            fetchCardData={this.props.fetchCardData}
+            fetchDatabaseMetadata={this.props.fetchDatabaseMetadata}
+            removeCardFromDashboard={this.props.removeCardFromDashboard}
+            setDashCardAttributes={this.props.setDashCardAttributes}
+            onClose={() => this.setState({ addSeriesModalDashCard: null })}
+          />
+        )}
+      </Modal>
+    );
+  }
 
-    // we use onMouseDownCapture to prevent dragging due to react-grid-layout bug referenced below
-    onDashCardMouseDown(e) {
-        if (!this.props.isEditing) {
-            e.stopPropagation();
-        }
+  // we need to track whether or not we're dragging so we can disable pointer events on action buttons :-/
+  onDrag() {
+    if (!this.state.isDragging) {
+      this.setState({ isDragging: true });
     }
+  }
+  onDragStop() {
+    this.setState({ isDragging: false });
+  }
 
-    onDashCardRemove(dc) {
-        this.setState({ removeModalDashCard: dc });
+  // we use onMouseDownCapture to prevent dragging due to react-grid-layout bug referenced below
+  onDashCardMouseDown(e) {
+    if (!this.props.isEditing) {
+      e.stopPropagation();
     }
+  }
 
-    onDashCardAddSeries(dc) {
-        this.setState({ addSeriesModalDashCard: dc });
-    }
+  onDashCardRemove(dc) {
+    this.setState({ removeModalDashCard: dc });
+  }
 
-    renderDashCard(dc, isMobile) {
-        return (
-            <DashCard
-                dashcard={dc}
-                dashcardData={this.props.dashcardData}
-                parameterValues={this.props.parameterValues}
-                slowCards={this.props.slowCards}
-                fetchCardData={this.props.fetchCardData}
-                markNewCardSeen={this.props.markNewCardSeen}
-                isEditing={this.props.isEditing}
-                isEditingParameter={this.props.isEditingParameter}
-                isFullscreen={this.props.isFullscreen}
-                isMobile={isMobile}
-                onRemove={this.onDashCardRemove.bind(this, dc)}
-                onAddSeries={this.onDashCardAddSeries.bind(this, dc)}
-                onUpdateVisualizationSettings={this.props.onUpdateDashCardVisualizationSettings.bind(this, dc.id)}
-                onReplaceAllVisualizationSettings={this.props.onReplaceAllDashCardVisualizationSettings.bind(this, dc.id)}
-                navigateToNewCardFromDashboard={this.props.navigateToNewCardFromDashboard}
-                metadata={this.props.metadata}
-            />
-        )
-    }
+  onDashCardAddSeries(dc) {
+    this.setState({ addSeriesModalDashCard: dc });
+  }
 
-    renderMobile() {
-        const { isEditing, isEditingParameter, width } = this.props;
-        const { dashcards } = this.state;
-        return (
+  renderDashCard(dc, isMobile) {
+    return (
+      <DashCard
+        dashcard={dc}
+        dashcardData={this.props.dashcardData}
+        parameterValues={this.props.parameterValues}
+        slowCards={this.props.slowCards}
+        fetchCardData={this.props.fetchCardData}
+        markNewCardSeen={this.props.markNewCardSeen}
+        isEditing={this.props.isEditing}
+        isEditingParameter={this.props.isEditingParameter}
+        isFullscreen={this.props.isFullscreen}
+        isMobile={isMobile}
+        onRemove={this.onDashCardRemove.bind(this, dc)}
+        onAddSeries={this.onDashCardAddSeries.bind(this, dc)}
+        onUpdateVisualizationSettings={this.props.onUpdateDashCardVisualizationSettings.bind(
+          this,
+          dc.id,
+        )}
+        onReplaceAllVisualizationSettings={this.props.onReplaceAllDashCardVisualizationSettings.bind(
+          this,
+          dc.id,
+        )}
+        navigateToNewCardFromDashboard={
+          this.props.navigateToNewCardFromDashboard
+        }
+        metadata={this.props.metadata}
+      />
+    );
+  }
+
+  renderMobile() {
+    const { isEditing, isEditingParameter, width } = this.props;
+    const { dashcards } = this.state;
+    return (
+      <div
+        className={cx("DashboardGrid", {
+          "Dash--editing": isEditing,
+          "Dash--editingParameter": isEditingParameter,
+          "Dash--dragging": this.state.isDragging,
+        })}
+        style={{ margin: 0 }}
+      >
+        {dashcards &&
+          dashcards.map(dc => (
             <div
-                className={cx("DashboardGrid", { "Dash--editing": isEditing, "Dash--editingParameter": isEditingParameter, "Dash--dragging": this.state.isDragging })}
-                style={{ margin: 0 }}
+              key={dc.id}
+              className="DashCard"
+              style={{
+                width: width,
+                marginTop: 10,
+                marginBottom: 10,
+                height: width / MOBILE_ASPECT_RATIO,
+              }}
             >
-                {dashcards && dashcards.map(dc =>
-                    <div key={dc.id} className="DashCard" style={{ width: width, marginTop: 10, marginBottom: 10, height: width / MOBILE_ASPECT_RATIO}}>
-                        {this.renderDashCard(dc, true)}
-                    </div>
-                )}
+              {this.renderDashCard(dc, true)}
             </div>
-        )
-    }
+          ))}
+      </div>
+    );
+  }
 
-    renderGrid() {
-        const { dashboard, isEditing, isEditingParameter, width } = this.props;
-        const rowHeight = Math.floor(width / GRID_WIDTH / GRID_ASPECT_RATIO);
-        return (
-            <GridLayout
-                className={cx("DashboardGrid", { "Dash--editing": isEditing, "Dash--editingParameter": isEditingParameter, "Dash--dragging": this.state.isDragging })}
-                layout={this.state.layout}
-                cols={GRID_WIDTH}
-                margin={GRID_MARGIN}
-                rowHeight={rowHeight}
-                onLayoutChange={(...args) => this.onLayoutChange(...args)}
-                onDrag={(...args) => this.onDrag(...args)}
-                onDragStop={(...args) => this.onDragStop(...args)}
-                isEditing={isEditing}
+  renderGrid() {
+    const { dashboard, isEditing, isEditingParameter, width } = this.props;
+    const rowHeight = Math.floor(width / GRID_WIDTH / GRID_ASPECT_RATIO);
+    return (
+      <GridLayout
+        className={cx("DashboardGrid", {
+          "Dash--editing": isEditing,
+          "Dash--editingParameter": isEditingParameter,
+          "Dash--dragging": this.state.isDragging,
+        })}
+        layout={this.state.layout}
+        cols={GRID_WIDTH}
+        margin={GRID_MARGIN}
+        rowHeight={rowHeight}
+        onLayoutChange={(...args) => this.onLayoutChange(...args)}
+        onDrag={(...args) => this.onDrag(...args)}
+        onDragStop={(...args) => this.onDragStop(...args)}
+        isEditing={isEditing}
+      >
+        {dashboard &&
+          dashboard.ordered_cards.map(dc => (
+            <div
+              key={dc.id}
+              className="DashCard"
+              onMouseDownCapture={this.onDashCardMouseDown}
+              onTouchStartCapture={this.onDashCardMouseDown}
             >
-                {dashboard && dashboard.ordered_cards.map(dc =>
-                    <div key={dc.id} className="DashCard" onMouseDownCapture={this.onDashCardMouseDown} onTouchStartCapture={this.onDashCardMouseDown}>
-                        {this.renderDashCard(dc, false)}
-                    </div>
-                )}
-            </GridLayout>
-        )
-    }
-
-    render() {
-        const { width } = this.props;
-        return (
-            <div className="flex layout-centered">
-                { width === 0 ?
-                    <div />
-                : width <= 752 ?
-                    this.renderMobile()
-                :
-                    this.renderGrid()
-                }
-                {this.renderRemoveModal()}
-                {this.renderAddSeriesModal()}
+              {this.renderDashCard(dc, false)}
             </div>
-        );
-    }
+          ))}
+      </GridLayout>
+    );
+  }
+
+  render() {
+    const { width } = this.props;
+    return (
+      <div className="flex layout-centered">
+        {width === 0 ? (
+          <div />
+        ) : width <= 752 ? (
+          this.renderMobile()
+        ) : (
+          this.renderGrid()
+        )}
+        {this.renderRemoveModal()}
+        {this.renderAddSeriesModal()}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
index c3935454747caf834fb27b2d4bd6ede995c7036e..771eb0f99993904f0ad6027ec8279a333871e7f8 100644
--- a/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
+++ b/frontend/src/metabase/dashboard/components/DashboardHeader.jsx
@@ -7,7 +7,6 @@ import ActionButton from "metabase/components/ActionButton.jsx";
 import AddToDashSelectQuestionModal from "./AddToDashSelectQuestionModal.jsx";
 import ArchiveDashboardModal from "./ArchiveDashboardModal.jsx";
 import Header from "metabase/components/Header.jsx";
-import HistoryModal from "metabase/components/HistoryModal.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
@@ -22,307 +21,330 @@ import MetabaseSettings from "metabase/lib/settings";
 
 import cx from "classnames";
 
-import type { LocationDescriptor, QueryParams, EntityType, EntityId } from "metabase/meta/types";
+import type { LocationDescriptor, QueryParams } 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";
+import type {
+  Parameter,
+  ParameterId,
+  ParameterOption,
+} from "metabase/meta/types/Parameter";
+import type {
+  DashboardWithCards,
+  DashboardId,
+  DashCardId,
+} from "metabase/meta/types/Dashboard";
+import type { RevisionId } from "metabase/meta/types/Revision";
+import { Link } from "react-router";
 
 type Props = {
-    location:               LocationDescriptor,
-
-    dashboard:              DashboardWithCards,
-    cards:                  Card[],
-    revisions:              { [key: string]: Revision[] },
-
-    isAdmin:                boolean,
-    isEditable:             boolean,
-    isEditing:              boolean,
-    isFullscreen:           boolean,
-    isNightMode:            boolean,
-
-    refreshPeriod:          ?number,
-    refreshElapsed:         ?number,
-
-    parametersWidget:       React$Element<*>,
-
-    addCardToDashboard:         ({ dashId: DashCardId, cardId: CardId }) => void,
-    addTextDashCardToDashboard: ({ dashId: DashCardId }) => void,
-    archiveDashboard:           (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:        (isEditing: boolean) => void,
-    onRefreshPeriodChange:  (?number) => void,
-    onNightModeChange:      (boolean) => void,
-    onFullscreenChange:     (boolean) => void,
-
-    onChangeLocation:       (string) => void,
-}
+  location: LocationDescriptor,
+
+  dashboard: DashboardWithCards,
+  cards: Card[],
+
+  isAdmin: boolean,
+  isEditable: boolean,
+  isEditing: boolean,
+  isFullscreen: boolean,
+  isNightMode: boolean,
+
+  refreshPeriod: ?number,
+  refreshElapsed: ?number,
+
+  parametersWidget: React$Element<*>,
+
+  addCardToDashboard: ({ dashId: DashCardId, cardId: CardId }) => void,
+  addTextDashCardToDashboard: ({ dashId: DashCardId }) => void,
+  archiveDashboard: (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: (isEditing: boolean) => void,
+  onRefreshPeriodChange: (?number) => void,
+  onNightModeChange: boolean => void,
+  onFullscreenChange: boolean => void,
+
+  onChangeLocation: string => void,
+};
 
 type State = {
-    modal: null|"parameters",
-}
+  modal: null | "parameters",
+};
 
 export default class DashboardHeader extends Component {
-    props: Props;
-    state: State = {
-        modal: null,
-    };
-
-    static propTypes = {
-        dashboard: PropTypes.object.isRequired,
-        revisions: PropTypes.object.isRequired,
-        isEditable: PropTypes.bool.isRequired,
-        isEditing: PropTypes.bool.isRequired,
-        isFullscreen: PropTypes.bool.isRequired,
-        isNightMode: PropTypes.bool.isRequired,
-
-        refreshPeriod: PropTypes.number,
-        refreshElapsed: PropTypes.number,
-
-        addCardToDashboard: PropTypes.func.isRequired,
-        addTextDashCardToDashboard: PropTypes.func.isRequired,
-        archiveDashboard: PropTypes.func.isRequired,
-        fetchCards: PropTypes.func.isRequired,
-        fetchDashboard: PropTypes.func.isRequired,
-        fetchRevisions: PropTypes.func.isRequired,
-        revertToRevision: PropTypes.func.isRequired,
-        saveDashboardAndCards: PropTypes.func.isRequired,
-        setDashboardAttribute: PropTypes.func.isRequired,
-
-        onEditingChange: PropTypes.func.isRequired,
-        onRefreshPeriodChange: PropTypes.func.isRequired,
-        onNightModeChange: PropTypes.func.isRequired,
-        onFullscreenChange: PropTypes.func.isRequired
-    };
-
-    onEdit() {
-        this.props.onEditingChange(true);
-    }
-
-    onAddTextBox() {
-        this.props.addTextDashCardToDashboard({ dashId: this.props.dashboard.id });
-    }
-
-    onDoneEditing() {
-        this.props.onEditingChange(false);
-    }
-
-    onRevert() {
-        this.props.fetchDashboard(this.props.dashboard.id, this.props.location.query);
-    }
-
-    async onSave() {
-        await this.props.saveDashboardAndCards(this.props.dashboard.id);
-        this.onDoneEditing();
-    }
-
-    async onCancel() {
-        this.onRevert();
-        this.onDoneEditing();
-    }
-
-    async onArchive() {
-        await this.props.archiveDashboard(this.props.dashboard.id);
-        this.props.onChangeLocation("/dashboards");
-    }
-
-    // 1. fetch revisions
-    onFetchRevisions({ entity, id }: { entity: EntityType, id: EntityId }) {
-        return this.props.fetchRevisions({ entity, id });
-    }
-
-    // 2. revert to a revision
-    onRevertToRevision({ entity, id, revision_id }: { entity: EntityType, id: EntityId, revision_id: RevisionId }) {
-        return this.props.revertToRevision({ entity, id, revision_id });
+  props: Props;
+  state: State = {
+    modal: null,
+  };
+
+  static propTypes = {
+    dashboard: PropTypes.object.isRequired,
+    isEditable: PropTypes.bool.isRequired,
+    isEditing: PropTypes.bool.isRequired,
+    isFullscreen: PropTypes.bool.isRequired,
+    isNightMode: PropTypes.bool.isRequired,
+
+    refreshPeriod: PropTypes.number,
+    refreshElapsed: PropTypes.number,
+
+    addCardToDashboard: PropTypes.func.isRequired,
+    addTextDashCardToDashboard: PropTypes.func.isRequired,
+    archiveDashboard: PropTypes.func.isRequired,
+    fetchCards: PropTypes.func.isRequired,
+    fetchDashboard: PropTypes.func.isRequired,
+    fetchRevisions: PropTypes.func.isRequired,
+    revertToRevision: PropTypes.func.isRequired,
+    saveDashboardAndCards: PropTypes.func.isRequired,
+    setDashboardAttribute: PropTypes.func.isRequired,
+
+    onEditingChange: PropTypes.func.isRequired,
+    onRefreshPeriodChange: PropTypes.func.isRequired,
+    onNightModeChange: PropTypes.func.isRequired,
+    onFullscreenChange: PropTypes.func.isRequired,
+  };
+
+  onEdit() {
+    this.props.onEditingChange(true);
+  }
+
+  onAddTextBox() {
+    this.props.addTextDashCardToDashboard({ dashId: this.props.dashboard.id });
+  }
+
+  onDoneEditing() {
+    this.props.onEditingChange(false);
+  }
+
+  onRevert() {
+    this.props.fetchDashboard(
+      this.props.dashboard.id,
+      this.props.location.query,
+    );
+  }
+
+  async onSave() {
+    await this.props.saveDashboardAndCards(this.props.dashboard.id);
+    this.onDoneEditing();
+  }
+
+  async onCancel() {
+    this.onRevert();
+    this.onDoneEditing();
+  }
+
+  async onArchive() {
+    await this.props.archiveDashboard(this.props.dashboard.id);
+    this.props.onChangeLocation("/dashboards");
+  }
+
+  getEditingButtons() {
+    return [
+      <a
+        data-metabase-event="Dashboard;Cancel Edits"
+        key="cancel"
+        className="Button Button--small"
+        onClick={() => this.onCancel()}
+      >
+        Cancel
+      </a>,
+      <ModalWithTrigger
+        key="archive"
+        ref="archiveDashboardModal"
+        triggerClasses="Button Button--small"
+        triggerElement="Archive"
+      >
+        <ArchiveDashboardModal
+          dashboard={this.props.dashboard}
+          onClose={() => this.refs.archiveDashboardModal.toggle()}
+          onArchive={() => this.onArchive()}
+        />
+      </ModalWithTrigger>,
+      <ActionButton
+        key="save"
+        actionFn={() => this.onSave()}
+        className="Button Button--small Button--primary"
+        normalText="Save"
+        activeText="Saving…"
+        failedText="Save failed"
+        successText="Saved"
+      />,
+    ];
+  }
+
+  getHeaderButtons() {
+    const {
+      dashboard,
+      parametersWidget,
+      isEditing,
+      isFullscreen,
+      isEditable,
+      isAdmin,
+      location,
+    } = this.props;
+    const isEmpty = !dashboard || dashboard.ordered_cards.length === 0;
+    const canEdit = isEditable && !!dashboard;
+
+    const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
+    const isEmbeddingEnabled = MetabaseSettings.get("embedding");
+
+    const buttons = [];
+
+    if (isFullscreen && parametersWidget) {
+      buttons.push(parametersWidget);
     }
 
-    // 3. finished reverting to a revision
-    onRevertedRevision() {
-        this.refs.dashboardHistory.toggle();
-        this.props.fetchDashboard(this.props.dashboard.id, this.props.location.query);
+    if (!isFullscreen && canEdit) {
+      buttons.push(
+        <ModalWithTrigger
+          full
+          key="add"
+          ref="addQuestionModal"
+          triggerElement={
+            <Tooltip tooltip="Add a question">
+              <span
+                data-metabase-event="Dashboard;Add Card Modal"
+                title="Add a question to this dashboard"
+              >
+                <Icon
+                  className={cx("text-brand-hover cursor-pointer", {
+                    "Icon--pulse": isEmpty,
+                  })}
+                  name="add"
+                  size={16}
+                />
+              </span>
+            </Tooltip>
+          }
+        >
+          <AddToDashSelectQuestionModal
+            dashboard={dashboard}
+            cards={this.props.cards}
+            fetchCards={this.props.fetchCards}
+            addCardToDashboard={this.props.addCardToDashboard}
+            onEditingChange={this.props.onEditingChange}
+            onClose={() => this.refs.addQuestionModal.toggle()}
+          />
+        </ModalWithTrigger>,
+      );
     }
 
-    getEditingButtons() {
-        return [
-            <a data-metabase-event="Dashboard;Cancel Edits" key="cancel" className="Button Button--small" onClick={() => this.onCancel()}>
-                Cancel
-            </a>,
-            <ModalWithTrigger
-                key="archive"
-                ref="archiveDashboardModal"
-                triggerClasses="Button Button--small"
-                triggerElement="Archive"
+    if (isEditing) {
+      // Parameters
+      buttons.push(
+        <span>
+          <Tooltip tooltip="Add a filter">
+            <a
+              key="parameters"
+              className={cx("text-brand-hover", {
+                "text-brand": this.state.modal == "parameters",
+              })}
+              title="Parameters"
+              onClick={() => this.setState({ modal: "parameters" })}
             >
-                <ArchiveDashboardModal
-                    dashboard={this.props.dashboard}
-                    onClose={() => this.refs.archiveDashboardModal.toggle()}
-                    onArchive={() => this.onArchive()}
+              <Icon name="funneladd" size={16} />
+            </a>
+          </Tooltip>
+
+          {this.state.modal &&
+            this.state.modal === "parameters" && (
+              <Popover onClose={() => this.setState({ modal: null })}>
+                <ParametersPopover
+                  onAddParameter={this.props.addParameter}
+                  onClose={() => this.setState({ modal: null })}
                 />
-            </ModalWithTrigger>,
-            <ActionButton
-                key="save"
-                actionFn={() => this.onSave()}
-                className="Button Button--small Button--primary"
-                normalText="Save"
-                activeText="Saving…"
-                failedText="Save failed"
-                successText="Saved"
-            />
-        ];
+              </Popover>
+            )}
+        </span>,
+      );
+
+      // Add text card button
+      buttons.push(
+        <Tooltip tooltip="Add a text box">
+          <a
+            data-metabase-event="Dashboard;Add Text Box"
+            key="add-text"
+            title="Add a text box"
+            className="text-brand-hover cursor-pointer"
+            onClick={() => this.onAddTextBox()}
+          >
+            <Icon name="string" size={20} />
+          </a>
+        </Tooltip>,
+      );
+
+      buttons.push(
+        <Tooltip tooltip="Revision history">
+          <Link
+            to={location.pathname + "/history"}
+            data-metabase-event={"Dashboard;Revisions"}
+          >
+            <Icon className="text-brand-hover" name="history" size={18} />
+          </Link>
+        </Tooltip>,
+      );
     }
 
-    getHeaderButtons() {
-        const { dashboard, parametersWidget, isEditing, isFullscreen, isEditable, isAdmin } = this.props;
-        const isEmpty = !dashboard || dashboard.ordered_cards.length === 0;
-        const canEdit = isEditable && !!dashboard;
-
-        const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
-        const isEmbeddingEnabled = MetabaseSettings.get("embedding");
-
-        const buttons = [];
-
-        if (isFullscreen && parametersWidget) {
-            buttons.push(parametersWidget);
-        }
-
-        if (!isFullscreen && canEdit) {
-            buttons.push(
-                <ModalWithTrigger
-                    full
-                    key="add"
-                    ref="addQuestionModal"
-                    triggerElement={
-                        <Tooltip tooltip="Add a question">
-                            <span data-metabase-event="Dashboard;Add Card Modal" title="Add a question to this dashboard">
-                                <Icon className={cx("text-brand-hover cursor-pointer", { "Icon--pulse": isEmpty })} name="add" size={16} />
-                            </span>
-                        </Tooltip>
-                    }
-                >
-                    <AddToDashSelectQuestionModal
-                        dashboard={dashboard}
-                        cards={this.props.cards}
-                        fetchCards={this.props.fetchCards}
-                        addCardToDashboard={this.props.addCardToDashboard}
-                        onEditingChange={this.props.onEditingChange}
-                        onClose={() => this.refs.addQuestionModal.toggle()}
-                    />
-                </ModalWithTrigger>
-            );
-        }
-
-        if (isEditing) {
-            // Parameters
-            buttons.push(
-                <span>
-                    <Tooltip tooltip="Add a filter">
-                        <a
-                          key="parameters"
-                          className={cx("text-brand-hover", { "text-brand": this.state.modal == "parameters" })}
-                          title="Parameters"
-                          onClick={() => this.setState({ modal: "parameters" })}
-                        >
-                            <Icon name="funneladd" size={16} />
-                        </a>
-                    </Tooltip>
-
-                    {this.state.modal && this.state.modal === "parameters" &&
-                        <Popover onClose={() => this.setState({ modal: null })}>
-                            <ParametersPopover
-                                onAddParameter={this.props.addParameter}
-                                onClose={() => this.setState({ modal: null })}
-                            />
-                        </Popover>
-                    }
-                </span>
-            );
-
-            // Add text card button
-            buttons.push(
-                <Tooltip tooltip="Add a text box">
-                    <a data-metabase-event="Dashboard;Add Text Box" key="add-text" title="Add a text box" className="text-brand-hover cursor-pointer" onClick={() => this.onAddTextBox()}>
-                        <Icon name="string" size={20} />
-                    </a>
-                </Tooltip>
-            );
-
-            buttons.push(
-                <ModalWithTrigger
-                    key="history"
-                    ref="dashboardHistory"
-                    triggerElement={
-                        <Tooltip tooltip="Revision history">
-                            <span data-metabase-event={"Dashboard;Revisions"}>
-                                <Icon className="text-brand-hover" name="history" size={18} />
-                            </span>
-                        </Tooltip>
-                    }
-                >
-                    <HistoryModal
-                        entityType="dashboard"
-                        entityId={dashboard.id}
-                        revisions={this.props.revisions["dashboard-"+dashboard.id]}
-                        onFetchRevisions={this.onFetchRevisions.bind(this)}
-                        onRevertToRevision={this.onRevertToRevision.bind(this)}
-                        onClose={() => this.refs.dashboardHistory.toggle()}
-                        onReverted={() => this.onRevertedRevision()}
-                    />
-                </ModalWithTrigger>
-            );
-        }
-
-        if (!isFullscreen && !isEditing && canEdit) {
-            buttons.push(
-                <Tooltip tooltip="Edit dashboard">
-                    <a data-metabase-event="Dashboard;Edit" key="edit" title="Edit Dashboard Layout" className="text-brand-hover cursor-pointer" onClick={() => this.onEdit()}>
-                        <Icon name="pencil" size={16} />
-                    </a>
-                </Tooltip>
-            );
-        }
-
-        if (!isFullscreen && (
-            (isPublicLinksEnabled && (isAdmin || dashboard.public_uuid)) ||
-            (isEmbeddingEnabled && isAdmin)
-        )) {
-            buttons.push(
-                <DashboardEmbedWidget dashboard={dashboard} />
-            )
-        }
-
-        buttons.push(...getDashboardActions(this.props));
-
-        return [buttons];
+    if (!isFullscreen && !isEditing && canEdit) {
+      buttons.push(
+        <Tooltip tooltip="Edit dashboard">
+          <a
+            data-metabase-event="Dashboard;Edit"
+            key="edit"
+            title="Edit Dashboard Layout"
+            className="text-brand-hover cursor-pointer"
+            onClick={() => this.onEdit()}
+          >
+            <Icon name="pencil" size={16} />
+          </a>
+        </Tooltip>,
+      );
     }
 
-    render() {
-        var { dashboard } = this.props;
-
-        return (
-            <Header
-                headerClassName="wrapper"
-                objectType="dashboard"
-                item={dashboard}
-                isEditing={this.props.isEditing}
-                isEditingInfo={this.props.isEditing}
-                headerButtons={this.getHeaderButtons()}
-                editingTitle="You are editing a dashboard"
-                editingButtons={this.getEditingButtons()}
-                setItemAttributeFn={this.props.setDashboardAttribute}
-                headerModalMessage={this.props.isEditingParameter ?
-                    "Select the field that should be filtered for each card" : null}
-                onHeaderModalDone={() => this.props.setEditingParameter(null)}
-            >
-            </Header>
-        );
+    if (
+      !isFullscreen &&
+      ((isPublicLinksEnabled && (isAdmin || dashboard.public_uuid)) ||
+        (isEmbeddingEnabled && isAdmin))
+    ) {
+      buttons.push(<DashboardEmbedWidget dashboard={dashboard} />);
     }
+
+    buttons.push(...getDashboardActions(this.props));
+
+    return [buttons];
+  }
+
+  render() {
+    var { dashboard } = this.props;
+
+    return (
+      <Header
+        headerClassName="wrapper"
+        objectType="dashboard"
+        item={dashboard}
+        isEditing={this.props.isEditing}
+        isEditingInfo={this.props.isEditing}
+        headerButtons={this.getHeaderButtons()}
+        editingTitle="You are editing a dashboard"
+        editingButtons={this.getEditingButtons()}
+        setItemAttributeFn={this.props.setDashboardAttribute}
+        headerModalMessage={
+          this.props.isEditingParameter
+            ? "Select the field that should be filtered for each card"
+            : null
+        }
+        onHeaderModalDone={() => this.props.setEditingParameter(null)}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/DashboardHistoryModal.jsx b/frontend/src/metabase/dashboard/components/DashboardHistoryModal.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..c9f628dad669f26dc26cb4858a6a7bcc88ca1698
--- /dev/null
+++ b/frontend/src/metabase/dashboard/components/DashboardHistoryModal.jsx
@@ -0,0 +1,79 @@
+import React from "react";
+import HistoryModal from "metabase/components/HistoryModal";
+import { withRouter } from "react-router";
+import { Component } from "react/lib/ReactBaseClasses";
+import * as dashboardActions from "../dashboard";
+import { connect } from "react-redux";
+import { getRevisions } from "metabase/dashboard/selectors";
+import type { EntityType, EntityId } from "metabase/meta/types";
+import type { RevisionId } from "metabase/meta/types/Revision";
+
+const mapStateToProps = (state, props) => {
+  return { revisions: getRevisions(state, props) };
+};
+
+const mapDispatchToProps = {
+  ...dashboardActions,
+};
+
+@connect(mapStateToProps, mapDispatchToProps)
+@withRouter
+export class DashboardHistoryModal extends Component {
+  props: {
+    location: Object,
+    onClose: () => any,
+    revisions: { [key: string]: Revision[] },
+    fetchRevisions: ({ entity: string, id: number }) => void,
+    revertToRevision: ({
+      entity: string,
+      id: number,
+      revision_id: RevisionId,
+    }) => void,
+  };
+
+  // 1. fetch revisions
+  onFetchRevisions = ({ entity, id }: { entity: EntityType, id: EntityId }) => {
+    return this.props.fetchRevisions({ entity, id });
+  };
+
+  // 2. revert to a revision
+  onRevertToRevision = ({
+    entity,
+    id,
+    revision_id,
+  }: {
+    entity: EntityType,
+    id: EntityId,
+    revision_id: RevisionId,
+  }) => {
+    return this.props.revertToRevision({ entity, id, revision_id });
+  };
+
+  // 3. finished reverting to a revision
+  onRevertedRevision = () => {
+    const { fetchDashboard, params, location, onClose } = this.props;
+    fetchDashboard(parseInt(params.dashboardId), location.query);
+    onClose();
+  };
+
+  render() {
+    const { params, onClose } = this.props;
+
+    // NOTE Atte Keinänen 1/17/18: While we still use react-router v3,
+    // we have to read the dashboard id from parsed route via `params`.
+    // Migrating to react-router v4 will make this easier because can
+    // have the route definition and the component in `<DashboardHeader>`
+    // which already knows the dashboard id
+    return (
+      <HistoryModal
+        entityType="dashboard"
+        entityId={parseInt(params.dashboardId)}
+        revisions={this.props.revisions["dashboard-" + params.dashboardId]}
+        onFetchRevisions={this.onFetchRevisions}
+        onRevertToRevision={this.onRevertToRevision}
+        onReverted={this.onRevertedRevision}
+        onClose={onClose}
+      />
+    );
+  }
+}
diff --git a/frontend/src/metabase/dashboard/components/ParametersPopover.jsx b/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
index cae70a4db179049b7ce73caf25703cda4b443043..e02847b468df7282a03577f6f0c58df601636e9f 100644
--- a/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
+++ b/frontend/src/metabase/dashboard/components/ParametersPopover.jsx
@@ -10,67 +10,117 @@ import _ from "underscore";
 import type { ParameterSection } from "metabase/meta/Dashboard";
 
 export default class ParametersPopover extends Component {
-    props: {
-        onAddParameter: (option: ParameterOption) => Promise<Parameter>,
-        onClose: () => void
-    };
-    state: {
-        section?: string
-    };
+  props: {
+    onAddParameter: (option: ParameterOption) => Promise<Parameter>,
+    onClose: () => void,
+  };
+  state: {
+    section?: string,
+  };
 
-    constructor(props: any, context: any) {
-        super(props, context);
-        this.state = {};
-    }
+  constructor(props: any, context: any) {
+    super(props, context);
+    this.state = {};
+  }
 
-    render() {
-        const { section } = this.state;
-        const { onClose, onAddParameter } = this.props;
-        if (section == null) {
-            return <ParameterOptionsSectionsPane sections={PARAMETER_SECTIONS} onSelectSection={(selectedSection) => {
-                let parameterSection = _.findWhere(PARAMETER_SECTIONS, { id: selectedSection.id });
-                if (parameterSection && parameterSection.options.length === 1) {
-                    onAddParameter(parameterSection.options[0]);
-                    onClose();
-                } else {
-                    this.setState({ section: selectedSection.id });
-                }
-            }} />
-        } else {
-            let parameterSection = _.findWhere(PARAMETER_SECTIONS, { id: section });
-            return <ParameterOptionsPane options={parameterSection && parameterSection.options} onSelectOption={(option) => { onAddParameter(option); onClose(); } }/>
-        }
+  render() {
+    const { section } = this.state;
+    const { onClose, onAddParameter } = this.props;
+    if (section == null) {
+      return (
+        <ParameterOptionsSectionsPane
+          sections={PARAMETER_SECTIONS}
+          onSelectSection={selectedSection => {
+            let parameterSection = _.findWhere(PARAMETER_SECTIONS, {
+              id: selectedSection.id,
+            });
+            if (parameterSection && parameterSection.options.length === 1) {
+              onAddParameter(parameterSection.options[0]);
+              onClose();
+            } else {
+              this.setState({ section: selectedSection.id });
+            }
+          }}
+        />
+      );
+    } else {
+      let parameterSection = _.findWhere(PARAMETER_SECTIONS, { id: section });
+      return (
+        <ParameterOptionsPane
+          options={parameterSection && parameterSection.options}
+          onSelectOption={option => {
+            onAddParameter(option);
+            onClose();
+          }}
+        />
+      );
     }
+  }
 }
 
-export const ParameterOptionsSection = ({ section, onClick }: { section: ParameterSection, onClick: () => any}) =>
-    <li onClick={onClick} className="p1 px2 cursor-pointer brand-hover">
-        <div className="text-brand text-bold">{section.name}</div>
-        <div>{section.description}</div>
-    </li>
+export const ParameterOptionsSection = ({
+  section,
+  onClick,
+}: {
+  section: ParameterSection,
+  onClick: () => any,
+}) => (
+  <li onClick={onClick} className="p1 px2 cursor-pointer brand-hover">
+    <div className="text-brand text-bold">{section.name}</div>
+    <div>{section.description}</div>
+  </li>
+);
 
-export const ParameterOptionsSectionsPane = ({ sections, onSelectSection }: { sections: Array<ParameterSection>, onSelectSection: (ParameterSection) => any}) =>
-    <div className="pb2">
-        <h3 className="p2">What do you want to filter?</h3>
-        <ul>
-            { sections.map(section =>
-                <ParameterOptionsSection section={section} onClick={() => onSelectSection(section) }/>
-            )}
-        </ul>
-    </div>
+export const ParameterOptionsSectionsPane = ({
+  sections,
+  onSelectSection,
+}: {
+  sections: Array<ParameterSection>,
+  onSelectSection: ParameterSection => any,
+}) => (
+  <div className="pb2">
+    <h3 className="p2">What do you want to filter?</h3>
+    <ul>
+      {sections.map(section => (
+        <ParameterOptionsSection
+          section={section}
+          onClick={() => onSelectSection(section)}
+        />
+      ))}
+    </ul>
+  </div>
+);
 
-export const ParameterOptionItem = ({ option, onClick }: { option: ParameterOption, onClick: () => any}) =>
-    <li onClick={onClick} className="p1 px2 cursor-pointer brand-hover">
-        <div className="text-brand text-bold">{option.menuName || option.name}</div>
-        <div>{option.description}</div>
-    </li>
+export const ParameterOptionItem = ({
+  option,
+  onClick,
+}: {
+  option: ParameterOption,
+  onClick: () => any,
+}) => (
+  <li onClick={onClick} className="p1 px2 cursor-pointer brand-hover">
+    <div className="text-brand text-bold">{option.menuName || option.name}</div>
+    <div>{option.description}</div>
+  </li>
+);
 
-export const ParameterOptionsPane = ({ options, onSelectOption }: { options: ?Array<ParameterOption>, onSelectOption: (ParameterOption) => any}) =>
-    <div className="pb2">
-        <h3 className="p2">What kind of filter?</h3>
-        <ul>
-            { options && options.map(option =>
-                <ParameterOptionItem option={option} onClick={() => onSelectOption(option)} />
-            )}
-        </ul>
-    </div>
+export const ParameterOptionsPane = ({
+  options,
+  onSelectOption,
+}: {
+  options: ?Array<ParameterOption>,
+  onSelectOption: ParameterOption => any,
+}) => (
+  <div className="pb2">
+    <h3 className="p2">What kind of filter?</h3>
+    <ul>
+      {options &&
+        options.map(option => (
+          <ParameterOptionItem
+            option={option}
+            onClick={() => onSelectOption(option)}
+          />
+        ))}
+    </ul>
+  </div>
+);
diff --git a/frontend/src/metabase/dashboard/components/RefreshWidget.css b/frontend/src/metabase/dashboard/components/RefreshWidget.css
index de5c31cd3f41a99048a2e0edcdfb6a76ae20ff03..a312808825005daebf74ce5f867ecf3b976b9980 100644
--- a/frontend/src/metabase/dashboard/components/RefreshWidget.css
+++ b/frontend/src/metabase/dashboard/components/RefreshWidget.css
@@ -21,7 +21,7 @@
   padding-bottom: 0.5em;
 }
 :local .option:hover,
-:local .option:hover .valueLabel  {
+:local .option:hover .valueLabel {
   color: var(--brand-color) !important;
 }
 :local .option.on.selected,
diff --git a/frontend/src/metabase/dashboard/components/RefreshWidget.jsx b/frontend/src/metabase/dashboard/components/RefreshWidget.jsx
index afcf9cf84e6f4e4ec7579af80ab11c8b68bf42d2..f235b936acfe61e536fbf92b8e8babfd55389e4c 100644
--- a/frontend/src/metabase/dashboard/components/RefreshWidget.jsx
+++ b/frontend/src/metabase/dashboard/components/RefreshWidget.jsx
@@ -10,52 +10,81 @@ import CountdownIcon from "metabase/components/icons/CountdownIcon.jsx";
 import cx from "classnames";
 
 const OPTIONS = [
-    { name: "Off",        period:    null },
-    { name: "1 minute",   period:  1 * 60 },
-    { name: "5 minutes",  period:  5 * 60 },
-    { name: "10 minutes", period: 10 * 60 },
-    { name: "15 minutes", period: 15 * 60 },
-    { name: "30 minutes", period: 30 * 60 },
-    { name: "60 minutes", period: 60 * 60 }
+  { name: "Off", period: null },
+  { name: "1 minute", period: 1 * 60 },
+  { name: "5 minutes", period: 5 * 60 },
+  { name: "10 minutes", period: 10 * 60 },
+  { name: "15 minutes", period: 15 * 60 },
+  { name: "30 minutes", period: 30 * 60 },
+  { name: "60 minutes", period: 60 * 60 },
 ];
 
 export default class RefreshWidget extends Component {
-    render() {
-        const { period, elapsed, onChangePeriod, className } = this.props;
-        const remaining = period - elapsed;
-        return (
-            <PopoverWithTrigger
-                ref="popover"
-                triggerElement={elapsed == null ?
-                    <Tooltip tooltip="Auto-refresh">
-                        <ClockIcon width={18} height={18} className={className} />
-                    </Tooltip>
-                :
-                    <Tooltip tooltip={"Refreshing in " + Math.floor(remaining / 60) + ":" + (remaining % 60 < 10 ? "0" : "") + Math.round(remaining % 60)}>
-                        <CountdownIcon width={18} height={18} className="text-green" percent={Math.min(0.95, (period - elapsed) / period)}/>
-                    </Tooltip>
-                }
-                targetOffsetY={10}
+  render() {
+    const { period, elapsed, onChangePeriod, className } = this.props;
+    const remaining = period - elapsed;
+    return (
+      <PopoverWithTrigger
+        ref="popover"
+        triggerElement={
+          elapsed == null ? (
+            <Tooltip tooltip="Auto-refresh">
+              <ClockIcon width={18} height={18} className={className} />
+            </Tooltip>
+          ) : (
+            <Tooltip
+              tooltip={
+                "Refreshing in " +
+                Math.floor(remaining / 60) +
+                ":" +
+                (remaining % 60 < 10 ? "0" : "") +
+                Math.round(remaining % 60)
+              }
             >
-                <div className={styles.popover}>
-                    <div className={styles.title}>Auto Refresh</div>
-                    <RefreshOptionList>
-                        { OPTIONS.map(option =>
-                            <RefreshOption key={option.period} name={option.name} period={option.period} selected={option.period === period} onClick={() => { this.refs.popover.close(); onChangePeriod(option.period) }} />
-                        ) }
-                    </RefreshOptionList>
-                </div>
-            </PopoverWithTrigger>
-        );
-    }
+              <CountdownIcon
+                width={18}
+                height={18}
+                className="text-green"
+                percent={Math.min(0.95, (period - elapsed) / period)}
+              />
+            </Tooltip>
+          )
+        }
+        targetOffsetY={10}
+      >
+        <div className={styles.popover}>
+          <div className={styles.title}>Auto Refresh</div>
+          <RefreshOptionList>
+            {OPTIONS.map(option => (
+              <RefreshOption
+                key={option.period}
+                name={option.name}
+                period={option.period}
+                selected={option.period === period}
+                onClick={() => {
+                  this.refs.popover.close();
+                  onChangePeriod(option.period);
+                }}
+              />
+            ))}
+          </RefreshOptionList>
+        </div>
+      </PopoverWithTrigger>
+    );
+  }
 }
 
-const RefreshOptionList = ({ children }) =>
-    <ul>{children}</ul>
+const RefreshOptionList = ({ children }) => <ul>{children}</ul>;
 
-const RefreshOption = ({ name, period, selected, onClick }) =>
-    <li className={cx(styles.option, styles[period == null ? "off" : "on"], { [styles.selected]: selected })} onClick={onClick}>
-        <Icon name="check" size={14} />
-        <span className={styles.name}>{ name.split(" ")[0] }</span>
-        <span className={styles.nameSuffix}> { name.split(" ")[1] }</span>
-    </li>
+const RefreshOption = ({ name, period, selected, onClick }) => (
+  <li
+    className={cx(styles.option, styles[period == null ? "off" : "on"], {
+      [styles.selected]: selected,
+    })}
+    onClick={onClick}
+  >
+    <Icon name="check" size={14} />
+    <span className={styles.name}>{name.split(" ")[0]}</span>
+    <span className={styles.nameSuffix}> {name.split(" ")[1]}</span>
+  </li>
+);
diff --git a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
index d9b29bd0e95059c3648e36b3213cbe8de0456f32..256c1a94a186050c5f5c23af0eaf6cee7e2f4373 100644
--- a/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
+++ b/frontend/src/metabase/dashboard/components/RemoveFromDashboardModal.jsx
@@ -4,46 +4,51 @@ import PropTypes from "prop-types";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import ModalContent from "metabase/components/ModalContent.jsx";
 
-
 export default class RemoveFromDashboardModal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = { deleteCard: false };
-    }
-
-    static propTypes = {
-        dashcard: PropTypes.object.isRequired,
-        dashboard: PropTypes.object.isRequired,
-        removeCardFromDashboard: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired
-    };
-
-    onRemove() {
-        this.props.removeCardFromDashboard({
-            dashId: this.props.dashboard.id,
-            dashcardId: this.props.dashcard.id
-        });
-        if (this.state.deleteCard) {
-            // this.props.dispatch(deleteCard(this.props.dashcard.card_id))
-            // this.props.dispatch(markCardForDeletion(this.props.dashcard.card_id))
-        }
-        this.props.onClose();
-
-        MetabaseAnalytics.trackEvent("Dashboard", "Remove Card");
-    }
-
-    render() {
-        return (
-            <ModalContent
-                title="Remove this question?"
-                onClose={() => this.props.onClose()}
-            >
-
-                <div className="Form-actions flex-align-right">
-                    <button className="Button Button" onClick={this.props.onClose}>Cancel</button>
-                    <button className="Button Button--danger ml2" onClick={() => this.onRemove()}>Remove</button>
-                </div>
-            </ModalContent>
-        );
+  constructor(props, context) {
+    super(props, context);
+    this.state = { deleteCard: false };
+  }
+
+  static propTypes = {
+    dashcard: PropTypes.object.isRequired,
+    dashboard: PropTypes.object.isRequired,
+    removeCardFromDashboard: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
+
+  onRemove() {
+    this.props.removeCardFromDashboard({
+      dashId: this.props.dashboard.id,
+      dashcardId: this.props.dashcard.id,
+    });
+    if (this.state.deleteCard) {
+      // this.props.dispatch(deleteCard(this.props.dashcard.card_id))
+      // this.props.dispatch(markCardForDeletion(this.props.dashcard.card_id))
     }
+    this.props.onClose();
+
+    MetabaseAnalytics.trackEvent("Dashboard", "Remove Card");
+  }
+
+  render() {
+    return (
+      <ModalContent
+        title="Remove this question?"
+        onClose={() => this.props.onClose()}
+      >
+        <div className="Form-actions flex-align-right">
+          <button className="Button Button" onClick={this.props.onClose}>
+            Cancel
+          </button>
+          <button
+            className="Button Button--danger ml2"
+            onClick={() => this.onRemove()}
+          >
+            Remove
+          </button>
+        </div>
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/grid/GridItem.jsx b/frontend/src/metabase/dashboard/components/grid/GridItem.jsx
index 79fed7cb458514273580531ffd32b0a716e4dd03..ab57dd3a6e139651a0d7ccbeddcc337b01f802ee 100644
--- a/frontend/src/metabase/dashboard/components/grid/GridItem.jsx
+++ b/frontend/src/metabase/dashboard/components/grid/GridItem.jsx
@@ -6,99 +6,105 @@ import { Resizable } from "react-resizable";
 import cx from "classnames";
 
 export default class GridItem extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      dragging: null,
+      resizing: null,
+    };
+  }
+
+  onDragHandler(handlerName) {
+    return (e, { node, x, y }) => {
+      // react-draggle seems to return undefined/NaN occasionally, which breaks things
+      if (isNaN(x) || isNaN(y)) {
+        return;
+      }
+
+      let { dragStartPosition, dragStartScrollTop } = this.state;
+      if (handlerName === "onDragStart") {
+        dragStartPosition = { x, y };
+        dragStartScrollTop = document.body.scrollTop;
+        this.setState({ dragStartPosition, dragStartScrollTop });
+      }
+
+      // track vertical scroll. we don't need horizontal  allow horizontal scrolling
+      let scrollTopDelta = document.body.scrollTop - dragStartScrollTop;
+      // compute new position
+      let pos = {
+        x: x - dragStartPosition.x,
+        y: y - dragStartPosition.y + scrollTopDelta,
+      };
 
-        this.state = {
-            dragging: null,
-            resizing: null
-        };
+      if (handlerName === "onDragStop") {
+        this.setState({ dragging: null });
+      } else {
+        this.setState({ dragging: pos });
+      }
+
+      this.props[handlerName](this.props.i, { e, node, position: pos });
+    };
+  }
+
+  onResizeHandler(handlerName) {
+    return (e, { element, size }) => {
+      if (handlerName === "onResize") {
+        this.setState({ resizing: size });
+      }
+      if (handlerName === "onResizeStop") {
+        this.setState({ resizing: null });
+      }
+
+      this.props[handlerName](this.props.i, { e, element, size });
+    };
+  }
+
+  render() {
+    let { width, height, top, left, minSize } = this.props;
+
+    if (this.state.dragging) {
+      left += this.state.dragging.x;
+      top += this.state.dragging.y;
     }
 
-    onDragHandler(handlerName) {
-        return (e, { node, x, y }) => {
-            // react-draggle seems to return undefined/NaN occasionally, which breaks things
-            if (isNaN(x) || isNaN(y)) {
-                return;
-            }
-
-            let { dragStartPosition, dragStartScrollTop } = this.state;
-            if (handlerName === "onDragStart") {
-                dragStartPosition = { x, y };
-                dragStartScrollTop = document.body.scrollTop
-                this.setState({ dragStartPosition, dragStartScrollTop });
-            }
-
-            // track vertical scroll. we don't need horizontal  allow horizontal scrolling
-            let scrollTopDelta = document.body.scrollTop - dragStartScrollTop;
-            // compute new position
-            let pos = {
-                x: x - dragStartPosition.x,
-                y: y - dragStartPosition.y + scrollTopDelta,
-            };
-
-            if (handlerName === "onDragStop") {
-                this.setState({ dragging: null });
-            } else {
-                this.setState({ dragging: pos });
-            }
-
-            this.props[handlerName](this.props.i, {e, node, position: pos });
-        };
+    if (this.state.resizing) {
+      width = Math.max(minSize.width, this.state.resizing.width);
+      height = Math.max(minSize.height, this.state.resizing.height);
     }
 
-    onResizeHandler(handlerName) {
-      return (e, {element, size}) => {
-
-        if (handlerName === "onResize") {
-            this.setState({ resizing: size });
-        } if (handlerName === "onResizeStop") {
-            this.setState({ resizing: null });
-        }
-
-        this.props[handlerName](this.props.i, {e, element, size});
-      };
-    }
-
-    render() {
-        let { width, height, top, left, minSize } = this.props;
-
-        if (this.state.dragging) {
-            left += this.state.dragging.x;
-            top += this.state.dragging.y;
-        }
-
-        if (this.state.resizing) {
-            width = Math.max(minSize.width, this.state.resizing.width);
-            height = Math.max(minSize.height, this.state.resizing.height);
-        }
-
-        let style = {
-            width, height, top, left,
-            position: "absolute"
-        };
-
-        let child = React.Children.only(this.props.children);
-        return (
-            <DraggableCore
-                cancel=".react-resizable-handle, .drag-disabled"
-                onStart={this.onDragHandler("onDragStart")}
-                onDrag={this.onDragHandler("onDrag")}
-                onStop={this.onDragHandler("onDragStop")}
-            >
-                <Resizable
-                    width={width}
-                    height={height}
-                    onResizeStart={this.onResizeHandler("onResizeStart")}
-                    onResize={this.onResizeHandler("onResize")}
-                    onResizeStop={this.onResizeHandler("onResizeStop")}
-                >
-                    {React.cloneElement(child, {
-                        style: style,
-                        className: cx(child.props.className, { dragging: !!this.state.dragging, resizing: !!this.state.resizing })
-                    })}
-                </Resizable>
-            </DraggableCore>
-        );
-    }
+    let style = {
+      width,
+      height,
+      top,
+      left,
+      position: "absolute",
+    };
+
+    let child = React.Children.only(this.props.children);
+    return (
+      <DraggableCore
+        cancel=".react-resizable-handle, .drag-disabled"
+        onStart={this.onDragHandler("onDragStart")}
+        onDrag={this.onDragHandler("onDrag")}
+        onStop={this.onDragHandler("onDragStop")}
+      >
+        <Resizable
+          width={width}
+          height={height}
+          onResizeStart={this.onResizeHandler("onResizeStart")}
+          onResize={this.onResizeHandler("onResize")}
+          onResizeStop={this.onResizeHandler("onResizeStop")}
+        >
+          {React.cloneElement(child, {
+            style: style,
+            className: cx(child.props.className, {
+              dragging: !!this.state.dragging,
+              resizing: !!this.state.resizing,
+            }),
+          })}
+        </Resizable>
+      </DraggableCore>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx b/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx
index 701934e1ca19e6cbd361019f7c15b6bbff8917e5..f8689a5170e23d35beb99a2f986953c3832a17f6 100644
--- a/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx
+++ b/frontend/src/metabase/dashboard/components/grid/GridLayout.jsx
@@ -6,253 +6,280 @@ import GridItem from "./GridItem.jsx";
 import _ from "underscore";
 
 export default class GridLayout extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            width: 0,
-            layout: props.layout,
-            dragging: false,
-            resizing: false,
-            placeholderLayout: null
-        };
-
-        _.bindAll(this,
-            "onDrag", "onDragStart", "onDragStop",
-            "onResize", "onResizeStart", "onResizeStop"
-        );
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      width: 0,
+      layout: props.layout,
+      dragging: false,
+      resizing: false,
+      placeholderLayout: null,
+    };
+
+    _.bindAll(
+      this,
+      "onDrag",
+      "onDragStart",
+      "onDragStop",
+      "onResize",
+      "onResizeStart",
+      "onResizeStop",
+    );
+  }
+
+  componentWillReceiveProps(newProps) {
+    const { dragging, resizing } = this.state;
+    if (!dragging && !resizing && this.state.layout !== newProps.layout) {
+      this.setState({ layout: newProps.layout });
     }
+  }
 
-    componentWillReceiveProps(newProps) {
-        const { dragging, resizing } = this.state;
-        if (!dragging && !resizing && this.state.layout !== newProps.layout) {
-            this.setState({ layout: newProps.layout });
-        }
-    }
-
-    componentDidMount() {
-        this.componentDidUpdate();
-    }
-
-    componentDidUpdate() {
-        let width = ReactDOM.findDOMNode(this).parentNode.offsetWidth;
-        if (this.state.width !== width) {
-            this.setState({ width });
-        }
-    }
-
-    onDragStart(i, { position }) {
-        // this.setState({ dragging: true })
-    }
-
-    layoutsOverlap(a, b) {
-        return (
-            a.x < (b.x + b.w) &&
-            b.x < (a.x + a.w) &&
-            a.y < (b.y + b.h) &&
-            b.y < (a.y + a.h)
-        );
-    }
-
-    onDrag(i, { position }) {
-        let placeholderLayout = {
-            ...this.computeDraggedLayout(i, position),
-            i: "placeholder"
-        }
-        this.setState({ dragging: true, placeholderLayout: placeholderLayout });
-        this.props.onDrag();
-    }
-
-    onDragStop(i, { position }) {
-        const { placeholderLayout } = this.state;
-        let newLayout;
-        if (placeholderLayout) {
-            let { x, y, w, h } = placeholderLayout;
-            newLayout = this.state.layout.map(l => l.i === i ?
-                { ...l, x, y, w, h } :
-                l
-            );
-        }
-        this.setState({ dragging: false, placeholderLayout: null });
-        if (newLayout) {
-            this.props.onLayoutChange(newLayout);
-        }
-        this.props.onDragStop();
-    }
-
-    computeDraggedLayout(i, position) {
-        const cellSize = this.getCellSize();
-        let originalLayout = this.getLayoutForItem(i);
-        let pos = this.getStyleForLayout(originalLayout);
-        pos.top += position.y;
-        pos.left += position.x;
-
-        let maxX = this.props.cols - originalLayout.w;
-        let maxY = Infinity;
-
-        let targetLayout = {
-            w: originalLayout.w,
-            h: originalLayout.h,
-            x: Math.min(maxX, Math.max(0, Math.round(pos.left / cellSize.width))),
-            y: Math.min(maxY, Math.max(0, Math.round(pos.top / cellSize.height)))
-        };
-        let proposedLayout = targetLayout;
-        for (let otherLayout of this.state.layout) {
-            if (originalLayout !== otherLayout && this.layoutsOverlap(proposedLayout, otherLayout)) {
-                return this.state.placeholderLayout || originalLayout;
-            }
-        }
-        return proposedLayout;
-    }
+  componentDidMount() {
+    this.componentDidUpdate();
+  }
 
-    onResizeStart(i, { size }) {
-        this.setState({ resizing: true });
+  componentDidUpdate() {
+    let width = ReactDOM.findDOMNode(this).parentNode.offsetWidth;
+    if (this.state.width !== width) {
+      this.setState({ width });
     }
-
-    onResize(i, { size }) {
-        let placeholderLayout = {
-            ...this.computeResizedLayout(i, size),
-            i: "placeholder"
-        };
-        this.setState({ placeholderLayout: placeholderLayout });
-    }
-
-    onResizeStop(i, { size }) {
-        let { x, y, w, h } = this.state.placeholderLayout;
-        let newLayout = this.state.layout.map(l => l.i === i ?
-            { ...l, x, y, w, h } :
-            l
-        );
-        this.setState({ resizing: false, placeholderLayout: null }, () =>
-            this.props.onLayoutChange(newLayout)
-        );
+  }
+
+  onDragStart(i, { position }) {
+    // this.setState({ dragging: true })
+  }
+
+  layoutsOverlap(a, b) {
+    return (
+      a.x < b.x + b.w && b.x < a.x + a.w && a.y < b.y + b.h && b.y < a.y + a.h
+    );
+  }
+
+  onDrag(i, { position }) {
+    let placeholderLayout = {
+      ...this.computeDraggedLayout(i, position),
+      i: "placeholder",
+    };
+    this.setState({ dragging: true, placeholderLayout: placeholderLayout });
+    this.props.onDrag();
+  }
+
+  onDragStop(i, { position }) {
+    const { placeholderLayout } = this.state;
+    let newLayout;
+    if (placeholderLayout) {
+      let { x, y, w, h } = placeholderLayout;
+      newLayout = this.state.layout.map(
+        l => (l.i === i ? { ...l, x, y, w, h } : l),
+      );
     }
-
-    computeResizedLayout(i, size) {
-        let cellSize = this.getCellSize();
-        let originalLayout = this.getLayoutForItem(i);
-
-        let minW = originalLayout.minSize.width;
-        let minH = originalLayout.minSize.height;
-        let maxW = this.props.cols - originalLayout.x;
-        let maxH = Infinity;
-        let targetLayout = {
-            w: Math.min(maxW, Math.max(minW, Math.round(size.width / cellSize.width))),
-            h: Math.min(maxH, Math.max(minH, Math.round(size.height / cellSize.height))),
-            x: originalLayout.x,
-            y: originalLayout.y
-        };
-
-        let proposedLayout = targetLayout;
-        for (let otherLayout of this.state.layout) {
-            if (originalLayout !== otherLayout && this.layoutsOverlap(proposedLayout, otherLayout)) {
-                return this.state.placeholderLayout || originalLayout;
-            }
-        }
-        return proposedLayout;
+    this.setState({ dragging: false, placeholderLayout: null });
+    if (newLayout) {
+      this.props.onLayoutChange(newLayout);
     }
-
-    getLayoutForItem(i) {
-        return _.findWhere(this.state.layout, { i: i });
+    this.props.onDragStop();
+  }
+
+  computeDraggedLayout(i, position) {
+    const cellSize = this.getCellSize();
+    let originalLayout = this.getLayoutForItem(i);
+    let pos = this.getStyleForLayout(originalLayout);
+    pos.top += position.y;
+    pos.left += position.x;
+
+    let maxX = this.props.cols - originalLayout.w;
+    let maxY = Infinity;
+
+    let targetLayout = {
+      w: originalLayout.w,
+      h: originalLayout.h,
+      x: Math.min(maxX, Math.max(0, Math.round(pos.left / cellSize.width))),
+      y: Math.min(maxY, Math.max(0, Math.round(pos.top / cellSize.height))),
+    };
+    let proposedLayout = targetLayout;
+    for (let otherLayout of this.state.layout) {
+      if (
+        originalLayout !== otherLayout &&
+        this.layoutsOverlap(proposedLayout, otherLayout)
+      ) {
+        return this.state.placeholderLayout || originalLayout;
+      }
     }
-
-    getCellSize() {
-        let { margin } = this.props;
-        // add 1 margin to make it fill the full width
-        return {
-            width: (this.state.width + margin) / this.props.cols,
-            height: this.props.rowHeight
-        };
+    return proposedLayout;
+  }
+
+  onResizeStart(i, { size }) {
+    this.setState({ resizing: true });
+  }
+
+  onResize(i, { size }) {
+    let placeholderLayout = {
+      ...this.computeResizedLayout(i, size),
+      i: "placeholder",
+    };
+    this.setState({ placeholderLayout: placeholderLayout });
+  }
+
+  onResizeStop(i, { size }) {
+    let { x, y, w, h } = this.state.placeholderLayout;
+    let newLayout = this.state.layout.map(
+      l => (l.i === i ? { ...l, x, y, w, h } : l),
+    );
+    this.setState({ resizing: false, placeholderLayout: null }, () =>
+      this.props.onLayoutChange(newLayout),
+    );
+  }
+
+  computeResizedLayout(i, size) {
+    let cellSize = this.getCellSize();
+    let originalLayout = this.getLayoutForItem(i);
+
+    let minW = originalLayout.minSize.width;
+    let minH = originalLayout.minSize.height;
+    let maxW = this.props.cols - originalLayout.x;
+    let maxH = Infinity;
+    let targetLayout = {
+      w: Math.min(
+        maxW,
+        Math.max(minW, Math.round(size.width / cellSize.width)),
+      ),
+      h: Math.min(
+        maxH,
+        Math.max(minH, Math.round(size.height / cellSize.height)),
+      ),
+      x: originalLayout.x,
+      y: originalLayout.y,
+    };
+
+    let proposedLayout = targetLayout;
+    for (let otherLayout of this.state.layout) {
+      if (
+        originalLayout !== otherLayout &&
+        this.layoutsOverlap(proposedLayout, otherLayout)
+      ) {
+        return this.state.placeholderLayout || originalLayout;
+      }
     }
-
-    getMinSizeForLayout(l) {
-        let { margin } = this.props;
-        let cellSize = this.getCellSize();
-        return {
-            width: cellSize.width * l.minSize.width - margin,
-            height: cellSize.height * l.minSize.height - margin
-        }
+    return proposedLayout;
+  }
+
+  getLayoutForItem(i) {
+    return _.findWhere(this.state.layout, { i: i });
+  }
+
+  getCellSize() {
+    let { margin } = this.props;
+    // add 1 margin to make it fill the full width
+    return {
+      width: (this.state.width + margin) / this.props.cols,
+      height: this.props.rowHeight,
+    };
+  }
+
+  getMinSizeForLayout(l) {
+    let { margin } = this.props;
+    let cellSize = this.getCellSize();
+    return {
+      width: cellSize.width * l.minSize.width - margin,
+      height: cellSize.height * l.minSize.height - margin,
+    };
+  }
+
+  getStyleForLayout(l) {
+    let { margin } = this.props;
+    let cellSize = this.getCellSize();
+    return {
+      width: cellSize.width * l.w - margin,
+      height: cellSize.height * l.h - margin,
+      left: cellSize.width * l.x + margin / 2,
+      top: cellSize.height * l.y + margin / 2,
+    };
+  }
+
+  renderChild(child) {
+    let l = this.getLayoutForItem(child.key);
+    let style = this.getStyleForLayout(l);
+    return (
+      <GridItem
+        {...l}
+        {...style}
+        key={l.i}
+        onDragStart={this.onDragStart}
+        onDrag={this.onDrag}
+        onDragStop={this.onDragStop}
+        onResizeStart={this.onResizeStart}
+        onResize={this.onResize}
+        onResizeStop={this.onResizeStop}
+        minSize={this.getMinSizeForLayout(l)}
+      >
+        {child}
+      </GridItem>
+    );
+  }
+
+  renderPlaceholder() {
+    if (this.state.placeholderLayout) {
+      let style = {
+        ...this.getStyleForLayout(this.state.placeholderLayout),
+      };
+      return <div className="react-grid-placeholder absolute" style={style} />;
     }
-
-    getStyleForLayout(l) {
-        let { margin } = this.props;
-        let cellSize = this.getCellSize();
-        return {
-            width: cellSize.width * l.w - margin,
-            height: cellSize.height * l.h - margin,
-            left: cellSize.width * l.x + margin / 2,
-            top: cellSize.height * l.y + margin / 2
-        };
-    }
-
-    renderChild(child) {
-        let l = this.getLayoutForItem(child.key);
-        let style = this.getStyleForLayout(l);
-        return (
-            <GridItem
-                {...l}
-                {...style}
-                key={l.i}
-                onDragStart={this.onDragStart}
-                onDrag={this.onDrag}
-                onDragStop={this.onDragStop}
-                onResizeStart={this.onResizeStart}
-                onResize={this.onResize}
-                onResizeStop={this.onResizeStop}
-                minSize={this.getMinSizeForLayout(l)}
-            >
-                {child}
-            </GridItem>
+  }
+
+  getGridBackground() {
+    let { margin, cols } = this.props;
+    let cellSize = this.getCellSize();
+    return (
+      `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='${cellSize.width *
+        cols}' height='${cellSize.height}'>` +
+      _(cols)
+        .times(
+          i =>
+            `<rect stroke='rgba(0, 0, 0, 0.117647)' stroke-width='1' fill='none' x='${Math.round(
+              margin / 2 + i * cellSize.width,
+            ) + 1.5}' y='${margin / 2 + 1.5}' width='${Math.round(
+              cellSize.width - margin - 3,
+            )}' height='${cellSize.height - margin - 3}'/>`,
         )
+        .join("") +
+      `</svg>")`
+    );
+  }
+
+  render() {
+    const { className, layout, cols, margin, isEditing } = this.props;
+
+    let cellSize = this.getCellSize();
+    let bottom = Math.max(...layout.map(l => l.y + l.h));
+
+    let backgroundImage;
+    if (isEditing) {
+      // render grid as a background image:
+      backgroundImage = this.getGridBackground();
+      // add one vertical screen worth of rows to ensure the grid fills the screen
+      bottom += Math.ceil(window.innerHeight / cellSize.height);
     }
 
-    renderPlaceholder() {
-        if (this.state.placeholderLayout) {
-            let style = {
-                ...this.getStyleForLayout(this.state.placeholderLayout)
-            }
-            return (
-                <div className="react-grid-placeholder absolute" style={style}></div>
-            );
-        }
-    }
-
-    getGridBackground() {
-        let { margin, cols } = this.props;
-        let cellSize = this.getCellSize();
-        return (
-            `url("data:image/svg+xml;utf8,<svg xmlns='http://www.w3.org/2000/svg' width='${cellSize.width * cols}' height='${cellSize.height}'>`+
-                _(cols).times((i) =>
-                    `<rect stroke='rgba(0, 0, 0, 0.117647)' stroke-width='1' fill='none' x='${Math.round(margin / 2 + i * cellSize.width) + 1.5}' y='${margin / 2 + 1.5}' width='${Math.round(cellSize.width - margin - 3)}' height='${cellSize.height - margin - 3}'/>`).join("") +
-            `</svg>")`
-        );
-    }
-
-    render() {
-        const { className, layout, cols, margin, isEditing } = this.props;
-
-        let cellSize = this.getCellSize();
-        let bottom = Math.max(...layout.map(l => l.y + l.h));
-
-        let backgroundImage;
-        if (isEditing) {
-            // render grid as a background image:
-            backgroundImage  = this.getGridBackground();
-            // add one vertical screen worth of rows to ensure the grid fills the screen
-            bottom += Math.ceil(window.innerHeight / cellSize.height);
-        }
-
-        let width = cellSize.width * cols;
-        let height = cellSize.height * bottom;
-
-        // subtract half of a margin to ensure it lines up with the edges
-        return (
-            <div className={className} style={{ position: "relative", width, height, backgroundImage, marginLeft: -margin / 2, marginRight: -margin / 2 }}>
-                {this.props.children.map(child =>
-                    this.renderChild(child)
-                )}
-                {this.renderPlaceholder()}
-            </div>
-        );
-    }
+    let width = cellSize.width * cols;
+    let height = cellSize.height * bottom;
+
+    // subtract half of a margin to ensure it lines up with the edges
+    return (
+      <div
+        className={className}
+        style={{
+          position: "relative",
+          width,
+          height,
+          backgroundImage,
+          marginLeft: -margin / 2,
+          marginRight: -margin / 2,
+        }}
+      >
+        {this.props.children.map(child => this.renderChild(child))}
+        {this.renderPlaceholder()}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.css b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.css
index 9f356b5a447fc2325edf5ef34fc95a694377e6c1..3356b83a489adc902727c0339c247689a3380f8b 100644
--- a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.css
+++ b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.css
@@ -1,25 +1,25 @@
 :local(.button) {
-    composes: flex align-center bg-white text-bold cursor-pointer from "style";
-    font-size: 16px;
-    border: 2px solid var(--brand-color);
-    border-radius: 4px;
-    min-height: 30px;
-    min-width: 100px;
-    padding: 0.25em 0.5em 0.25em 0.5em;
-    color: var(--default-font-color);
+  composes: flex align-center bg-white text-bold cursor-pointer from "style";
+  font-size: 16px;
+  border: 2px solid var(--brand-color);
+  border-radius: 4px;
+  min-height: 30px;
+  min-width: 100px;
+  padding: 0.25em 0.5em 0.25em 0.5em;
+  color: var(--default-font-color);
 }
 
 :local(.mapped) {
-    border-color: var(--green-color);
-    color: var(--green-color);
+  border-color: var(--green-color);
+  color: var(--green-color);
 }
 
 :local(.warn) {
-    border-color: var(--warning-color) !important;
-    color: var(--warning-color) !important;
+  border-color: var(--warning-color) !important;
+  color: var(--warning-color) !important;
 }
 
 :local(.disabled) {
-    composes: disabled from "style";
-    border-color: inherit;
+  composes: disabled from "style";
+  border-color: inherit;
 }
diff --git a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
index 8379e218025f4712b207736c8f14a6e1f1f21025..12189bdd02d72efa84e9a9396c8d6d37d4ae2a62 100644
--- a/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashCardCardParameterMapper.jsx
@@ -13,7 +13,12 @@ import Tooltip from "metabase/components/Tooltip.jsx";
 
 import { fetchDatabaseMetadata } from "metabase/redux/metadata";
 
-import { getEditingParameter, getParameterTarget, makeGetParameterMappingOptions, getMappingsByParameter } from "../selectors";
+import {
+  getEditingParameter,
+  getParameterTarget,
+  makeGetParameterMappingOptions,
+  getMappingsByParameter,
+} from "../selectors";
 import { setParameterMapping } from "../dashboard";
 
 import _ from "underscore";
@@ -22,139 +27,202 @@ import { getIn } from "icepick";
 
 import type { Card } from "metabase/meta/types/Card";
 import type { DashCard } from "metabase/meta/types/Dashboard";
-import type { Parameter, ParameterId, ParameterMappingUIOption, ParameterTarget } from "metabase/meta/types/Parameter";
+import type {
+  Parameter,
+  ParameterId,
+  ParameterMappingUIOption,
+  ParameterTarget,
+} from "metabase/meta/types/Parameter";
 import type { DatabaseId } from "metabase/meta/types/Database";
 
 import type { MappingsByParameter } from "../selectors";
 import AtomicQuery from "metabase-lib/lib/queries/AtomicQuery";
 
 const makeMapStateToProps = () => {
-    const getParameterMappingOptions = makeGetParameterMappingOptions()
-    const mapStateToProps = (state, props) => ({
-        parameter:           getEditingParameter(state, props),
-        mappingOptions:      getParameterMappingOptions(state, props),
-        mappingOptionSections: _.groupBy(getParameterMappingOptions(state, props), "sectionName"),
-        target:              getParameterTarget(state, props),
-        mappingsByParameter: getMappingsByParameter(state, props)
-    });
-    return mapStateToProps;
-}
+  const getParameterMappingOptions = makeGetParameterMappingOptions();
+  const mapStateToProps = (state, props) => ({
+    parameter: getEditingParameter(state, props),
+    mappingOptions: getParameterMappingOptions(state, props),
+    mappingOptionSections: _.groupBy(
+      getParameterMappingOptions(state, props),
+      "sectionName",
+    ),
+    target: getParameterTarget(state, props),
+    mappingsByParameter: getMappingsByParameter(state, props),
+  });
+  return mapStateToProps;
+};
 
 const mapDispatchToProps = {
-    setParameterMapping,
-    fetchDatabaseMetadata
+  setParameterMapping,
+  fetchDatabaseMetadata,
 };
 
-
 @connect(makeMapStateToProps, mapDispatchToProps)
 export default class DashCardCardParameterMapper extends Component {
-    props: {
-        card: Card,
-        dashcard: DashCard,
-        parameter: Parameter,
-        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: ?ParameterTarget) => void,
-    };
-
-    static propTypes = {
-        dashcard: PropTypes.object.isRequired,
-        card: PropTypes.object.isRequired
-    };
-    static defaultProps = {};
-
-    componentDidMount() {
-        const { card } = this.props;
-        // Type check for Flow
-
-        card.dataset_query instanceof AtomicQuery && this.props.fetchDatabaseMetadata(card.dataset_query.database);
-    }
-
-    onChange = (option: ?ParameterMappingUIOption) => {
-        const { setParameterMapping, parameter, dashcard, card } = this.props;
-        setParameterMapping(parameter.id, dashcard.id, card.id, option ? option.target : null);
-        this.refs.popover.close()
+  props: {
+    card: Card,
+    dashcard: DashCard,
+    parameter: Parameter,
+    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: ?ParameterTarget,
+    ) => void,
+  };
+
+  static propTypes = {
+    dashcard: PropTypes.object.isRequired,
+    card: PropTypes.object.isRequired,
+  };
+  static defaultProps = {};
+
+  componentDidMount() {
+    const { card } = this.props;
+    // Type check for Flow
+
+    card.dataset_query instanceof AtomicQuery &&
+      this.props.fetchDatabaseMetadata(card.dataset_query.database);
+  }
+
+  onChange = (option: ?ParameterMappingUIOption) => {
+    const { setParameterMapping, parameter, dashcard, card } = this.props;
+    setParameterMapping(
+      parameter.id,
+      dashcard.id,
+      card.id,
+      option ? option.target : null,
+    );
+    this.refs.popover.close();
+  };
+
+  render() {
+    const {
+      mappingOptions,
+      mappingOptionSections,
+      target,
+      mappingsByParameter,
+      parameter,
+      dashcard,
+      card,
+    } = this.props;
+
+    // TODO: move some of these to selectors?
+    const disabled = mappingOptions.length === 0;
+    const selected = _.find(mappingOptions, o => _.isEqual(o.target, target));
+
+    const mapping = getIn(mappingsByParameter, [
+      parameter.id,
+      dashcard.id,
+      card.id,
+    ]);
+    const noOverlap = !!(
+      mapping &&
+      mapping.mappingsWithValues > 1 &&
+      mapping.overlapMax === 1
+    );
+
+    const hasFkOption = _.any(mappingOptions, o => !!o.isFk);
+
+    const sections = _.map(mappingOptionSections, options => ({
+      name: options[0].sectionName,
+      items: options,
+    }));
+
+    let tooltipText = null;
+    if (disabled) {
+      tooltipText =
+        "This card doesn't have any fields or parameters that can be mapped to this parameter type.";
+    } else if (noOverlap) {
+      tooltipText =
+        "The values in this field don't overlap with the values of any other fields you've chosen.";
     }
 
-    render() {
-        const { mappingOptions, mappingOptionSections, target, mappingsByParameter, parameter, dashcard, card } = this.props;
-
-        // TODO: move some of these to selectors?
-        const disabled = mappingOptions.length === 0;
-        const selected = _.find(mappingOptions, (o) => _.isEqual(o.target, target));
-
-        const mapping = getIn(mappingsByParameter, [parameter.id, dashcard.id, card.id]);
-        const noOverlap = !!(mapping && mapping.mappingsWithValues > 1 && mapping.overlapMax === 1);
-
-        const hasFkOption = _.any(mappingOptions, (o) => !!o.isFk);
-
-        const sections = _.map(mappingOptionSections, (options) => ({
-            name: options[0].sectionName,
-            items: options
-        }));
-
-        let tooltipText = null;
-        if (disabled) {
-            tooltipText = "This card doesn't have any fields or parameters that can be mapped to this parameter type.";
-        } else if (noOverlap) {
-            tooltipText = "The values in this field don't overlap with the values of any other fields you've chosen.";
-        }
-
-        return (
-            <div className="mx1 flex flex-column align-center" onMouseDown={(e) => e.stopPropagation()}>
-                { dashcard.series && dashcard.series.length > 0 &&
-                    <div className="h5 mb1 text-bold" style={{ textOverflow: "ellipsis", whiteSpace: "nowrap", overflowX: "hidden", maxWidth: 100 }}>{card.name}</div>
-                }
-                <PopoverWithTrigger
-                    ref="popover"
-                    triggerClasses={cx({ "disabled": disabled })}
-                    sizeToFit
-                    triggerElement={
-                        <Tooltip tooltip={tooltipText} verticalAttachments={["bottom", "top"]}>
-                            {/* using div instead of button due to
+    return (
+      <div
+        className="mx1 flex flex-column align-center"
+        onMouseDown={e => e.stopPropagation()}
+      >
+        {dashcard.series &&
+          dashcard.series.length > 0 && (
+            <div
+              className="h5 mb1 text-bold"
+              style={{
+                textOverflow: "ellipsis",
+                whiteSpace: "nowrap",
+                overflowX: "hidden",
+                maxWidth: 100,
+              }}
+            >
+              {card.name}
+            </div>
+          )}
+        <PopoverWithTrigger
+          ref="popover"
+          triggerClasses={cx({ disabled: disabled })}
+          sizeToFit
+          triggerElement={
+            <Tooltip
+              tooltip={tooltipText}
+              verticalAttachments={["bottom", "top"]}
+            >
+              {/* using div instead of button due to
                                 https://bugzilla.mozilla.org/show_bug.cgi?id=984869
                                 and click event on close button not propagating in FF
                             */}
-                            <div
-                                className={cx(S.button, {
-                                    [S.mapped]: !!selected,
-                                    [S.warn]: noOverlap,
-                                    [S.disabled]: disabled
-                                })}
-                            >
-                                <span className="text-centered mr1">
-                                { disabled ?
-                                    "No valid fields"
-                                : selected ?
-                                    selected.name
-                                :
-                                    "Select…"
-                                }
-                                </span>
-                                { selected ?
-                                    <Icon className="flex-align-right" name="close" size={16} onClick={(e) => { this.onChange(null); e.stopPropagation(); }}/>
-                                : !disabled ?
-                                    <Icon className="flex-align-right" name="chevrondown" size={16} />
-                                : null }
-                            </div>
-                        </Tooltip>
-                    }
-                >
-                    <AccordianList
-                        className="text-brand scroll-show scroll-y"
-                        style={{ maxHeight: 600 }}
-                        sections={sections}
-                        onChange={this.onChange}
-                        itemIsSelected={(item) => _.isEqual(item.target, target)}
-                        renderItemIcon={(item) => <Icon name={item.icon || "unknown"} size={18} />}
-                        alwaysExpanded={true}
-                        hideSingleSectionTitle={!hasFkOption}
-                    />
-                </PopoverWithTrigger>
-            </div>
-        );
-    }
+              <div
+                className={cx(S.button, {
+                  [S.mapped]: !!selected,
+                  [S.warn]: noOverlap,
+                  [S.disabled]: disabled,
+                })}
+              >
+                <span className="text-centered mr1">
+                  {disabled
+                    ? "No valid fields"
+                    : selected ? selected.name : "Select…"}
+                </span>
+                {selected ? (
+                  <Icon
+                    className="flex-align-right"
+                    name="close"
+                    size={16}
+                    onClick={e => {
+                      this.onChange(null);
+                      e.stopPropagation();
+                    }}
+                  />
+                ) : !disabled ? (
+                  <Icon
+                    className="flex-align-right"
+                    name="chevrondown"
+                    size={16}
+                  />
+                ) : null}
+              </div>
+            </Tooltip>
+          }
+        >
+          <AccordianList
+            className="text-brand scroll-show scroll-y"
+            style={{ maxHeight: 600 }}
+            sections={sections}
+            onChange={this.onChange}
+            itemIsSelected={item => _.isEqual(item.target, target)}
+            renderItemIcon={item => (
+              <Icon name={item.icon || "unknown"} size={18} />
+            )}
+            alwaysExpanded={true}
+            hideSingleSectionTitle={!hasFkOption}
+          />
+        </PopoverWithTrigger>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
index 9f53afad1f6e9efddab9261df2f891bc3cdbeac3..f7d6b7d66642368cb35145c005268c9859f41f5c 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardApp.jsx
@@ -10,62 +10,80 @@ 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, getSlowCards, getEditingParameter, getParameters, getParameterValues } from "../selectors";
+import {
+  getIsEditing,
+  getIsEditingParameter,
+  getIsDirty,
+  getDashboardComplete,
+  getCardList,
+  getRevisions,
+  getCardData,
+  getSlowCards,
+  getEditingParameter,
+  getParameters,
+  getParameterValues,
+} from "../selectors";
 import { getDatabases, getMetadata } from "metabase/selectors/metadata";
 import { getUserIsAdmin } from "metabase/selectors/user";
 
 import * as dashboardActions from "../dashboard";
-import {archiveDashboard} from "metabase/dashboards/dashboards"
-import {parseHashOptions} from "metabase/lib/browser";
+import { archiveDashboard } from "metabase/dashboards/dashboards";
+import { parseHashOptions } from "metabase/lib/browser";
 
 const mapStateToProps = (state, props) => {
   return {
-      dashboardId:          props.params.dashboardId,
+    dashboardId: props.params.dashboardId,
 
-      isAdmin:              getUserIsAdmin(state, props),
-      isEditing:            getIsEditing(state, props),
-      isEditingParameter:   getIsEditingParameter(state, props),
-      isDirty:              getIsDirty(state, props),
-      dashboard:            getDashboardComplete(state, props),
-      cards:                getCardList(state, props),
-      revisions:            getRevisions(state, props),
-      dashcardData:         getCardData(state, props),
-      slowCards:            getSlowCards(state, props),
-      databases:            getDatabases(state, props),
-      editingParameter:     getEditingParameter(state, props),
-      parameters:           getParameters(state, props),
-      parameterValues:      getParameterValues(state, props),
-      metadata:             getMetadata(state)
-  }
-}
+    isAdmin: getUserIsAdmin(state, props),
+    isEditing: getIsEditing(state, props),
+    isEditingParameter: getIsEditingParameter(state, props),
+    isDirty: getIsDirty(state, props),
+    dashboard: getDashboardComplete(state, props),
+    cards: getCardList(state, props),
+    revisions: getRevisions(state, props),
+    dashcardData: getCardData(state, props),
+    slowCards: getSlowCards(state, props),
+    databases: getDatabases(state, props),
+    editingParameter: getEditingParameter(state, props),
+    parameters: getParameters(state, props),
+    parameterValues: getParameterValues(state, props),
+    metadata: getMetadata(state),
+  };
+};
 
 const mapDispatchToProps = {
-    ...dashboardActions,
-    archiveDashboard,
-    fetchDatabaseMetadata,
-    setErrorPage,
-    onChangeLocation: push
-}
+  ...dashboardActions,
+  archiveDashboard,
+  fetchDatabaseMetadata,
+  setErrorPage,
+  onChangeLocation: push,
+};
 
 type DashboardAppState = {
-    addCardOnLoad: number|null
-}
+  addCardOnLoad: number | null,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @title(({ dashboard }) => dashboard && dashboard.name)
 export default class DashboardApp extends Component {
-    state: DashboardAppState = {
-        addCardOnLoad: null
-    };
+  state: DashboardAppState = {
+    addCardOnLoad: null,
+  };
 
-    componentWillMount() {
-        let options = parseHashOptions(window.location.hash);
-        if (options.add) {
-            this.setState({addCardOnLoad: parseInt(options.add)})
-        }
+  componentWillMount() {
+    let options = parseHashOptions(window.location.hash);
+    if (options.add) {
+      this.setState({ addCardOnLoad: parseInt(options.add) });
     }
+  }
 
-    render() {
-        return <Dashboard addCardOnLoad={this.state.addCardOnLoad} {...this.props} />;
-    }
+  render() {
+    return (
+      <div>
+        <Dashboard addCardOnLoad={this.state.addCardOnLoad} {...this.props} />;
+        {/* For rendering modal urls */}
+        {this.props.children}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
index 72dd16754d8024a047a8b6f72fb33fcfabac24c6..ab1ea1da18fbc9e3fa7de0834077130c580033a6 100644
--- a/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
+++ b/frontend/src/metabase/dashboard/containers/DashboardEmbedWidget.jsx
@@ -7,33 +7,49 @@ import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
 import * as Urls from "metabase/lib/urls";
 
-import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams } from "../dashboard";
-
+import {
+  createPublicLink,
+  deletePublicLink,
+  updateEnableEmbedding,
+  updateEmbeddingParams,
+} from "../dashboard";
 
 const mapDispatchToProps = {
-    createPublicLink,
-    deletePublicLink,
-    updateEnableEmbedding,
-    updateEmbeddingParams
-}
+  createPublicLink,
+  deletePublicLink,
+  updateEnableEmbedding,
+  updateEmbeddingParams,
+};
 
 @connect(null, mapDispatchToProps)
 export default class DashboardEmbedWidget extends Component {
-    render() {
-        const { className, dashboard, createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams, ...props } = this.props;
-        return (
-            <EmbedWidget
-                {...props}
-                className={className}
-                resource={dashboard}
-                resourceType="dashboard"
-                resourceParameters={dashboard && dashboard.parameters}
-                onCreatePublicLink={() => createPublicLink(dashboard)}
-                onDisablePublicLink={() => deletePublicLink(dashboard)}
-                onUpdateEnableEmbedding={(enableEmbedding) => updateEnableEmbedding(dashboard, enableEmbedding)}
-                onUpdateEmbeddingParams={(embeddingParams) => updateEmbeddingParams(dashboard, embeddingParams)}
-                getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
-            />
-        );
-    }
+  render() {
+    const {
+      className,
+      dashboard,
+      createPublicLink,
+      deletePublicLink,
+      updateEnableEmbedding,
+      updateEmbeddingParams,
+      ...props
+    } = this.props;
+    return (
+      <EmbedWidget
+        {...props}
+        className={className}
+        resource={dashboard}
+        resourceType="dashboard"
+        resourceParameters={dashboard && dashboard.parameters}
+        onCreatePublicLink={() => createPublicLink(dashboard)}
+        onDisablePublicLink={() => deletePublicLink(dashboard)}
+        onUpdateEnableEmbedding={enableEmbedding =>
+          updateEnableEmbedding(dashboard, enableEmbedding)
+        }
+        onUpdateEmbeddingParams={embeddingParams =>
+          updateEmbeddingParams(dashboard, embeddingParams)
+        }
+        getPublicUrl={({ public_uuid }) => Urls.publicDashboard(public_uuid)}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/dashboard/dashboard.js b/frontend/src/metabase/dashboard/dashboard.js
index 70b31fc8d60708a51bec1f318b914fb1eca953d4..1aa7a42c607c8f7a6a6d0317cb5f406c5860d923 100644
--- a/frontend/src/metabase/dashboard/dashboard.js
+++ b/frontend/src/metabase/dashboard/dashboard.js
@@ -4,16 +4,29 @@ import { assoc, dissoc, assocIn, getIn, chain } from "icepick";
 import _ from "underscore";
 import moment from "moment";
 
-import { handleActions, combineReducers, createAction, createThunkAction } from "metabase/lib/redux";
+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 {
+  createParameter,
+  setParameterName as setParamName,
+  setParameterDefaultValue as setParamDefaultValue,
+} from "metabase/meta/Dashboard";
 import { applyParameters, questionUrlWithParameters } from "metabase/meta/Card";
 import { getParametersBySlug } from "metabase/meta/Parameter";
 
-import type { DashboardWithCards, 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";
@@ -23,18 +36,24 @@ import { createCard } from "metabase/lib/card";
 import { addParamValues, fetchDatabaseMetadata } from "metabase/redux/metadata";
 import { push } from "react-router-redux";
 
-import { DashboardApi, CardApi, RevisionApi, PublicApi, EmbedApi } from "metabase/services";
+import {
+  DashboardApi,
+  CardApi,
+  RevisionApi,
+  PublicApi,
+  EmbedApi,
+} from "metabase/services";
 
 import { getDashboard, getDashboardComplete } from "./selectors";
-import {getCardAfterVisualizationClick} from "metabase/visualizations/lib/utils";
+import { getCardAfterVisualizationClick } from "metabase/visualizations/lib/utils";
 
 const DATASET_SLOW_TIMEOUT = 15 * 1000;
 
 // normalizr schemas
-const dashcard = new schema.Entity('dashcard');
-const card = new schema.Entity('card');
-const dashboard = new schema.Entity('dashboard', {
-    ordered_cards: [dashcard]
+const dashcard = new schema.Entity("dashcard");
+const card = new schema.Entity("card");
+const dashboard = new schema.Entity("dashboard", {
+  ordered_cards: [dashcard],
 });
 
 // action constants
@@ -48,17 +67,23 @@ 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 SAVE_DASHBOARD_AND_CARDS = "metabase/dashboard/SAVE_DASHBOARD_AND_CARDS";
-export const SET_DASHBOARD_ATTRIBUTES = "metabase/dashboard/SET_DASHBOARD_ATTRIBUTES";
+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";
 export const REMOVE_CARD_FROM_DASH = "metabase/dashboard/REMOVE_CARD_FROM_DASH";
-export const SET_DASHCARD_ATTRIBUTES = "metabase/dashboard/SET_DASHCARD_ATTRIBUTES";
-export const UPDATE_DASHCARD_VISUALIZATION_SETTINGS = "metabase/dashboard/UPDATE_DASHCARD_VISUALIZATION_SETTINGS";
-export const REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS = "metabase/dashboard/REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS";
-export const UPDATE_DASHCARD_ID = "metabase/dashboard/UPDATE_DASHCARD_ID"
-
-export const FETCH_DASHBOARD_CARD_DATA = "metabase/dashboard/FETCH_DASHBOARD_CARD_DATA";
+export const SET_DASHCARD_ATTRIBUTES =
+  "metabase/dashboard/SET_DASHCARD_ATTRIBUTES";
+export const UPDATE_DASHCARD_VISUALIZATION_SETTINGS =
+  "metabase/dashboard/UPDATE_DASHCARD_VISUALIZATION_SETTINGS";
+export const REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS =
+  "metabase/dashboard/REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS";
+export const UPDATE_DASHCARD_ID = "metabase/dashboard/UPDATE_DASHCARD_ID";
+
+export const FETCH_DASHBOARD_CARD_DATA =
+  "metabase/dashboard/FETCH_DASHBOARD_CARD_DATA";
 export const FETCH_CARD_DATA = "metabase/dashboard/FETCH_CARD_DATA";
 export const MARK_CARD_AS_SLOW = "metabase/dashboard/MARK_CARD_AS_SLOW";
 export const CLEAR_CARD_DATA = "metabase/dashboard/CLEAR_CARD_DATA";
@@ -68,22 +93,24 @@ export const REVERT_TO_REVISION = "metabase/dashboard/REVERT_TO_REVISION";
 
 export const MARK_NEW_CARD_SEEN = "metabase/dashboard/MARK_NEW_CARD_SEEN";
 
-export const SET_EDITING_PARAMETER_ID = "metabase/dashboard/SET_EDITING_PARAMETER_ID";
+export const SET_EDITING_PARAMETER_ID =
+  "metabase/dashboard/SET_EDITING_PARAMETER_ID";
 export const ADD_PARAMETER = "metabase/dashboard/ADD_PARAMETER";
 export const REMOVE_PARAMETER = "metabase/dashboard/REMOVE_PARAMETER";
 export const SET_PARAMETER_MAPPING = "metabase/dashboard/SET_PARAMETER_MAPPING";
 export const SET_PARAMETER_NAME = "metabase/dashboard/SET_PARAMETER_NAME";
 export const SET_PARAMETER_VALUE = "metabase/dashboard/SET_PARAMETER_VALUE";
-export const SET_PARAMETER_DEFAULT_VALUE = "metabase/dashboard/SET_PARAMETER_DEFAULT_VALUE";
+export const SET_PARAMETER_DEFAULT_VALUE =
+  "metabase/dashboard/SET_PARAMETER_DEFAULT_VALUE";
 
 function getDashboardType(id) {
-    if (Utils.isUUID(id)) {
-        return "public";
-    } else if (Utils.isJWT(id)) {
-        return "embed";
-    } else {
-        return "normal";
-    }
+  if (Utils.isUUID(id)) {
+    return "public";
+  } else if (Utils.isJWT(id)) {
+    return "embed";
+  } else {
+    return "normal";
+  }
 }
 
 // action creators
@@ -98,443 +125,601 @@ export const setDashboardAttributes = createAction(SET_DASHBOARD_ATTRIBUTES);
 export const setDashCardAttributes = createAction(SET_DASHCARD_ATTRIBUTES);
 
 // TODO: consolidate with questions reducer
-export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode = "all") {
-    return async function(dispatch, getState) {
-        let cards = await CardApi.list({ f: filterMode });
-        for (var c of cards) {
-            c.updated_at = moment(c.updated_at);
-        }
-        return normalize(cards, [card]);
-    };
+export const fetchCards = createThunkAction(FETCH_CARDS, function(
+  filterMode = "all",
+) {
+  return async function(dispatch, getState) {
+    let cards = await CardApi.list({ f: filterMode });
+    for (var c of cards) {
+      c.updated_at = moment(c.updated_at);
+    }
+    return normalize(cards, [card]);
+  };
 });
 
 export const deleteCard = createThunkAction(DELETE_CARD, function(cardId) {
-    return async function(dispatch, getState) {
-        await CardApi.delete({ cardId });
-        return cardId;
-    };
+  return async function(dispatch, getState) {
+    await CardApi.delete({ cardId });
+    return cardId;
+  };
 });
 
-export const addCardToDashboard = function({ dashId, cardId }: { dashId: DashCardId, cardId: CardId }) {
-    return function(dispatch, getState) {
-        const { dashboards, dashcards, cards } = getState().dashboard;
-        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 = {
-            id: Math.random(), // temporary id
-            dashboard_id: dashId,
-            card_id: card.id,
-            card: card,
-            series: [],
-            ...getPositionForNewDashCard(existingCards),
-            parameter_mappings: [],
-            visualization_settings: {}
-        };
-        dispatch(createAction(ADD_CARD_TO_DASH)(dashcard));
-        dispatch(fetchCardData(card, dashcard, { reload: true, clear: true }));
-    };
-}
-
-export const addDashCardToDashboard = function({ dashId, dashcardOverrides }: { dashId: DashCardId, dashcardOverrides: { } }) {
-    return function(dispatch, getState) {
-        const { dashboards, dashcards } = getState().dashboard;
-        const dashboard: DashboardWithCards = dashboards[dashId];
-        const existingCards: Array<DashCard> = dashboard.ordered_cards.map(id => dashcards[id]).filter(dc => !dc.isRemoved);
-        const dashcard: DashCard = {
-            id: Math.random(), // temporary id
-            card_id: null,
-            card: null,
-            dashboard_id: dashId,
-            series: [],
-            ...getPositionForNewDashCard(existingCards),
-            parameter_mappings: [],
-            visualization_settings: {}
-        };
-        _.extend(dashcard, dashcardOverrides);
-        dispatch(createAction(ADD_CARD_TO_DASH)(dashcard));
+export const addCardToDashboard = function({
+  dashId,
+  cardId,
+}: {
+  dashId: DashCardId,
+  cardId: CardId,
+}) {
+  return function(dispatch, getState) {
+    const { dashboards, dashcards, cards } = getState().dashboard;
+    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 = {
+      id: Math.random(), // temporary id
+      dashboard_id: dashId,
+      card_id: card.id,
+      card: card,
+      series: [],
+      ...getPositionForNewDashCard(existingCards),
+      parameter_mappings: [],
+      visualization_settings: {},
     };
-}
-
-export const addTextDashCardToDashboard = function({ dashId }: { dashId: DashCardId }) {
-    const virtualTextCard = createCard();
-    virtualTextCard.display = "text";
-    virtualTextCard.archived = false;
-
-    const dashcardOverrides = {
-        card: virtualTextCard,
-        visualization_settings: {
-            virtual_card: virtualTextCard
-        }
+    dispatch(createAction(ADD_CARD_TO_DASH)(dashcard));
+    dispatch(fetchCardData(card, dashcard, { reload: true, clear: true }));
+  };
+};
+
+export const addDashCardToDashboard = function({
+  dashId,
+  dashcardOverrides,
+}: {
+  dashId: DashCardId,
+  dashcardOverrides: {},
+}) {
+  return function(dispatch, getState) {
+    const { dashboards, dashcards } = getState().dashboard;
+    const dashboard: DashboardWithCards = dashboards[dashId];
+    const existingCards: Array<DashCard> = dashboard.ordered_cards
+      .map(id => dashcards[id])
+      .filter(dc => !dc.isRemoved);
+    const dashcard: DashCard = {
+      id: Math.random(), // temporary id
+      card_id: null,
+      card: null,
+      dashboard_id: dashId,
+      series: [],
+      ...getPositionForNewDashCard(existingCards),
+      parameter_mappings: [],
+      visualization_settings: {},
     };
-    return addDashCardToDashboard({ dashId: dashId, dashcardOverrides: dashcardOverrides });
-}
-
-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 }));
-        }
+    _.extend(dashcard, dashcardOverrides);
+    dispatch(createAction(ADD_CARD_TO_DASH)(dashcard));
+  };
+};
+
+export const addTextDashCardToDashboard = function({
+  dashId,
+}: {
+  dashId: DashCardId,
+}) {
+  const virtualTextCard = createCard();
+  virtualTextCard.display = "text";
+  virtualTextCard.archived = false;
+
+  const dashcardOverrides = {
+    card: virtualTextCard,
+    visualization_settings: {
+      virtual_card: virtualTextCard,
+    },
+  };
+  return addDashCardToDashboard({
+    dashId: dashId,
+    dashcardOverrides: dashcardOverrides,
+  });
+};
+
+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 })),
+              ),
+          }),
+        );
 
-        // 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);
-            }
+        const result = await DashboardApi.reposition_cards({
+          dashId: dashboard.id,
+          cards,
+        });
+        if (result.status !== "ok") {
+          throw new Error(result.status);
         }
+      }
 
-        await dispatch(saveDashboard(dashboard));
+      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
-    }
-});
+      // 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 }));
+const updateDashcardId = createAction(
+  UPDATE_DASHCARD_ID,
+  (oldDashcardId, newDashcardId) => ({ oldDashcardId, newDashcardId }),
+);
 
-export const clearCardData = createAction(CLEAR_CARD_DATA, (cardId, dashcardId) => ({ cardId, dashcardId }));
+export const clearCardData = createAction(
+  CLEAR_CARD_DATA,
+  (cardId, dashcardId) => ({ cardId, dashcardId }),
+);
 
 export async function fetchDataOrError(dataPromise) {
-    try {
-        return await dataPromise;
-    }
-    catch (error) {
-        return { error };
-    }
+  try {
+    return await dataPromise;
+  } catch (error) {
+    return { error };
+  }
 }
 
-export const fetchDashboardCardData = createThunkAction(FETCH_DASHBOARD_CARD_DATA, (options) =>
-    async (dispatch, getState) => {
-        const dashboard = getDashboardComplete(getState());
-        if (dashboard) {
-            for (const dashcard of dashboard.ordered_cards) {
-                // we skip over virtual cards, i.e. dashcards that do not have backing cards in the backend
-                if (_.isObject(dashcard.visualization_settings.virtual_card)) { continue }
-                const cards = [dashcard.card].concat(dashcard.series || []);
-                for (const card of cards) {
-                    dispatch(fetchCardData(card, dashcard, options));
-                }
-            }
+export const fetchDashboardCardData = createThunkAction(
+  FETCH_DASHBOARD_CARD_DATA,
+  options => async (dispatch, getState) => {
+    const dashboard = getDashboardComplete(getState());
+    if (dashboard) {
+      for (const dashcard of dashboard.ordered_cards) {
+        // we skip over virtual cards, i.e. dashcards that do not have backing cards in the backend
+        if (_.isObject(dashcard.visualization_settings.virtual_card)) {
+          continue;
+        }
+        const cards = [dashcard.card].concat(dashcard.series || []);
+        for (const card of cards) {
+          dispatch(fetchCardData(card, dashcard, options));
         }
+      }
     }
+  },
 );
 
-export const fetchCardData = createThunkAction(FETCH_CARD_DATA, function(card, dashcard, { reload, clear } = {}) {
-    return async function(dispatch, getState) {
-        // If the dataset_query was filtered then we don't have permisison to view this card, so
-        // shortcircuit and return a fake 403
-        if (!card.dataset_query) {
-            return {
-                dashcard_id: dashcard.id,
-                card_id: card.id,
-                result: { error: { status: 403 }}
-            };
-        }
+export const fetchCardData = createThunkAction(FETCH_CARD_DATA, function(
+  card,
+  dashcard,
+  { reload, clear } = {},
+) {
+  return async function(dispatch, getState) {
+    // If the dataset_query was filtered then we don't have permisison to view this card, so
+    // shortcircuit and return a fake 403
+    if (!card.dataset_query) {
+      return {
+        dashcard_id: dashcard.id,
+        card_id: card.id,
+        result: { error: { status: 403 } },
+      };
+    }
 
-        const dashboardType = getDashboardType(dashcard.dashboard_id);
-
-        const { dashboardId, dashboards, parameterValues, dashcardData } = getState().dashboard;
-        const dashboard = dashboards[dashboardId];
-
-        // if we have a parameter, apply it to the card query before we execute
-        const datasetQuery = applyParameters(card, dashboard.parameters, parameterValues, dashcard && dashcard.parameter_mappings);
-
-        if (!reload) {
-            // if reload not set, check to see if the last result has the same query dict and return that
-            const lastResult = getIn(dashcardData, [dashcard.id, card.id]);
-            // "constraints" is added by the backend, remove it when comparing
-            if (lastResult && Utils.equals(_.omit(lastResult.json_query, "constraints"), datasetQuery)) {
-                return {
-                    dashcard_id: dashcard.id,
-                    card_id: card.id,
-                    result: lastResult
-                };
-            }
-        }
+    const dashboardType = getDashboardType(dashcard.dashboard_id);
+
+    const {
+      dashboardId,
+      dashboards,
+      parameterValues,
+      dashcardData,
+    } = getState().dashboard;
+    const dashboard = dashboards[dashboardId];
+
+    // if we have a parameter, apply it to the card query before we execute
+    const datasetQuery = applyParameters(
+      card,
+      dashboard.parameters,
+      parameterValues,
+      dashcard && dashcard.parameter_mappings,
+    );
+
+    if (!reload) {
+      // if reload not set, check to see if the last result has the same query dict and return that
+      const lastResult = getIn(dashcardData, [dashcard.id, card.id]);
+      // "constraints" is added by the backend, remove it when comparing
+      if (
+        lastResult &&
+        Utils.equals(_.omit(lastResult.json_query, "constraints"), datasetQuery)
+      ) {
+        return {
+          dashcard_id: dashcard.id,
+          card_id: card.id,
+          result: lastResult,
+        };
+      }
+    }
 
-        if (clear) {
-            // clears the card data to indicate the card is reloading
-            dispatch(clearCardData(card.id, dashcard.id));
-        }
+    if (clear) {
+      // clears the card data to indicate the card is reloading
+      dispatch(clearCardData(card.id, dashcard.id));
+    }
 
-        let result = null;
-
-        // start a timer that will show the expected card duration if the query takes too long
-        let slowCardTimer = setTimeout(() => {
-            if (result === null) {
-                dispatch(markCardAsSlow(card, datasetQuery));
-            }
-        }, DATASET_SLOW_TIMEOUT);
-
-        // make the actual request
-        if (dashboardType === "public") {
-            result = await fetchDataOrError(PublicApi.dashboardCardQuery({
-                uuid: dashcard.dashboard_id,
-                cardId: card.id,
-                parameters: datasetQuery.parameters ? JSON.stringify(datasetQuery.parameters) : undefined
-            }));
-        } else if (dashboardType === "embed") {
-            result = await fetchDataOrError(EmbedApi.dashboardCardQuery({
-                token: dashcard.dashboard_id,
-                dashcardId: dashcard.id,
-                cardId: card.id,
-                ...getParametersBySlug(dashboard.parameters, parameterValues)
-            }));
-        } else {
-            result = await fetchDataOrError(CardApi.query({cardId: card.id, parameters: datasetQuery.parameters}));
-        }
+    let result = null;
+
+    // start a timer that will show the expected card duration if the query takes too long
+    let slowCardTimer = setTimeout(() => {
+      if (result === null) {
+        dispatch(markCardAsSlow(card, datasetQuery));
+      }
+    }, DATASET_SLOW_TIMEOUT);
+
+    // make the actual request
+    if (dashboardType === "public") {
+      result = await fetchDataOrError(
+        PublicApi.dashboardCardQuery({
+          uuid: dashcard.dashboard_id,
+          cardId: card.id,
+          parameters: datasetQuery.parameters
+            ? JSON.stringify(datasetQuery.parameters)
+            : undefined,
+        }),
+      );
+    } else if (dashboardType === "embed") {
+      result = await fetchDataOrError(
+        EmbedApi.dashboardCardQuery({
+          token: dashcard.dashboard_id,
+          dashcardId: dashcard.id,
+          cardId: card.id,
+          ...getParametersBySlug(dashboard.parameters, parameterValues),
+        }),
+      );
+    } else {
+      result = await fetchDataOrError(
+        CardApi.query({ cardId: card.id, parameters: datasetQuery.parameters }),
+      );
+    }
 
-        clearTimeout(slowCardTimer);
+    clearTimeout(slowCardTimer);
 
-        return {
-            dashcard_id: dashcard.id,
-            card_id: card.id,
-            result: result
-        };
+    return {
+      dashcard_id: dashcard.id,
+      card_id: card.id,
+      result: result,
     };
+  };
 });
 
-export const markCardAsSlow = createAction(MARK_CARD_AS_SLOW, (card) => ({
-    id: card.id,
-    result: true
+export const markCardAsSlow = createAction(MARK_CARD_AS_SLOW, card => ({
+  id: card.id,
+  result: true,
 }));
 
+export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(
+  dashId,
+  queryParams,
+  enableDefaultParameters = true,
+) {
+  let result;
+  return async function(dispatch, getState) {
+    const dashboardType = getDashboardType(dashId);
+    if (dashboardType === "public") {
+      result = await PublicApi.dashboard({ uuid: dashId });
+      result = {
+        ...result,
+        id: dashId,
+        ordered_cards: result.ordered_cards.map(dc => ({
+          ...dc,
+          dashboard_id: dashId,
+        })),
+      };
+    } else if (dashboardType === "embed") {
+      result = await EmbedApi.dashboard({ token: dashId });
+      result = {
+        ...result,
+        id: dashId,
+        ordered_cards: result.ordered_cards.map(dc => ({
+          ...dc,
+          dashboard_id: dashId,
+        })),
+      };
+    } else {
+      result = await DashboardApi.get({ dashId: dashId });
+    }
 
-export const fetchDashboard = createThunkAction(FETCH_DASHBOARD, function(dashId, queryParams, enableDefaultParameters = true) {
-    let result;
-    return async function(dispatch, getState) {
-        const dashboardType = getDashboardType(dashId);
-        if (dashboardType === "public") {
-            result = await PublicApi.dashboard({ uuid: dashId });
-            result = {
-                ...result,
-                id: dashId,
-                ordered_cards: result.ordered_cards.map(dc => ({
-                    ...dc,
-                    dashboard_id: dashId
-                }))
-            };
-        } else if (dashboardType === "embed") {
-            result = await EmbedApi.dashboard({ token: dashId });
-            result = {
-                ...result,
-                id: dashId,
-                ordered_cards: result.ordered_cards.map(dc => ({
-                    ...dc,
-                    dashboard_id: dashId
-                }))
-            };
-        } else {
-            result = await DashboardApi.get({ dashId: dashId });
-        }
-
-        const parameterValues = {};
-        if (result.parameters) {
-            for (const parameter of result.parameters) {
-                if (queryParams && queryParams[parameter.slug] != null) {
-                    parameterValues[parameter.id] = queryParams[parameter.slug];
-                } else if (enableDefaultParameters && parameter.default != null) {
-                    parameterValues[parameter.id] = parameter.default;
-                }
-            }
+    const parameterValues = {};
+    if (result.parameters) {
+      for (const parameter of result.parameters) {
+        if (queryParams && queryParams[parameter.slug] != null) {
+          parameterValues[parameter.id] = queryParams[parameter.slug];
+        } else if (enableDefaultParameters && parameter.default != null) {
+          parameterValues[parameter.id] = parameter.default;
         }
+      }
+    }
 
-        if (dashboardType === "normal") {
-            // fetch database metadata for every card
-            _.chain(result.ordered_cards)
-                .map((dc) => [dc.card].concat(dc.series))
-                .flatten()
-                .filter(card => card && card.dataset_query && card.dataset_query.database)
-                .map(card => card.dataset_query.database)
-                .uniq()
-                .each((dbId) => dispatch(fetchDatabaseMetadata(dbId)));
-        }
+    if (dashboardType === "normal") {
+      // fetch database metadata for every card
+      _.chain(result.ordered_cards)
+        .map(dc => [dc.card].concat(dc.series))
+        .flatten()
+        .filter(
+          card => card && card.dataset_query && card.dataset_query.database,
+        )
+        .map(card => card.dataset_query.database)
+        .uniq()
+        .each(dbId => dispatch(fetchDatabaseMetadata(dbId)));
+    }
 
-        // copy over any virtual cards from the dashcard to the underlying card/question
-        result.ordered_cards.forEach((card) => {
-            if (card.visualization_settings.virtual_card) {
-                _.extend(card.card, card.visualization_settings.virtual_card);
-            }
-        });
+    // copy over any virtual cards from the dashcard to the underlying card/question
+    result.ordered_cards.forEach(card => {
+      if (card.visualization_settings.virtual_card) {
+        _.extend(card.card, card.visualization_settings.virtual_card);
+      }
+    });
 
-        if (result.param_values) {
-            dispatch(addParamValues(result.param_values));
-        }
+    if (result.param_values) {
+      dispatch(addParamValues(result.param_values));
+    }
 
-        return {
-            ...normalize(result, dashboard), // includes `result` and `entities`
-            dashboardId: dashId,
-            parameterValues: parameterValues
-        };
+    return {
+      ...normalize(result, dashboard), // includes `result` and `entities`
+      dashboardId: dashId,
+      parameterValues: parameterValues,
     };
+  };
 });
 
 const UPDATE_ENABLE_EMBEDDING = "metabase/dashboard/UPDATE_ENABLE_EMBEDDING";
-export const updateEnableEmbedding = createAction(UPDATE_ENABLE_EMBEDDING, ({ id }, enable_embedding) =>
-    DashboardApi.update({ id, enable_embedding })
+export const updateEnableEmbedding = createAction(
+  UPDATE_ENABLE_EMBEDDING,
+  ({ id }, enable_embedding) => DashboardApi.update({ id, enable_embedding }),
 );
 
 const UPDATE_EMBEDDING_PARAMS = "metabase/dashboard/UPDATE_EMBEDDING_PARAMS";
-export const updateEmbeddingParams = createAction(UPDATE_EMBEDDING_PARAMS, ({ id }, embedding_params) =>
-    DashboardApi.update({ id, embedding_params })
+export const updateEmbeddingParams = createAction(
+  UPDATE_EMBEDDING_PARAMS,
+  ({ id }, embedding_params) => DashboardApi.update({ id, embedding_params }),
 );
 
-export const fetchRevisions = createThunkAction(FETCH_REVISIONS, function({ entity, id }) {
-    return async function(dispatch, getState) {
-        let revisions = await RevisionApi.list({ entity, id });
-        return { entity, id, revisions };
-    };
+export const fetchRevisions = createThunkAction(FETCH_REVISIONS, function({
+  entity,
+  id,
+}) {
+  return async function(dispatch, getState) {
+    let revisions = await RevisionApi.list({ entity, id });
+    return { entity, id, revisions };
+  };
 });
 
-export const revertToRevision = createThunkAction(REVERT_TO_REVISION, function({ entity, id, revision_id }) {
-    return async function(dispatch, getState) {
-        await RevisionApi.revert({ entity, id, revision_id });
-    };
+export const revertToRevision = createThunkAction(REVERT_TO_REVISION, function({
+  entity,
+  id,
+  revision_id,
+}) {
+  return async function(dispatch, getState) {
+    await RevisionApi.revert({ entity, id, revision_id });
+  };
 });
 
-export const onUpdateDashCardVisualizationSettings = createAction(UPDATE_DASHCARD_VISUALIZATION_SETTINGS, (id, settings) => ({ id, settings }));
-export const onReplaceAllDashCardVisualizationSettings = createAction(REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS, (id, settings) => ({ id, settings }));
+export const onUpdateDashCardVisualizationSettings = createAction(
+  UPDATE_DASHCARD_VISUALIZATION_SETTINGS,
+  (id, settings) => ({ id, settings }),
+);
+export const onReplaceAllDashCardVisualizationSettings = createAction(
+  REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS,
+  (id, settings) => ({ id, settings }),
+);
 
 export const setEditingParameter = createAction(SET_EDITING_PARAMETER_ID);
-export const setParameterMapping = createThunkAction(SET_PARAMETER_MAPPING, (parameter_id, dashcard_id, card_id, target) =>
-    (dispatch, getState) => {
-        let parameter_mappings = getState().dashboard.dashcards[dashcard_id].parameter_mappings || [];
-        parameter_mappings = parameter_mappings.filter(m => m.card_id !== card_id || m.parameter_id !== parameter_id);
-        if (target) {
-            parameter_mappings = parameter_mappings.concat({ parameter_id, card_id, target })
-        }
-        dispatch(setDashCardAttributes({ id: dashcard_id, attributes: { parameter_mappings }}));
+export const setParameterMapping = createThunkAction(
+  SET_PARAMETER_MAPPING,
+  (parameter_id, dashcard_id, card_id, target) => (dispatch, getState) => {
+    let parameter_mappings =
+      getState().dashboard.dashcards[dashcard_id].parameter_mappings || [];
+    parameter_mappings = parameter_mappings.filter(
+      m => m.card_id !== card_id || m.parameter_id !== parameter_id,
+    );
+    if (target) {
+      parameter_mappings = parameter_mappings.concat({
+        parameter_id,
+        card_id,
+        target,
+      });
     }
+    dispatch(
+      setDashCardAttributes({
+        id: dashcard_id,
+        attributes: { parameter_mappings },
+      }),
+    );
+  },
 );
 
 function updateParameter(dispatch, getState, id, parameterUpdater) {
-    const dashboard = getDashboard(getState());
-    const index = _.findIndex(dashboard && dashboard.parameters, (p) => p.id === id);
-    if (index >= 0) {
-        const parameters = assoc(dashboard.parameters, index, parameterUpdater(dashboard.parameters[index]));
-        dispatch(setDashboardAttributes({ id: dashboard.id, attributes: { parameters } }));
-    }
+  const dashboard = getDashboard(getState());
+  const index = _.findIndex(
+    dashboard && dashboard.parameters,
+    p => p.id === id,
+  );
+  if (index >= 0) {
+    const parameters = assoc(
+      dashboard.parameters,
+      index,
+      parameterUpdater(dashboard.parameters[index]),
+    );
+    dispatch(
+      setDashboardAttributes({ id: dashboard.id, attributes: { parameters } }),
+    );
+  }
 }
 
 function updateParameters(dispatch, getState, parametersUpdater) {
-    const dashboard = getDashboard(getState());
-    if (dashboard) {
-        const parameters = parametersUpdater(dashboard.parameters || []);
-        dispatch(setDashboardAttributes({ id: dashboard.id, attributes: { parameters } }))
-    }
+  const dashboard = getDashboard(getState());
+  if (dashboard) {
+    const parameters = parametersUpdater(dashboard.parameters || []);
+    dispatch(
+      setDashboardAttributes({ id: dashboard.id, attributes: { parameters } }),
+    );
+  }
 }
 
-export const addParameter = createThunkAction(ADD_PARAMETER, (parameterOption) =>
-    (dispatch, getState) => {
-        let parameter;
-        updateParameters(dispatch, getState, (parameters) => {
-            parameter = createParameter(parameterOption, parameters);
-            return parameters.concat(parameter);
-        })
-        return parameter;
-    }
+export const addParameter = createThunkAction(
+  ADD_PARAMETER,
+  parameterOption => (dispatch, getState) => {
+    let parameter;
+    updateParameters(dispatch, getState, parameters => {
+      parameter = createParameter(parameterOption, parameters);
+      return parameters.concat(parameter);
+    });
+    return parameter;
+  },
 );
 
-export const removeParameter = createThunkAction(REMOVE_PARAMETER, (parameterId) =>
-    (dispatch, getState) => {
-        updateParameters(dispatch, getState, (parameters) =>
-            parameters.filter(p => p.id !== parameterId)
-        );
-        return { id: parameterId };
-    }
+export const removeParameter = createThunkAction(
+  REMOVE_PARAMETER,
+  parameterId => (dispatch, getState) => {
+    updateParameters(dispatch, getState, parameters =>
+      parameters.filter(p => p.id !== parameterId),
+    );
+    return { id: parameterId };
+  },
 );
 
-export const setParameterName = createThunkAction(SET_PARAMETER_NAME, (parameterId, name) =>
-    (dispatch, getState) => {
-        updateParameter(dispatch, getState, parameterId, (parameter) =>
-            setParamName(parameter, name)
-        )
-        return { id: parameterId, name };
-    }
-)
+export const setParameterName = createThunkAction(
+  SET_PARAMETER_NAME,
+  (parameterId, name) => (dispatch, getState) => {
+    updateParameter(dispatch, getState, parameterId, parameter =>
+      setParamName(parameter, name),
+    );
+    return { id: parameterId, name };
+  },
+);
 
-export const setParameterDefaultValue = createThunkAction(SET_PARAMETER_DEFAULT_VALUE, (parameterId, defaultValue) =>
-    (dispatch, getState) => {
-        updateParameter(dispatch, getState, parameterId, (parameter) =>
-            setParamDefaultValue(parameter, defaultValue)
-        )
-        return { id: parameterId, defaultValue };
-    }
-)
+export const setParameterDefaultValue = createThunkAction(
+  SET_PARAMETER_DEFAULT_VALUE,
+  (parameterId, defaultValue) => (dispatch, getState) => {
+    updateParameter(dispatch, getState, parameterId, parameter =>
+      setParamDefaultValue(parameter, defaultValue),
+    );
+    return { id: parameterId, defaultValue };
+  },
+);
 
-export const setParameterValue = createThunkAction(SET_PARAMETER_VALUE, (parameterId, value) =>
-    (dispatch, getState) => {
-        return { id: parameterId, value };
-    }
-)
+export const setParameterValue = createThunkAction(
+  SET_PARAMETER_VALUE,
+  (parameterId, value) => (dispatch, getState) => {
+    return { id: parameterId, value };
+  },
+);
 
 export const CREATE_PUBLIC_LINK = "metabase/dashboard/CREATE_PUBLIC_LINK";
-export const createPublicLink = createAction(CREATE_PUBLIC_LINK, async ({ id }) => {
+export const createPublicLink = createAction(
+  CREATE_PUBLIC_LINK,
+  async ({ id }) => {
     let { uuid } = await DashboardApi.createPublicLink({ id });
     return { id, uuid };
-});
+  },
+);
 
 export const DELETE_PUBLIC_LINK = "metabase/dashboard/DELETE_PUBLIC_LINK";
-export const deletePublicLink = createAction(DELETE_PUBLIC_LINK, async ({ id }) => {
+export const deletePublicLink = createAction(
+  DELETE_PUBLIC_LINK,
+  async ({ id }) => {
     await DashboardApi.deletePublicLink({ id });
     return { id };
-});
+  },
+);
 
 /**
  * All navigation actions from dashboards to cards (e.x. clicking a title, drill through)
@@ -553,165 +738,260 @@ export const deletePublicLink = createAction(DELETE_PUBLIC_LINK, async ({ id })
 
 const NAVIGATE_TO_NEW_CARD = "metabase/dashboard/NAVIGATE_TO_NEW_CARD";
 export const navigateToNewCardFromDashboard = createThunkAction(
-    NAVIGATE_TO_NEW_CARD,
-    ({ nextCard, previousCard, dashcard }) =>
-        (dispatch, getState) => {
-            const {metadata} = getState();
-            const {dashboardId, dashboards, parameterValues} = getState().dashboard;
-            const dashboard = dashboards[dashboardId];
-            const cardIsDirty = !_.isEqual(previousCard.dataset_query, nextCard.dataset_query);
-            const cardAfterClick = getCardAfterVisualizationClick(nextCard, previousCard);
-
-            // clicking graph title with a filter applied loses display type and visualization settings; see #5278
-            const cardWithVizSettings = {
-                ...cardAfterClick,
-                display: cardAfterClick.display || previousCard.display,
-                visualization_settings: cardAfterClick.visualization_settings || previousCard.visualization_settings
-            }
-
-            const url = questionUrlWithParameters(
-                cardWithVizSettings,
-                metadata,
-                dashboard.parameters,
-                parameterValues,
-                dashcard && dashcard.parameter_mappings,
-                cardIsDirty
-            );
-
-            dispatch(push(url));
-        }
+  NAVIGATE_TO_NEW_CARD,
+  ({ nextCard, previousCard, dashcard }) => (dispatch, getState) => {
+    const { metadata } = getState();
+    const { dashboardId, dashboards, parameterValues } = getState().dashboard;
+    const dashboard = dashboards[dashboardId];
+    const cardIsDirty = !_.isEqual(
+      previousCard.dataset_query,
+      nextCard.dataset_query,
+    );
+    const cardAfterClick = getCardAfterVisualizationClick(
+      nextCard,
+      previousCard,
+    );
+
+    // clicking graph title with a filter applied loses display type and visualization settings; see #5278
+    const cardWithVizSettings = {
+      ...cardAfterClick,
+      display: cardAfterClick.display || previousCard.display,
+      visualization_settings:
+        cardAfterClick.visualization_settings ||
+        previousCard.visualization_settings,
+    };
+
+    const url = questionUrlWithParameters(
+      cardWithVizSettings,
+      metadata,
+      dashboard.parameters,
+      parameterValues,
+      dashcard && dashcard.parameter_mappings,
+      cardIsDirty,
+    );
+
+    dispatch(push(url));
+  },
 );
 
 // reducers
 
-const dashboardId = handleActions({
-    [INITIALIZE]: { next: (state) => null },
-    [FETCH_DASHBOARD]: { next: (state, { payload: { dashboardId } }) => dashboardId }
-}, null);
+const dashboardId = handleActions(
+  {
+    [INITIALIZE]: { next: state => null },
+    [FETCH_DASHBOARD]: {
+      next: (state, { payload: { dashboardId } }) => dashboardId,
+    },
+  },
+  null,
+);
 
-const isEditing = handleActions({
-    [INITIALIZE]: { next: (state) => false },
-    [SET_EDITING_DASHBOARD]: { next: (state, { payload }) => payload }
-}, false);
+const isEditing = handleActions(
+  {
+    [INITIALIZE]: { next: state => false },
+    [SET_EDITING_DASHBOARD]: { next: (state, { payload }) => payload },
+  },
+  false,
+);
 
 // TODO: consolidate with questions reducer
-const cards = handleActions({
-    [FETCH_CARDS]: { next: (state, { payload }) => ({ ...payload.entities.card }) }
-}, {});
+const cards = handleActions(
+  {
+    [FETCH_CARDS]: {
+      next: (state, { payload }) => ({ ...payload.entities.card }),
+    },
+  },
+  {},
+);
 
-const cardList = handleActions({
+const cardList = handleActions(
+  {
     [FETCH_CARDS]: { next: (state, { payload }) => payload.result },
-    [DELETE_CARD]: { next: (state, { payload }) => state }
-}, null);
+    [DELETE_CARD]: { next: (state, { payload }) => state },
+  },
+  null,
+);
 
-const dashboards = handleActions({
-    [FETCH_DASHBOARD]: { next: (state, { payload }) => ({ ...state, ...payload.entities.dashboard }) },
+const dashboards = handleActions(
+  {
+    [FETCH_DASHBOARD]: {
+      next: (state, { payload }) => ({
+        ...state,
+        ...payload.entities.dashboard,
+      }),
+    },
     [SET_DASHBOARD_ATTRIBUTES]: {
-        next: (state, { payload: { id, attributes } }) => ({
-            ...state,
-            [id]: { ...state[id], ...attributes, isDirty: true }
-        })
+      next: (state, { payload: { id, attributes } }) => ({
+        ...state,
+        [id]: { ...state[id], ...attributes, isDirty: true },
+      }),
     },
     [ADD_CARD_TO_DASH]: (state, { payload: dashcard }) => ({
-        ...state, [dashcard.dashboard_id]: { ...state[dashcard.dashboard_id], ordered_cards: [...state[dashcard.dashboard_id].ordered_cards, dashcard.id] }
+      ...state,
+      [dashcard.dashboard_id]: {
+        ...state[dashcard.dashboard_id],
+        ordered_cards: [
+          ...state[dashcard.dashboard_id].ordered_cards,
+          dashcard.id,
+        ],
+      },
     }),
-    [CREATE_PUBLIC_LINK]: { next: (state, { payload }) =>
-        assocIn(state, [payload.id, "public_uuid"], payload.uuid)
+    [CREATE_PUBLIC_LINK]: {
+      next: (state, { payload }) =>
+        assocIn(state, [payload.id, "public_uuid"], payload.uuid),
     },
-    [DELETE_PUBLIC_LINK]: { next: (state, { payload }) =>
-        assocIn(state, [payload.id, "public_uuid"], null)
+    [DELETE_PUBLIC_LINK]: {
+      next: (state, { payload }) =>
+        assocIn(state, [payload.id, "public_uuid"], null),
     },
-    [UPDATE_EMBEDDING_PARAMS]: { next: (state, { payload }) =>
-        assocIn(state, [payload.id, "embedding_params"], payload.embedding_params)
+    [UPDATE_EMBEDDING_PARAMS]: {
+      next: (state, { payload }) =>
+        assocIn(
+          state,
+          [payload.id, "embedding_params"],
+          payload.embedding_params,
+        ),
     },
-    [UPDATE_ENABLE_EMBEDDING]: { next: (state, { payload }) =>
-        assocIn(state, [payload.id, "enable_embedding"], payload.enable_embedding)
+    [UPDATE_ENABLE_EMBEDDING]: {
+      next: (state, { payload }) =>
+        assocIn(
+          state,
+          [payload.id, "enable_embedding"],
+          payload.enable_embedding,
+        ),
     },
-}, {});
+  },
+  {},
+);
 
-const dashcards = handleActions({
-    [FETCH_DASHBOARD]:  { next: (state, { payload }) => ({ ...state, ...payload.entities.dashcard }) },
+const dashcards = handleActions(
+  {
+    [FETCH_DASHBOARD]: {
+      next: (state, { payload }) => ({
+        ...state,
+        ...payload.entities.dashcard,
+      }),
+    },
     [SET_DASHCARD_ATTRIBUTES]: {
-        next: (state, { payload: { id, attributes } }) => ({
-            ...state,
-            [id]: { ...state[id], ...attributes, isDirty: true }
-        })
+      next: (state, { payload: { id, attributes } }) => ({
+        ...state,
+        [id]: { ...state[id], ...attributes, isDirty: true },
+      }),
     },
     [UPDATE_DASHCARD_VISUALIZATION_SETTINGS]: {
-        next: (state, { payload: { id, settings } }) =>
-            chain(state)
-                .updateIn([id, "visualization_settings"], (value = {}) => ({ ...value, ...settings }))
-                .assocIn([id, "isDirty"], true)
-                .value()
+      next: (state, { payload: { id, settings } }) =>
+        chain(state)
+          .updateIn([id, "visualization_settings"], (value = {}) => ({
+            ...value,
+            ...settings,
+          }))
+          .assocIn([id, "isDirty"], true)
+          .value(),
     },
     [REPLACE_ALL_DASHCARD_VISUALIZATION_SETTINGS]: {
-        next: (state, { payload: { id, settings } }) =>
-            chain(state)
-                .assocIn([id, "visualization_settings"], settings)
-                .assocIn([id, "isDirty"], true)
-                .value()
+      next: (state, { payload: { id, settings } }) =>
+        chain(state)
+          .assocIn([id, "visualization_settings"], settings)
+          .assocIn([id, "isDirty"], true)
+          .value(),
     },
     [ADD_CARD_TO_DASH]: (state, { payload: dashcard }) => ({
-        ...state,
-        [dashcard.id]: { ...dashcard, isAdded: true, justAdded: true }
+      ...state,
+      [dashcard.id]: { ...dashcard, isAdded: true, justAdded: true },
     }),
-    [REMOVE_CARD_FROM_DASH]: (state, { payload: { dashcardId }}) => ({
-        ...state,
-        [dashcardId]: { ...state[dashcardId], isRemoved: true }
+    [REMOVE_CARD_FROM_DASH]: (state, { payload: { dashcardId } }) => ({
+      ...state,
+      [dashcardId]: { ...state[dashcardId], isRemoved: true },
     }),
     [MARK_NEW_CARD_SEEN]: (state, { payload: dashcardId }) => ({
-        ...state,
-        [dashcardId]: { ...state[dashcardId], justAdded: false }
-    })
-}, {});
+      ...state,
+      [dashcardId]: { ...state[dashcardId], justAdded: false },
+    }),
+  },
+  {},
+);
 
-const editingParameterId = handleActions({
+const editingParameterId = handleActions(
+  {
     [SET_EDITING_PARAMETER_ID]: { next: (state, { payload }) => payload },
-    [ADD_PARAMETER]: { next: (state, { payload: { id } }) => id }
-}, null);
+    [ADD_PARAMETER]: { next: (state, { payload: { id } }) => id },
+  },
+  null,
+);
 
-const revisions = handleActions({
-    [FETCH_REVISIONS]: { next: (state, { payload: { entity, id, revisions } }) => ({ ...state, [entity+'-'+id]: revisions })}
-}, {});
+const revisions = handleActions(
+  {
+    [FETCH_REVISIONS]: {
+      next: (state, { payload: { entity, id, revisions } }) => ({
+        ...state,
+        [entity + "-" + id]: revisions,
+      }),
+    },
+  },
+  {},
+);
 
-const dashcardData = handleActions({
+const dashcardData = handleActions(
+  {
     // clear existing dashboard data when loading a dashboard
-    [INITIALIZE]: { next: (state) => ({}) },
-    [FETCH_CARD_DATA]: { next: (state, { payload: { dashcard_id, card_id, result }}) =>
-        assocIn(state, [dashcard_id, card_id], result)
+    [INITIALIZE]: { next: state => ({}) },
+    [FETCH_CARD_DATA]: {
+      next: (state, { payload: { dashcard_id, card_id, result } }) =>
+        assocIn(state, [dashcard_id, card_id], result),
     },
-    [CLEAR_CARD_DATA]: { next: (state, { payload: { cardId, dashcardId }}) =>
-        assocIn(state, [dashcardId, cardId])
+    [CLEAR_CARD_DATA]: {
+      next: (state, { payload: { cardId, dashcardId } }) =>
+        assocIn(state, [dashcardId, cardId]),
     },
-    [UPDATE_DASHCARD_ID]: { next: (state, { payload: { oldDashcardId, newDashcardId }}) =>
+    [UPDATE_DASHCARD_ID]: {
+      next: (state, { payload: { oldDashcardId, newDashcardId } }) =>
         chain(state)
-            .assoc(newDashcardId, state[oldDashcardId])
-            .dissoc(oldDashcardId)
-            .value()
-    }
-}, {});
+          .assoc(newDashcardId, state[oldDashcardId])
+          .dissoc(oldDashcardId)
+          .value(),
+    },
+  },
+  {},
+);
 
-const slowCards = handleActions({
-    [MARK_CARD_AS_SLOW]: { next: (state, { payload: { id, result }}) => ({ ...state, [id]: result }) }
-}, {});
+const slowCards = handleActions(
+  {
+    [MARK_CARD_AS_SLOW]: {
+      next: (state, { payload: { id, result } }) => ({
+        ...state,
+        [id]: result,
+      }),
+    },
+  },
+  {},
+);
 
-const parameterValues = handleActions({
+const parameterValues = handleActions(
+  {
     [INITIALIZE]: { next: () => ({}) }, // reset values
-    [SET_PARAMETER_VALUE]: { next: (state, { payload: { id, value }}) => assoc(state, id, value) },
-    [REMOVE_PARAMETER]: { next: (state, { payload: { id }}) => dissoc(state, id) },
-    [FETCH_DASHBOARD]: { next: (state, { payload: { parameterValues }}) => parameterValues },
-}, {});
+    [SET_PARAMETER_VALUE]: {
+      next: (state, { payload: { id, value } }) => assoc(state, id, value),
+    },
+    [REMOVE_PARAMETER]: {
+      next: (state, { payload: { id } }) => dissoc(state, id),
+    },
+    [FETCH_DASHBOARD]: {
+      next: (state, { payload: { parameterValues } }) => parameterValues,
+    },
+  },
+  {},
+);
 
 export default combineReducers({
-    dashboardId,
-    isEditing,
-    cards,
-    cardList,
-    dashboards,
-    dashcards,
-    editingParameterId,
-    revisions,
-    dashcardData,
-    slowCards,
-    parameterValues
+  dashboardId,
+  isEditing,
+  cards,
+  cardList,
+  dashboards,
+  dashcards,
+  editingParameterId,
+  revisions,
+  dashcardData,
+  slowCards,
+  parameterValues,
 });
diff --git a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
index eb4234dfd2d0b607870177b728d23cb2fe85ec25..6a8318b676bd87732d3749f3996883ae50577cc1 100644
--- a/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
+++ b/frontend/src/metabase/dashboard/hoc/DashboardControls.jsx
@@ -13,19 +13,19 @@ import screenfull from "screenfull";
 import type { LocationDescriptor } from "metabase/meta/types";
 
 type Props = {
-    dashboardId:            string,
-    fetchDashboard:         (dashboardId: string) => Promise<any>;
-    fetchDashboardCardData: () => void;
+  dashboardId: string,
+  fetchDashboard: (dashboardId: string) => Promise<any>,
+  fetchDashboardCardData: () => void,
 
-    location:               LocationDescriptor,
-    replace:                (location: LocationDescriptor) => void,
+  location: LocationDescriptor,
+  replace: (location: LocationDescriptor) => void,
 };
 
 type State = {
-    isFullscreen: boolean,
-    isNightMode: boolean,
-    refreshPeriod: ?number,
-    refreshElapsed: ?number
+  isFullscreen: boolean,
+  isNightMode: boolean,
+  refreshPeriod: ?number,
+  refreshElapsed: ?number,
 };
 
 const TICK_PERIOD = 0.25; // seconds
@@ -34,187 +34,191 @@ const TICK_PERIOD = 0.25; // seconds
  * It should probably be in Redux?
  */
 export default (ComposedComponent: ReactClass<any>) =>
-    connect(null, { replace })(
-        class extends Component {
-            static displayName = "DashboardControls["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
-
-            props: Props;
-            state: 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 = () => {
-                const { location } = this.props;
-
-                let options = parseHashOptions(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 = () => {
-                const { location, replace } = this.props;
-
-                let options = parseHashOptions(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
-
-                // Delete the "add card to dashboard" parameter if it's present because we don't
-                // want to add the card again on page refresh. The `add` parameter is already handled in
-                // DashboardApp before this method is called.
-                delete options.add;
-
-                let hash = stringifyHashOptions(options);
-                hash = hash ? "#" + hash : "";
-
-                if (hash !== location.hash) {
-                    replace({
-                        pathname: location.pathname,
-                        search: 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 => {
-                isNightMode = !!isNightMode;
-                this.setState({ isNightMode });
-            };
-
-            setFullscreen = (isFullscreen, browserFullscreen = true) => {
-                isFullscreen = !!isFullscreen;
-                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) {
-                // NOTE Atte Keinänen 8/10/17: For some reason `document` object isn't present in Jest tests
-                // when _showNav is called for the first time
-                if (window.document) {
-                    const nav = window.document.querySelector(".Nav");
-                    if (show && nav) {
-                        nav.classList.remove("hide");
-                    } else if (!show && nav) {
-                        nav.classList.add("hide");
-                    }
-                }
+  connect(null, { replace })(
+    class extends Component {
+      static displayName = "DashboardControls[" +
+        (ComposedComponent.displayName || ComposedComponent.name) +
+        "]";
+
+      props: Props;
+      state: 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 = () => {
+        const { location } = this.props;
+
+        let options = parseHashOptions(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 = () => {
+        const { location, replace } = this.props;
+
+        let options = parseHashOptions(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
+
+        // Delete the "add card to dashboard" parameter if it's present because we don't
+        // want to add the card again on page refresh. The `add` parameter is already handled in
+        // DashboardApp before this method is called.
+        delete options.add;
+
+        let hash = stringifyHashOptions(options);
+        hash = hash ? "#" + hash : "";
+
+        if (hash !== location.hash) {
+          replace({
+            pathname: location.pathname,
+            search: 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 => {
+        isNightMode = !!isNightMode;
+        this.setState({ isNightMode });
+      };
+
+      setFullscreen = (isFullscreen, browserFullscreen = true) => {
+        isFullscreen = !!isFullscreen;
+        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 });
+        }
+      };
 
-            _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}
-                    />
-                );
-            }
+      _clearRefreshInterval() {
+        if (this._interval != null) {
+          clearInterval(this._interval);
+        }
+      }
+
+      _showNav(show) {
+        // NOTE Atte Keinänen 8/10/17: For some reason `document` object isn't present in Jest tests
+        // when _showNav is called for the first time
+        if (window.document) {
+          const nav = window.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 e74666a5f889ddcee7bf93a85c1626857caf8db8..f8e8358265bd32b110b9d33ef0215d0d3d79ea01 100644
--- a/frontend/src/metabase/dashboard/selectors.js
+++ b/frontend/src/metabase/dashboard/selectors.js
@@ -3,7 +3,7 @@
 import _ from "underscore";
 import { setIn } from "icepick";
 
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 
 import { getMetadata } from "metabase/selectors/metadata";
 
@@ -13,165 +13,231 @@ import { getParameterTargetFieldId } from "metabase/meta/Parameter";
 
 import type { CardId, Card } from "metabase/meta/types/Card";
 import type { DashCardId } from "metabase/meta/types/Dashboard";
-import type { ParameterId, Parameter, ParameterMapping, ParameterMappingUIOption } from "metabase/meta/types/Parameter";
+import type {
+  ParameterId,
+  Parameter,
+  ParameterMapping,
+  ParameterMappingUIOption,
+} from "metabase/meta/types/Parameter";
 
 export type AugmentedParameterMapping = ParameterMapping & {
-    dashcard_id: DashCardId,
-    overlapMax?: number,
-    mappingsWithValues?: number,
-    values: Array<string>
-}
+  dashcard_id: DashCardId,
+  overlapMax?: number,
+  mappingsWithValues?: number,
+  values: Array<string>,
+};
 
 export type MappingsByParameter = {
-    [key: ParameterId]: {
-        [key: DashCardId]: {
-            [key: CardId]: AugmentedParameterMapping
-        }
-    }
-}
-
-export const getDashboardId       = state => state.dashboard.dashboardId;
-export const getIsEditing         = state => state.dashboard.isEditing;
-export const getCards             = state => state.dashboard.cards;
-export const getDashboards        = state => state.dashboard.dashboards;
-export const getDashcards         = state => state.dashboard.dashcards;
-export const getCardData          = state => state.dashboard.dashcardData;
-export const getSlowCards         = state => state.dashboard.slowCards;
-export const getCardIdList        = state => state.dashboard.cardList;
-export const getRevisions         = state => state.dashboard.revisions;
-export const getParameterValues   = state => state.dashboard.parameterValues;
+  [key: ParameterId]: {
+    [key: DashCardId]: {
+      [key: CardId]: AugmentedParameterMapping,
+    },
+  },
+};
+
+export const getDashboardId = state => state.dashboard.dashboardId;
+export const getIsEditing = state => state.dashboard.isEditing;
+export const getCards = state => state.dashboard.cards;
+export const getDashboards = state => state.dashboard.dashboards;
+export const getDashcards = state => state.dashboard.dashcards;
+export const getCardData = state => state.dashboard.dashcardData;
+export const getSlowCards = state => state.dashboard.slowCards;
+export const getCardIdList = state => state.dashboard.cardList;
+export const getRevisions = state => state.dashboard.revisions;
+export const getParameterValues = state => state.dashboard.parameterValues;
 
 export const getDashboard = createSelector(
-    [getDashboardId, getDashboards],
-    (dashboardId, dashboards) => dashboards[dashboardId]
+  [getDashboardId, getDashboards],
+  (dashboardId, dashboards) => dashboards[dashboardId],
 );
 
 export const getDashboardComplete = createSelector(
-    [getDashboard, getDashcards],
-    (dashboard, dashcards) => (dashboard && {
-        ...dashboard,
-        ordered_cards: dashboard.ordered_cards.map(id => dashcards[id]).filter(dc => !dc.isRemoved)
-    })
+  [getDashboard, getDashcards],
+  (dashboard, dashcards) =>
+    dashboard && {
+      ...dashboard,
+      ordered_cards: dashboard.ordered_cards
+        .map(id => dashcards[id])
+        .filter(dc => !dc.isRemoved),
+    },
 );
 
 export const getIsDirty = createSelector(
-    [getDashboard, getDashcards],
-    (dashboard, dashcards) => !!(
-        dashboard && (
-            dashboard.isDirty ||
-            _.some(dashboard.ordered_cards, id => (
-                !(dashcards[id].isAdded && dashcards[id].isRemoved) &&
-                (dashcards[id].isDirty || dashcards[id].isAdded || dashcards[id].isRemoved)
-            ))
-        )
-    )
+  [getDashboard, getDashcards],
+  (dashboard, dashcards) =>
+    !!(
+      dashboard &&
+      (dashboard.isDirty ||
+        _.some(
+          dashboard.ordered_cards,
+          id =>
+            !(dashcards[id].isAdded && dashcards[id].isRemoved) &&
+            (dashcards[id].isDirty ||
+              dashcards[id].isAdded ||
+              dashcards[id].isRemoved),
+        ))
+    ),
 );
 
 export const getCardList = createSelector(
-    [getCardIdList, getCards],
-    (cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id])
+  [getCardIdList, getCards],
+  (cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id]),
 );
 
-export const getEditingParameterId = (state) => state.dashboard.editingParameterId;
+export const getEditingParameterId = state =>
+  state.dashboard.editingParameterId;
 
 export const getEditingParameter = createSelector(
-    [getDashboard, getEditingParameterId],
-    (dashboard, editingParameterId) => editingParameterId != null ? _.findWhere(dashboard.parameters, { id: editingParameterId }) : null
+  [getDashboard, getEditingParameterId],
+  (dashboard, editingParameterId) =>
+    editingParameterId != null
+      ? _.findWhere(dashboard.parameters, { id: editingParameterId })
+      : null,
 );
 
-export const getIsEditingParameter = (state) => state.dashboard.editingParameterId != null;
+export const getIsEditingParameter = state =>
+  state.dashboard.editingParameterId != null;
 
 const getCard = (state, props) => props.card;
 const getDashCard = (state, props) => props.dashcard;
 
 export const getParameterTarget = createSelector(
-    [getEditingParameter, getCard, getDashCard],
-    (parameter, card, dashcard) => {
-        const mapping = _.findWhere(dashcard.parameter_mappings, { card_id: card.id, parameter_id: parameter.id });
-        return mapping && mapping.target;
-    }
+  [getEditingParameter, getCard, getDashCard],
+  (parameter, card, dashcard) => {
+    const mapping = _.findWhere(dashcard.parameter_mappings, {
+      card_id: card.id,
+      parameter_id: parameter.id,
+    });
+    return mapping && mapping.target;
+  },
 );
 
 export const getMappingsByParameter = createSelector(
-    [getMetadata, getDashboardComplete],
-    (metadata, dashboard) => {
-        if (!dashboard) {
-            return {};
-        }
+  [getMetadata, getDashboardComplete],
+  (metadata, dashboard) => {
+    if (!dashboard) {
+      return {};
+    }
 
-        let mappingsByParameter: MappingsByParameter = {};
-        let mappings: Array<AugmentedParameterMapping> = [];
-        let countsByParameter = {};
-        for (const dashcard of dashboard.ordered_cards) {
-            const cards: Array<Card> = [dashcard.card].concat(dashcard.series);
-            for (let mapping: ParameterMapping of (dashcard.parameter_mappings || [])) {
-                const card = _.findWhere(cards, { id: mapping.card_id });
-                const fieldId = card && getParameterTargetFieldId(mapping.target, card.dataset_query);
-                const field = metadata.fields[fieldId];
-                const values = field && field.fieldValues() || [];
-                if (values.length) {
-                    countsByParameter[mapping.parameter_id] = countsByParameter[mapping.parameter_id] || {};
-                }
-                for (const value of values) {
-                    countsByParameter[mapping.parameter_id][value] = (countsByParameter[mapping.parameter_id][value] || 0) + 1
-                }
-
-                let augmentedMapping: AugmentedParameterMapping = {
-                    ...mapping,
-                    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);
-                mappings.push(augmentedMapping);
-            }
-        }
-        let mappingsWithValuesByParameter = {};
-        // update max values overlap for each mapping
-        for (let mapping of mappings) {
-            if (mapping.values && mapping.values.length > 0) {
-                let overlapMax = Math.max(...mapping.values.map(value => countsByParameter[mapping.parameter_id][value]))
-                mappingsByParameter = setIn(mappingsByParameter, [mapping.parameter_id, mapping.dashcard_id, mapping.card_id, "overlapMax"], overlapMax);
-                mappingsWithValuesByParameter[mapping.parameter_id] = (mappingsWithValuesByParameter[mapping.parameter_id] || 0) + 1;
-            }
+    let mappingsByParameter: MappingsByParameter = {};
+    let mappings: Array<AugmentedParameterMapping> = [];
+    let countsByParameter = {};
+    for (const dashcard of dashboard.ordered_cards) {
+      const cards: Array<Card> = [dashcard.card].concat(dashcard.series);
+      for (let mapping: ParameterMapping of dashcard.parameter_mappings || []) {
+        const card = _.findWhere(cards, { id: mapping.card_id });
+        const fieldId =
+          card && getParameterTargetFieldId(mapping.target, card.dataset_query);
+        const field = metadata.fields[fieldId];
+        const values = (field && field.fieldValues()) || [];
+        if (values.length) {
+          countsByParameter[mapping.parameter_id] =
+            countsByParameter[mapping.parameter_id] || {};
         }
-        // update count of mappings with values
-        for (let mapping of mappings) {
-            mappingsByParameter = setIn(mappingsByParameter, [mapping.parameter_id, mapping.dashcard_id, mapping.card_id, "mappingsWithValues"], mappingsWithValuesByParameter[mapping.parameter_id] || 0);
+        for (const value of values) {
+          countsByParameter[mapping.parameter_id][value] =
+            (countsByParameter[mapping.parameter_id][value] || 0) + 1;
         }
 
-        return mappingsByParameter;
+        let augmentedMapping: AugmentedParameterMapping = {
+          ...mapping,
+          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,
+        );
+        mappings.push(augmentedMapping);
+      }
     }
+    let mappingsWithValuesByParameter = {};
+    // update max values overlap for each mapping
+    for (let mapping of mappings) {
+      if (mapping.values && mapping.values.length > 0) {
+        let overlapMax = Math.max(
+          ...mapping.values.map(
+            value => countsByParameter[mapping.parameter_id][value],
+          ),
+        );
+        mappingsByParameter = setIn(
+          mappingsByParameter,
+          [
+            mapping.parameter_id,
+            mapping.dashcard_id,
+            mapping.card_id,
+            "overlapMax",
+          ],
+          overlapMax,
+        );
+        mappingsWithValuesByParameter[mapping.parameter_id] =
+          (mappingsWithValuesByParameter[mapping.parameter_id] || 0) + 1;
+      }
+    }
+    // update count of mappings with values
+    for (let mapping of mappings) {
+      mappingsByParameter = setIn(
+        mappingsByParameter,
+        [
+          mapping.parameter_id,
+          mapping.dashcard_id,
+          mapping.card_id,
+          "mappingsWithValues",
+        ],
+        mappingsWithValuesByParameter[mapping.parameter_id] || 0,
+      );
+    }
+
+    return mappingsByParameter;
+  },
 );
 
 /** 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()
-                .filter(fieldId => fieldId != null)
-                .value();
-            return {
-                ...parameter,
-                field_ids: fieldIds
-            }
-        })
-)
+  [getMetadata, getDashboard, getMappingsByParameter],
+  (metadata, 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()
+        .filter(fieldId => fieldId != null)
+        .value();
+      const fieldIdsWithFKResolved = _.chain(fieldIds)
+        .map(id => metadata.fields[id])
+        .filter(f => f)
+        .map(f => (f.target || f).id)
+        .uniq()
+        .value();
+      return {
+        ...parameter,
+        field_ids: fieldIds,
+        // if there's a single uniqe field (accounting for FKs) then
+        // include it as the one true field_id
+        field_id:
+          fieldIdsWithFKResolved.length === 1
+            ? fieldIdsWithFKResolved[0]
+            : null,
+      };
+    }),
+);
 
 export const makeGetParameterMappingOptions = () => {
-    const getParameterMappingOptions = createSelector(
-        [getMetadata, getEditingParameter, getCard],
-        (metadata, parameter: Parameter, card: Card): Array<ParameterMappingUIOption> => {
-            return Dashboard.getParameterMappingOptions(metadata, parameter, card);
-        }
-    );
-    return getParameterMappingOptions;
-}
+  const getParameterMappingOptions = createSelector(
+    [getMetadata, getEditingParameter, getCard],
+    (
+      metadata,
+      parameter: Parameter,
+      card: Card,
+    ): Array<ParameterMappingUIOption> => {
+      return Dashboard.getParameterMappingOptions(metadata, parameter, card);
+    },
+  );
+  return getParameterMappingOptions;
+};
diff --git a/frontend/src/metabase/dashboards/components/DashboardList.jsx b/frontend/src/metabase/dashboards/components/DashboardList.jsx
index 2f5c3f042131038593548ed192929d61e391b0fa..cf5c729e9ca8d6871dd8c31723afbc06802f36f0 100644
--- a/frontend/src/metabase/dashboards/components/DashboardList.jsx
+++ b/frontend/src/metabase/dashboards/components/DashboardList.jsx
@@ -1,173 +1,192 @@
 /* @flow */
 
-import React, {Component} from "react";
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import {Link} from "react-router";
+import { Link } from "react-router";
 import cx from "classnames";
 import moment from "moment";
-import { t } from 'c-3po'
+import { t } from "c-3po";
 
 import { withBackground } from "metabase/hoc/Background";
 
 import * as Urls from "metabase/lib/urls";
 
-import type {Dashboard} from "metabase/meta/types/Dashboard";
+import type { Dashboard } from "metabase/meta/types/Dashboard";
 import Icon from "metabase/components/Icon";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 import Tooltip from "metabase/components/Tooltip";
 
 type DashboardListItemProps = {
-    dashboard: Dashboard,
-    setFavorited: (dashId: number, favorited: boolean) => void,
-    setArchived: (dashId: number, archived: boolean) => void
-}
+  dashboard: Dashboard,
+  setFavorited: (dashId: number, favorited: boolean) => void,
+  setArchived: (dashId: number, archived: boolean) => void,
+};
 
 export class DashboardListItem extends Component {
-    props: DashboardListItemProps
-
-    state = {
-        hover: false,
-        fadingOut: false
-    }
-
-    render() {
-        const {dashboard, setFavorited, setArchived} = this.props
-        const {hover, fadingOut} = this.state
-
-        const {id, name, created_at, archived, favorite} = dashboard
-
-        const archivalButton =
-            <Tooltip tooltip={archived ? t`Unarchive` : t`Archive`}>
-                <Icon
-                    className="flex cursor-pointer text-light-blue text-brand-hover ml2 archival-button"
-                    name={archived ? "unarchive" : "archive"}
-                    size={21}
-                    onClick={(e) => {
-                        e.preventDefault();
-
-                        // Let the 0.2s transition finish before the archival API call (`setArchived` action)
-                        this.setState({fadingOut: true})
-                        setTimeout(() => setArchived(id, !archived, true), 300);
-                    } }
-                />
-            </Tooltip>
-
-        const favoritingButton =
-            <Tooltip tooltip={favorite ? t`Unfavorite` : t`Favorite`}>
-                <Icon
-                    className={cx(
-                        "flex cursor-pointer ml2 favoriting-button",
-                        {"text-light-blue text-brand-hover": !favorite},
-                        {"text-gold": favorite}
-                    )}
-                    name={favorite ? "star" : "staroutline"}
-                    size={22}
-                    onClick={(e) => {
-                        e.preventDefault();
-                        setFavorited(id, !favorite)
-                    } }
-                />
-            </Tooltip>
-
-        const dashboardIcon =
-            <Icon name="dashboard"
-                  className={"ml2 text-grey-1"}
-                  size={25}/>
-
-        return (
-            <li className="Grid-cell shrink-below-content-size" style={{maxWidth: "550px"}}>
-                <Link to={Urls.dashboard(id)}
-                      data-metabase-event={"Navbar;Dashboards;Open Dashboard;" + id}
-                      className={"flex align-center border-box p2 rounded no-decoration transition-background bg-white transition-all relative"}
-                      style={{
-                          border: "1px solid rgba(220,225,228,0.50)",
-                          boxShadow: hover ? "0 3px 8px 0 rgba(220,220,220,0.50)" : "0 1px 3px 0 rgba(220,220,220,0.50)",
-                          height: "70px",
-                          opacity: fadingOut ? 0 : 1,
-                          top: fadingOut ? "10px" : 0
-                      }}
-                      onMouseOver={() => this.setState({hover: true})}
-                      onMouseLeave={() => this.setState({hover: false})}>
-                    <div className={"flex-full shrink-below-content-size"}>
-                        <div className="flex align-center">
-                            <div className={"flex-full shrink-below-content-size"}>
-                                <h3
-                                    className={cx(
-                                        "text-ellipsis text-nowrap overflow-hidden text-bold transition-all",
-                                        {"text-slate": !hover},
-                                        {"text-brand": hover}
-                                    )}
-                                    style={{marginBottom: "0.3em"}}
-                                >
-                                    <Ellipsified>{name}</Ellipsified>
-                                </h3>
-                                <div
-                                    className={"text-smaller text-uppercase text-bold text-grey-3"}>
-                                    {/* NOTE: Could these time formats be centrally stored somewhere? */}
-                                    {moment(created_at).format('MMM D, YYYY')}
-                                </div>
-                            </div>
-
-                            {/* Hidden flexbox item which makes sure that long titles are ellipsified correctly */}
-                            <div className="flex align-center hidden">
-                                { hover && archivalButton }
-                                { (favorite || hover) && favoritingButton }
-                                { !hover && !favorite && dashboardIcon }
-                            </div>
-
-                            {/* Non-hover dashboard icon, only rendered if the dashboard isn't favorited */}
-                            {!favorite &&
-                            <div className="flex align-center absolute right transition-all"
-                                 style={{right: "16px", opacity: hover ? 0 : 1}}>
-                                { dashboardIcon }
-                            </div>
-                            }
-
-                            {/* Favorite icon, only rendered if the dashboard is favorited */}
-                            {/* Visible also in the hover state (under other button) because hiding leads to an ugly animation */}
-                            {favorite &&
-                            <div className="flex align-center absolute right transition-all"
-                                 style={{right: "16px", opacity: 1}}>
-                                { favoritingButton }
-                            </div>
-                            }
-
-                            {/* Hover state buttons, both archival and favoriting */}
-                            <div className="flex align-center absolute right transition-all"
-                                 style={{right: "16px", opacity: hover ? 1 : 0}}>
-                                { archivalButton }
-                                { favoritingButton }
-                            </div>
-
-                        </div>
-                    </div>
-
-                </Link>
-            </li>
-        )
-    }
-
+  props: DashboardListItemProps;
+
+  state = {
+    hover: false,
+    fadingOut: false,
+  };
+
+  render() {
+    const { dashboard, setFavorited, setArchived } = this.props;
+    const { hover, fadingOut } = this.state;
+
+    const { id, name, created_at, archived, favorite } = dashboard;
+
+    const archivalButton = (
+      <Tooltip tooltip={archived ? t`Unarchive` : t`Archive`}>
+        <Icon
+          className="flex cursor-pointer text-light-blue text-brand-hover ml2 archival-button"
+          name={archived ? "unarchive" : "archive"}
+          size={21}
+          onClick={e => {
+            e.preventDefault();
+
+            // Let the 0.2s transition finish before the archival API call (`setArchived` action)
+            this.setState({ fadingOut: true });
+            setTimeout(() => setArchived(id, !archived, true), 300);
+          }}
+        />
+      </Tooltip>
+    );
+
+    const favoritingButton = (
+      <Tooltip tooltip={favorite ? t`Unfavorite` : t`Favorite`}>
+        <Icon
+          className={cx(
+            "flex cursor-pointer ml2 favoriting-button",
+            { "text-light-blue text-brand-hover": !favorite },
+            { "text-gold": favorite },
+          )}
+          name={favorite ? "star" : "staroutline"}
+          size={22}
+          onClick={e => {
+            e.preventDefault();
+            setFavorited(id, !favorite);
+          }}
+        />
+      </Tooltip>
+    );
+
+    const dashboardIcon = (
+      <Icon name="dashboard" className={"ml2 text-grey-1"} size={25} />
+    );
+
+    return (
+      <li
+        className="Grid-cell shrink-below-content-size"
+        style={{ maxWidth: "550px" }}
+      >
+        <Link
+          to={Urls.dashboard(id)}
+          data-metabase-event={"Navbar;Dashboards;Open Dashboard;" + id}
+          className={
+            "flex align-center border-box p2 rounded no-decoration transition-background bg-white transition-all relative"
+          }
+          style={{
+            border: "1px solid rgba(220,225,228,0.50)",
+            boxShadow: hover
+              ? "0 3px 8px 0 rgba(220,220,220,0.50)"
+              : "0 1px 3px 0 rgba(220,220,220,0.50)",
+            height: "70px",
+            opacity: fadingOut ? 0 : 1,
+            top: fadingOut ? "10px" : 0,
+          }}
+          onMouseOver={() => this.setState({ hover: true })}
+          onMouseLeave={() => this.setState({ hover: false })}
+        >
+          <div className={"flex-full shrink-below-content-size"}>
+            <div className="flex align-center">
+              <div className={"flex-full shrink-below-content-size"}>
+                <h3
+                  className={cx(
+                    "text-ellipsis text-nowrap overflow-hidden text-bold transition-all",
+                    { "text-slate": !hover },
+                    { "text-brand": hover },
+                  )}
+                  style={{ marginBottom: "0.3em" }}
+                >
+                  <Ellipsified>{name}</Ellipsified>
+                </h3>
+                <div
+                  className={
+                    "text-smaller text-uppercase text-bold text-grey-3"
+                  }
+                >
+                  {/* NOTE: Could these time formats be centrally stored somewhere? */}
+                  {moment(created_at).format("MMM D, YYYY")}
+                </div>
+              </div>
+
+              {/* Hidden flexbox item which makes sure that long titles are ellipsified correctly */}
+              <div className="flex align-center hidden">
+                {hover && archivalButton}
+                {(favorite || hover) && favoritingButton}
+                {!hover && !favorite && dashboardIcon}
+              </div>
+
+              {/* Non-hover dashboard icon, only rendered if the dashboard isn't favorited */}
+              {!favorite && (
+                <div
+                  className="flex align-center absolute right transition-all"
+                  style={{ right: "16px", opacity: hover ? 0 : 1 }}
+                >
+                  {dashboardIcon}
+                </div>
+              )}
+
+              {/* Favorite icon, only rendered if the dashboard is favorited */}
+              {/* Visible also in the hover state (under other button) because hiding leads to an ugly animation */}
+              {favorite && (
+                <div
+                  className="flex align-center absolute right transition-all"
+                  style={{ right: "16px", opacity: 1 }}
+                >
+                  {favoritingButton}
+                </div>
+              )}
+
+              {/* Hover state buttons, both archival and favoriting */}
+              <div
+                className="flex align-center absolute right transition-all"
+                style={{ right: "16px", opacity: hover ? 1 : 0 }}
+              >
+                {archivalButton}
+                {favoritingButton}
+              </div>
+            </div>
+          </div>
+        </Link>
+      </li>
+    );
+  }
 }
 
 class DashboardList extends Component {
-    static propTypes = {
-        dashboards: PropTypes.array.isRequired
-    };
-
-    render() {
-        const {dashboards, isArchivePage, setFavorited, setArchived} = 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}
-                                       setFavorited={setFavorited}
-                                       setArchived={setArchived}
-                                       disableLink={isArchivePage}/>
-                )}
-            </ol>
-        );
-    }
+  static propTypes = {
+    dashboards: PropTypes.array.isRequired,
+  };
+
+  render() {
+    const { dashboards, isArchivePage, setFavorited, setArchived } = 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}
+            setFavorited={setFavorited}
+            setArchived={setArchived}
+            disableLink={isArchivePage}
+          />
+        ))}
+      </ol>
+    );
+  }
 }
 
-export default withBackground('bg-slate-extra-light')(DashboardList)
+export default withBackground("bg-slate-extra-light")(DashboardList);
diff --git a/frontend/src/metabase/dashboards/containers/Dashboards.jsx b/frontend/src/metabase/dashboards/containers/Dashboards.jsx
index 34faba92b001294c25d8d0b9ee039203938ec248..9e398cd85bfa8062e81a891e954eaa72d61a45e5 100644
--- a/frontend/src/metabase/dashboards/containers/Dashboards.jsx
+++ b/frontend/src/metabase/dashboards/containers/Dashboards.jsx
@@ -1,13 +1,13 @@
 /* @flow */
 
-import React, {Component} from 'react';
-import {connect} from "react-redux";
-import {Link} from "react-router";
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import { Link } from "react-router";
 import cx from "classnames";
-import _ from "underscore"
+import _ from "underscore";
 import { t, jt } from "c-3po";
 
-import type {Dashboard} from "metabase/meta/types/Dashboard";
+import type { Dashboard } from "metabase/meta/types/Dashboard";
 
 import DashboardList from "../components/DashboardList";
 
@@ -19,209 +19,229 @@ import Icon from "metabase/components/Icon.jsx";
 import SearchHeader from "metabase/components/SearchHeader";
 import EmptyState from "metabase/components/EmptyState";
 import ListFilterWidget from "metabase/components/ListFilterWidget";
-import type {ListFilterWidgetItem} from "metabase/components/ListFilterWidget";
+import type { ListFilterWidgetItem } from "metabase/components/ListFilterWidget";
 
-import {caseInsensitiveSearch} from "metabase/lib/string"
+import { caseInsensitiveSearch } from "metabase/lib/string";
 
-import type {SetFavoritedAction, SetArchivedAction} from "../dashboards";
-import type {User} from "metabase/meta/types/User"
+import type { SetFavoritedAction, SetArchivedAction } from "../dashboards";
+import type { User } from "metabase/meta/types/User";
 import * as dashboardsActions from "../dashboards";
-import {getDashboardListing} from "../selectors";
-import {getUser} from "metabase/selectors/user";
+import { getDashboardListing } from "../selectors";
+import { getUser } from "metabase/selectors/user";
 
 const mapStateToProps = (state, props) => ({
-    dashboards: getDashboardListing(state),
-    user: getUser(state)
+  dashboards: getDashboardListing(state),
+  user: getUser(state),
 });
 
 const mapDispatchToProps = dashboardsActions;
 
-const SECTION_ID_ALL = 'all';
-const SECTION_ID_MINE = 'mine';
-const SECTION_ID_FAVORITES = 'fav';
+const SECTION_ID_ALL = "all";
+const SECTION_ID_MINE = "mine";
+const SECTION_ID_FAVORITES = "fav";
 
 const SECTIONS: ListFilterWidgetItem[] = [
-    {
-        id: SECTION_ID_ALL,
-        name: t`All dashboards`,
-        icon: 'dashboard',
-        // empty: 'No questions have been saved yet.',
-    },
-    {
-        id: SECTION_ID_FAVORITES,
-        name: t`Favorites`,
-        icon: 'star',
-        // empty: 'You haven\'t favorited any questions yet.',
-    },
-    {
-        id: SECTION_ID_MINE,
-        name: t`Saved by me`,
-        icon: 'mine',
-        // empty:  'You haven\'t saved any questions yet.'
-    },
+  {
+    id: SECTION_ID_ALL,
+    name: t`All dashboards`,
+    icon: "dashboard",
+    // empty: 'No questions have been saved yet.',
+  },
+  {
+    id: SECTION_ID_FAVORITES,
+    name: t`Favorites`,
+    icon: "star",
+    // empty: 'You haven\'t favorited any questions yet.',
+  },
+  {
+    id: SECTION_ID_MINE,
+    name: t`Saved by me`,
+    icon: "mine",
+    // empty:  'You haven\'t saved any questions yet.'
+  },
 ];
 
 export class Dashboards extends Component {
-    props: {
-        dashboards: Dashboard[],
-        createDashboard: (Dashboard) => any,
-        fetchDashboards: () => void,
-        setFavorited: SetFavoritedAction,
-        setArchived: SetArchivedAction,
-        user: User
-    };
-
-    state = {
-        modalOpen: false,
-        searchText: "",
-        section: SECTIONS[0]
+  props: {
+    dashboards: Dashboard[],
+    createDashboard: Dashboard => any,
+    fetchDashboards: () => void,
+    setFavorited: SetFavoritedAction,
+    setArchived: SetArchivedAction,
+    user: User,
+  };
+
+  state = {
+    modalOpen: false,
+    searchText: "",
+    section: SECTIONS[0],
+  };
+
+  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);
     }
-
-    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>
-        );
-    }
-
-    searchTextFilter = (searchText: string) =>
-        ({name, description}: Dashboard) =>
-            (caseInsensitiveSearch(name, searchText) || (description && caseInsensitiveSearch(description, searchText)))
-
-    sectionFilter = (section: ListFilterWidgetItem) =>
-        ({creator_id, favorite}: Dashboard) =>
-        (section.id === SECTION_ID_ALL) ||
-        (section.id === SECTION_ID_MINE && creator_id === this.props.user.id) ||
-        (section.id === SECTION_ID_FAVORITES && favorite === true)
-
-    getFilteredDashboards = () => {
-        const {searchText, section} = this.state;
-        const {dashboards} = this.props;
-        const noOpFilter = _.constant(true)
-
-        return _.chain(dashboards)
-            .filter(searchText != "" ? this.searchTextFilter(searchText) : noOpFilter)
-            .filter(this.sectionFilter(section))
-            .value()
-            .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
-    }
-
-    updateSection = (section: ListFilterWidgetItem) => {
-        this.setState({section});
-    }
-
-    render() {
-        let {modalOpen, searchText, section} = this.state;
-
-        const isLoading = this.props.dashboards === null
-        const noDashboardsCreated = this.props.dashboards && this.props.dashboards.length === 0
-        const filteredDashboards = isLoading ? [] : this.getFilteredDashboards();
-        const noResultsFound = filteredDashboards.length === 0;
-
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading}
-                className={cx("relative px4 full-height", {"flex flex-full flex-column": noDashboardsCreated})}
-                noBackground
-            >
-                { modalOpen ? this.renderCreateDashboardModal() : null }
-                <div className="flex align-center pt4 pb1">
-                    <TitleAndDescription title={t`Dashboards`}/>
-
-                    <div className="flex-align-right cursor-pointer text-grey-5">
-                        <Link to="/dashboards/archive">
-                            <Icon name="viewArchive"
-                                  className="mr2 text-brand-hover"
-                                  tooltip={t`View the archive`}
-                                  size={20}/>
-                        </Link>
-
-                        {!noDashboardsCreated &&
-                        <Icon name="add"
-                              className="text-brand-hover"
-                              tooltip={t`Add new dashboard`}
-                              size={20}
-                              onClick={this.showCreateDashboard}/>
-                        }
-                    </div>
-                </div>
-                { noDashboardsCreated ?
-                    <div className="mt2 flex-full flex align-center justify-center">
-                        <EmptyState
-                            message={<span>{jt`Put the charts and graphs you look at ${<br/>}frequently in a single, handy place.`}</span>}
-                            image="/app/img/dashboard_illustration"
-                            action={t`Create a dashboard`}
-                            onActionClick={this.showCreateDashboard}
-                            className="mt2"
-                            imageClassName="mln2"
-                        />
-                    </div>
-                    : <div>
-                        <div className="flex-full flex align-center pb1">
-                            <SearchHeader
-                                searchText={searchText}
-                                setSearchText={(text) => this.setState({searchText: text})}
-                            />
-                            <div className="flex-align-right">
-                                <ListFilterWidget
-                                    items={SECTIONS.filter(item => item.id !== "archived")}
-                                    activeItem={section}
-                                    onChange={this.updateSection}
-                                />
-                            </div>
-                        </div>
-                        { noResultsFound ?
-                            <div className="flex justify-center">
-                                <EmptyState
-                                    message={
-                                        <div className="mt4">
-                                            <h3 className="text-grey-5">{t`No results found`}</h3>
-                                            <p className="text-grey-4">{t`Try adjusting your filter to find what you’re
+  }
+
+  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>
+    );
+  }
+
+  searchTextFilter = (searchText: string) => ({
+    name,
+    description,
+  }: Dashboard) =>
+    caseInsensitiveSearch(name, searchText) ||
+    (description && caseInsensitiveSearch(description, searchText));
+
+  sectionFilter = (section: ListFilterWidgetItem) => ({
+    creator_id,
+    favorite,
+  }: Dashboard) =>
+    section.id === SECTION_ID_ALL ||
+    (section.id === SECTION_ID_MINE && creator_id === this.props.user.id) ||
+    (section.id === SECTION_ID_FAVORITES && favorite === true);
+
+  getFilteredDashboards = () => {
+    const { searchText, section } = this.state;
+    const { dashboards } = this.props;
+    const noOpFilter = _.constant(true);
+
+    return _.chain(dashboards)
+      .filter(searchText != "" ? this.searchTextFilter(searchText) : noOpFilter)
+      .filter(this.sectionFilter(section))
+      .value()
+      .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+  };
+
+  updateSection = (section: ListFilterWidgetItem) => {
+    this.setState({ section });
+  };
+
+  render() {
+    let { modalOpen, searchText, section } = this.state;
+
+    const isLoading = this.props.dashboards === null;
+    const noDashboardsCreated =
+      this.props.dashboards && this.props.dashboards.length === 0;
+    const filteredDashboards = isLoading ? [] : this.getFilteredDashboards();
+    const noResultsFound = filteredDashboards.length === 0;
+
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading}
+        className={cx("relative px4 full-height", {
+          "flex flex-full flex-column": noDashboardsCreated,
+        })}
+        noBackground
+      >
+        {modalOpen ? this.renderCreateDashboardModal() : null}
+        <div className="flex align-center pt4 pb1">
+          <TitleAndDescription title={t`Dashboards`} />
+
+          <div className="flex-align-right cursor-pointer text-grey-5">
+            <Link to="/dashboards/archive">
+              <Icon
+                name="viewArchive"
+                className="mr2 text-brand-hover"
+                tooltip={t`View the archive`}
+                size={20}
+              />
+            </Link>
+
+            {!noDashboardsCreated && (
+              <Icon
+                name="add"
+                className="text-brand-hover"
+                tooltip={t`Add new dashboard`}
+                size={20}
+                onClick={this.showCreateDashboard}
+              />
+            )}
+          </div>
+        </div>
+        {noDashboardsCreated ? (
+          <div className="mt2 flex-full flex align-center justify-center">
+            <EmptyState
+              message={
+                <span>{jt`Put the charts and graphs you look at ${(
+                  <br />
+                )}frequently in a single, handy place.`}</span>
+              }
+              image="/app/img/dashboard_illustration"
+              action={t`Create a dashboard`}
+              onActionClick={this.showCreateDashboard}
+              className="mt2"
+              imageClassName="mln2"
+            />
+          </div>
+        ) : (
+          <div>
+            <div className="flex-full flex align-center pb1">
+              <SearchHeader
+                searchText={searchText}
+                setSearchText={text => this.setState({ searchText: text })}
+              />
+              <div className="flex-align-right">
+                <ListFilterWidget
+                  items={SECTIONS.filter(item => item.id !== "archived")}
+                  activeItem={section}
+                  onChange={this.updateSection}
+                />
+              </div>
+            </div>
+            {noResultsFound ? (
+              <div className="flex justify-center">
+                <EmptyState
+                  message={
+                    <div className="mt4">
+                      <h3 className="text-grey-5">{t`No results found`}</h3>
+                      <p className="text-grey-4">{t`Try adjusting your filter to find what you’re
                                                 looking for.`}</p>
-                                        </div>
-                                    }
-                                    image="/app/img/empty_dashboard"
-                                    imageHeight="210px"
-                                    action={t`Create a dashboard`}
-                                    imageClassName="mln2"
-                                    smallDescription
-                                />
-                            </div>
-                            : <DashboardList dashboards={filteredDashboards}
-                                             setFavorited={this.props.setFavorited}
-                                             setArchived={this.props.setArchived}/>
-                        }
                     </div>
-
-                }
-            </LoadingAndErrorWrapper>
-        );
-    }
+                  }
+                  image="/app/img/empty_dashboard"
+                  imageHeight="210px"
+                  action={t`Create a dashboard`}
+                  imageClassName="mln2"
+                  smallDescription
+                />
+              </div>
+            ) : (
+              <DashboardList
+                dashboards={filteredDashboards}
+                setFavorited={this.props.setFavorited}
+                setArchived={this.props.setArchived}
+              />
+            )}
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(Dashboards)
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboards);
diff --git a/frontend/src/metabase/dashboards/containers/DashboardsArchive.jsx b/frontend/src/metabase/dashboards/containers/DashboardsArchive.jsx
index 9b8a042603d4698e41c5bc4f16f53b497c77bfd8..ce4aa7fe3ffbff5d276dbef114a7ce27d31ade16 100644
--- a/frontend/src/metabase/dashboards/containers/DashboardsArchive.jsx
+++ b/frontend/src/metabase/dashboards/containers/DashboardsArchive.jsx
@@ -1,12 +1,12 @@
 /* @flow */
 
-import React, {Component} from 'react';
-import {connect} from "react-redux";
+import React, { Component } from "react";
+import { connect } from "react-redux";
 import cx from "classnames";
-import _ from "underscore"
+import _ from "underscore";
 import { t, jt } from "c-3po";
 
-import type {Dashboard} from "metabase/meta/types/Dashboard";
+import type { Dashboard } from "metabase/meta/types/Dashboard";
 
 import HeaderWithBack from "../../components/HeaderWithBack";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
@@ -14,117 +14,137 @@ import SearchHeader from "metabase/components/SearchHeader";
 import EmptyState from "metabase/components/EmptyState";
 import ArchivedItem from "metabase/components/ArchivedItem";
 
-import {caseInsensitiveSearch} from "metabase/lib/string"
+import { caseInsensitiveSearch } from "metabase/lib/string";
 
-import type {SetArchivedAction} from "../dashboards";
-import {fetchArchivedDashboards, setArchived} from "../dashboards";
-import {getArchivedDashboards} from "../selectors";
-import {getUserIsAdmin} from "metabase/selectors/user";
+import type { SetArchivedAction } from "../dashboards";
+import { fetchArchivedDashboards, setArchived } from "../dashboards";
+import { getArchivedDashboards } from "../selectors";
+import { getUserIsAdmin } from "metabase/selectors/user";
 
 const mapStateToProps = (state, props) => ({
-    dashboards: getArchivedDashboards(state),
-    isAdmin: getUserIsAdmin(state, props)
+  dashboards: getArchivedDashboards(state),
+  isAdmin: getUserIsAdmin(state, props),
 });
 
-const mapDispatchToProps = {fetchArchivedDashboards, setArchived};
+const mapDispatchToProps = { fetchArchivedDashboards, setArchived };
 
 export class Dashboards extends Component {
-    props: {
-        dashboards: Dashboard[],
-        fetchArchivedDashboards: () => void,
-        setArchived: SetArchivedAction,
-        isAdmin: boolean
-    };
-
-    state = {
-        searchText: "",
-    }
-
-    componentWillMount() {
-        this.props.fetchArchivedDashboards();
-    }
-
-    searchTextFilter = (searchText: string) =>
-        ({name, description}: Dashboard) =>
-            (caseInsensitiveSearch(name, searchText) || (description && caseInsensitiveSearch(description, searchText)))
-
-    getFilteredDashboards = () => {
-        const {searchText} = this.state;
-        const {dashboards} = this.props;
-        const noOpFilter = _.constant(true)
-
-        return _.chain(dashboards)
-            .filter(searchText != "" ? this.searchTextFilter(searchText) : noOpFilter)
-            .sortBy((dash) => dash.name.toLowerCase())
-            .value()
-            .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()))
-    }
-
-    render() {
-        let {searchText} = this.state;
-
-        const isLoading = this.props.dashboards === null
-        const noDashboardsArchived = this.props.dashboards && this.props.dashboards.length === 0
-        const filteredDashboards = isLoading ? [] : this.getFilteredDashboards();
-        const noSearchResults = searchText !== "" && filteredDashboards.length === 0;
-
-        const headerWithBackContainer =
-            <div className="flex align-center pt4 pb1">
-                <HeaderWithBack name={t`Archive`}/>
-            </div>
-
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading}
-                className={cx("relative mx4", {"flex-full ": noDashboardsArchived})}
+  props: {
+    dashboards: Dashboard[],
+    fetchArchivedDashboards: () => void,
+    setArchived: SetArchivedAction,
+    isAdmin: boolean,
+  };
+
+  state = {
+    searchText: "",
+  };
+
+  componentWillMount() {
+    this.props.fetchArchivedDashboards();
+  }
+
+  searchTextFilter = (searchText: string) => ({
+    name,
+    description,
+  }: Dashboard) =>
+    caseInsensitiveSearch(name, searchText) ||
+    (description && caseInsensitiveSearch(description, searchText));
+
+  getFilteredDashboards = () => {
+    const { searchText } = this.state;
+    const { dashboards } = this.props;
+    const noOpFilter = _.constant(true);
+
+    return _.chain(dashboards)
+      .filter(searchText != "" ? this.searchTextFilter(searchText) : noOpFilter)
+      .sortBy(dash => dash.name.toLowerCase())
+      .value()
+      .sort((a, b) => a.name.toLowerCase().localeCompare(b.name.toLowerCase()));
+  };
+
+  render() {
+    let { searchText } = this.state;
+
+    const isLoading = this.props.dashboards === null;
+    const noDashboardsArchived =
+      this.props.dashboards && this.props.dashboards.length === 0;
+    const filteredDashboards = isLoading ? [] : this.getFilteredDashboards();
+    const noSearchResults =
+      searchText !== "" && filteredDashboards.length === 0;
+
+    const headerWithBackContainer = (
+      <div className="flex align-center pt4 pb1">
+        <HeaderWithBack name={t`Archive`} />
+      </div>
+    );
+
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading}
+        className={cx("relative mx4", { "flex-full ": noDashboardsArchived })}
+      >
+        {noDashboardsArchived ? (
+          <div>
+            {headerWithBackContainer}
+            <div
+              className="full flex justify-center"
+              style={{ marginTop: "75px" }}
             >
-                { noDashboardsArchived ?
-                    <div>
-                        {headerWithBackContainer}
-                        <div className="full flex justify-center" style={{marginTop: "75px"}}>
-                            <EmptyState
-                                message={<span>{jt`No dashboards have been ${<br />} archived yet`}</span>}
-                                icon="viewArchive"
-                            />
-                        </div>
-                    </div>
-                    : <div>
-                        {headerWithBackContainer}
-                        <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">{t`No results found`}</h3>
-                                            <p className="text-grey-4">{t`Try adjusting your filter to find what you’re looking for.`}</p>
-                                        </div>
-                                    }
-                                    image="/app/img/empty_dashboard"
-                                    imageClassName="mln2"
-                                    smallDescription
-                                />
-                            </div>
-                            : <div>
-                                { filteredDashboards.map((dashboard) =>
-                                    <ArchivedItem key={dashboard.id} name={dashboard.name} type="dashboard"
-                                                  icon="dashboard"
-                                                  isAdmin={true} onUnarchive={async () => {
-                                        await this.props.setArchived(dashboard.id, false);
-                                    }}/>
-                                )}
-                            </div>
-                        }
-                    </div>
+              <EmptyState
+                message={
+                  <span>{jt`No dashboards have been ${(
+                    <br />
+                  )} archived yet`}</span>
                 }
-            </LoadingAndErrorWrapper>
-        );
-    }
+                icon="viewArchive"
+              />
+            </div>
+          </div>
+        ) : (
+          <div>
+            {headerWithBackContainer}
+            <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">{t`No results found`}</h3>
+                      <p className="text-grey-4">{t`Try adjusting your filter to find what you’re looking for.`}</p>
+                    </div>
+                  }
+                  image="/app/img/empty_dashboard"
+                  imageClassName="mln2"
+                  smallDescription
+                />
+              </div>
+            ) : (
+              <div>
+                {filteredDashboards.map(dashboard => (
+                  <ArchivedItem
+                    key={dashboard.id}
+                    name={dashboard.name}
+                    type="dashboard"
+                    icon="dashboard"
+                    isAdmin={true}
+                    onUnarchive={async () => {
+                      await this.props.setArchived(dashboard.id, false);
+                    }}
+                  />
+                ))}
+              </div>
+            )}
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(Dashboards)
+export default connect(mapStateToProps, mapDispatchToProps)(Dashboards);
diff --git a/frontend/src/metabase/dashboards/dashboards.js b/frontend/src/metabase/dashboards/dashboards.js
index 4f24d54113ba72c9e108ead7ab8067799eeac6c5..c7b4e80542f964efca7505eb17124195d02b7cb7 100644
--- a/frontend/src/metabase/dashboards/dashboards.js
+++ b/frontend/src/metabase/dashboards/dashboards.js
@@ -1,167 +1,215 @@
 /* @flow weak */
 
-import { handleActions, combineReducers, createThunkAction } from "metabase/lib/redux";
+import {
+  handleActions,
+  combineReducers,
+  createThunkAction,
+} from "metabase/lib/redux";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import * as Urls from "metabase/lib/urls";
 import { DashboardApi } from "metabase/services";
 import { addUndo, createUndo } from "metabase/redux/undo";
 
 import { push } from "react-router-redux";
-import moment from 'moment';
+import moment from "moment";
 import React from "react";
 
 import type { Dashboard } from "metabase/meta/types/Dashboard";
 
 export const FETCH_DASHBOARDS = "metabase/dashboards/FETCH_DASHBOARDS";
-export const FETCH_ARCHIVE    = "metabase/dashboards/FETCH_ARCHIVE";
+export const FETCH_ARCHIVE = "metabase/dashboards/FETCH_ARCHIVE";
 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 SAVE_DASHBOARD = "metabase/dashboards/SAVE_DASHBOARD";
 export const UPDATE_DASHBOARD = "metabase/dashboards/UPDATE_DASHBOARD";
-export const SET_FAVORITED    = "metabase/dashboards/SET_FAVORITED";
-export const SET_ARCHIVED     = "metabase/dashboards/SET_ARCHIVED";
+export const SET_FAVORITED = "metabase/dashboards/SET_FAVORITED";
+export const SET_ARCHIVED = "metabase/dashboards/SET_ARCHIVED";
 
 /**
  * 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, () =>
+export const fetchDashboards = createThunkAction(
+  FETCH_DASHBOARDS,
+  () =>
     async function(dispatch, getState) {
-        const dashboards = await DashboardApi.list({f: "all"})
+      const dashboards = await DashboardApi.list({ f: "all" });
 
-        for (const dashboard of dashboards) {
-            dashboard.updated_at = moment(dashboard.updated_at);
-        }
+      for (const dashboard of dashboards) {
+        dashboard.updated_at = moment(dashboard.updated_at);
+      }
 
-        return dashboards;
-    }
+      return dashboards;
+    },
 );
 
-export const fetchArchivedDashboards = createThunkAction(FETCH_ARCHIVE, () =>
+export const fetchArchivedDashboards = createThunkAction(
+  FETCH_ARCHIVE,
+  () =>
     async function(dispatch, getState) {
-        const dashboards = await DashboardApi.list({f: "archived"})
+      const dashboards = await DashboardApi.list({ f: "archived" });
 
-        for (const dashboard of dashboards) {
-            dashboard.updated_at = moment(dashboard.updated_at);
-        }
+      for (const dashboard of dashboards) {
+        dashboard.updated_at = moment(dashboard.updated_at);
+      }
 
-        return dashboards;
-    }
+      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;
+  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)));
     }
-);
 
-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;
-    }
+    return createdDashboard;
+  },
 );
 
-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 });
+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 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 });
+  };
 });
 
 export type SetFavoritedAction = (dashId: number, favorited: boolean) => void;
-export const setFavorited: SetFavoritedAction = createThunkAction(SET_FAVORITED, (dashId, favorited) => {
+export const setFavorited: SetFavoritedAction = createThunkAction(
+  SET_FAVORITED,
+  (dashId, favorited) => {
     return async (dispatch, getState) => {
-        if (favorited) {
-            await DashboardApi.favorite({ dashId });
-        } else {
-            await DashboardApi.unfavorite({ dashId });
-        }
-        MetabaseAnalytics.trackEvent("Dashboard", favorited ? "Favorite" : "Unfavorite");
-        return { id: dashId, favorite: favorited };
-    }
-});
+      if (favorited) {
+        await DashboardApi.favorite({ dashId });
+      } else {
+        await DashboardApi.unfavorite({ dashId });
+      }
+      MetabaseAnalytics.trackEvent(
+        "Dashboard",
+        favorited ? "Favorite" : "Unfavorite",
+      );
+      return { id: dashId, favorite: favorited };
+    };
+  },
+);
 
-export type SetArchivedAction = (dashId: number, archived: boolean, undoable?: boolean) => void;
-export const setArchived = createThunkAction(SET_ARCHIVED, (dashId, archived, undoable = false) => {
+export type SetArchivedAction = (
+  dashId: number,
+  archived: boolean,
+  undoable?: boolean,
+) => void;
+export const setArchived = createThunkAction(
+  SET_ARCHIVED,
+  (dashId, archived, undoable = false) => {
     return async (dispatch, getState) => {
-        const response = await DashboardApi.update({
-            id: dashId,
-            archived: archived
-        });
-
-        if (undoable) {
-            const type = archived ? "archived" : "unarchived"
-            dispatch(addUndo(createUndo({
-                type,
-                message: <div>{`Dashboard was ${type}.`}</div>,
-                action: setArchived(dashId, !archived)
-            })));
-        }
-
-        MetabaseAnalytics.trackEvent("Dashboard", archived ? "Archive" : "Unarchive");
-        return response;
-    }
-});
+      const response = await DashboardApi.update({
+        id: dashId,
+        archived: archived,
+      });
+
+      if (undoable) {
+        const type = archived ? "archived" : "unarchived";
+        dispatch(
+          addUndo(
+            createUndo({
+              type,
+              message: <div>{`Dashboard was ${type}.`}</div>,
+              action: setArchived(dashId, !archived),
+            }),
+          ),
+        );
+      }
+
+      MetabaseAnalytics.trackEvent(
+        "Dashboard",
+        archived ? "Archive" : "Unarchive",
+      );
+      return response;
+    };
+  },
+);
 // Convenience shorthand
-export const archiveDashboard = async (dashId) => await setArchived(dashId, true);
+export const archiveDashboard = async dashId => await setArchived(dashId, true);
 
-const archive = handleActions({
+const archive = handleActions(
+  {
     [FETCH_ARCHIVE]: (state, { payload }) => payload,
-    [SET_ARCHIVED]: (state, {payload}) => payload.archived
+    [SET_ARCHIVED]: (state, { payload }) =>
+      payload.archived
         ? (state || []).concat(payload)
-        : (state || []).filter(d => d.id !== payload.id)
-}, null);
+        : (state || []).filter(d => d.id !== payload.id),
+  },
+  null,
+);
 
-const dashboardListing = handleActions({
+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),
-    [SET_FAVORITED]:    (state, { payload }) => (state || []).map(d => d.id === payload.id ? {...d, favorite: payload.favorite} : d),
-    [SET_ARCHIVED]: (state, {payload}) => payload.archived
+    [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)),
+    [SET_FAVORITED]: (state, { payload }) =>
+      (state || []).map(
+        d => (d.id === payload.id ? { ...d, favorite: payload.favorite } : d),
+      ),
+    [SET_ARCHIVED]: (state, { payload }) =>
+      payload.archived
         ? (state || []).filter(d => d.id !== payload.id)
-        : (state || []).concat(payload)
-}, null);
+        : (state || []).concat(payload),
+  },
+  null,
+);
 
 export default combineReducers({
-    dashboardListing,
-    archive
+  dashboardListing,
+  archive,
 });
-
diff --git a/frontend/src/metabase/dashboards/selectors.js b/frontend/src/metabase/dashboards/selectors.js
index e49de25414e6924faa2833abf43bb523602d89b3..d087faf50c581e19f6105ae1722ca7c968f74ae3 100644
--- a/frontend/src/metabase/dashboards/selectors.js
+++ b/frontend/src/metabase/dashboards/selectors.js
@@ -1,2 +1,2 @@
-export const getDashboardListing = (state) => state.dashboards.dashboardListing;
-export const getArchivedDashboards = (state) => state.dashboards.archive;
+export const getDashboardListing = state => state.dashboards.dashboardListing;
+export const getArchivedDashboards = state => state.dashboards.archive;
diff --git a/frontend/src/metabase/hoc/AutoExpanding.jsx b/frontend/src/metabase/hoc/AutoExpanding.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..7d809610d4bfd4fb6629dd3c01f891c9e6ea3eb0
--- /dev/null
+++ b/frontend/src/metabase/hoc/AutoExpanding.jsx
@@ -0,0 +1,28 @@
+import React from "react";
+
+import ExplicitSize from "metabase/components/ExplicitSize";
+
+// If the composed element increases from it's original width, sets `expand` to true
+//
+// Used for components which we initially want to be small, but if they expand
+// beyond their initial size we want to fix their size to be larger so it doesn't
+// jump around, etc
+export default ComposedComponent =>
+  @ExplicitSize
+  class AutoExpanding extends React.Component {
+    state = {
+      expand: false,
+    };
+    componentWillReceiveProps(nextProps) {
+      if (
+        nextProps.width != null &&
+        this.props.width != null &&
+        nextProps.width > this.props.width
+      ) {
+        this.setState({ expand: true });
+      }
+    }
+    render() {
+      return <ComposedComponent {...this.props} {...this.state} />;
+    }
+  };
diff --git a/frontend/src/metabase/hoc/Background.jsx b/frontend/src/metabase/hoc/Background.jsx
index d951cdbf0d1e4a18c3fa2d3933b0bf41b51f0b6c..1a3160c1719a4be2283c61a6c2f610c13cda55b6 100644
--- a/frontend/src/metabase/hoc/Background.jsx
+++ b/frontend/src/metabase/hoc/Background.jsx
@@ -1,20 +1,19 @@
-import React, { Component } from 'react'
+import React, { Component } from "react";
 
-export const withBackground = (className) => (ComposedComponent) => {
-    return class extends Component {
-        static displayName = 'BackgroundApplicator'
+export const withBackground = className => ComposedComponent => {
+  return class extends Component {
+    static displayName = "BackgroundApplicator";
 
-        componentWillMount () {
-            document.body.classList.add(className)
-        }
-
-        componentWillUnmount () {
-            document.body.classList.remove(className)
-        }
+    componentWillMount() {
+      document.body.classList.add(className);
+    }
 
-        render () {
-            return <ComposedComponent {...this.props} />
-        }
+    componentWillUnmount() {
+      document.body.classList.remove(className);
     }
-}
 
+    render() {
+      return <ComposedComponent {...this.props} />;
+    }
+  };
+};
diff --git a/frontend/src/metabase/hoc/ModalRoute.jsx b/frontend/src/metabase/hoc/ModalRoute.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..e488bbceb9645355fbec1e409110dde1b76ed3ca
--- /dev/null
+++ b/frontend/src/metabase/hoc/ModalRoute.jsx
@@ -0,0 +1,47 @@
+import React, { Component } from "react";
+import { Route } from "react-router";
+import { push } from "react-router-redux";
+import { connect } from "react-redux";
+import Modal from "metabase/components/Modal";
+
+const ModalWithRoute = ComposedModal =>
+  connect(null, { onChangeLocation: push })(
+    class extends Component {
+      static displayName = `ModalWithRoute[${ComposedModal.displayName ||
+        ComposedModal.name}]`;
+
+      onClose = () => {
+        const { location: { pathname } } = this.props;
+        const urlWithoutLastSegment = pathname.substring(
+          0,
+          pathname.lastIndexOf("/"),
+        );
+        this.props.onChangeLocation(urlWithoutLastSegment);
+      };
+
+      render() {
+        return (
+          <Modal isOpen={true}>
+            <ComposedModal onClose={this.onClose} />
+          </Modal>
+        );
+      }
+    },
+  );
+
+// react-router Route wrapper that handles routed modals
+export class ModalRoute extends Route {
+  static createRouteFromReactElement(element) {
+    const { modal } = element.props;
+
+    if (modal) {
+      element = React.cloneElement(element, {
+        component: ModalWithRoute(modal),
+      });
+
+      return Route.createRouteFromReactElement(element);
+    } else {
+      throw new Error("`modal` property is missing from ModalRoute");
+    }
+  }
+}
diff --git a/frontend/src/metabase/hoc/Remapped.jsx b/frontend/src/metabase/hoc/Remapped.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b0b94feebcce97bc06b7408b4be775802d34aa98
--- /dev/null
+++ b/frontend/src/metabase/hoc/Remapped.jsx
@@ -0,0 +1,52 @@
+import React, { Component } from "react";
+import { connect } from "react-redux";
+
+import { getMetadata } from "metabase/selectors/metadata";
+import { fetchRemapping } from "metabase/redux/metadata";
+
+const mapStateToProps = (state, props) => ({
+  metadata: getMetadata(state, props),
+});
+
+const mapDispatchToProps = {
+  fetchRemapping,
+};
+
+export default ComposedComponent =>
+  @connect(mapStateToProps, mapDispatchToProps)
+  class extends Component {
+    static displayName = "Remapped[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
+
+    componentWillMount() {
+      if (this.props.column) {
+        this.props.fetchRemapping(this.props.value, this.props.column.id);
+      }
+    }
+    componentWillReceiveProps(nextProps) {
+      if (
+        nextProps.column &&
+        (this.props.value !== nextProps.value ||
+          this.props.column !== nextProps.column)
+      ) {
+        this.props.fetchRemapping(nextProps.value, nextProps.column.id);
+      }
+    }
+
+    render() {
+      // eslint-disable-next-line no-unused-vars
+      const { metadata, fetchRemapping, ...props } = this.props;
+      const field = metadata.field(props.column && props.column.id);
+      const displayValue = field && field.remappedValue(props.value);
+      const displayColumn =
+        (displayValue != null && field && field.remappedField()) || null;
+      return (
+        <ComposedComponent
+          {...props}
+          displayValue={displayValue}
+          displayColumn={displayColumn}
+        />
+      );
+    }
+  };
diff --git a/frontend/src/metabase/hoc/Routeless.jsx b/frontend/src/metabase/hoc/Routeless.jsx
index 1cea1278cec640220a68af2f42bd7aaff03d90b6..b420bd014fe71555fb194e9fd645e1954d2518fd 100644
--- a/frontend/src/metabase/hoc/Routeless.jsx
+++ b/frontend/src/metabase/hoc/Routeless.jsx
@@ -9,67 +9,82 @@ import _ from "underscore";
 
 // namespace under _routeless_
 const mapStateToProps = (state, props) => ({
-    _routeless_location: state.routing.locationBeforeTransitions
+  _routeless_location: state.routing.locationBeforeTransitions,
 });
 
 const mapDispatchToProps = {
-    _routeless_push: push,
-    _routeless_goBack: goBack
+  _routeless_push: push,
+  _routeless_goBack: goBack,
 };
 
 // this higher order component wraps any component (typically a fullscreen modal) with an "onClose"
 // prop, injects an entry in the browser history, and closes the component if the back button is pressed
-@connect(mapStateToProps, mapDispatchToProps)
-export default (ComposedComponent) => class extends Component {
-    static displayName = "Routeless["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+export default (
+  ComposedComponent,
+  // clone the state object otherwise the state will be replaced rather than pushed
+  // save the state object so that we know when it's changed
+  // if the state previously was the saved one and is now not, then we probably
+  // hit the back button, so close the wrapped component
+  // perform this in a timeout because the component may be unmounted anyway, in which
+  // case calling onClose again may cause problems.
+  // alternatively may be able to tighten up the logic above
 
-    _state: any;
-    _timeout: any;
+  // if we unmount (e.x. hit the close button which calls onClose directly) and still have the
+  // same state then go back to the original state
+  // NOTE: ideally we would remove the current state from the history so the forward
+  // button wouldn't be enabled, maybe using `replace`
+) =>
+  connect(mapStateToProps, mapDispatchToProps)(
+    class extends Component {
+      static displayName = "Routeless[" +
+        (ComposedComponent.displayName || ComposedComponent.name) +
+        "]";
 
-    componentWillMount() {
+      _state: any;
+      _timeout: any;
+
+      componentWillMount() {
         const push = this.props._routeless_push;
         const location = this.props._routeless_location;
         const { pathname, query, search, hash, state } = location;
-        // clone the state object otherwise the state will be replaced rather than pushed
-        // save the state object so that we know when it's changed
         this._state = typeof state === "object" ? Object.create(state) : {};
         push({ pathname, query, search, hash, state: this._state });
-    }
+      }
 
-    componentWillReceiveProps(nextProps) {
+      componentWillReceiveProps(nextProps) {
         const location = this.props._routeless_location;
         const nextLocation = nextProps._routeless_location;
-        // if the state previously was the saved one and is now not, then we probably
-        // hit the back button, so close the wrapped component
-        if (location.state === this._state && nextLocation.state !== this._state) {
-            // perform this in a timeout because the component may be unmounted anyway, in which
-            // case calling onClose again may cause problems.
-            // alternatively may be able to tighten up the logic above
-            this._timeout = setTimeout(() => {
-                this.props.onClose();
-            }, 100);
+        if (
+          location.state === this._state &&
+          nextLocation.state !== this._state
+        ) {
+          this._timeout = setTimeout(() => {
+            this.props.onClose();
+          }, 100);
         }
-    }
+      }
 
-    componentWillUnmount() {
+      componentWillUnmount() {
         const location = this.props._routeless_location;
         const goBack = this.props._routeless_goBack;
 
         if (this._timeout != null) {
-            clearTimeout(this._timeout);
+          clearTimeout(this._timeout);
         }
 
-        // if we unmount (e.x. hit the close button which calls onClose directly) and still have the
-        // same state then go back to the original state
-        // NOTE: ideally we would remove the current state from the history so the forward
-        // button wouldn't be enabled, maybe using `replace`
         if (location.state === this._state) {
-            goBack();
+          goBack();
         }
-    }
+      }
 
-    render() {
-        const props = _.omit(this.props, "_routeless_location", "_routeless_goBack", "_routeless_push");
-        return <ComposedComponent {...props} />
-    }
-}
+      render() {
+        const props = _.omit(
+          this.props,
+          "_routeless_location",
+          "_routeless_goBack",
+          "_routeless_push",
+        );
+        return <ComposedComponent {...props} />;
+      }
+    },
+  );
diff --git a/frontend/src/metabase/hoc/Title.jsx b/frontend/src/metabase/hoc/Title.jsx
index d150131d270ef4d6c2fca11c2f1721000be623ea..2364182a0efb90709b23c413e12bd0b32d9f3024 100644
--- a/frontend/src/metabase/hoc/Title.jsx
+++ b/frontend/src/metabase/hoc/Title.jsx
@@ -8,68 +8,70 @@ let SEPARATOR = " · ";
 let HIERARCHICAL = true;
 let BASE_NAME = null;
 
-export const setSeparator = (separator) => SEPARATOR = separator;
-export const setHierarchical = (hierarchical) => HIERARCHICAL = hierarchical;
-export const setBaseName = (baseName) => BASE_NAME = baseName;
+export const setSeparator = separator => (SEPARATOR = separator);
+export const setHierarchical = hierarchical => (HIERARCHICAL = hierarchical);
+export const setBaseName = baseName => (BASE_NAME = baseName);
 
 const updateDocumentTitle = _.debounce(() => {
-    if (HIERARCHICAL) {
-        document.title = componentStack
-            .map(component => component._documentTitle)
-            .filter(title => title)
-            .reverse()
-            .join(SEPARATOR);
-    } else {
-        // update with the top-most title
-        for (let i = componentStack.length - 1; i >= 0; i--) {
-            let title = componentStack[i]._documentTitle;
-            if (title) {
-                if (BASE_NAME) {
-                    title += SEPARATOR + BASE_NAME;
-                }
-                if (document.title !== title) {
-                    document.title = title;
-                }
-                break;
-            }
+  if (HIERARCHICAL) {
+    document.title = componentStack
+      .map(component => component._documentTitle)
+      .filter(title => title)
+      .reverse()
+      .join(SEPARATOR);
+  } else {
+    // update with the top-most title
+    for (let i = componentStack.length - 1; i >= 0; i--) {
+      let title = componentStack[i]._documentTitle;
+      if (title) {
+        if (BASE_NAME) {
+          title += SEPARATOR + BASE_NAME;
         }
+        if (document.title !== title) {
+          document.title = title;
+        }
+        break;
+      }
     }
-})
+  }
+});
 
-const title = (documentTitleOrGetter) => (ComposedComponent) =>
-    class extends React.Component {
-        static displayName = "Title["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+const title = documentTitleOrGetter => ComposedComponent =>
+  class extends React.Component {
+    static displayName = "Title[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
-        componentWillMount() {
-            componentStack.push(this);
-            this._updateDocumentTitle();
-        }
-        componentDidUpdate() {
-            this._updateDocumentTitle();
-        }
-        componentWillUnmount() {
-            for (let i = 0; i < componentStack.length; i++) {
-                if (componentStack[i] === this) {
-                    componentStack.splice(i, 1);
-                    break;
-                }
-            }
-            this._updateDocumentTitle();
+    componentWillMount() {
+      componentStack.push(this);
+      this._updateDocumentTitle();
+    }
+    componentDidUpdate() {
+      this._updateDocumentTitle();
+    }
+    componentWillUnmount() {
+      for (let i = 0; i < componentStack.length; i++) {
+        if (componentStack[i] === this) {
+          componentStack.splice(i, 1);
+          break;
         }
+      }
+      this._updateDocumentTitle();
+    }
 
-        _updateDocumentTitle() {
-            if (typeof documentTitleOrGetter === "string") {
-                this._documentTitle = documentTitleOrGetter;
-            } else if (typeof documentTitleOrGetter === "function") {
-                this._documentTitle = documentTitleOrGetter(this.props);
-            }
-            updateDocumentTitle();
-        }
+    _updateDocumentTitle() {
+      if (typeof documentTitleOrGetter === "string") {
+        this._documentTitle = documentTitleOrGetter;
+      } else if (typeof documentTitleOrGetter === "function") {
+        this._documentTitle = documentTitleOrGetter(this.props);
+      }
+      updateDocumentTitle();
+    }
 
-        render() {
-            return <ComposedComponent {...this.props} />;
-        }
+    render() {
+      return <ComposedComponent {...this.props} />;
     }
+  };
 
 export default title;
 
@@ -77,12 +79,14 @@ import { Route as _Route } from "react-router";
 
 // react-router Route wrapper that adds a `title` property
 export class Route extends _Route {
-    static createRouteFromReactElement(element) {
-        if (element.props.title) {
-            element = React.cloneElement(element, {
-                component: title(element.props.title)(element.props.component || (({ children }) => children))
-            });
-        }
-        return _Route.createRouteFromReactElement(element);
+  static createRouteFromReactElement(element) {
+    if (element.props.title) {
+      element = React.cloneElement(element, {
+        component: title(element.props.title)(
+          element.props.component || (({ children }) => children),
+        ),
+      });
     }
+    return _Route.createRouteFromReactElement(element);
+  }
 }
diff --git a/frontend/src/metabase/hoc/Tooltipify.jsx b/frontend/src/metabase/hoc/Tooltipify.jsx
index 5aaae09c1eb677ea25508976425a156de05df041..9534c9002c5430770d6838cc7f183ffe082a8ecf 100644
--- a/frontend/src/metabase/hoc/Tooltipify.jsx
+++ b/frontend/src/metabase/hoc/Tooltipify.jsx
@@ -2,16 +2,23 @@ import React, { Component } from "react";
 
 import Tooltip from "metabase/components/Tooltip";
 
-const Tooltipify = (ComposedComponent) => class extends Component {
-    static displayName = "Tooltipify["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+const Tooltipify = ComposedComponent =>
+  class extends Component {
+    static displayName = "Tooltipify[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
     render() {
-        const { tooltip, ...props } = this.props;
-        if (tooltip) {
-            return <Tooltip tooltip={tooltip}><ComposedComponent {...props} /></Tooltip>;
-        } else {
-            return <ComposedComponent {...props} />;
-        }
+      const { tooltip, ...props } = this.props;
+      if (tooltip) {
+        return (
+          <Tooltip tooltip={tooltip}>
+            <ComposedComponent {...props} />
+          </Tooltip>
+        );
+      } else {
+        return <ComposedComponent {...props} />;
+      }
     }
-}
+  };
 
 export default Tooltipify;
diff --git a/frontend/src/metabase/hoc/Typeahead.jsx b/frontend/src/metabase/hoc/Typeahead.jsx
index b705eee7b683dbc23d9c7f3a577249660b0643fc..3d40f3243d95eea339336573913379f9da13a520 100644
--- a/frontend/src/metabase/hoc/Typeahead.jsx
+++ b/frontend/src/metabase/hoc/Typeahead.jsx
@@ -6,111 +6,132 @@ import _ from "underscore";
 import { KEYCODE_ENTER, KEYCODE_UP, KEYCODE_DOWN } from "metabase/lib/keyboard";
 
 const DEFAULT_FILTER_OPTIONS = (value, option) => {
-    try {
-        return JSON.stringify(option).includes(value);
-    } catch (e) {
-        return false;
-    }
-}
-
-const DEFAULT_OPTION_IS_EQUAL = (a, b) => a === b
-
-export default ({ optionFilter = DEFAULT_FILTER_OPTIONS, optionIsEqual = DEFAULT_OPTION_IS_EQUAL }) => (ComposedComponent) => class extends Component {
-    static displayName = "Typeahead["+(ComposedComponent.displayName || ComposedComponent.name)+"]";
+  try {
+    return JSON.stringify(option).includes(value);
+  } catch (e) {
+    return false;
+  }
+};
+
+const DEFAULT_OPTION_IS_EQUAL = (a, b) => a === b;
+
+export default ({
+  optionFilter = DEFAULT_FILTER_OPTIONS,
+  optionIsEqual = DEFAULT_OPTION_IS_EQUAL,
+  defaultFirstSuggestion = false,
+  defaultSingleSuggestion = false,
+}) => ComposedComponent =>
+  class extends Component {
+    static displayName = "Typeahead[" +
+      (ComposedComponent.displayName || ComposedComponent.name) +
+      "]";
 
     constructor(props, context) {
-        super(props, context);
-        this.state = {
-            suggestions: [],
-            selectedSuggestion: null
-        };
+      super(props, context);
+      this.state = {
+        suggestions: [],
+        selectedSuggestion: null,
+      };
     }
 
     static propTypes = {
-        value: PropTypes.string,
-        options: PropTypes.array
+      value: PropTypes.string,
+      options: PropTypes.array,
     };
 
     componentDidMount() {
-        window.addEventListener("keydown", this.onKeyDown, true);
+      window.addEventListener("keydown", this.onKeyDown, true);
     }
 
     componentWillUnmount() {
-        window.removeEventListener("keydown", this.onKeyDown, true);
+      window.removeEventListener("keydown", this.onKeyDown, true);
     }
 
-    onKeyDown = (e) => {
-        if (e.keyCode === KEYCODE_UP) {
-            e.preventDefault();
-            this.onPressUp();
-        } else if (e.keyCode === KEYCODE_DOWN) {
-            e.preventDefault();
-            this.onPressDown();
-        } else if (e.keyCode === KEYCODE_ENTER) {
-            e.preventDefault();
-            this.onSuggestionAccepted(this.state.selectedSuggestion);
+    onKeyDown = e => {
+      if (e.keyCode === KEYCODE_UP) {
+        e.preventDefault();
+        this.onPressUp();
+      } else if (e.keyCode === KEYCODE_DOWN) {
+        e.preventDefault();
+        this.onPressDown();
+      } else if (e.keyCode === KEYCODE_ENTER) {
+        if (this.state.selectedSuggestion != null) {
+          e.preventDefault();
+          this.onSuggestionAccepted(this.state.selectedSuggestion);
         }
-    }
+      }
+    };
 
     componentWillReceiveProps({ options, value }) {
-        let filtered = value ? options.filter(optionFilter.bind(null, value)) : [];
-        this.setState({
-            suggestions: filtered,
-            isOpen: filtered.length > 0
-        });
+      const filtered = value
+        ? options.filter(optionFilter.bind(null, value))
+        : [];
+      const selectFirstSuggestion =
+        (defaultFirstSuggestion && filtered.length > 0) ||
+        (defaultSingleSuggestion && filtered.length === 1);
+      this.setState({
+        suggestions: filtered,
+        selectedSuggestion: selectFirstSuggestion ? filtered[0] : null,
+        isOpen: filtered.length > 0,
+      });
     }
 
     indexOfSelectedSuggestion() {
-        return _.findIndex(this.state.suggestions, (suggestion) =>
-            optionIsEqual(suggestion, this.state.selectedSuggestion)
-        );
+      return _.findIndex(this.state.suggestions, suggestion =>
+        optionIsEqual(suggestion, this.state.selectedSuggestion),
+      );
     }
 
     setSelectedIndex(newIndex) {
-        let index = Math.max(Math.min(newIndex, this.state.suggestions.length - 1), 0);
-        this.setState({
-            selectedSuggestion: this.state.suggestions[index]
-        });
+      let index = Math.max(
+        Math.min(newIndex, this.state.suggestions.length - 1),
+        0,
+      );
+      this.setState({
+        selectedSuggestion: this.state.suggestions[index],
+      });
     }
 
-    onSuggestionAccepted = (suggestion) => {
-        this.props.onSuggestionAccepted(suggestion)
-    }
+    onSuggestionAccepted = suggestion => {
+      this.props.onSuggestionAccepted(suggestion);
+    };
 
     onPressUp = () => {
-        const { suggestions, selectedSuggestion } = this.state;
-        if (suggestions.length === 0) {
-            return;
-        } else if (!selectedSuggestion) {
-            this.setState({ selectedSuggestion: suggestions[suggestions.length - 1] });
-        } else {
-            this.setSelectedIndex(this.indexOfSelectedSuggestion() - 1);
-        }
-    }
+      const { suggestions, selectedSuggestion } = this.state;
+      if (suggestions.length === 0) {
+        return;
+      } else if (!selectedSuggestion) {
+        this.setState({
+          selectedSuggestion: suggestions[suggestions.length - 1],
+        });
+      } else {
+        this.setSelectedIndex(this.indexOfSelectedSuggestion() - 1);
+      }
+    };
 
     onPressDown = () => {
-        const { suggestions, selectedSuggestion } = this.state;
-        if (suggestions.length === 0) {
-            return;
-        } else if (!selectedSuggestion) {
-            this.setState({ selectedSuggestion: suggestions[0] });
-        } else {
-            this.setSelectedIndex(this.indexOfSelectedSuggestion() + 1);
-        }
-    }
+      const { suggestions, selectedSuggestion } = this.state;
+      if (suggestions.length === 0) {
+        return;
+      } else if (!selectedSuggestion) {
+        this.setState({ selectedSuggestion: suggestions[0] });
+      } else {
+        this.setSelectedIndex(this.indexOfSelectedSuggestion() + 1);
+      }
+    };
 
     render() {
-        const { suggestions, selectedSuggestion } = this.state;
-        if (suggestions.length === 0) {
-            return null;
-        }
-        return (
-            <ComposedComponent
-                {...this.props}
-                suggestions={suggestions}
-                selectedSuggestion={selectedSuggestion}
-                onSuggestionAccepted={this.onSuggestionAccepted}
-            />
-        );
+      const { suggestions, selectedSuggestion } = this.state;
+      if (suggestions.length === 0) {
+        return null;
+      }
+      return (
+        <ComposedComponent
+          {...this.props}
+          suggestions={suggestions}
+          selectedSuggestion={selectedSuggestion}
+          onSuggestionAccepted={this.onSuggestionAccepted}
+        />
+      );
     }
-}
+  };
diff --git a/frontend/src/metabase/hoc/__mocks__/Remapped.jsx b/frontend/src/metabase/hoc/__mocks__/Remapped.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..b9e0111936007b244b0dc1905b5d136f9f1b4167
--- /dev/null
+++ b/frontend/src/metabase/hoc/__mocks__/Remapped.jsx
@@ -0,0 +1,3 @@
+const MockRemapped = ComposedComponent => ComposedComponent;
+
+export default MockRemapped;
diff --git a/frontend/src/metabase/home/actions.js b/frontend/src/metabase/home/actions.js
index a12d73762728e3ebadb65290e5cf8202b0c6e80b..1d40a0aa69d97de437f06e291d4ae7611a9feccd 100644
--- a/frontend/src/metabase/home/actions.js
+++ b/frontend/src/metabase/home/actions.js
@@ -5,33 +5,34 @@ import { createThunkAction } from "metabase/lib/redux";
 
 import { ActivityApi } from "metabase/services";
 
-
 // action constants
-export const FETCH_ACTIVITY = 'FETCH_ACTIVITY';
-export const FETCH_RECENT_VIEWS = 'FETCH_RECENT_VIEWS';
-
+export const FETCH_ACTIVITY = "FETCH_ACTIVITY";
+export const FETCH_RECENT_VIEWS = "FETCH_RECENT_VIEWS";
 
 // action creators
 
 export const fetchActivity = createThunkAction(FETCH_ACTIVITY, function() {
-    return async function(dispatch, getState) {
-        let activity = await ActivityApi.list();
-        for (var ai of activity) {
-            ai.timestamp = moment(ai.timestamp);
-            ai.hasLinkableModel = function() {
-                return (_.contains(["card", "dashboard"], this.model));
-            };
-        }
-        return activity;
-    };
+  return async function(dispatch, getState) {
+    let activity = await ActivityApi.list();
+    for (var ai of activity) {
+      ai.timestamp = moment(ai.timestamp);
+      ai.hasLinkableModel = function() {
+        return _.contains(["card", "dashboard"], this.model);
+      };
+    }
+    return activity;
+  };
 });
 
-export const fetchRecentViews = createThunkAction(FETCH_RECENT_VIEWS, function() {
+export const fetchRecentViews = createThunkAction(
+  FETCH_RECENT_VIEWS,
+  function() {
     return async function(dispatch, getState) {
-        let recentViews = await ActivityApi.recent_views();
-        for (var v of recentViews) {
-            v.timestamp = moment(v.timestamp);
-        }
-        return recentViews;
+      let recentViews = await ActivityApi.recent_views();
+      for (var v of recentViews) {
+        v.timestamp = moment(v.timestamp);
+      }
+      return recentViews;
     };
-});
+  },
+);
diff --git a/frontend/src/metabase/home/components/Activity.jsx b/frontend/src/metabase/home/components/Activity.jsx
index 7f0b16aa68c950eaf531fb1143767b461302c294..355c2f8704c8bece7d32b74c093b60abbe608d94 100644
--- a/frontend/src/metabase/home/components/Activity.jsx
+++ b/frontend/src/metabase/home/components/Activity.jsx
@@ -1,462 +1,556 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import _ from 'underscore';
-import { t } from 'c-3po'
+import _ from "underscore";
+import { t } from "c-3po";
 
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper.jsx';
-import ActivityItem from './ActivityItem.jsx';
-import ActivityStory from './ActivityStory.jsx';
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
+import ActivityItem from "./ActivityItem.jsx";
+import ActivityStory from "./ActivityStory.jsx";
 
 import * as Urls from "metabase/lib/urls";
 
 export default class Activity extends Component {
+  constructor(props, context) {
+    super(props, context);
+    this.state = { error: null, userColors: {} };
 
-    constructor(props, context) {
-        super(props, context);
-        this.state = { error: null, userColors: {} };
+    this.colorClasses = [
+      "bg-brand",
+      "bg-purple",
+      "bg-error",
+      "bg-green",
+      "bg-gold",
+      "bg-grey-2",
+    ];
+  }
 
-        this.colorClasses = ['bg-brand', 'bg-purple', 'bg-error', 'bg-green', 'bg-gold', 'bg-grey-2'];
-    }
+  static propTypes = {
+    user: PropTypes.object.isRequired,
+    activity: PropTypes.array,
+    fetchActivity: PropTypes.func.isRequired,
+  };
 
-    static propTypes = {
-        user: PropTypes.object.isRequired,
-        activity: PropTypes.array,
-        fetchActivity: PropTypes.func.isRequired
+  async componentDidMount() {
+    try {
+      await this.props.fetchActivity();
+    } catch (error) {
+      this.setState({ error });
     }
+  }
 
-    async componentDidMount() {
-        try {
-            await this.props.fetchActivity();
-        } catch (error) {
-            this.setState({ error });
-        }
-    }
+  componentWillReceiveProps(nextProps) {
+    // do a quick pass over the activity and make sure we've assigned colors to all users which have activity
+    let { activity, user } = nextProps;
+    let { userColors } = this.state;
 
-    componentWillReceiveProps(nextProps) {
-        // do a quick pass over the activity and make sure we've assigned colors to all users which have activity
-        let { activity, user } = nextProps;
-        let { userColors } = this.state;
+    const colors = [1, 2, 3, 4, 5];
+    const maxColorUsed = _.isEmpty(userColors)
+      ? 0
+      : _.max(_.values(userColors));
+    var currColor =
+      maxColorUsed && maxColorUsed < colors.length ? maxColorUsed : 0;
 
-        const colors = [1,2,3,4,5];
-        const maxColorUsed = (_.isEmpty(userColors)) ? 0 : _.max(_.values(userColors));
-        var currColor =  (maxColorUsed && maxColorUsed < colors.length) ? maxColorUsed : 0;
+    if (user && activity) {
+      for (var item of activity) {
+        if (!(item.user_id in userColors)) {
+          // assign the user a color
+          if (item.user_id === user.id) {
+            userColors[item.user_id] = 0;
+          } else if (item.user_id === null) {
+            // just skip this scenario, we handle this differently
+          } else {
+            userColors[item.user_id] = colors[currColor];
+            currColor++;
 
-        if (user && activity) {
-            for (var item of activity) {
-                if (!(item.user_id in userColors)) {
-                    // assign the user a color
-                    if (item.user_id === user.id) {
-                        userColors[item.user_id] = 0;
-                    } else if (item.user_id === null) {
-                        // just skip this scenario, we handle this differently
-                    } else {
-                        userColors[item.user_id] = colors[currColor];
-                        currColor++;
-
-                        // if we hit the end of the colors list then just go back to the beginning again
-                        if (currColor >= colors.length) {
-                            currColor = 0;
-                        }
-                    }
-                }
+            // if we hit the end of the colors list then just go back to the beginning again
+            if (currColor >= colors.length) {
+              currColor = 0;
             }
+          }
         }
-
-        this.setState({ userColors });
+      }
     }
 
-    userName(user, currentUser) {
-        if (user && currentUser && user.id === currentUser.id) {
-            return t`You`;
-        } else if (user) {
-            return user.first_name;
-        } else {
-            return "Metabase";
-        }
-    }
+    this.setState({ userColors });
+  }
 
-    activityHeader(item, user) {
+  userName(user, currentUser) {
+    if (user && currentUser && user.id === currentUser.id) {
+      return t`You`;
+    } else if (user) {
+      return user.first_name;
+    } else {
+      return "Metabase";
+    }
+  }
 
-        // this is a base to start with
-        const description = {
-            userName: this.userName(item.user, user),
-            summary: t`did some super awesome stuff thats hard to describe`,
-            timeSince: item.timestamp.fromNow()
-        };
+  activityHeader(item, user) {
+    // this is a base to start with
+    const description = {
+      userName: this.userName(item.user, user),
+      summary: t`did some super awesome stuff thats hard to describe`,
+      timeSince: item.timestamp.fromNow(),
+    };
 
-        switch (item.topic) {
-            case "alert-create":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`created an alert about - `}
-                            <Link to={Urls.modelToUrl(item.model, item.model_id)}
-                                  data-metabase-event={"Activity Feed;Header Clicked;Database -> " + item.topic}
-                                  className="link text-dark"
-                            >
-                                  {item.details.name}
-                            </Link>
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`created an alert about - `}<span className="text-dark">{item.details.name}</span></span>
-                    );
+    switch (item.topic) {
+      case "alert-create":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`created an alert about - `}
+              <Link
+                to={Urls.modelToUrl(item.model, item.model_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Database -> " + item.topic
                 }
-                break;
-            case "alert-delete":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`deleted an alert about - `}
-                            <Link to={Urls.modelToUrl(item.model, item.model_id)}
-                                  data-metabase-event={"Activity Feed;Header Clicked;Database -> " + item.topic}
-                                  className="link text-dark"
-                            >
-                                  {item.details.name}
-                            </Link>
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`deleted an alert about- `}<span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`created an alert about - `}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "alert-delete":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`deleted an alert about - `}
+              <Link
+                to={Urls.modelToUrl(item.model, item.model_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Database -> " + item.topic
                 }
-                break;
-            case "card-create":
-            case "card-update":
-                if(item.table) {
-                    description.summary = (
-                        <span>
-                            {t`saved a question about `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Database -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.table.display_name}
-                            </Link>
-                        </span>
-                    );
-                } else {
-                    description.summary = t`saved a question`;
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`deleted an alert about- `}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "card-create":
+      case "card-update":
+        if (item.table) {
+          description.summary = (
+            <span>
+              {t`saved a question about `}
+              <Link
+                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Database -> " + item.topic
                 }
-                break;
-            case "card-delete":
-                description.summary = t`deleted a question`;
-                break;
-            case "dashboard-create":
-                description.summary = t`created a dashboard`;
-                break;
-            case "dashboard-delete":
-                description.summary = t`deleted a dashboard`;
-                break;
-            case "dashboard-add-cards":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`added a question to the dashboard - `}
-                            <Link
-                                to={Urls.dashboard(item.model_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Dashboard -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.details.name}
-                            </Link>
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`added a question to the dashboard - `}<span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.table.display_name}
+              </Link>
+            </span>
+          );
+        } else {
+          description.summary = t`saved a question`;
+        }
+        break;
+      case "card-delete":
+        description.summary = t`deleted a question`;
+        break;
+      case "dashboard-create":
+        description.summary = t`created a dashboard`;
+        break;
+      case "dashboard-delete":
+        description.summary = t`deleted a dashboard`;
+        break;
+      case "dashboard-add-cards":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`added a question to the dashboard - `}
+              <Link
+                to={Urls.dashboard(item.model_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Dashboard -> " + item.topic
                 }
-                break;
-            case "dashboard-remove-cards":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`removed a question from the dashboard - `}
-                            <Link
-                                to={Urls.dashboard(item.model_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Dashboard -> " + item.topic}
-                                className="link text-dark"
-                            >
-                                {item.details.name}
-                            </Link>
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`removed a question from the dashboard - `}<span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`added a question to the dashboard - `}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "dashboard-remove-cards":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`removed a question from the dashboard - `}
+              <Link
+                to={Urls.dashboard(item.model_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Dashboard -> " + item.topic
                 }
-                break;
-            case "database-sync":
-                // NOTE: this is a relic from the very early days of the activity feed when we accidentally didn't
-                //       capture the name/description/engine of a Database properly in the details and so it was
-                //       possible for a database to be deleted and we'd lose any way of knowing what it's name was :(
-                const oldName = (item.database && 'name' in item.database) ? item.database.name : t`Unknown`;
-                if(item.details.name) {
-                    description.summary = (
-                        <span>{t`received the latest data from`} <span className="text-dark">{item.details.name}</span></span>
-                    );
-                } else {
-                    description.summary = (
-                            <span>{t`received the latest data from`} <span className="text-dark">{oldName}</span></span>
-                        );
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`removed a question from the dashboard - `}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "database-sync":
+        // NOTE: this is a relic from the very early days of the activity feed when we accidentally didn't
+        //       capture the name/description/engine of a Database properly in the details and so it was
+        //       possible for a database to be deleted and we'd lose any way of knowing what it's name was :(
+        const oldName =
+          item.database && "name" in item.database
+            ? item.database.name
+            : t`Unknown`;
+        if (item.details.name) {
+          description.summary = (
+            <span>
+              {t`received the latest data from`}{" "}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`received the latest data from`}{" "}
+              <span className="text-dark">{oldName}</span>
+            </span>
+          );
+        }
+        break;
+      case "install":
+        description.userName = t`Hello World!`;
+        description.summary = t`Metabase is up and running.`;
+        break;
+      case "metric-create":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`added the metric `}
+              <Link
+                to={Urls.tableRowsQuery(
+                  item.database_id,
+                  item.table_id,
+                  item.model_id,
+                )}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Metric -> " + item.topic
+                }
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+              {t` to the `}
+              <Link
+                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Table -> " + item.topic
                 }
-                break;
-            case "install":
-                description.userName = t`Hello World!`;
-                description.summary = t`Metabase is up and running.`;
-                break;
-            case "metric-create":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`added the metric `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id, item.model_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Metric -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.details.name}
-                            </Link>
-                            {t` to the `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Table -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.table.display_name}
-                            </Link>
-                            {t` table`}
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`added the metric `} <span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.table.display_name}
+              </Link>
+              {t` table`}
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`added the metric `}{" "}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "metric-update":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`made changes to the metric `}
+              <Link
+                to={Urls.tableRowsQuery(
+                  item.database_id,
+                  item.table_id,
+                  item.model_id,
+                )}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Metric -> " + item.topic
                 }
-                break;
-            case "metric-update":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`made changes to the metric `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id, item.model_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Metric -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.details.name}
-                            </Link>
-                            {t` in the `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Table -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.table.display_name}
-                            </Link>
-                            {t` table`}
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`made changes to the metric `} <span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+              {t` in the `}
+              <Link
+                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Table -> " + item.topic
                 }
-                break;
-            case "metric-delete":
-                description.summary = t`removed the metric `+item.details.name;
-                break;
-            case "pulse-create":
-                description.summary = t`created a pulse`;
-                break;
-            case "pulse-delete":
-                description.summary = t`deleted a pulse`;
-                break;
-            case "segment-create":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`added the filter `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id, null, item.model_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Segment -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.details.name}
-                            </Link>
-                            {t` to the `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Table -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.table.display_name}
-                            </Link>
-                            {t` table`}
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`added the filter`} <span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.table.display_name}
+              </Link>
+              {t` table`}
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`made changes to the metric `}{" "}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "metric-delete":
+        description.summary = t`removed the metric ` + item.details.name;
+        break;
+      case "pulse-create":
+        description.summary = t`created a pulse`;
+        break;
+      case "pulse-delete":
+        description.summary = t`deleted a pulse`;
+        break;
+      case "segment-create":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`added the filter `}
+              <Link
+                to={Urls.tableRowsQuery(
+                  item.database_id,
+                  item.table_id,
+                  null,
+                  item.model_id,
+                )}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Segment -> " + item.topic
                 }
-                break;
-            case "segment-update":
-                if(item.model_exists) {
-                    description.summary = (
-                        <span>
-                            {t`made changes to the filter `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id, null, item.model_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Segment -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.details.name}
-                            </Link>
-                            {t` in the `}
-                            <Link
-                                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
-                                data-metabase-event={"Activity Feed;Header Clicked;Table -> "+item.topic}
-                                className="link text-dark"
-                            >
-                                {item.table.display_name}
-                            </Link>
-                            {t` table`}
-                        </span>
-                    );
-                } else {
-                    description.summary = (
-                        <span>{t`made changes to the filter`} <span className="text-dark">{item.details.name}</span></span>
-                    );
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+              {t` to the `}
+              <Link
+                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Table -> " + item.topic
                 }
-                break;
-            case "segment-delete":
-                description.summary = t`removed the filter ${item.details.name}`;
-                break;
-            case "user-joined":
-                description.summary = t`joined!`;
-                break;
+                className="link text-dark"
+              >
+                {item.table.display_name}
+              </Link>
+              {t` table`}
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`added the filter`}{" "}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
         }
-
-        return description;
+        break;
+      case "segment-update":
+        if (item.model_exists) {
+          description.summary = (
+            <span>
+              {t`made changes to the filter `}
+              <Link
+                to={Urls.tableRowsQuery(
+                  item.database_id,
+                  item.table_id,
+                  null,
+                  item.model_id,
+                )}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Segment -> " + item.topic
+                }
+                className="link text-dark"
+              >
+                {item.details.name}
+              </Link>
+              {t` in the `}
+              <Link
+                to={Urls.tableRowsQuery(item.database_id, item.table_id)}
+                data-metabase-event={
+                  "Activity Feed;Header Clicked;Table -> " + item.topic
+                }
+                className="link text-dark"
+              >
+                {item.table.display_name}
+              </Link>
+              {t` table`}
+            </span>
+          );
+        } else {
+          description.summary = (
+            <span>
+              {t`made changes to the filter`}{" "}
+              <span className="text-dark">{item.details.name}</span>
+            </span>
+          );
+        }
+        break;
+      case "segment-delete":
+        description.summary = t`removed the filter ${item.details.name}`;
+        break;
+      case "user-joined":
+        description.summary = t`joined!`;
+        break;
     }
 
-    activityStory(item) {
+    return description;
+  }
 
-        // this is a base to start with
-        const description = {
-            topic: item.topic,
-            body: null,
-            bodyLink: null
-        };
+  activityStory(item) {
+    // this is a base to start with
+    const description = {
+      topic: item.topic,
+      body: null,
+      bodyLink: null,
+    };
 
-        switch (item.topic) {
-            case "card-create":
-            case "card-update":
-                description.body = item.details.name;
-                description.bodyLink = (item.model_exists) ? Urls.modelToUrl(item.model, item.model_id) : null;
-                break;
-            case "card-delete":
-                description.body = item.details.name;
-                break;
-            case "dashboard-create":
-                description.body = item.details.name;
-                description.bodyLink = (item.model_exists) ? Urls.modelToUrl(item.model, item.model_id) : null;
-                break;
-            case "dashboard-delete":
-                description.body = item.details.name;
-                break;
-            case "dashboard-add-cards":
-            case "dashboard-remove-cards":
-                description.body = item.details.dashcards[0].name;
-                if (item.details.dashcards[0].exists) {
-                    description.bodyLink = Urls.question(item.details.dashcards[0].card_id);
-                }
-                break;
-            case "metric-create":
-                description.body = item.details.description;
-                break;
-            case "metric-update":
-                description.body = item.details.revision_message;
-                break;
-            case "metric-delete":
-                description.body = item.details.revision_message;
-                break;
-            case "pulse-create":
-                description.body = item.details.name;
-                description.bodyLink = (item.model_exists) ? Urls.modelToUrl(item.model, item.model_id) : null;
-                break;
-            case "pulse-delete":
-                description.body = item.details.name;
-                break;
-            case "segment-create":
-                description.body = item.details.description;
-                break;
-            case "segment-update":
-                description.body = item.details.revision_message;
-                break;
-            case "segment-delete":
-                description.body = item.details.revision_message;
-                break;
+    switch (item.topic) {
+      case "card-create":
+      case "card-update":
+        description.body = item.details.name;
+        description.bodyLink = item.model_exists
+          ? Urls.modelToUrl(item.model, item.model_id)
+          : null;
+        break;
+      case "card-delete":
+        description.body = item.details.name;
+        break;
+      case "dashboard-create":
+        description.body = item.details.name;
+        description.bodyLink = item.model_exists
+          ? Urls.modelToUrl(item.model, item.model_id)
+          : null;
+        break;
+      case "dashboard-delete":
+        description.body = item.details.name;
+        break;
+      case "dashboard-add-cards":
+      case "dashboard-remove-cards":
+        description.body = item.details.dashcards[0].name;
+        if (item.details.dashcards[0].exists) {
+          description.bodyLink = Urls.question(
+            item.details.dashcards[0].card_id,
+          );
         }
-
-        return description;
+        break;
+      case "metric-create":
+        description.body = item.details.description;
+        break;
+      case "metric-update":
+        description.body = item.details.revision_message;
+        break;
+      case "metric-delete":
+        description.body = item.details.revision_message;
+        break;
+      case "pulse-create":
+        description.body = item.details.name;
+        description.bodyLink = item.model_exists
+          ? Urls.modelToUrl(item.model, item.model_id)
+          : null;
+        break;
+      case "pulse-delete":
+        description.body = item.details.name;
+        break;
+      case "segment-create":
+        description.body = item.details.description;
+        break;
+      case "segment-update":
+        description.body = item.details.revision_message;
+        break;
+      case "segment-delete":
+        description.body = item.details.revision_message;
+        break;
     }
 
-    initialsCssClasses(user) {
-        let { userColors } = this.state;
+    return description;
+  }
 
-        if (user) {
-            const userColorIndex = userColors[user.id];
-            const colorCssClass = this.colorClasses[userColorIndex];
+  initialsCssClasses(user) {
+    let { userColors } = this.state;
 
-            return colorCssClass;
-        }
+    if (user) {
+      const userColorIndex = userColors[user.id];
+      const colorCssClass = this.colorClasses[userColorIndex];
+
+      return colorCssClass;
     }
+  }
 
-    render() {
-        let { activity, user } = this.props;
-        let { error } = this.state;
+  render() {
+    let { activity, user } = this.props;
+    let { error } = this.state;
 
-        return (
-            <LoadingAndErrorWrapper loading={!activity} error={error}>
-            {() =>
-                <div className="full flex flex-column">
-                    <div className="">
-                        { activity.length === 0 ?
-                            <div className="flex flex-column layout-centered mt4">
-                                <span className="QuestionCircle">!</span>
-                                <div className="text-normal mt3 mb1">
-                                    {t`Hmmm, looks like nothing has happened yet.`}
-                                </div>
-                                <div className="text-normal text-grey-2">
-                                    {t`Save a question and get this baby going!`}
-                                </div>
-                            </div>
-                        :
-                            <ul className="pb4 relative">
-                                {activity.map(item =>
-                                    <li key={item.id} className="mt3">
-                                        <ActivityItem
-                                            item={item}
-                                            description={this.activityHeader(item, user)}
-                                            userColors={this.initialsCssClasses(item.user)}
-                                        />
-                                        <ActivityStory story={this.activityStory(item)} />
-                                    </li>
-                                )}
-                            </ul>
-                        }
-                    </div>
+    return (
+      <LoadingAndErrorWrapper loading={!activity} error={error}>
+        {() => (
+          <div className="full flex flex-column">
+            <div className="">
+              {activity.length === 0 ? (
+                <div className="flex flex-column layout-centered mt4">
+                  <span className="QuestionCircle">!</span>
+                  <div className="text-normal mt3 mb1">
+                    {t`Hmmm, looks like nothing has happened yet.`}
+                  </div>
+                  <div className="text-normal text-grey-2">
+                    {t`Save a question and get this baby going!`}
+                  </div>
                 </div>
-            }
-            </LoadingAndErrorWrapper>
-        );
-    }
+              ) : (
+                <ul className="pb4 relative">
+                  {activity.map(item => (
+                    <li key={item.id} className="mt3">
+                      <ActivityItem
+                        item={item}
+                        description={this.activityHeader(item, user)}
+                        userColors={this.initialsCssClasses(item.user)}
+                      />
+                      <ActivityStory story={this.activityStory(item)} />
+                    </li>
+                  ))}
+                </ul>
+              )}
+            </div>
+          </div>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/home/components/ActivityItem.jsx b/frontend/src/metabase/home/components/ActivityItem.jsx
index 8d7ba0966acc930656e531126128437b1cfd1792..53af487187923da4eb53e214df5a724ea878463a 100644
--- a/frontend/src/metabase/home/components/ActivityItem.jsx
+++ b/frontend/src/metabase/home/components/ActivityItem.jsx
@@ -1,42 +1,45 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import Icon from 'metabase/components/Icon.jsx';
-import IconBorder from 'metabase/components/IconBorder.jsx';
-import UserAvatar from 'metabase/components/UserAvatar.jsx';
+import Icon from "metabase/components/Icon.jsx";
+import IconBorder from "metabase/components/IconBorder.jsx";
+import UserAvatar from "metabase/components/UserAvatar.jsx";
 
 export default class ActivityItem extends Component {
-    static propTypes = {
-        item: PropTypes.object.isRequired,
-        description: PropTypes.object.isRequired,
-        userColors: PropTypes.string
-    };
+  static propTypes = {
+    item: PropTypes.object.isRequired,
+    description: PropTypes.object.isRequired,
+    userColors: PropTypes.string,
+  };
 
-    render() {
-        const { item, description, userColors } = this.props;
+  render() {
+    const { item, description, userColors } = this.props;
 
-        return (
-            <div className="ml1 flex align-center mr2">
-                <span>
-                    { item.user ?
-                        <UserAvatar user={item.user} background={userColors} style={{color: '#fff', borderWidth: '0'}}/>
-                    :
-                        <IconBorder style={{color: '#B8C0C8'}}>
-                            <Icon name='sync' size={16} />
-                        </IconBorder>
-                    }
-                </span>
+    return (
+      <div className="ml1 flex align-center mr2">
+        <span>
+          {item.user ? (
+            <UserAvatar
+              user={item.user}
+              background={userColors}
+              style={{ color: "#fff", borderWidth: "0" }}
+            />
+          ) : (
+            <IconBorder style={{ color: "#B8C0C8" }}>
+              <Icon name="sync" size={16} />
+            </IconBorder>
+          )}
+        </span>
 
-                <div className="ml2 full flex align-center">
-                    <div className="text-grey-4">
-                        <span className="text-dark">{description.userName}</span>&nbsp;
-
-                        {description.summary}
-                    </div>
-                    <div className="flex-align-right text-right text-grey-2">
-                        {description.timeSince}
-                    </div>
-                </div>
-            </div>
-        )
-    }
+        <div className="ml2 full flex align-center">
+          <div className="text-grey-4">
+            <span className="text-dark">{description.userName}</span>&nbsp;
+            {description.summary}
+          </div>
+          <div className="flex-align-right text-right text-grey-2">
+            {description.timeSince}
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/home/components/ActivityStory.jsx b/frontend/src/metabase/home/components/ActivityStory.jsx
index 5c520197a9f9e1c631059ae2e4c6695de42a483f..6b3eecdbfcb748726079ba5ee633ce7aa9b7a273 100644
--- a/frontend/src/metabase/home/components/ActivityStory.jsx
+++ b/frontend/src/metabase/home/components/ActivityStory.jsx
@@ -1,38 +1,51 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 
 export default class ActivityStory extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.styles = {
-            borderWidth: '2px',
-            borderColor: '#DFE8EA',
-        }
-    }
-
-    static propTypes = {
-        story: PropTypes.object.isRequired
-    }
+    this.styles = {
+      borderWidth: "2px",
+      borderColor: "#DFE8EA",
+    };
+  }
 
-    render() {
-        const { story } = this.props;
+  static propTypes = {
+    story: PropTypes.object.isRequired,
+  };
 
-        if (!story.body) {
-            return null;
-        }
+  render() {
+    const { story } = this.props;
 
-        return (
-            <div className="mt1 border-left flex mr2" style={{borderWidth: '3px', marginLeft: '22px', borderColor: '#F2F5F6'}}>
-                <div className="flex full ml4 bordered rounded p2" style={this.styles}>
-                    { story.bodyLink ?
-                        <Link to={story.bodyLink} data-metabase-event={"Activity Feed;Story Clicked;"+story.topic} className="link">{story.body}</Link>
-                    :
-                        <span>{story.body}</span>
-                    }
-                </div>
-            </div>
-        )
+    if (!story.body) {
+      return null;
     }
+
+    return (
+      <div
+        className="mt1 border-left flex mr2"
+        style={{
+          borderWidth: "3px",
+          marginLeft: "22px",
+          borderColor: "#F2F5F6",
+        }}
+      >
+        <div className="flex full ml4 bordered rounded p2" style={this.styles}>
+          {story.bodyLink ? (
+            <Link
+              to={story.bodyLink}
+              data-metabase-event={"Activity Feed;Story Clicked;" + story.topic}
+              className="link"
+            >
+              {story.body}
+            </Link>
+          ) : (
+            <span>{story.body}</span>
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
index d0a383af5448ef71082d34da71e19d351af090fc..6867bb0b14388a68d06dd0657223f2a68411898a 100644
--- a/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
+++ b/frontend/src/metabase/home/components/NewUserOnboardingModal.jsx
@@ -1,116 +1,118 @@
 /* @flow */
 import React, { Component } from "react";
-import StepIndicators from 'metabase/components/StepIndicators';
-import RetinaImage from 'react-retina-image'
-import { t } from 'c-3po';
+import StepIndicators from "metabase/components/StepIndicators";
+import RetinaImage from "react-retina-image";
+import { t } from "c-3po";
 import MetabaseSettings from "metabase/lib/settings";
 
 type Props = {
-    onClose: () => void,
-}
+  onClose: () => void,
+};
 
 type State = {
-    step: number
-}
+  step: number,
+};
 
 const STEPS = [
-    {
-        title: t`Ask questions and explore`,
-        text: t`Click on charts or tables to explore, or ask a new question using the easy interface or the powerful SQL editor.`,
-        image: (
-            <RetinaImage
-                className="absolute full"
-                style={{ top: 30 }}
-                src={`app/assets/img/welcome-modal-1.png`}
-            />
-        )
-    },
-    {
-        title: t`Make your own charts`,
-        text: t`Create line charts, scatter plots, maps, and more.`,
-        image: (
-            <RetinaImage
-                className="absolute ml-auto mr-auto inline-block left right"
-                style={{ bottom: -20,}}
-                src={`app/assets/img/welcome-modal-2.png`}
-            />
-        )
-    },
-    {
-        title: t`Share what you find`,
-        text: t`Create powerful and flexible dashboards, and send regular updates via email or Slack.`,
-        image: (
-            <RetinaImage
-                className="absolute ml-auto mr-auto inline-block left right"
-                style={{ bottom: -30 }}
-                src={`app/assets/img/welcome-modal-3.png`}
-            />
-        )
-    },
-]
-
+  {
+    title: t`Ask questions and explore`,
+    text: t`Click on charts or tables to explore, or ask a new question using the easy interface or the powerful SQL editor.`,
+    image: (
+      <RetinaImage
+        className="absolute full"
+        style={{ top: 30 }}
+        src={`app/assets/img/welcome-modal-1.png`}
+      />
+    ),
+  },
+  {
+    title: t`Make your own charts`,
+    text: t`Create line charts, scatter plots, maps, and more.`,
+    image: (
+      <RetinaImage
+        className="absolute ml-auto mr-auto inline-block left right"
+        style={{ bottom: -20 }}
+        src={`app/assets/img/welcome-modal-2.png`}
+      />
+    ),
+  },
+  {
+    title: t`Share what you find`,
+    text: t`Create powerful and flexible dashboards, and send regular updates via email or Slack.`,
+    image: (
+      <RetinaImage
+        className="absolute ml-auto mr-auto inline-block left right"
+        style={{ bottom: -30 }}
+        src={`app/assets/img/welcome-modal-3.png`}
+      />
+    ),
+  },
+];
 
 export default class NewUserOnboardingModal extends Component {
+  props: Props;
+  state: State = {
+    step: 1,
+  };
 
-    props: Props
-    state: State = {
-        step: 1
-    }
-
-    nextStep = () => {
-        const stepCount = MetabaseSettings.get("has_sample_dataset") ? 3 : 2
-        const nextStep = this.state.step + 1;
+  nextStep = () => {
+    const stepCount = MetabaseSettings.get("has_sample_dataset") ? 3 : 2;
+    const nextStep = this.state.step + 1;
 
-        if (nextStep <= stepCount) {
-            this.setState({ step: nextStep });
-        } else {
-            this.props.onClose();
-        }
+    if (nextStep <= stepCount) {
+      this.setState({ step: nextStep });
+    } else {
+      this.props.onClose();
     }
+  };
 
-    render() {
-        const { step } = this.state;
-        const currentStep = STEPS[step -1]
+  render() {
+    const { step } = this.state;
+    const currentStep = STEPS[step - 1];
 
-        return (
-            <div>
-                <OnboardingImages
-                    currentStep={currentStep}
-                />
-                <div className="p4 pb3 text-centered">
-                    <h2>{currentStep.title}</h2>
-                    <p className="ml-auto mr-auto text-paragraph" style={{ maxWidth: 420 }}>
-                        {currentStep.text}
-                    </p>
-                    <div className="flex align-center py2 relative">
-                        <div className="ml-auto mr-auto">
-                            <StepIndicators
-                                currentStep={step}
-                                steps={STEPS}
-                                goToStep={step => this.setState({ step })}
-                            />
-                        </div>
-                        <a
-                            className="link flex-align-right text-bold absolute right"
-                            onClick={() => (this.nextStep())}
-                        >
-                            { step === 3 ? t`Let's go` : t`Next` }
-                        </a>
-                    </div>
-                </div>
+    return (
+      <div>
+        <OnboardingImages currentStep={currentStep} />
+        <div className="p4 pb3 text-centered">
+          <h2>{currentStep.title}</h2>
+          <p
+            className="ml-auto mr-auto text-paragraph"
+            style={{ maxWidth: 420 }}
+          >
+            {currentStep.text}
+          </p>
+          <div className="flex align-center py2 relative">
+            <div className="ml-auto mr-auto">
+              <StepIndicators
+                currentStep={step}
+                steps={STEPS}
+                goToStep={step => this.setState({ step })}
+              />
             </div>
-        );
-    }
+            <a
+              className="link flex-align-right text-bold absolute right"
+              onClick={() => this.nextStep()}
+            >
+              {step === 3 ? t`Let's go` : t`Next`}
+            </a>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
 
-const OnboardingImages = ({ currentStep }, { currentStep: object }) =>
-    <div style={{
-        position: 'relative',
-        backgroundColor: '#F5F9FE',
-        borderBottom: '1px solid #DCE1E4',
-        height: 254,
-        paddingTop: '3em',
-        paddingBottom: '3em'
-    }}>
-        { currentStep.image }
-    </div>
+const OnboardingImages = ({ currentStep }, { currentStep: object }) => (
+  <div
+    style={{
+      position: "relative",
+      backgroundColor: "#F5F9FE",
+      borderBottom: "1px solid #DCE1E4",
+      height: 254,
+      paddingTop: "3em",
+      paddingBottom: "3em",
+    }}
+  >
+    {currentStep.image}
+  </div>
+);
diff --git a/frontend/src/metabase/home/components/NextStep.jsx b/frontend/src/metabase/home/components/NextStep.jsx
index 8318612c3ed76526202b6e7a0b945a4969d9d61e..3ed896b50c1f1f9552305f42391a0924b9674b73 100644
--- a/frontend/src/metabase/home/components/NextStep.jsx
+++ b/frontend/src/metabase/home/components/NextStep.jsx
@@ -1,46 +1,53 @@
 import React, { Component } from "react";
 import { Link } from "react-router";
-import { t } from 'c-3po'
+import { t } from "c-3po";
 
 import { SetupApi } from "metabase/services";
 
 import SidebarSection from "./SidebarSection.jsx";
 
 export default class NextStep extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            next: null
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      next: null,
+    };
+  }
 
-    async componentWillMount() {
-        const sections = await SetupApi.admin_checklist(null, { noEvent: true });
-        for (let section of sections) {
-            for (let task of section.tasks) {
-                if (task.is_next_step) {
-                    this.setState({ next: task });
-                    break;
-                }
-            }
+  async componentWillMount() {
+    const sections = await SetupApi.admin_checklist(null, { noEvent: true });
+    for (let section of sections) {
+      for (let task of section.tasks) {
+        if (task.is_next_step) {
+          this.setState({ next: task });
+          break;
         }
+      }
     }
+  }
 
-    render() {
-        const { next } = this.state;
-        if (next) {
-            return (
-                <SidebarSection title={t`Setup Tip`} icon="info" extra={
-                    <Link to="/admin/settings" className="text-brand no-decoration">{t`View all`}</Link>
-                }>
-                    <Link to={next.link} className="block p3 no-decoration">
-                        <h4 className="text-brand text-bold">{next.title}</h4>
-                        <p className="m0 mt1">{next.description}</p>
-                    </Link>
-                </SidebarSection>
-            )
-        } else {
-            return <span className="hide" />
-        }
+  render() {
+    const { next } = this.state;
+    if (next) {
+      return (
+        <SidebarSection
+          title={t`Setup Tip`}
+          icon="info"
+          extra={
+            <Link
+              to="/admin/settings"
+              className="text-brand no-decoration"
+            >{t`View all`}</Link>
+          }
+        >
+          <Link to={next.link} className="block p3 no-decoration">
+            <h4 className="text-brand text-bold">{next.title}</h4>
+            <p className="m0 mt1">{next.description}</p>
+          </Link>
+        </SidebarSection>
+      );
+    } else {
+      return <span className="hide" />;
     }
+  }
 }
diff --git a/frontend/src/metabase/home/components/RecentViews.jsx b/frontend/src/metabase/home/components/RecentViews.jsx
index 083622575f50b7a3b69ebbebe0ea4616b0eb9e40..6696cf92a6e4827ba4259f969cd26a6a9a8503f8 100644
--- a/frontend/src/metabase/home/components/RecentViews.jsx
+++ b/frontend/src/metabase/home/components/RecentViews.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po'
+import { t } from "c-3po";
 
 import Icon from "metabase/components/Icon.jsx";
 import SidebarSection from "./SidebarSection.jsx";
@@ -10,59 +10,71 @@ import * as Urls from "metabase/lib/urls";
 import { normal } from "metabase/lib/colors";
 
 export default class RecentViews extends Component {
-    static propTypes = {
-        fetchRecentViews: PropTypes.func.isRequired,
-        recentViews: PropTypes.array.isRequired
-    }
+  static propTypes = {
+    fetchRecentViews: PropTypes.func.isRequired,
+    recentViews: PropTypes.array.isRequired,
+  };
 
-    static defaultProps = {
-        recentViews: []
-    }
+  static defaultProps = {
+    recentViews: [],
+  };
 
-    async componentDidMount() {
-        this.props.fetchRecentViews();
-    }
+  async componentDidMount() {
+    this.props.fetchRecentViews();
+  }
 
-    getIconName({ model, model_object}) {
-        if (model === 'card' && 'display' in model_object) {
-            return model_object.display
-        } else if(model === 'dashboard') {
-            return 'dashboard'
-        } else {
-            return null;
-        }
+  getIconName({ model, model_object }) {
+    if (model === "card" && "display" in model_object) {
+      return model_object.display;
+    } else if (model === "dashboard") {
+      return "dashboard";
+    } else {
+      return null;
     }
+  }
 
-    render() {
-        const { recentViews } = this.props;
-        return (
-            <SidebarSection title={t`Recently Viewed`} icon="clock">
-                {recentViews.length > 0 ?
-                    <ul className="p2">
-                        {recentViews.map((item, index) => {
-                            const iconName = this.getIconName(item);
-                            return (
-                                <li key={index} className="py1 ml1 flex align-center clearfix">
-                                    <Icon
-                                        name={iconName}
-                                        size={18}
-                                        style={{ color: iconName === 'dashboard' ? normal.purple : normal.blue }}
-                                    />
-                                    <Link to={Urls.modelToUrl(item.model, item.model_id)} data-metabase-event={"Recent Views;"+item.model+";"+item.cnt} className="ml1 flex-full link">
-                                        {item.model_object.name}
-                                    </Link>
-                                </li>
-                            );
-                        })}
-                    </ul>
-                :
-                    <div className="flex flex-column layout-centered text-normal text-grey-2">
-                        <p className="p3 text-centered text-grey-2" style={{ "maxWidth": "100%" }}>
-                            {t`You haven't looked at any dashboards or questions recently`}
-                        </p>
-                    </div>
-                }
-            </SidebarSection>
-        );
-    }
+  render() {
+    const { recentViews } = this.props;
+    return (
+      <SidebarSection title={t`Recently Viewed`} icon="clock">
+        {recentViews.length > 0 ? (
+          <ul className="p2">
+            {recentViews.map((item, index) => {
+              const iconName = this.getIconName(item);
+              return (
+                <li key={index} className="py1 ml1 flex align-center clearfix">
+                  <Icon
+                    name={iconName}
+                    size={18}
+                    style={{
+                      color:
+                        iconName === "dashboard" ? normal.purple : normal.blue,
+                    }}
+                  />
+                  <Link
+                    to={Urls.modelToUrl(item.model, item.model_id)}
+                    data-metabase-event={
+                      "Recent Views;" + item.model + ";" + item.cnt
+                    }
+                    className="ml1 flex-full link"
+                  >
+                    {item.model_object.name}
+                  </Link>
+                </li>
+              );
+            })}
+          </ul>
+        ) : (
+          <div className="flex flex-column layout-centered text-normal text-grey-2">
+            <p
+              className="p3 text-centered text-grey-2"
+              style={{ maxWidth: "100%" }}
+            >
+              {t`You haven't looked at any dashboards or questions recently`}
+            </p>
+          </div>
+        )}
+      </SidebarSection>
+    );
+  }
 }
diff --git a/frontend/src/metabase/home/components/SidebarSection.jsx b/frontend/src/metabase/home/components/SidebarSection.jsx
index cfbd4537bf6a9a9e1473e5c7eb08418f0addc884..2816ac1887252d6a0625b80c54efbf2c8e012145 100644
--- a/frontend/src/metabase/home/components/SidebarSection.jsx
+++ b/frontend/src/metabase/home/components/SidebarSection.jsx
@@ -2,16 +2,17 @@ import React from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const SidebarSection = ({ title, icon, extra, children }) =>
-    <div className="px2 pt1">
-        <div className="text-dark-grey clearfix pt2 pb2">
-            <Icon className="float-left" name={icon} size={18}></Icon>
-            <span className="pl1 Sidebar-header">{title}</span>
-            { extra && <span className="float-right">{extra}</span>}
-        </div>
-        <div className="rounded bg-white" style={{border: '1px solid #E5E5E5'}}>
-            { children }
-        </div>
+const SidebarSection = ({ title, icon, extra, children }) => (
+  <div className="px2 pt1">
+    <div className="text-dark-grey clearfix pt2 pb2">
+      <Icon className="float-left" name={icon} size={18} />
+      <span className="pl1 Sidebar-header">{title}</span>
+      {extra && <span className="float-right">{extra}</span>}
     </div>
+    <div className="rounded bg-white" style={{ border: "1px solid #E5E5E5" }}>
+      {children}
+    </div>
+  </div>
+);
 
 export default SidebarSection;
diff --git a/frontend/src/metabase/home/components/Smile.jsx b/frontend/src/metabase/home/components/Smile.jsx
index 08b5adba0920c8b70d1c8bee26b94ded345c5357..79c1c09127c4efb5670655748b8c701191ac0eec 100644
--- a/frontend/src/metabase/home/components/Smile.jsx
+++ b/frontend/src/metabase/home/components/Smile.jsx
@@ -1,12 +1,12 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 
 export default class Smile extends Component {
-    render() {
-        const styles = {
-            width: '48px',
-            height: '48px',
-            backgroundImage: 'url("app/assets/img/smile.svg")',
-        }
-        return <div style={styles} className="hide md-show"></div>
-    }
+  render() {
+    const styles = {
+      width: "48px",
+      height: "48px",
+      backgroundImage: 'url("app/assets/img/smile.svg")',
+    };
+    return <div style={styles} className="hide md-show" />;
+  }
 }
diff --git a/frontend/src/metabase/home/containers/HomepageApp.jsx b/frontend/src/metabase/home/containers/HomepageApp.jsx
index 025e6720434334869de20b1d2b459b6a3322b164..048b9baccb1b215aeda14834a33ae75d069f8222 100644
--- a/frontend/src/metabase/home/containers/HomepageApp.jsx
+++ b/frontend/src/metabase/home/containers/HomepageApp.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po'
+import { t } from "c-3po";
 import { push } from "react-router-redux";
 
 import Greeting from "metabase/lib/greeting";
@@ -9,97 +9,90 @@ import Modal from "metabase/components/Modal";
 
 import Activity from "../components/Activity";
 import RecentViews from "../components/RecentViews";
-import Smile from '../components/Smile';
-import NewUserOnboardingModal from '../components/NewUserOnboardingModal';
+import Smile from "../components/Smile";
+import NewUserOnboardingModal from "../components/NewUserOnboardingModal";
 import NextStep from "../components/NextStep";
 
 import * as homepageActions from "../actions";
 import { getActivity, getRecentViews, getUser } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    activity:       getActivity(state),
-    recentViews:    getRecentViews(state),
-    user:           getUser(state),
-    showOnboarding: "new" in props.location.query
-})
-
+  activity: getActivity(state),
+  recentViews: getRecentViews(state),
+  user: getUser(state),
+  showOnboarding: "new" in props.location.query,
+});
 
 const mapDispatchToProps = {
-    ...homepageActions,
-    onChangeLocation: push
-}
-
+  ...homepageActions,
+  onChangeLocation: push,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class HomepageApp extends Component {
+  static propTypes = {
+    onChangeLocation: PropTypes.func.isRequired,
+    showOnboarding: PropTypes.bool.isRequired,
+    user: PropTypes.object.isRequired,
+    // TODO - these should be used by their call sites rather than passed
+    activity: PropTypes.array,
+    fetchActivity: PropTypes.func.isRequired,
 
-    static propTypes = {
-        onChangeLocation: PropTypes.func.isRequired,
-        showOnboarding: PropTypes.bool.isRequired,
-        user: PropTypes.object.isRequired,
-        // TODO - these should be used by their call sites rather than passed
-        activity: PropTypes.array,
-        fetchActivity: PropTypes.func.isRequired,
+    recentViews: PropTypes.array,
+    fetchRecentViews: PropTypes.func.isRequired,
+  };
 
-        recentViews: PropTypes.array,
-        fetchRecentViews: PropTypes.func.isRequired
+  constructor(props) {
+    super(props);
+    this.state = {
+      greeting: Greeting.sayHello(props.user && props.user.first_name),
+      onboarding: props.showOnboarding,
     };
+  }
 
-    constructor(props) {
-        super(props)
-        this.state = {
-            greeting: Greeting.sayHello(props.user && props.user.first_name),
-            onboarding: props.showOnboarding,
-        }
-    }
+  completeOnboarding() {
+    this.setState({ onboarding: false });
+  }
 
-    completeOnboarding() {
-        this.setState({ onboarding: false });
-    }
+  render() {
+    const { user } = this.props;
 
-    render() {
-        const { user } = this.props;
+    return (
+      <div className="full">
+        {this.state.onboarding ? (
+          <Modal>
+            <NewUserOnboardingModal
+              user={user}
+              onClose={() => this.completeOnboarding()}
+            />
+          </Modal>
+        ) : null}
 
-        return (
-            <div className="full">
-                { this.state.onboarding ?
-                    <Modal>
-                        <NewUserOnboardingModal
-                            user={user}
-                            onClose={() => (this.completeOnboarding())}
-                        />
-                    </Modal>
-                : null }
-
-                <div className="bg-white md-bg-brand text-brand md-text-white md-pl4">
-                    <div className="HomepageGreeting">
-                        <div className="Layout-mainColumn">
-                            <header className="flex align-center px2 py3 md-pb4">
-                                <Smile />
-                                <div className="h1 text-bold md-ml2">
-                                    {this.state.greeting}
-                                </div>
-                            </header>
-                        </div>
-                    </div>
-                </div>
-                <div className="flex">
-                    <div className="wrapper">
-                        <div className="Layout-mainColumn pl2">
-                            <div className="md-pt4 h3 md-h2">
-                                {t`Activity`}
-                            </div>
-                            <Activity {...this.props} />
-                        </div>
-                    </div>
-                    <div className="Layout-sidebar flex-no-shrink hide sm-show">
-                        <div>
-                            <NextStep />
-                            <RecentViews {...this.props} />
-                        </div>
-                    </div>
-                </div>
+        <div className="bg-white md-bg-brand text-brand md-text-white md-pl4">
+          <div className="HomepageGreeting">
+            <div className="Layout-mainColumn">
+              <header className="flex align-center px2 py3 md-pb4">
+                <Smile />
+                <div className="h1 text-bold md-ml2">{this.state.greeting}</div>
+              </header>
+            </div>
+          </div>
+        </div>
+        <div className="flex">
+          <div className="wrapper">
+            <div className="Layout-mainColumn pl2">
+              <div className="md-pt4 h3 md-h2">{t`Activity`}</div>
+              <Activity {...this.props} />
+            </div>
+          </div>
+          <div className="Layout-sidebar flex-no-shrink hide sm-show">
+            <div>
+              <NextStep />
+              <RecentViews {...this.props} />
             </div>
-        );
-    }
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/home/reducers.js b/frontend/src/metabase/home/reducers.js
index d837790d9527340d17640702791addad92796e85..9c645f5fe4fd3f358cb0f37a715273fa16393b26 100644
--- a/frontend/src/metabase/home/reducers.js
+++ b/frontend/src/metabase/home/reducers.js
@@ -1,16 +1,17 @@
-import { handleActions } from 'redux-actions';
+import { handleActions } from "redux-actions";
 
-import {
-    FETCH_ACTIVITY,
-    FETCH_RECENT_VIEWS
-} from './actions';
+import { FETCH_ACTIVITY, FETCH_RECENT_VIEWS } from "./actions";
 
+export const activity = handleActions(
+  {
+    [FETCH_ACTIVITY]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
 
-
-export const activity = handleActions({
-    [FETCH_ACTIVITY]: { next: (state, { payload }) => payload }
-}, null);
-
-export const recentViews = handleActions({
-	[FETCH_RECENT_VIEWS]: { next: (state, { payload }) => payload }
-}, []);
+export const recentViews = handleActions(
+  {
+    [FETCH_RECENT_VIEWS]: { next: (state, { payload }) => payload },
+  },
+  [],
+);
diff --git a/frontend/src/metabase/home/selectors.js b/frontend/src/metabase/home/selectors.js
index 45f2d4420af81b47c25bb8aaa6498873b79d8179..035fa516a385b4fcd14cca705f4124ccfbf0a7bf 100644
--- a/frontend/src/metabase/home/selectors.js
+++ b/frontend/src/metabase/home/selectors.js
@@ -1,3 +1,3 @@
-export const getActivity 		= (state) => state.home && state.home.activity
-export const getRecentViews 	= (state) => state.home && state.home.recentViews
-export const getUser 			= (state) => state.currentUser
+export const getActivity = state => state.home && state.home.activity;
+export const getRecentViews = state => state.home && state.home.recentViews;
+export const getUser = state => state.currentUser;
diff --git a/frontend/src/metabase/icon_paths.js b/frontend/src/metabase/icon_paths.js
index a64e9b9174157ef0c0a814726f176914392a74b1..fc929f165a8b8d535931c00d229f842947e98522 100644
--- a/frontend/src/metabase/icon_paths.js
+++ b/frontend/src/metabase/icon_paths.js
@@ -8,310 +8,433 @@
 */
 
 export var ICON_PATHS = {
-    add: 'M19,13 L19,2 L14,2 L14,13 L2,13 L2,18 L14,18 L14,30 L19,30 L19,18 L30,18 L30,13 L19,13 Z',
-    addtodash: {
-        path: 'M21,23 L16,23 L16,27 L21,27 L21,32 L25,32 L25,27 L30,27 L30,23 L25,23 L25,18 L21,18 L21,23 Z M32,7 L32,14 L28,14 L28,8 L0,8 L0,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 L4,0 L28,0 C30.209139,-4.05812251e-16 32,1.790861 32,4 L32,7 Z M0,8 L4,8 L4,28 L0,28 L0,8 Z M0,28 L12,28 L12,32 L4,32 C1.790861,32 2.705415e-16,30.209139 0,28 Z',
-        attrs: { fillRule: "evenodd" }
-    },
-    alert: {
-        path: 'M14.677 7.339c-4.77.562-5.23 4.75-5.23 7.149 0 2.576 0 3.606-.53 4.121-.352.344-1.058.515-2.117.515V21.7h18v-2.576c-1.059 0-1.588 0-2.118-.515-.353-.343-.53-2.06-.53-5.151-.316-3.705-2.06-5.745-5.23-6.12a1.52 1.52 0 0 0 .466-1.093c0-.853-.71-1.545-1.588-1.545-.877 0-1.588.692-1.588 1.545 0 .427.178.814.465 1.094zM16.05 0c2.473 0 5.57 1.851 6.22 4.12 3.057 1.58 4.868 4.503 5.223 8.706l.013.158v.157c0 .905.014 1.682.042 2.327H30.6V25.73H1.5V15.468h3.091c.002-.326.003-.725.003-1.222 0-2.308.316-4.322 1.26-6.233.881-1.784 2.223-2.988 3.976-3.893C10.48 1.85 13.576 0 16.05 0zM13.1 25.8c.25 1.6 1.166 2.4 2.75 2.4s2.5-.8 2.75-2.4h-5.5zm-4.35-3.16h14.191l-.586 3.261c-.497 3.607-2.919 6.001-6.51 6.001-3.59 0-6.012-2.394-6.508-6L8.75 22.64z',
-        attrs: { fillRule: 'nonzero' }
-    },
-    alertConfirm: {
-      path:'M24.326 7.184a9.604 9.604 0 0 0-.021-.034c-.876-1.39-2.056-2.47-3.518-3.19-.509-2.269-2.51-3.96-4.9-3.96-2.361 0-4.344 1.652-4.881 3.88C7.113 5.63 5.68 9.55 5.68 14.424c0 .88-.003 1.473-.01 1.902H2.8v9.605h26.175v-9.602h-3.297v6.257H6.097V19.67c1.152 0 1.92-.194 2.304-.583.576-.583.576-1.75.576-4.664 0-2.716.5-7.456 5.69-8.091a1.754 1.754 0 0 1-.507-1.238c0-.966.773-1.749 1.727-1.749.955 0 1.728.783 1.728 1.75 0 .483-.194.92-.507 1.237 2.2.27 3.768 1.308 4.705 3.112.037-.04.874-.793 2.513-2.26zm-11.312 18.7H9.741C10.214 29.398 12.48 32 15.887 32c3.409 0 5.674-2.602 6.147-6.116H18.76c-.27 1.911-1.228 2.77-2.874 2.77-1.645 0-2.603-.859-2.873-2.77zm.297-12.466l2.504-2.707 3.819 4.106 7.653-8.254L29.8 9.38 19.636 20.295l-6.325-6.877z',
-      attrs: { fillRule: 'nonzero'}
-    },
-    all: 'M30.595 13.536c1.85.755 1.879 2.05.053 2.9l-11.377 5.287c-1.82.846-4.763.858-6.583.022L1.344 16.532c-1.815-.835-1.785-2.131.05-2.89l1.637-.677 8.977 4.125c2.194 1.009 5.74.994 7.934-.026l9.022-4.193 1.63.665zm-1.63 7.684l1.63.666c1.85.755 1.879 2.05.053 2.898l-11.377 5.288c-1.82.847-4.763.859-6.583.022L1.344 24.881c-1.815-.834-1.785-2.131.05-2.89l1.637-.677 8.977 4.126c2.194 1.008 5.74.993 7.934-.026l9.022-4.194zM12.686 1.576c1.843-.762 4.834-.77 6.687-.013l11.22 4.578c1.85.755 1.88 2.05.054 2.899l-11.377 5.288c-1.82.846-4.763.858-6.583.022L1.344 9.136c-1.815-.834-1.785-2.13.05-2.89l11.293-4.67z',
-    archive: {
-        path: 'M27.557 1v2.356a1 1 0 0 1-1 1H5.443a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h21.114a1 1 0 0 1 1 1zM4.356 26.644h23.288v-15.57H4.356v15.57zM32 8.718V29a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.718a2 2 0 0 1 2-2h28a2 2 0 0 1 2 2zM16.116 25.076l5.974-6.57h-3.983V12.93h-3.982v5.575h-3.982l5.973 6.571z',
-        attrs: { fillRule: "evenodd" }
-    },
-    area: 'M31.154 28.846l.852.004V8.64l-1.15 2.138-6.818 6.37c-.13.122-9.148 1.622-9.148 1.622l-.545.096-.383.4-7.93 8.31-1.016 1.146 2.227.017 23.91.107L7.25 28.74l7.93-8.31 9.615-1.684 7.211-6.737v15.984a.855.855 0 0 1-.852.854zM0 28.74l11.79-13.362 11.788-3.369 8.077-8.07c.194-.193.351-.128.351.15V28.85L0 28.74z',
-    attachment: {
-        path: "M22.162 8.704c.029 8.782-.038 14.123-.194 15.926-.184 2.114-2.922 4.322-5.9 4.322-3.06 0-5.542-1.98-5.836-4.376-.294-2.392-.195-14.266.01-18.699.077-1.661 1.422-2.83 3.548-2.83 2.067 0 3.488 1.335 3.594 3.164.06 1.052.074 3.49.053 7.107-.006.928-.013 1.891-.023 3.072l-.023 2.527c-.006.824-.01 1.358-.01 1.718 0 1.547-.39 2.011-1.475 2.011-.804 0-1.202-.522-1.202-1.38V8.699a1.524 1.524 0 0 0-3.048 0v12.567c0 2.389 1.554 4.428 4.25 4.428 2.897 0 4.523-1.934 4.523-5.06 0-.348.003-.875.01-1.691l.022-2.526c.01-1.184.018-2.15.024-3.082.021-3.697.008-6.155-.058-7.3C20.227 2.592 17.469 0 13.79 0c-3.695 0-6.438 2.382-6.593 5.737-.213 4.613-.312 16.585.01 19.21C7.697 28.94 11.53 32 16.067 32c4.482 0 8.61-3.327 8.937-7.106.168-1.935.235-7.302.206-16.2a1.524 1.524 0 0 0-3.048.01z",
-        attrs: { fillRule: 'nonzero'}
-    },
-    backArrow: 'M11.7416687,19.0096 L18.8461178,26.4181004 L14.2696969,30.568 L0.38960831,16.093881 L0,15.6875985 L0.49145276,15.241949 L14.6347557,1 L19.136,5.22693467 L11.3214393,13.096 L32,13.096 L32,19.0096 L11.7416687,19.0096 Z',
-    bar: 'M2 23.467h6.4V32H2v-8.533zm10.667-12.8h6.4V32h-6.4V10.667zM23.333 0h6.4v32h-6.4V0z',
-    beaker: 'M4.31736354,31.1631075 C3.93810558,30.6054137 3.89343681,29.6635358 4.20559962,29.0817181 L11.806982,14.9140486 L11.8069821,10.5816524 L10.7015144,10.4653256 C10.0309495,10.394763 9.48734928,9.78799739 9.48734928,9.12166999 L9.48734928,7.34972895 C9.48734928,6.67821106 10.0368737,6.13383825 10.7172248,6.13383825 L21.8462005,6.13383825 C22.525442,6.13383825 23.0760761,6.68340155 23.0760761,7.34972895 L23.0760761,9.12166999 C23.0760761,9.79318788 22.5250158,10.3375607 21.856025,10.3375607 L20.9787023,10.3375607 L20.9787024,14.9281806 L28.77277,29.0827118 C29.0983515,29.6739888 29.0709073,30.6193105 28.7174156,31.1846409 L28.852457,30.9686726 C28.4963041,31.538259 27.6541076,32 26.9865771,32 L6.10749779,32 C5.43315365,32 4.58248747,31.5529687 4.19978245,30.9902061 L4.31736354,31.1631075 Z M15.5771418,17.6040443 C16.5170398,17.6040443 17.2789777,16.8377777 17.2789777,15.89254 C17.2789777,14.9473023 16.5170398,14.1810358 15.5771418,14.1810358 C14.6372438,14.1810358 13.8753059,14.9473023 13.8753059,15.89254 C13.8753059,16.8377777 14.6372438,17.6040443 15.5771418,17.6040443 Z M16.5496195,12.8974079 C17.8587633,12.8974079 18.9200339,11.830108 18.9200339,10.5135268 C18.9200339,9.1969457 17.8587633,8.1296458 16.5496195,8.1296458 C15.2404758,8.1296458 14.1792052,9.1969457 14.1792052,10.5135268 C14.1792052,11.830108 15.2404758,12.8974079 16.5496195,12.8974079 Z M5.71098553,30.2209651 L10.9595331,20.5151267 C10.9595331,20.5151267 12.6834557,21.2672852 14.3734184,21.2672852 C16.0633811,21.2672852 16.8198616,19.2872624 17.588452,18.6901539 C18.3570425,18.0930453 19.9467191,17.1113296 19.9467191,17.1113296 L27.0506095,30.1110325 L5.71098553,30.2209651 Z M13.6608671,4.37817079 C14.4114211,4.37817079 15.0198654,3.78121712 15.0198654,3.04483745 C15.0198654,2.30845779 14.4114211,1.71150412 13.6608671,1.71150412 C12.9103132,1.71150412 12.3018689,2.30845779 12.3018689,3.04483745 C12.3018689,3.78121712 12.9103132,4.37817079 13.6608671,4.37817079 Z M17.9214578,2.45333328 C18.6119674,2.45333328 19.1717361,1.90413592 19.1717361,1.22666664 C19.1717361,0.549197362 18.6119674,0 17.9214578,0 C17.2309481,0 16.6711794,0.549197362 16.6711794,1.22666664 C16.6711794,1.90413592 17.2309481,2.45333328 17.9214578,2.45333328 Z',
-    breakout: 'M24.47 1H32v7.53h-7.53V1zm0 11.294H32v7.53h-7.53v-7.53zm0 11.294H32v7.53h-7.53v-7.53zM0 1h9.412v30.118H0V1zm11.731 13.714c.166-.183.452-.177.452-.177h6.475s-1.601-2.053-2.07-2.806c-.469-.753-.604-1.368 0-1.905.603-.536 1.226-.281 1.878.497.652.779 2.772 3.485 3.355 4.214.583.73.65 1.965 0 2.835-.65.87-2.65 4.043-3.163 4.65-.514.607-1.123.713-1.732.295-.609-.419-.838-1.187-.338-1.872.5-.684 2.07-3.073 2.07-3.073h-6.475s-.27 0-.46-.312-.151-.612-.151-.612l.007-1.246s-.014-.306.152-.488z',
-    bubble: 'M18.155 20.882c-5.178-.638-9.187-5.051-9.187-10.402C8.968 4.692 13.66 0 19.448 0c5.789 0 10.48 4.692 10.48 10.48 0 3.05-1.302 5.797-3.383 7.712a7.127 7.127 0 1 1-8.39 2.69zm-6.392 10.14a2.795 2.795 0 1 1 0-5.59 2.795 2.795 0 0 1 0 5.59zm-6.079-6.288a4.541 4.541 0 1 1 0-9.083 4.541 4.541 0 0 1 0 9.083z',
-    burger: 'M2.5 3.6h27a2.5 2.5 0 1 1 0 5h-27a2.5 2.5 0 0 1 0-5zm0 9.931h27a2.5 2.5 0 1 1 0 5h-27a2.5 2.5 0 1 1 0-5zm0 9.931h27a2.5 2.5 0 1 1 0 5h-27a2.5 2.5 0 0 1 0-5z',
-    calendar: {
-        path: 'M21,2 L21,0 L18,0 L18,2 L6,2 L6,0 L3,0 L3,2 L2.99109042,2 C1.34177063,2 0,3.34314575 0,5 L0,6.99502651 L0,20.009947 C0,22.2157067 1.78640758,24 3.99005301,24 L20.009947,24 C22.2157067,24 24,22.2135924 24,20.009947 L24,6.99502651 L24,5 C24,3.34651712 22.6608432,2 21.0089096,2 L21,2 L21,2 Z M22,8 L22,20.009947 C22,21.1099173 21.1102431,22 20.009947,22 L3.99005301,22 C2.89008272,22 2,21.1102431 2,20.009947 L2,8 L22,8 L22,8 Z M6,12 L10,12 L10,16 L6,16 L6,12 Z',
-        attrs: { viewBox: '0 0 24 24'}
-    },
-    check: 'M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z ',
-    chevrondown: 'M1 12 L16 26 L31 12 L27 8 L16 18 L5 8 z ',
-    chevronleft: 'M20 1 L24 5 L14 16 L24 27 L20 31 L6 16 z',
-    chevronright: 'M12 1 L26 16 L12 31 L8 27 L18 16 L8 5 z ',
-    chevronup: 'M1 20 L16 6 L31 20 L27 24 L16 14 L5 24 z',
-    clipboard: 'M8.54667751,5.50894675 L6.00494659,5.50894675 C4.89702623,5.50894675 4,6.40070914 4,7.50075379 L4,30.0171397 C4,31.1120596 4.89764516,32.0089468 6.00494659,32.0089468 L25.9950534,32.0089468 C27.1029738,32.0089468 28,31.1171844 28,30.0171397 L28,7.50075379 C28,6.40583387 27.1023548,5.50894675 25.9950534,5.50894675 L23.5373296,5.50894675 L23.5373296,3.0446713 L19.9106557,3.0446713 C19.9106557,3.0446713 19.6485834,8.05825522e-08 16.0837607,0 C12.518938,-8.05825523e-08 12.1644547,3.04776207 12.1644547,3.04776207 L8.57253264,3.04776207 L8.54667751,5.50894675 Z M23.5373296,7.50894675 L26,7.50894675 L26,30.0089468 L6,30.0089468 L6,7.50894675 L8.52566721,7.50894675 L8.4996301,9.98745456 L23.5373296,9.98745456 L23.5373296,7.50894675 Z M10.573037,5.01478303 L13.9861608,5.01478303 L13.9861608,3.76128231 C13.9861608,3.76128231 14.0254332,1.94834752 16.0135743,1.94834752 C18.0017155,1.94834752 18.0017156,3.7055821 18.0017156,3.7055821 L18.0017156,4.94060459 L21.4955568,4.94060459 L21.4955568,8.03924122 L10.5173901,8.03924122 L10.573037,5.01478303 Z M16,5.00894675 C16.5522847,5.00894675 17,4.5612315 17,4.00894675 C17,3.456662 16.5522847,3.00894675 16,3.00894675 C15.4477153,3.00894675 15,3.456662 15,4.00894675 C15,4.5612315 15.4477153,5.00894675 16,5.00894675 Z M8.5,18.0089468 L8.5,21.0082323 L11.5,21.0082323 L11.5,18.0446111 L8.5,18.0089468 Z M8.5,23.0089468 L8.5,26.0082323 L11.5,26.0082323 L11.5,23.0446111 L8.5,23.0089468 Z M8.5,13.0089468 L8.5,16.0082323 L11.5,16.0082323 L11.5,13.0446111 L8.5,13.0089468 Z M13.5,13.0193041 L13.5,16 L23.5,16 L23.5,13 L13.5,13.0193041 Z M13.5,23.0193041 L13.5,26 L23.5,26 L23.5,23 L13.5,23.0193041 Z M13.5,18.0193041 L13.5,21 L23.5,21 L23.5,18 L13.5,18.0193041 Z',
-    clock: 'M16 0 A16 16 0 0 0 0 16 A16 16 0 0 0 16 32 A16 16 0 0 0 32 16 A16 16 0 0 0 16 0 M16 4 A12 12 0 0 1 28 16 A12 12 0 0 1 16 28 A12 12 0 0 1 4 16 A12 12 0 0 1 16 4 M14 6 L14 17.25 L22 22 L24.25 18.5 L18 14.75 L18 6z',
-    clone: {
-        path: 'M12,11 L16,11 L16,0 L5,0 L5,3 L12,3 L12,11 L12,11 Z M0,4 L11,4 L11,15 L0,15 L0,4 Z',
-        attrs: { viewBox: '0 0 16 15' }
-    },
-    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',
-    compare: {
-        path: 'M8.514 23.486C3.587 21.992 0 17.416 0 12 0 5.373 5.373 0 12 0c5.415 0 9.992 3.587 11.486 8.514C28.413 10.008 32 14.584 32 20c0 6.627-5.373 12-12 12-5.415 0-9.992-3.587-11.486-8.514zm2.293.455A10.003 10.003 0 0 0 20 30c5.523 0 10-4.477 10-10 0-4.123-2.496-7.664-6.059-9.193.04.392.059.79.059 1.193 0 6.627-5.373 12-12 12-.403 0-.8-.02-1.193-.059z',
-        attrs: {
-            fillRule: 'nonzero'
-        }
-    },
-    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'
-    },
-    connections: {
-        path: 'M5.37815706,11.5570815 C5.55061975,11.1918363 5.64705882,10.783651 5.64705882,10.3529412 C5.64705882,9.93118218 5.55458641,9.53102128 5.38881053,9.1716274 L11.1846365,4.82475792 C11.6952189,5.33295842 12.3991637,5.64705882 13.1764706,5.64705882 C14.7358628,5.64705882 16,4.38292165 16,2.82352941 C16,1.26413718 14.7358628,0 13.1764706,0 C11.6170784,0 10.3529412,1.26413718 10.3529412,2.82352941 C10.3529412,3.2452884 10.4454136,3.64544931 10.6111895,4.00484319 L10.6111895,4.00484319 L4.81536351,8.35171266 C4.3047811,7.84351217 3.60083629,7.52941176 2.82352941,7.52941176 C1.26413718,7.52941176 0,8.79354894 0,10.3529412 C0,11.9123334 1.26413718,13.1764706 2.82352941,13.1764706 C3.59147157,13.1764706 4.28780867,12.8698929 4.79682555,12.3724528 L10.510616,16.0085013 C10.408473,16.3004758 10.3529412,16.6143411 10.3529412,16.9411765 C10.3529412,18.5005687 11.6170784,19.7647059 13.1764706,19.7647059 C14.7358628,19.7647059 16,18.5005687 16,16.9411765 C16,15.3817842 14.7358628,14.1176471 13.1764706,14.1176471 C12.3029783,14.1176471 11.5221273,14.5142917 11.0042049,15.1372938 L5.37815706,11.5570815 Z',
-        attrs: { viewBox: '0 0 16 19.7647' }
-    },
-    contract: 'M18.0015892,0.327942852 L18.0015892,14 L31.6736463,14 L26.6544389,8.98079262 L32,3.63523156 L28.3647684,0 L23.0192074,5.34556106 L18.0015892,0.327942852 Z M14,31.6720571 L14,18 L0.327942852,18 L5.34715023,23.0192074 L0.00158917013,28.3647684 L3.63682073,32 L8.98238179,26.6544389 L14,31.6720571 Z',
-    copy: {
-        path: "M10.329 6.4h-3.33c-.95 0-1.72.77-1.72 1.72v.413h17.118V8.12c0-.941-.77-1.719-1.72-1.719h-3.31l-1.432-1.705a2.137 2.137 0 0 0-2.097-2.562 2.137 2.137 0 0 0-2.054 2.73L10.329 6.4zm12.808 4.267h1.4v8.557h-4.42l.111-4.188-5.981 6.064 5.805 6.264v-3.966h4.485v6.469H3.14v-19.2h19.997zm3.54 12.731v6.888c0 .947-.769 1.714-1.725 1.714H2.725C1.772 32 1 31.23 1 30.286V5.981c0-.947.768-1.714 1.725-1.714h6.834A4.273 4.273 0 0 1 13.839 0c2.363 0 4.279 1.91 4.279 4.267h6.834c.953 0 1.725.77 1.725 1.714v13.243H31v4.174h-4.323zM5.279 12.8h10.7v2.133h-10.7V12.8zm0 4.267h5.564V19.2H5.279v-2.133zm0 4.266h5.564v2.134H5.279v-2.134zm0 4.267h8.56v2.133h-8.56V25.6z",
-        attrs: { fillRule: "evenodd" }
-    },
-    cursor_move: 'M14.8235294,14.8235294 L14.8235294,6.58823529 L17.1764706,6.58823529 L17.1764706,14.8235294 L25.4117647,14.8235294 L25.4117647,17.1764706 L17.1764706,17.1764706 L17.1764706,25.4117647 L14.8235294,25.4117647 L14.8235294,17.1764706 L6.58823529,17.1764706 L6.58823529,14.8235294 L14.8235294,14.8235294 L14.8235294,14.8235294 Z M16,0 L20.1176471,6.58823529 L11.8823529,6.58823529 L16,0 Z M11.8823529,25.4117647 L20.1176471,25.4117647 L16,32 L11.8823529,25.4117647 Z M32,16 L25.4117647,20.1176471 L25.4117647,11.8823529 L32,16 Z M6.58823529,11.8823529 L6.58823529,20.1176471 L0,16 L6.58823529,11.8823529 Z',
-    cursor_resize: 'M17.4017952,6.81355995 L15.0488541,6.81355995 L15.0488541,25.6370894 L17.4017952,25.6370894 L17.4017952,6.81355995 Z M16.2253247,0.225324657 L20.3429717,6.81355995 L12.1076776,6.81355995 L16.2253247,0.225324657 Z M12.1076776,25.6370894 L20.3429717,25.6370894 L16.2253247,32.2253247 L12.1076776,25.6370894 Z',
-    costapproximate: 'M27 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM16 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 22a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM5 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6z',
-    costexact: 'M27 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM16 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 22a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM5 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm11 0a3 3 0 1 1 0-6 3 3 0 0 1 0 6z',
-    costextended: 'M27,19 C25.3431458,19 24,17.6568542 24,16 C24,14.3431458 25.3431458,13 27,13 C28.6568542,13 30,14.3431458 30,16 C30,17.6568542 28.6568542,19 27,19 Z M16,8 C14.3431458,8 13,6.65685425 13,5 C13,3.34314575 14.3431458,2 16,2 C17.6568542,2 19,3.34314575 19,5 C19,6.65685425 17.6568542,8 16,8 Z M16,30 C14.3431458,30 13,28.6568542 13,27 C13,25.3431458 14.3431458,24 16,24 C17.6568542,24 19,25.3431458 19,27 C19,28.6568542 17.6568542,30 16,30 Z M5,19 C3.34314575,19 2,17.6568542 2,16 C2,14.3431458 3.34314575,13 5,13 C6.65685425,13 8,14.3431458 8,16 C8,17.6568542 6.65685425,19 5,19 Z M16,19 C14.3431458,19 13,17.6568542 13,16 C13,14.3431458 14.3431458,13 16,13 C17.6568542,13 19,14.3431458 19,16 C19,17.6568542 17.6568542,19 16,19 Z M10,12 C8.8954305,12 8,11.1045695 8,10 C8,8.8954305 8.8954305,8 10,8 C11.1045695,8 12,8.8954305 12,10 C12,11.1045695 11.1045695,12 10,12 Z M22,12 C20.8954305,12 20,11.1045695 20,10 C20,8.8954305 20.8954305,8 22,8 C23.1045695,8 24,8.8954305 24,10 C24,11.1045695 23.1045695,12 22,12 Z M22,24 C20.8954305,24 20,23.1045695 20,22 C20,20.8954305 20.8954305,20 22,20 C23.1045695,20 24,20.8954305 24,22 C24,23.1045695 23.1045695,24 22,24 Z M10,24 C8.8954305,24 8,23.1045695 8,22 C8,20.8954305 8.8954305,20 10,20 C11.1045695,20 12,20.8954305 12,22 C12,23.1045695 11.1045695,24 10,24 Z',
-    database: 'M1.18285296e-08,10.5127919 C-1.47856568e-08,7.95412848 1.18285298e-08,4.57337284 1.18285298e-08,4.57337284 C1.18285298e-08,4.57337284 1.58371041,5.75351864e-10 15.6571342,0 C29.730558,-5.7535027e-10 31.8900148,4.13849684 31.8900148,4.57337284 L31.8900148,10.4843058 C31.8900148,10.4843058 30.4448001,15.1365942 16.4659751,15.1365944 C2.48715012,15.1365947 2.14244494e-08,11.4353349 1.18285296e-08,10.5127919 Z M0.305419478,21.1290071 C0.305419478,21.1290071 0.0405133833,21.2033291 0.0405133833,21.8492606 L0.0405133833,27.3032816 C0.0405133833,27.3032816 1.46515486,31.941655 15.9641228,31.941655 C30.4630908,31.941655 32,27.3446712 32,27.3446712 C32,27.3446712 32,21.7986104 32,21.7986105 C32,21.2073557 31.6620557,21.0987647 31.6620557,21.0987647 C31.6620557,21.0987647 29.7146434,25.22314 16.0318829,25.22314 C2.34912233,25.22314 0.305419478,21.1290071 0.305419478,21.1290071 Z M0.305419478,12.656577 C0.305419478,12.656577 0.0405133833,12.730899 0.0405133833,13.3768305 L0.0405133833,18.8308514 C0.0405133833,18.8308514 1.46515486,23.4692249 15.9641228,23.4692249 C30.4630908,23.4692249 32,18.8722411 32,18.8722411 C32,18.8722411 32,13.3261803 32,13.3261803 C32,12.7349256 31.6620557,12.6263346 31.6620557,12.6263346 C31.6620557,12.6263346 29.7146434,16.7507099 16.0318829,16.7507099 C2.34912233,16.7507099 0.305419478,12.656577 0.305419478,12.656577 Z',
-    dashboard: 'M32,29 L32,4 L32,0 L0,0 L0,8 L28,8 L28,28 L4,28 L4,8 L0,8 L0,29.5 L0,32 L32,32 L32,29 Z M7.27272727,18.9090909 L17.4545455,18.9090909 L17.4545455,23.2727273 L7.27272727,23.2727273 L7.27272727,18.9090909 Z M7.27272727,12.0909091 L24.7272727,12.0909091 L24.7272727,16.4545455 L7.27272727,16.4545455 L7.27272727,12.0909091 Z M20.3636364,18.9090909 L24.7272727,18.9090909 L24.7272727,23.2727273 L20.3636364,23.2727273 L20.3636364,18.9090909 Z',
-    curve: 'M3.033 3.791v22.211H31.09c.403 0 .882.872.882 1.59 0 .717-.48 1.408-.882 1.408H0V3.791c0-.403.875-.914 1.487-.914.612 0 1.546.511 1.546.914zm3.804 17.912C5.714 21.495 5 20.318 5 19.355c0-.963.831-2.296 1.837-2.296 2.093 0 2.965-1.207 4.204-5.242l.148-.482C12.798 6.077 14.18 3 17.968 3c3.792 0 5.17 3.08 6.765 8.343l.145.478c1.227 4.034 2.093 5.238 4.181 5.238 1.006 0 1.875 1.29 1.875 2.296 0 1.007-.898 2.184-1.875 2.348-3.656.612-6.004-2.364-7.665-7.821l-.146-.482c-1.14-3.76-1.8-6.754-3.28-6.754-1.483 0-2.147 2.995-3.297 6.754l-.148.486c-1.675 5.454-3.93 8.514-7.686 7.817z',
-    document: 'M29,10.1052632 L29,28.8325291 C29,30.581875 27.5842615,32 25.8337327,32 L7.16626728,32 C5.41758615,32 4,30.5837102 4,28.8441405 L4,3.15585953 C4,1.41292644 5.42339685,9.39605581e-15 7.15970573,8.42009882e-15 L20.713352,8.01767853e-16 L20.713352,8.42105263 L22.3846872,8.42105263 L22.3846872,0.310375032 L28.7849894,8.42105263 L20.713352,8.42105263 L20.713352,10.1052632 L29,10.1052632 Z M7.3426704,12.8000006 L25.7273576,12.8000006 L25.7273576,14.4842112 L7.3426704,14.4842112 L7.3426704,12.8000006 Z M7.3426704,17.3473687 L25.7273576,17.3473687 L25.7273576,19.0315793 L7.3426704,19.0315793 L7.3426704,17.3473687 Z M7.3426704,21.8947352 L25.7273576,21.8947352 L25.7273576,23.5789458 L7.3426704,23.5789458 L7.3426704,21.8947352 Z M7.43137255,26.2736849 L16.535014,26.2736849 L16.535014,27.9578954 L7.43137255,27.9578954 L7.43137255,26.2736849 Z',
-    downarrow: 'M12.2782161,19.3207547 L12.2782161,0 L19.5564322,0 L19.5564322,19.3207547 L26.8346484,19.3207547 L15.9173242,32 L5,19.3207547 L12.2782161,19.3207547 Z',
-    download: 'M19.3636364,15.0588235 L19.3636364,0 L13.6363636,0 L13.6363636,15.0588235 L7.90909091,15.0588235 L16.5,24.9411765 L25.0909091,15.0588235 L19.3636364,15.0588235 Z M27,26.3529412 L27,32 L6,32 L6,26.3529412 L27,26.3529412 Z',
-    editdocument: 'M19.27 20.255l-5.642 2.173 1.75-6.085L28.108 3.45 32 7.363 19.27 20.255zM20.442 6.9l-2.044-2.049H4.79v23.29h18.711v-6.577l4.787-4.83V31a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h18.024a1 1 0 0 1 .711.297L23.85 3.45 20.442 6.9z',
-    ellipsis: {
-        path: 'M26.1111111,19 C27.7066004,19 29,17.6568542 29,16 C29,14.3431458 27.7066004,13 26.1111111,13 C24.5156218,13 23.2222222,14.3431458 23.2222222,16 C23.2222222,17.6568542 24.5156218,19 26.1111111,19 Z M5.88888889,19 C7.48437817,19 8.77777778,17.6568542 8.77777778,16 C8.77777778,14.3431458 7.48437817,13 5.88888889,13 C4.29339961,13 3,14.3431458 3,16 C3,17.6568542 4.29339961,19 5.88888889,19 Z M16,19 C17.5954893,19 18.8888889,17.6568542 18.8888889,16 C18.8888889,14.3431458 17.5954893,13 16,13 C14.4045107,13 13.1111111,14.3431458 13.1111111,16 C13.1111111,17.6568542 14.4045107,19 16,19 Z',
-        attrs: { width: 32, height: 32 }
-    },
-    embed: 'M12.734 9.333L6.099 16l6.635 6.667a2.547 2.547 0 0 1 0 3.59 2.518 2.518 0 0 1-3.573 0L.74 17.795a2.547 2.547 0 0 1 0-3.59L9.16 5.743a2.518 2.518 0 0 1 3.573 0 2.547 2.547 0 0 1 0 3.59zm6.527 13.339l6.64-6.71-6.63-6.623a2.547 2.547 0 0 1-.01-3.59 2.518 2.518 0 0 1 3.573-.01l8.42 8.412c.99.988.995 2.596.011 3.59l-8.42 8.51a2.518 2.518 0 0 1-3.574.01 2.547 2.547 0 0 1-.01-3.59z',
-    emojiactivity: 'M4.58360576,27.9163942 C7.44354038,30.7763289 17.5452495,30.2077351 24.1264923,23.6264923 C30.7077351,17.0452495 31.2763289,6.94354038 28.4163942,4.08360576 C25.5564596,1.22367115 15.4547505,1.79226488 8.87350769,8.37350769 C2.29226488,14.9547505 1.72367115,25.0564596 4.58360576,27.9163942 Z M18.0478143,6.51491123 C17.0327353,6.95492647 16.0168474,7.462507 15.0611336,8.03487206 C13.9504884,8.70002358 12.9907793,9.41185633 12.2241295,10.1785061 C11.6753609,10.7272747 11.1524326,11.3411471 10.6544469,12.0086598 C9.95174829,12.950575 9.33131183,13.9528185 8.79588947,14.9547475 C8.47309082,15.5587964 8.24755055,16.0346972 8.12501633,16.3206104 L10.6033735,17.3827634 C10.6970997,17.1640688 10.8900635,16.7569059 11.1739957,16.2255873 C11.6497914,15.335237 12.2007659,14.4452012 12.8156543,13.6209891 C13.2395954,13.0527276 13.6791325,12.5367492 14.1307526,12.0851292 C14.7228522,11.4930296 15.5113715,10.9081712 16.4465129,10.3481268 C17.2918299,9.84187694 18.205618,9.38530978 19.1202149,8.98885142 C19.6674377,8.75164195 20.0881481,8.58915552 20.3167167,8.50897949 L19.4242138,5.96460106 C19.1382021,6.06492664 18.6583971,6.25023653 18.0478143,6.51491123 Z',
-    emojifood: 'M14.9166667,8.71296296 L14.9166667,4.85648148 L14.9636504,4.84865086 L18.8123012,1 L21.149682,3.33738075 L18.2222222,6.26484052 L18.2222222,8.71296296 L27.037037,8.71296296 L27.037037,10.9166667 L24.6968811,10.9166667 L22.2407407,30.75 L10.3148148,30.75 L7.36744639,10.9166667 L5,10.9166667 L5,8.71296296 L14.9166667,8.71296296 Z',
-    emojiflags: 'M14.4,17.8888889 L7.9,17.8888889 L7.9,28.9485494 C7.9,30.0201693 7.0344636,30.8888889 5.95,30.8888889 C4.87304474,30.8888889 4,30.0243018 4,28.9485494 L4,2.99442095 C4,2.44521742 4.44737959,2 5.00434691,2 L7.25,2 L19.1921631,2 C20.8138216,2 22.135741,3.27793211 22.1977269,4.88888889 L29.0004187,4.88888889 C29.5524722,4.88888889 30,5.33043204 30,5.88281005 L30,17.7705198 C30,19.4313825 28.6564509,20.7777778 26.9921631,20.7777778 L14.4,20.7777778 L14.4,17.8888889 Z',
-    emojinature: 'M19.0364085,24.2897898 L29.4593537,24.2897898 C30.0142087,24.2897898 30.2190588,23.9075249 29.9310364,23.4359772 L17.0209996,2.29978531 C16.7300287,1.82341061 16.2660004,1.82823762 15.9779779,2.29978531 L3.06794114,23.4359772 C2.77697033,23.9123519 2.9910982,24.2897898 3.53962381,24.2897898 L13.962569,24.2897898 L13.962569,31.5582771 L16.5093674,31.5582771 L19.0364085,31.5582771 L19.0364085,24.2897898 Z',
-    mojiobjects: 'M10.4444444,25.2307692 L10.4444444,20.0205203 C7.76447613,18.1576095 6,14.9850955 6,11.3846154 C6,5.64935068 10.4771525,1 16,1 C21.5228475,1 26,5.64935068 26,11.3846154 C26,14.9850955 24.2355239,18.1576095 21.5555556,20.0205203 L21.5555556,25.2307692 C21.5555556,28.4170274 19.0682486,31 16,31 C12.9317514,31 10.4444444,28.4170274 10.4444444,25.2307692 Z',
-    emojipeople: 'M16,31 C24.2842712,31 31,24.2842712 31,16 C31,7.71572875 24.2842712,1 16,1 C7.71572875,1 1,7.71572875 1,16 C1,24.2842712 7.71572875,31 16,31 Z M22.9642857,19.2142857 C22.9642857,22.3818012 19.8462688,24.9495798 16,24.9495798 C12.1537312,24.9495798 9.03571429,22.3818012 9.03571429,19.2142857 L22.9642857,19.2142857 Z M19.75,14.9285714 C20.6376005,14.9285714 21.3571429,13.2496392 21.3571429,11.1785714 C21.3571429,9.10750362 20.6376005,7.42857143 19.75,7.42857143 C18.8623995,7.42857143 18.1428571,9.10750362 18.1428571,11.1785714 C18.1428571,13.2496392 18.8623995,14.9285714 19.75,14.9285714 Z M12.25,14.9285714 C13.1376005,14.9285714 13.8571429,13.2496392 13.8571429,11.1785714 C13.8571429,9.10750362 13.1376005,7.42857143 12.25,7.42857143 C11.3623995,7.42857143 10.6428571,9.10750362 10.6428571,11.1785714 C10.6428571,13.2496392 11.3623995,14.9285714 12.25,14.9285714 Z',
-    emojisymbols: 'M15.915,6.8426232 L13.8540602,4.78168347 C10.8078936,1.73551684 5.86858639,1.73495294 2.82246208,4.78107725 C-0.217463984,7.82100332 -0.223390824,12.7662163 2.82306829,15.8126754 L15.6919527,28.6815598 L15.915,28.4585125 L16.1380473,28.6815598 L29.0069316,15.8126754 C32.0533907,12.7662163 32.0474639,7.82100332 29.0075378,4.78107725 C25.9614135,1.73495294 21.0221063,1.73551684 17.9759397,4.78168347 L15.915,6.8426232 Z',
-    emojitravel: 'M21.5273864,25.0150018 L21.5273864,1.91741484 C21.5273864,0.0857778237 20.049926,-1.38499821 18.2273864,-1.38499821 C16.4085552,-1.38499821 14.9273864,0.0935424793 14.9273864,1.91741484 L14.9273864,25.0150018 L11.6273864,28.3150018 L24.8273864,28.3150018 L21.5273864,25.0150018 Z M1.72738636,18.4150018 L14.9273864,5.21500179 L14.9273864,15.7750018 L1.72738636,23.6950018 L1.72738636,18.4150018 Z M34.7273864,18.4150018 L21.5273864,5.21500179 L21.5273864,15.7750018 L34.7273864,23.6950018 L34.7273864,18.4150018 Z',
-    empty: ' ',
-    enterorreturn: 'M6.81 16.784l6.14-4.694a1.789 1.789 0 0 0 .341-2.49 1.748 1.748 0 0 0-2.464-.344L.697 17a1.788 1.788 0 0 0-.01 2.826l10.058 7.806c.77.598 1.875.452 2.467-.326a1.79 1.79 0 0 0-.323-2.492l-5.766-4.475h23.118c.971 0 1.759-.796 1.759-1.777V6.777C32 5.796 31.212 5 30.24 5c-.971 0-1.759.796-1.759 1.777v10.007H6.811z',
-    expand: 'M29,13.6720571 L29,8.26132482e-16 L15.3279429,8.64083276e-16 L20.3471502,5.01920738 L15.0015892,10.3647684 L18.6368207,14 L23.9823818,8.65443894 L29,13.6720571 Z M0.00158917013,15.3279429 L0.00158917013,29 L13.6736463,29 L8.65443894,23.9807926 L14,18.6352316 L10.3647684,15 L5.01920738,20.3455611 L0.00158917013,15.3279429 Z',
-    expandarrow: 'M16.429 28.429L.429 5.57h32z',
-    external: 'M13.7780693,4.44451732 L5.1588494,4.44451732 C2.32615959,4.44451732 0,6.75504816 0,9.60367661 L0,25.1192379 C0,27.9699171 2.30950226,30.2783972 5.1588494,30.2783972 L18.9527718,30.2783972 C21.7854617,30.2783972 24.1116212,27.9678664 24.1116212,25.1192379 L24.1116212,19.9448453 L20.6671039,19.9448453 L20.6671039,25.1192379 C20.6671039,26.0662085 19.882332,26.8338799 18.9527718,26.8338799 L5.1588494,26.8338799 C4.21204994,26.8338799 3.44451732,26.0677556 3.44451732,25.1192379 L3.44451732,9.60367661 C3.44451732,8.656706 4.22928927,7.88903464 5.1588494,7.88903464 L13.7780693,7.88903464 L13.7780693,4.44451732 L13.7780693,4.44451732 Z M30.9990919,14.455325 L30.9990919,1 L17.5437669,1 L22.4834088,5.93964193 L17.2225866,11.2004641 L20.8001918,14.7780693 L26.061014,9.51724709 L30.9990919,14.455325 L30.9990919,14.455325 L30.9990919,14.455325 Z',
-    everything: {
-        path: 'M26.656 17.273a400.336 400.336 0 0 1 2.632.017l.895.008-2.824-9.56c-.122-.411.1-.848.495-.975a.743.743 0 0 1 .936.516l3.177 10.752.033.23V31H0V17.994L3.303 7.212a.743.743 0 0 1 .94-.507c.394.132.611.57.486.982L1.763 17.37h3.486V9.758c0-.865.669-1.558 1.505-1.558h18.398c.83 0 1.504.69 1.504 1.56v7.513zm-1.497 0h-.176c-.362.002-.648.006-.853.012a7.264 7.264 0 0 0-.272.011c-.056.004-.056.004-.117.011-.058-.001-.058-.001-.355.135-.233.164-.344.37-.452.68-.05.147-.236.807-.287.97-.132.419-.279.775-.469 1.108-.43.752-1.048 1.322-1.959 1.693-.15.062-2.005.093-4.356.056-1.561-.024-4.056-.096-4.053-.095-.92-.185-1.634-.826-2.173-1.818a7.023 7.023 0 0 1-.566-1.4 5.72 5.72 0 0 1-.147-.614.758.758 0 0 0-.738-.652h-1.44V9.76l18.406.002c.003 0 .006 4.98.007 7.51zM1.497 18.931v10.507h29.006V18.863a780.193 780.193 0 0 0-3.023-.025 233.572 233.572 0 0 0-2.489-.003c-.287.001-.524.004-.706.008-.067.23-.17.59-.216.736-.164.52-.352.977-.605 1.42-.594 1.042-1.468 1.846-2.7 2.349-.681.278-8.668.153-9.236.04-1.407-.283-2.456-1.225-3.194-2.582a8.596 8.596 0 0 1-.74-1.874H1.498zM6.982 5.828h18.074c.375 0 .679-.35.679-.78 0-.432-.304-.782-.679-.782H6.982c-.375 0-.678.35-.678.781 0 .431.303.781.678.781zm1.38-3.267h15.28c.369 0 .668-.35.668-.78 0-.431-.299-.781-.668-.781H8.362c-.37 0-.668.35-.668.78 0 .432.299.781.668.781z',
-        attrs: { fillRule: 'evenodd' }
-    },
-    eye: 'M30.622 18.49c-.549.769-1.46 1.86-2.737 3.273-1.276 1.414-2.564 2.614-3.866 3.602-2.297 1.757-4.963 2.635-8 2.635-3.062 0-5.741-.878-8.038-2.635-1.302-.988-2.59-2.188-3.866-3.602-1.276-1.413-2.188-2.504-2.737-3.272-.549-.769-.9-1.277-1.053-1.524-.433-.63-.433-1.276 0-1.934.128-.247.472-.755 1.034-1.524.561-.768 1.48-1.852 2.756-3.252 1.276-1.4 2.564-2.593 3.866-3.581C10.303 4.892 12.982 4 16.019 4c3.011 0 5.678.892 8 2.676 1.302.988 2.59 2.182 3.866 3.581 1.276 1.4 2.195 2.484 2.756 3.252.562.769.906 1.277 1.034 1.524.433.63.433 1.276 0 1.934-.153.247-.504.755-1.053 1.524zm-1.516-3.214c-.248.376-.248 1.089.034 1.499l-.11-.16-.088-.17a21.93 21.93 0 0 0-.784-1.121c-.483-.66-1.338-1.67-2.546-2.995-1.154-1.266-2.306-2.333-3.466-3.214-1.781-1.368-3.788-2.04-6.127-2.04-2.365 0-4.385.673-6.179 2.05-1.146.87-2.298 1.938-3.452 3.204-1.208 1.325-2.063 2.334-2.546 2.995a21.93 21.93 0 0 0-.784 1.12l-.075.145-.09.135c.249-.376.249-1.089-.033-1.499l.08.122c.105.17.432.644.941 1.356.466.653 1.313 1.666 2.517 3 1.152 1.275 2.3 2.346 3.451 3.22 1.752 1.339 3.773 2.001 6.17 2.001 2.37 0 4.379-.661 6.14-2.008 1.143-.867 2.291-1.938 3.443-3.214 1.204-1.333 2.05-2.346 2.517-2.999.509-.712.836-1.186.942-1.356l.045-.071zm-17.353 5.663C10.584 19.709 10 18.237 10 16.522c0-1.744.584-3.224 1.753-4.439 1.168-1.215 2.59-1.822 4.268-1.822 1.65 0 3.058.607 4.226 1.822C21.416 13.298 22 14.778 22 16.522c0 1.715-.584 3.187-1.753 4.417-1.168 1.229-2.577 1.844-4.226 1.844-1.677 0-3.1-.615-4.268-1.844zm6.265-2.12c.624-.655.906-1.368.906-2.297 0-.957-.281-1.67-.893-2.307-.592-.616-1.203-.879-2.01-.879-.84 0-1.462.266-2.052.88-.612.636-.893 1.35-.893 2.306 0 .929.282 1.642.906 2.298.59.62 1.207.887 2.039.887.8 0 1.405-.264 1.997-.887z',
-    field: 'M10,1 L22,1 L22,4 L10,4 L10,1 Z M10,6 L22,6 L22,12 L10,12 L10,6 Z M10,14 L22,14 L22,20 L10,20 L10,14 Z M10,22 L22,22 L22,28 L16.1421494,32 L10,28 L10,22 Z',
-    fields: 'M0,0 L7.51851852,0 L7.51851852,3.2 L0,3.2 L0,0 Z M10.7407407,0 L18.2592593,0 L18.2592593,3.2 L10.7407407,3.2 L10.7407407,0 Z M21.4814815,0 L29,0 L29,3.2 L21.4814815,3.2 L21.4814815,0 Z M0,5.33333333 L7.51851852,5.33333333 L7.51851852,29.8666667 L3.85540334,32 L0,29.8666667 L0,5.33333333 Z M10.7407407,5.33333333 L18.2592593,5.33333333 L18.2592593,29.8666667 L14.5690136,32 L10.7407407,29.8666667 L10.7407407,5.33333333 Z M21.4814815,5.33333333 L29,5.33333333 L29,29.8666667 L25.2114718,32 L21.4814815,29.8666667 L21.4814815,5.33333333 Z',
-    filter: {
-    	svg: '<g><path d="M1,12 L17,12 L17,14 L1,14 L1,12 Z M1,7 L17,7 L17,9 L1,9 L1,7 Z M1,2 L17,2 L17,4 L1,4 L1,2 Z" fill="currentcolor"></path><path d="M9,15.5 C10.3807119,15.5 11.5,14.3807119 11.5,13 C11.5,11.6192881 10.3807119,10.5 9,10.5 C7.61928813,10.5 6.5,11.6192881 6.5,13 C6.5,14.3807119 7.61928813,15.5 9,15.5 Z M13,5.5 C14.3807119,5.5 15.5,4.38071187 15.5,3 C15.5,1.61928813 14.3807119,0.5 13,0.5 C11.6192881,0.5 10.5,1.61928813 10.5,3 C10.5,4.38071187 11.6192881,5.5 13,5.5 Z M3,10.5 C4.38071187,10.5 5.5,9.38071187 5.5,8 C5.5,6.61928813 4.38071187,5.5 3,5.5 C1.61928813,5.5 0.5,6.61928813 0.5,8 C0.5,9.38071187 1.61928813,10.5 3,10.5 Z" id="Path" fill="currentcolor"></path><path d="M13,4.5 C12.1715729,4.5 11.5,3.82842712 11.5,3 C11.5,2.17157288 12.1715729,1.5 13,1.5 C13.8284271,1.5 14.5,2.17157288 14.5,3 C14.5,3.82842712 13.8284271,4.5 13,4.5 Z M9,14.5 C8.17157288,14.5 7.5,13.8284271 7.5,13 C7.5,12.1715729 8.17157288,11.5 9,11.5 C9.82842712,11.5 10.5,12.1715729 10.5,13 C10.5,13.8284271 9.82842712,14.5 9,14.5 Z M3,9.5 C2.17157288,9.5 1.5,8.82842712 1.5,8 C1.5,7.17157288 2.17157288,6.5 3,6.5 C3.82842712,6.5 4.5,7.17157288 4.5,8 C4.5,8.82842712 3.82842712,9.5 3,9.5 Z" fill="#FFFFFF"></path></g>',
-        attrs: { viewBox: '0 0 19 16' }
-    },
-    funnel: 'M3.18586974,3.64621479 C2.93075885,3.28932022 3.08031197,3 3.5066208,3 L28.3780937,3 C28.9190521,3 29.0903676,3.34981042 28.7617813,3.77995708 L18.969764,16.5985181 L18.969764,24.3460671 C18.969764,24.8899179 18.5885804,25.5564176 18.133063,25.8254534 C18.133063,25.8254534 12.5698889,29.1260709 12.5673818,28.9963552 C12.4993555,25.4767507 12.5749031,16.7812673 12.5749031,16.7812673 L3.18586974,3.64621479 Z',
-    funneladd: 'M22.5185184,5.27947653 L17.2510286,5.27947653 L17.2510286,9.50305775 L22.5185184,9.50305775 L22.5185184,14.7825343 L26.7325102,14.7825343 L26.7325102,9.50305775 L32,9.50305775 L32,5.27947653 L26.7325102,5.27947653 L26.7325102,0 L22.5185184,0 L22.5185184,5.27947653 Z M14.9369872,0.791920724 C14.9369872,0.791920724 2.77552871,0.83493892 1.86648164,0.83493892 C0.957434558,0.83493892 0.45215388,1.50534608 0.284450368,1.77831828 C0.116746855,2.05129048 -0.317642562,2.91298361 0.398382661,3.9688628 C1.11440788,5.024742 9.74577378,17.8573356 9.74577378,17.8573356 C9.74577378,17.8573356 9.74577394,28.8183645 9.74577378,29.6867194 C9.74577362,30.5550744 9.83306175,31.1834301 10.7557323,31.6997692 C11.6784029,32.2161084 12.4343349,31.9564284 12.7764933,31.7333621 C13.1186517,31.5102958 19.6904355,27.7639669 20.095528,27.4682772 C20.5006204,27.1725875 20.7969652,26.5522071 20.7969651,25.7441659 C20.7969649,24.9361247 20.7969651,18.2224765 20.7969651,18.2224765 L21.6163131,16.9859755 L18.152048,15.0670739 C18.152048,15.0670739 17.3822517,16.199685 17.2562629,16.4000338 C17.1302741,16.6003826 16.8393552,16.9992676 16.8393551,17.7062886 C16.8393549,18.4133095 16.8393551,24.9049733 16.8393551,24.9049733 L13.7519708,26.8089871 C13.7519708,26.8089871 13.7318369,18.3502323 13.7318367,17.820601 C13.7318366,17.2909696 13.8484216,16.6759061 13.2410236,15.87149 C12.6336257,15.0670739 5.59381579,4.76288686 5.59381579,4.76288686 L14.9359238,4.76288686 L14.9369872,0.791920724 Z',
-    funneloutline: {
-        path: 'M3.186 3.646C2.93 3.29 3.08 3 3.506 3h24.872c.541 0 .712.35.384.78L18.97 16.599v7.747c0 .544-.381 1.21-.837 1.48 0 0-5.563 3.3-5.566 3.17-.068-3.52.008-12.215.008-12.215L3.185 3.646z',
-        attrs: {
-            stroke: "currentcolor",
-            strokeWidth: "4",
-            fill: "none",
-            fillRule: "evenodd"
-        }
-    },
-    folder: "M3.96901618e-15,5.41206355 L0.00949677904,29 L31.8821132,29 L31.8821132,10.8928571 L18.2224205,10.8928571 L15.0267944,5.41206355 L3.96901618e-15,5.41206355 Z M16.8832349,5.42402804 L16.8832349,4.52140947 C16.8832349,3.68115822 17.5639241,3 18.4024298,3 L27.7543992,3 L30.36417,3 C31.2031259,3 31.8832341,3.67669375 31.8832341,4.51317691 L31.8832341,7.86669975 L31.8832349,8.5999999 L18.793039,8.5999999 L16.8832349,5.42402804 Z",
-    gear: 'M14 0 H18 L19 6 L20.707 6.707 L26 3.293 L28.707 6 L25.293 11.293 L26 13 L32 14 V18 L26 19 L25.293 20.707 L28.707 26 L26 28.707 L20.707 25.293 L19 26 L18 32 L14 32 L13 26 L11.293 25.293 L6 28.707 L3.293 26 L6.707 20.707 L6 19 L0 18 L0 14 L6 13 L6.707 11.293 L3.293 6 L6 3.293 L11.293 6.707 L13 6 L14 0 z M16 10 A6 6 0 0 0 16 22 A6 6 0 0 0 16 10',
-    grabber: 'M0,5 L32,5 L32,9.26666667 L0,9.26666667 L0,5 Z M0,13.5333333 L32,13.5333333 L32,17.8 L0,17.8 L0,13.5333333 Z M0,22.0666667 L32,22.0666667 L32,26.3333333 L0,26.3333333 L0,22.0666667 Z',
-    grid: 'M2 2 L10 2 L10 10 L2 10z M12 2 L20 2 L20 10 L12 10z M22 2 L30 2 L30 10 L22 10z M2 12 L10 12 L10 20 L2 20z M12 12 L20 12 L20 20 L12 20z M22 12 L30 12 L30 20 L22 20z M2 22 L10 22 L10 30 L2 30z M12 22 L20 22 L20 30 L12 30z M22 22 L30 22 L30 30 L22 30z',
-    google: {
-        svg: '<g fill="none" fill-rule="evenodd"><path d="M16 32c4.32 0 7.947-1.422 10.596-3.876l-5.05-3.91c-1.35.942-3.164 1.6-5.546 1.6-4.231 0-7.822-2.792-9.102-6.65l-5.174 4.018C4.356 28.41 9.742 32 16 32z" fill="#34A853"/><path d="M6.898 19.164A9.85 9.85 0 0 1 6.364 16c0-1.102.196-2.169.516-3.164L1.707 8.818A16.014 16.014 0 0 0 0 16c0 2.578.622 5.013 1.707 7.182l5.19-4.018z" fill="#FBBC05"/><path d="M31.36 16.356c0-1.316-.107-2.276-.338-3.272H16v5.938h8.818c-.178 1.476-1.138 3.698-3.271 5.191l5.049 3.911c3.022-2.79 4.764-6.897 4.764-11.768z" fill="#4285F4"/><path d="M16 6.187c3.004 0 5.031 1.297 6.187 2.382l4.515-4.409C23.93 1.582 20.32 0 16 0 9.742 0 4.338 3.591 1.707 8.818l5.173 4.018c1.298-3.858 4.889-6.65 9.12-6.65z" fill="#EA4335"/></g>'
-    },
-    history: {
-        path: 'M4.03074198,15 C4.54693838,6.62927028 11.4992947,0 20,0 C28.836556,0 36,7.163444 36,16 C36,24.836556 28.836556,32 20,32 C16.9814511,32 14.1581361,31.164104 11.7489039,29.7111608 L14.1120194,26.4586113 C15.8515127,27.4400159 17.8603607,28 20,28 C26.627417,28 32,22.627417 32,16 C32,9.372583 26.627417,4 20,4 C13.7093362,4 8.54922468,8.84046948 8.04107378,15 L11,15 L6,22 L1.34313965,15 L4.03074198,15 Z M22,15.2218254 L24.5913352,17.8131606 L24.5913352,17.8131606 C25.3723838,18.5942092 25.3723838,19.8605392 24.5913352,20.6415878 C23.8176686,21.4152544 22.5633071,21.4152544 21.7896404,20.6415878 C21.7852062,20.6371536 21.7807931,20.6326983 21.7764012,20.6282222 L18.8194549,17.6145768 C18.3226272,17.2506894 18,16.6630215 18,16 L18,10 C18,8.8954305 18.8954305,8 20,8 C21.1045695,8 22,8.8954305 22,10 L22,15.2218254 Z',
-        attrs: { viewBox: "0 0 36 33" }
-    },
-    info: 'M16 0 A16 16 0 0 1 16 32 A16 16 0 0 1 16 0 M19 15 L13 15 L13 26 L19 26 z M16 6 A3 3 0 0 0 16 12 A3 3 0 0 0 16 6',
-    infooutlined: 'M16 29c7.18 0 13-5.82 13-13S23.18 3 16 3 3 8.82 3 16s5.82 13 13 13zm0 3C7.163 32 0 24.837 0 16S7.163 0 16 0s16 7.163 16 16-7.163 16-16 16zm1.697-20h-4.185v14h4.185V12zm.432-3.834c0-.342-.067-.661-.203-.958a2.527 2.527 0 0 0-1.37-1.31 2.613 2.613 0 0 0-.992-.188c-.342 0-.661.062-.959.189a2.529 2.529 0 0 0-1.33 1.309c-.13.297-.195.616-.195.958 0 .334.065.646.196.939.13.292.31.549.54.77.23.22.492.395.79.526.297.13.616.196.958.196.351 0 .682-.066.992-.196.31-.13.583-.306.817-.527a2.47 2.47 0 0 0 .553-.77c.136-.292.203-.604.203-.938z',
-    insight: 'M12.6325203 19.3674797 0 16 12.6325203 12.6325203 16 0 19.3674797 12.6325203 32 16 19.3674797 19.3674797 16 32z',
-    int: {
-        path: 'M15.141,15.512 L14.294,20 L13.051,20 C12.8309989,20 12.6403341,19.9120009 12.479,19.736 C12.3176659,19.5599991 12.237,19.343668 12.237,19.087 C12.237,19.0503332 12.2388333,19.0155002 12.2425,18.9825 C12.2461667,18.9494998 12.2516666,18.9146668 12.259,18.878 L12.908,15.512 L10.653,15.512 L10.015,19.01 C9.94899967,19.3620018 9.79866784,19.6149992 9.564,19.769 C9.32933216,19.9230008 9.06900143,20 8.783,20 L7.584,20 L8.42,15.512 L7.155,15.512 C6.92033216,15.512 6.74066729,15.4551672 6.616,15.3415 C6.49133271,15.2278328 6.429,15.0390013 6.429,14.775 C6.429,14.6723328 6.43999989,14.5550007 6.462,14.423 L6.605,13.554 L8.695,13.554 L9.267,10.518 L6.913,10.518 L7.122,9.385 C7.17333359,9.10633194 7.28699912,8.89916734 7.463,8.7635 C7.63900088,8.62783266 7.92499802,8.56 8.321,8.56 L9.542,8.56 L10.224,5.018 C10.282667,4.7246652 10.4183323,4.49733414 10.631,4.336 C10.8436677,4.17466586 11.0929986,4.094 11.379,4.094 L12.611,4.094 L11.775,8.56 L14.019,8.56 L14.866,4.094 L16.076,4.094 C16.3326679,4.094 16.5416659,4.1673326 16.703,4.314 C16.8643341,4.4606674 16.945,4.64766553 16.945,4.875 C16.945,4.9483337 16.9413334,5.00333315 16.934,5.04 L16.252,8.56 L18.485,8.56 L18.276,9.693 C18.2246664,9.97166806 18.1091676,10.1788327 17.9295,10.3145 C17.7498324,10.4501673 17.4656686,10.518 17.077,10.518 L15.977,10.518 L15.416,13.554 L16.978,13.554 C17.2126678,13.554 17.3904994,13.6108328 17.5115,13.7245 C17.6325006,13.8381672 17.693,14.0306653 17.693,14.302 C17.693,14.4046672 17.6820001,14.5219993 17.66,14.654 L17.528,15.512 L15.141,15.512 Z M10.928,13.554 L13.183,13.554 L13.744,10.518 L11.5,10.518 L10.928,13.554 Z',
-        attrs: { viewBox: '0 0 24 24' }
-    },
-    io: 'M1,9 L6,9 L6,24 L1,24 L1,9 Z M31,16 C31,11.581722 27.418278,8 23,8 C18.581722,8 15,11.581722 15,16 C15,20.418278 18.581722,24 23,24 C27.418278,24 31,20.418278 31,16 Z M19,16 C19,13.790861 20.790861,12 23,12 C25.209139,12 27,13.790861 27,16 C27,18.209139 25.209139,20 23,20 C20.790861,20 19,18.209139 19,16 Z M15.3815029,9 L13.4537572,9 L7,23.5 L8.92774566,23.5 L15.3815029,9 Z',
-    key: {
-        path: 'M11.5035746,7.9975248 C10.8617389,5.26208051 13.0105798,1.44695394 16.9897081,1.44695394 C20.919315,1.44695394 23.1811258,5.37076315 22.2565255,8.42469226 C21.3223229,7.86427598 20.2283376,7.54198814 19.0589133,7.54198814 C17.3567818,7.54198814 15.8144729,8.22477622 14.6920713,9.33083544 C14.4930673,9.31165867 14.2913185,9.30184676 14.087273,9.30184676 C10.654935,9.30184676 7.87247532,12.0782325 7.87247532,15.5030779 C7.87247532,17.1058665 8.48187104,18.5666337 9.48208198,19.6672763 L8.98356958,20.658345 L9.19925633,22.7713505 L7.5350473,23.4587525 C7.37507672,23.5248284 7.30219953,23.707739 7.37031308,23.8681037 L7.95501877,25.2447188 L6.28291833,25.7863476 C6.10329817,25.8445303 6.01548404,26.0233452 6.06755757,26.1919683 L6.54426059,27.7356153 L5.02460911,28.2609385 C4.86686602,28.3154681 4.7743984,28.501653 4.83652351,28.6704172 L6.04508836,31.95351 C6.10987939,32.1295162 6.29662279,32.2151174 6.46814592,32.160881 L9.48965349,31.2054672 C9.66187554,31.1510098 9.86840241,30.9790422 9.95250524,30.8208731 L14.8228902,21.6613229 C15.8820565,21.5366928 16.8596786,21.1462953 17.6869404,20.558796 C17.5652123,20.567429 17.4424042,20.5718139 17.318643,20.5718139 C14.2753735,20.5718139 11.8083161,17.9204625 11.8083161,14.6498548 C11.8083161,12.518229 12.8562751,10.6496514 14.428709,9.60671162 C13.4433608,10.7041074 12.8441157,12.1538355 12.8441157,13.7432193 C12.8441157,16.9974306 15.3562245,19.6661883 18.5509945,19.9240384 L19.1273026,21.5699573 L20.7971002,22.8826221 L20.1355191,24.5572635 C20.0719252,24.7182369 20.1528753,24.8977207 20.3155476,24.9601226 L21.7119724,25.4957977 L20.9400489,27.0748531 C20.8571275,27.2444782 20.9247553,27.4318616 21.082226,27.5115385 L22.5237784,28.2409344 L21.8460256,29.6990003 C21.7756734,29.8503507 21.8453702,30.0462011 22.0099247,30.1187455 L25.2111237,31.5300046 C25.3827397,31.6056621 25.5740388,31.5307937 25.6541745,31.3697345 L27.0658228,28.5325576 C27.1462849,28.3708422 27.1660474,28.1028205 27.1106928,27.9324485 L23.8023823,17.7500271 C24.7201964,16.6692906 25.273711,15.270754 25.273711,13.7432193 C25.273711,12.0364592 24.582689,10.4907436 23.4645818,9.36943333 C25.0880384,5.38579616 22.187534,0 16.9897081,0 C12.1196563,0 9.42801686,4.46934651 10.0266074,7.9975248 L11.5035746,7.9975248 Z M19.0589133,14.7767578 C20.203026,14.7767578 21.1305126,13.8512959 21.1305126,12.7096808 C21.1305126,11.5680656 20.203026,10.6426037 19.0589133,10.6426037 C17.9148007,10.6426037 16.9873141,11.5680656 16.9873141,12.7096808 C16.9873141,13.8512959 17.9148007,14.7767578 19.0589133,14.7767578 Z',
-        attrs: { fillRule: "evenodd" }
-    },
-    label: 'M14.577 31.042a2.005 2.005 0 0 1-2.738-.733L1.707 12.759c-.277-.477-.298-1.265-.049-1.757L6.45 1.537C6.7 1.044 7.35.67 7.9.7l10.593.582c.551.03 1.22.44 1.498.921l10.132 17.55a2.002 2.002 0 0 1-.734 2.737l-14.812 8.552zm.215-22.763a3.016 3.016 0 1 0-5.224 3.016 3.016 3.016 0 0 0 5.224-3.016z',
-    ldap: {
-        path: 'M1.006 3h13.702c.554 0 1.178.41 1.39.915l.363.874c.21.504.827.915 1.376.915h13.169c.54 0 .994.448.994 1.001v20.952a.99.99 0 0 1-.992 1H1.002c-.54 0-.993-.45-.993-1.005l-.01-23.646C0 3.446.45 3 1.007 3zM16.5 19.164c1.944 0 3.52-1.828 3.52-4.082 0-2.254-1.576-4.082-3.52-4.082-1.945 0-3.52 1.828-3.52 4.082 0 2.254 1.575 4.082 3.52 4.082zm6.5 4.665c0-1.872-1.157-3.521-2.913-4.484-.927.97-2.192 1.568-3.587 1.568s-2.66-.597-3.587-1.568C11.157 20.308 10 21.957 10 23.83h13z',
-        attrs: { fillRule: 'evenodd' }
-    },
-    left: "M21,0 L5,16 L21,32 L21,5.47117907e-13 L21,0 Z",
-    lightbulb: 'M16.1 11.594L18.756 8.9a1.03 1.03 0 0 1 1.446-.018c.404.39.412 1.03.018 1.43l-3.193 3.24v4.975c0 .559-.458 1.011-1.022 1.011a1.017 1.017 0 0 1-1.023-1.01v-5.17l-3.003-3.046c-.394-.4-.386-1.04.018-1.43a1.03 1.03 0 0 1 1.446.018l2.657 2.695zM11.03 28.815h9.938a1.01 1.01 0 1 1 0 2.02 376.72 376.72 0 0 0-2.964.002C18.005 31.857 16.767 32 16 32c-.767 0-1.993-.139-1.993-1.163H11.03a1.011 1.011 0 0 1 0-2.022zm0-3.033h9.938a1.011 1.011 0 0 1 0 2.022H11.03a1.011 1.011 0 1 1 0-2.022zM8.487 20.43A11.659 11.659 0 0 1 4.5 11.627C4.5 5.214 9.64 0 16 0s11.5 5.214 11.5 11.627c0 3.43-1.481 6.617-3.987 8.803v1.308c0 1.954-1.601 3.538-3.577 3.538h-7.872c-1.976 0-3.577-1.584-3.577-3.538V20.43zm2.469-1.915l.597.455v2.768c0 .279.23.505.511.505h7.872a.508.508 0 0 0 .51-.505V18.97l.598-.455a8.632 8.632 0 0 0 3.39-6.888c0-4.755-3.785-8.594-8.434-8.594-4.649 0-8.433 3.84-8.433 8.594a8.632 8.632 0 0 0 3.389 6.888z',
-    link: "M12.56 17.04c-1.08 1.384-1.303 1.963 1.755 4.04 3.058 2.076 7.29.143 8.587-1.062 1.404-1.304 4.81-4.697 7.567-7.842 2.758-3.144 1.338-8.238-.715-9.987-5.531-4.71-9.5-.554-11.088.773-2.606 2.176-5.207 5.144-5.207 5.144s1.747-.36 2.784 0c1.036.36 2.102.926 2.102.926l4.003-3.969s2.367-1.907 4.575 0 .674 4.404 0 5.189c-.674.784-6.668 6.742-6.668 6.742s-1.52.811-2.37.811c-.85 0-2.582-.528-2.582-.932 0-.405-1.665-1.22-2.744.166zm7.88-2.08c1.08-1.384 1.303-1.963-1.755-4.04-3.058-2.076-7.29-.143-8.587 1.062-1.404 1.304-4.81 4.697-7.567 7.842-2.758 3.144-1.338 8.238.715 9.987 5.531 4.71 9.5.554 11.088-.773 2.606-2.176 5.207-5.144 5.207-5.144s-1.747.36-2.784 0a17.379 17.379 0 0 1-2.102-.926l-4.003 3.969s-2.367 1.907-4.575 0-.674-4.404 0-5.189c.674-.784 6.668-6.742 6.668-6.742s1.52-.811 2.37-.811c.85 0 2.582.528 2.582.932 0 .405 1.665 1.22 2.744-.166z",
-    line: 'M18.867 16.377l-3.074-3.184-.08.077-.002-.002.01-.01-.53-.528-.066-.07-.001.002-2.071-2.072L-.002 23.645l2.668 2.668 10.377-10.377 3.074 3.183.08-.076.001.003-.008.008.5.501.094.097.002-.001 2.072 2.072L31.912 8.669 29.244 6 18.867 16.377z',
-    list: 'M3 8 A3 3 0 0 0 9 8 A3 3 0 0 0 3 8 M12 6 L28 6 L28 10 L12 10z M3 16 A3 3 0 0 0 9 16 A3 3 0 0 0 3 16 M12 14 L28 14 L28 18 L12 18z M3 24 A3 3 0 0 0 9 24 A3 3 0 0 0 3 24 M12 22 L28 22 L28 26 L12 26z',
-    location: {
-        path: 'M19.4917776,13.9890373 C20.4445763,12.5611169 21,10.8454215 21,9 C21,4.02943725 16.9705627,0 12,0 C7.02943725,0 3,4.02943725 3,9 C3,10.8454215 3.5554237,12.5611168 4.50822232,13.9890371 L4.49999986,14.0000004 L4.58010869,14.0951296 C4.91305602,14.5790657 5.29212089,15.0288088 5.71096065,15.4380163 L12.5,23.5 L19.4999993,13.9999996 L19.4917776,13.9890373 L19.4917776,13.9890373 Z M12,12 C13.6568542,12 15,10.6568542 15,9 C15,7.34314575 13.6568542,6 12,6 C10.3431458,6 9,7.34314575 9,9 C9,10.6568542 10.3431458,12 12,12 Z',
-        attrs: { viewBox: '0 0 24 24' }
-    },
-    lock: {
-        path: 'M7.30894737,12.4444444 L4.91725192,12.4444444 C3.2943422,12.4444444 2,13.7457504 2,15.3509926 L2,29.0934518 C2,30.7017608 3.30609817,32 4.91725192,32 L27.0827481,32 C28.7056578,32 30,30.6986941 30,29.0934518 L30,15.3509926 C30,13.7426837 28.6939018,12.4444444 27.0827481,12.4444444 L24.6910526,12.4444444 L24.6910526,7.44176009 C24.6910526,3.33441301 21.3568185,0 17.2438323,0 L14.7561677,0 C10.6398254,0 7.30894737,3.33178948 7.30894737,7.44176009 L7.30894737,12.4444444 Z M10.8678947,8.21027479 C10.8678947,5.65010176 12.9450109,3.57467145 15.5045167,3.57467145 L16.4954833,3.57467145 C19.0562189,3.57467145 21.1321053,5.65531119 21.1321053,8.21027479 L21.1321053,12.8458781 L10.8678947,12.8458781 L10.8678947,8.21027479 Z M16,26.6666667 C17.9329966,26.6666667 19.5,25.0747902 19.5,23.1111111 C19.5,21.147432 17.9329966,19.5555556 16,19.5555556 C14.0670034,19.5555556 12.5,21.147432 12.5,23.1111111 C12.5,25.0747902 14.0670034,26.6666667 16,26.6666667 Z',
-        attrs: { fillRule: "evenodd" }
-    },
-    lockoutline: 'M7 12H5.546A3.548 3.548 0 0 0 2 15.553v12.894A3.547 3.547 0 0 0 5.546 32h20.908C28.414 32 30 30.41 30 28.447V15.553A3.547 3.547 0 0 0 26.454 12H25V8.99C25 4.029 20.97 0 16 0c-4.972 0-9 4.025-9 8.99V12zm4-3.766c0-2.338 1.89-4.413 4.219-4.634L16 3.525l.781.075C19.111 3.82 21 5.896 21 8.234V12H11V8.234zm-5 9.537C6 16.793 6.796 16 7.775 16h16.45c.98 0 1.775.787 1.775 1.77v8.46c0 .977-.796 1.77-1.775 1.77H7.775A1.77 1.77 0 0 1 6 26.23v-8.46zM16 25a3 3 0 1 0 0-6 3 3 0 0 0 0 6z',
-    mail: 'M1.503 6h28.994C31.327 6 32 6.673 32 7.503v16.06A3.436 3.436 0 0 1 28.564 27H3.436A3.436 3.436 0 0 1 0 23.564V7.504C0 6.673.673 6 1.503 6zm4.403 2.938l10.63 8.052 10.31-8.052H5.906zm-2.9 1.632v11.989c0 .83.674 1.503 1.504 1.503h23.087c.83 0 1.504-.673 1.504-1.503V11.005l-11.666 8.891a1.503 1.503 0 0 1-1.806.013l-12.622-9.34z',
-    mine: 'M28.4907419,50 C25.5584999,53.6578499 21.0527692,56 16,56 C10.9472308,56 6.44150015,53.6578499 3.50925809,50 L28.4907419,50 Z M29.8594823,31.9999955 C27.0930063,27.217587 21.922257,24 16,24 C10.077743,24 4.9069937,27.217587 2.1405177,31.9999955 L29.8594849,32 Z M16,21 C19.8659932,21 23,17.1944204 23,12.5 C23,7.80557963 22,3 16,3 C10,3 9,7.80557963 9,12.5 C9,17.1944204 12.1340068,21 16,21 Z',
-    moon: 'M11.6291702,1.84239429e-11 C19.1234093,1.22958025 24.8413559,7.73631246 24.8413559,15.5785426 C24.8413559,24.2977683 17.7730269,31.3660972 9.05380131,31.3660972 C7.28632096,31.3660972 5.58667863,31.0756481 4,30.5398754 C11.5007933,28.2096945 16.9475786,21.2145715 16.9475786,12.9472835 C16.9475786,7.90001143 14.9174312,3.32690564 11.6291702,1.70246039e-11 L11.6291702,1.84239429e-11 Z',
-    move: 'M23 27h-8v-5h8v-4l8 6-8 7v-4zM17.266 0h.86a2 2 0 0 1 1.42.592L27.49 8.61a2 2 0 0 1 .58 1.407v6h-5.01v-5.065L17.133 5H0V2a2 2 0 0 1 2-2h15.266zM5 27h7v5H2a2 2 0 0 1-2-2V5h5v22z',
-    number: 'M0 .503A.5.5 0 0 1 .503 0h30.994A.5.5 0 0 1 32 .503v30.994a.5.5 0 0 1-.503.503H.503A.5.5 0 0 1 0 31.497V.503zM8.272 22V10.8H6.464c-.064.427-.197.784-.4 1.072-.203.288-.45.52-.744.696a2.984 2.984 0 0 1-.992.368c-.368.07-.75.099-1.144.088v1.712H6V22h2.272zm2.96-5.648c0 1.12.11 2.056.328 2.808.219.752.515 1.352.888 1.8.373.448.808.768 1.304.96a4.327 4.327 0 0 0 1.576.288c.565 0 1.096-.096 1.592-.288a3.243 3.243 0 0 0 1.312-.96c.379-.448.677-1.048.896-1.8.219-.752.328-1.688.328-2.808 0-1.088-.11-2.003-.328-2.744-.219-.741-.517-1.336-.896-1.784a3.243 3.243 0 0 0-1.312-.96 4.371 4.371 0 0 0-1.592-.288c-.555 0-1.08.096-1.576.288-.496.192-.93.512-1.304.96-.373.448-.67 1.043-.888 1.784-.219.741-.328 1.656-.328 2.744zm2.272 0c0-.192.003-.424.008-.696.005-.272.024-.552.056-.84.032-.288.085-.573.16-.856a2.95 2.95 0 0 1 .312-.76 1.67 1.67 0 0 1 .512-.544c.208-.139.467-.208.776-.208.31 0 .57.07.784.208.213.139.39.32.528.544.139.224.243.477.312.76a7.8 7.8 0 0 1 .224 1.696 25.247 25.247 0 0 1-.024 1.856c-.021.453-.088.89-.2 1.312a2.754 2.754 0 0 1-.544 1.08c-.25.299-.61.448-1.08.448-.459 0-.81-.15-1.056-.448a2.815 2.815 0 0 1-.536-1.08 6.233 6.233 0 0 1-.2-1.312c-.021-.453-.032-.84-.032-1.16zm6.624 0c0 1.12.11 2.056.328 2.808.219.752.515 1.352.888 1.8.373.448.808.768 1.304.96a4.327 4.327 0 0 0 1.576.288c.565 0 1.096-.096 1.592-.288a3.243 3.243 0 0 0 1.312-.96c.379-.448.677-1.048.896-1.8.219-.752.328-1.688.328-2.808 0-1.088-.11-2.003-.328-2.744-.219-.741-.517-1.336-.896-1.784a3.243 3.243 0 0 0-1.312-.96 4.371 4.371 0 0 0-1.592-.288c-.555 0-1.08.096-1.576.288-.496.192-.93.512-1.304.96-.373.448-.67 1.043-.888 1.784-.219.741-.328 1.656-.328 2.744zm2.272 0c0-.192.003-.424.008-.696.005-.272.024-.552.056-.84.032-.288.085-.573.16-.856a2.95 2.95 0 0 1 .312-.76 1.67 1.67 0 0 1 .512-.544c.208-.139.467-.208.776-.208.31 0 .57.07.784.208.213.139.39.32.528.544.139.224.243.477.312.76a7.8 7.8 0 0 1 .224 1.696 25.247 25.247 0 0 1-.024 1.856c-.021.453-.088.89-.2 1.312a2.754 2.754 0 0 1-.544 1.08c-.25.299-.61.448-1.08.448-.459 0-.81-.15-1.056-.448a2.815 2.815 0 0 1-.536-1.08 6.233 6.233 0 0 1-.2-1.312c-.021-.453-.032-.84-.032-1.16z',
-    pencil: 'M11.603 29.428a2.114 2.114 0 0 1-.345.111l-8.58 2.381c-1.606.446-3.073-1.047-2.582-2.627l2.482-7.974c.06-.424.25-.834.575-1.164L22.279.633A2.12 2.12 0 0 1 25.257.59l6.103 5.872c.835.803.855 2.124.046 2.952l-18.983 19.41c-.21.256-.486.467-.82.604zm-6.424-2.562l4.605-1.015L21.04 14.185l-3.172-3.043-11.46 11.666-1.229 4.058zM20.733 8.2l3.07 2.942 3.07-3.145-3.07-2.942-3.07 3.145z',
-    permissionsLimited: 'M0,16 C0,7.163444 7.163444,0 16,0 C24.836556,0 32,7.163444 32,16 C32,24.836556 24.836556,32 16,32 C7.163444,32 0,24.836556 0,16 Z M29,16 C29,8.82029825 23.1797017,3 16,3 C8.82029825,3 3,8.82029825 3,16 C3,23.1797017 8.82029825,29 16,29 C23.1797017,29 29,23.1797017 29,16 Z M16,5 C11.0100706,5.11743299 5.14533409,7.90852303 5,15.5 C4.85466591,23.091477 11.0100706,26.882567 16,27 L16,5 Z',
-    pie: 'M15.181 15.435V.021a15.94 15.94 0 0 1 11.42 3.995l-11.42 11.42zm1.131 1.384H31.98a15.941 15.941 0 0 0-4.114-11.553L16.312 16.819zm15.438 2.013H13.168V.25C5.682 1.587 0 8.13 0 16c0 8.837 7.163 16 16 16 7.87 0 14.413-5.682 15.75-13.168z',
-    pinmap: 'M13.4 18.987v8.746L15.533 32l2.134-4.25v-8.763a10.716 10.716 0 0 1-4.267 0zm2.133-1.92a8.533 8.533 0 1 0 0-17.067 8.533 8.533 0 0 0 0 17.067z',
-    popular: 'M23.29 11.224l-7.067 7.067-2.658-2.752.007-.007-.386-.385-.126-.131-.003.002-1.789-1.79L.705 23.793A.994.994 0 0 0 .704 25.2l.896.897a1 1 0 0 0 1.408-.002l8.253-8.252 2.654 2.748.226-.218-.161.161 1.152 1.152c.64.64 1.668.636 2.304 0l8.158-8.159L32 19.933V5H17.067l6.223 6.224z',
-    pulse: 'M16.9862306,27.387699 C17.4904976,29.2137955 20.0148505,29.3806482 20.7550803,27.6368095 L24.8588086,17.9692172 L31.7352165,17.9692172 L31.7352165,13.9692172 L23.5350474,13.9692172 C22.7324769,13.9692172 22.0076375,14.4489743 21.6940431,15.1877423 L19.314793,20.7927967 L14.8067319,4.4678059 C14.3010535,2.63659841 11.7668377,2.47581319 11.033781,4.22842821 L6.99549907,13.8832799 L0,13.8832799 L0,17.8832799 L8.32686781,17.8832799 C9.13327931,17.8832799 9.86080237,17.3989791 10.1719732,16.655022 L12.491241,11.1100437 L16.9862306,27.387699 Z',
-    recents: 'M15.689 17.292l-.689.344V6.992c0-.55.448-.992 1.001-.992h.907c.547 0 1.001.445 1.001.995v9.187l-.372.186 4.362 5.198a1.454 1.454 0 1 1-2.228 1.87L15 17.87l.689-.578zM16 32c8.837 0 16-7.163 16-16S24.837 0 16 0 0 7.163 0 16s7.163 16 16 16z',
-    share: {
-        path: "M13.714 8.96H9.143L16 0l6.857 8.96h-4.571v19.16h-4.572V8.96zM4.571 32.52a4 4 0 0 0 4 4H23.43a4 4 0 0 0 4-4V20.36a4 4 0 0 0-4-4h-.572v-4.48H28a4 4 0 0 1 4 4V37a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V15.88a4 4 0 0 1 4-4h5.143v4.48H8.57a4 4 0 0 0-4 4v12.16z",
-        attrs: { fillRule: 'evenodd', viewBox: '0 0 32 41' }
-    },
-    sql: {
-        path: 'M4,0 L28,0 C30.209139,-4.05812251e-16 32,1.790861 32,4 L32,28 C32,30.209139 30.209139,32 28,32 L4,32 C1.790861,32 2.705415e-16,30.209139 0,28 L0,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 L4,0 Z M6,6 C4.8954305,6 4,6.8954305 4,8 L4,26 C4,27.1045695 4.8954305,28 6,28 L26,28 C27.1045695,28 28,27.1045695 28,26 L28,8 C28,6.8954305 27.1045695,6 26,6 L6,6 Z M14,20 L25,20 L25,24 L14,24 L14,20 Z M14,13.5 L8,17 L8,10 L14,13.5 Z',
-        attrs: { fillRule: 'evenodd' }
-    },
-    progress: {
-        path: 'M0 11.996A3.998 3.998 0 0 1 4.004 8h23.992A4 4 0 0 1 32 11.996v8.008A3.998 3.998 0 0 1 27.996 24H4.004A4 4 0 0 1 0 20.004v-8.008zM22 11h3.99A3.008 3.008 0 0 1 29 14v4c0 1.657-1.35 3-3.01 3H22V11z',
-        attrs: { fillRule: 'evenodd' }
-    },
-    sort: 'M14.615.683c.765-.926 2.002-.93 2.77 0L26.39 11.59c.765.927.419 1.678-.788 1.678H6.398c-1.2 0-1.557-.747-.788-1.678L14.615.683zm2.472 30.774c-.6.727-1.578.721-2.174 0l-9.602-11.63c-.6-.727-.303-1.316.645-1.316h20.088c.956 0 1.24.595.645 1.316l-9.602 11.63z',
-    sum: 'M3 27.41l1.984 4.422L27.895 32l.04-5.33-17.086-.125 8.296-9.457-.08-3.602L11.25 5.33H27.43V0H5.003L3.08 4.51l10.448 10.9z',
-    sync: 'M16 2 A14 14 0 0 0 2 16 A14 14 0 0 0 16 30 A14 14 0 0 0 26 26 L 23.25 23 A10 10 0 0 1 16 26 A10 10 0 0 1 6 16 A10 10 0 0 1 16 6 A10 10 0 0 1 23.25 9 L19 13 L30 13 L30 2 L26 6 A14 14 0 0 0 16 2',
-    question: "M16,32 C24.836556,32 32,24.836556 32,16 C32,7.163444 24.836556,0 16,0 C7.163444,0 0,7.163444 0,16 C0,24.836556 7.163444,32 16,32 L16,32 Z M16,29.0909091 C8.77009055,29.0909091 2.90909091,23.2299095 2.90909091,16 C2.90909091,8.77009055 8.77009055,2.90909091 16,2.90909091 C23.2299095,2.90909091 29.0909091,8.77009055 29.0909091,16 C29.0909091,23.2299095 23.2299095,29.0909091 16,29.0909091 Z M12,9.56020942 C12.2727286,9.34380346 12.5694087,9.1413622 12.8900491,8.95287958 C13.2106896,8.76439696 13.5552807,8.59860455 13.9238329,8.45549738 C14.2923851,8.31239021 14.6885728,8.20069848 15.1124079,8.12041885 C15.5362429,8.04013921 15.9950835,8 16.4889435,8 C17.1818216,8 17.8065083,8.08725916 18.3630221,8.2617801 C18.919536,8.43630105 19.3931184,8.68586225 19.7837838,9.0104712 C20.1744491,9.33508016 20.4748147,9.7260012 20.6848894,10.1832461 C20.8949642,10.6404909 21,11.1483393 21,11.7068063 C21,12.2373499 20.9226052,12.6963331 20.7678133,13.0837696 C20.6130213,13.4712061 20.4176916,13.8080265 20.1818182,14.0942408 C19.9459448,14.3804552 19.6861194,14.6282712 19.4023342,14.8376963 C19.1185489,15.0471215 18.8495099,15.2408368 18.5952088,15.4188482 C18.3409078,15.5968595 18.1197798,15.773123 17.9318182,15.947644 C17.7438566,16.1221649 17.6240789,16.3176254 17.5724816,16.5340314 L17.2628993,18 L14.9189189,18 L14.6756757,16.3141361 C14.6167073,15.9720751 14.653562,15.6736487 14.7862408,15.4188482 C14.9189196,15.1640476 15.1013502,14.9336834 15.3335381,14.7277487 C15.565726,14.521814 15.8255514,14.3263535 16.1130221,14.1413613 C16.4004928,13.9563691 16.6695319,13.7574182 16.9201474,13.5445026 C17.1707629,13.3315871 17.3826773,13.0942421 17.5558968,12.8324607 C17.7291163,12.5706793 17.8157248,12.2582915 17.8157248,11.895288 C17.8157248,11.4764377 17.6701489,11.1431077 17.3789926,10.895288 C17.0878364,10.6474682 16.6879632,10.5235602 16.1793612,10.5235602 C15.7886958,10.5235602 15.462532,10.5619542 15.20086,10.6387435 C14.9391879,10.7155327 14.7143744,10.8010466 14.5264128,10.895288 C14.3384511,10.9895293 14.1744479,11.0750432 14.034398,11.1518325 C13.8943482,11.2286217 13.7543005,11.2670157 13.6142506,11.2670157 C13.2972957,11.2670157 13.0614258,11.1378721 12.9066339,10.8795812 L12,9.56020942 Z M14,22 C14,21.7192968 14.0511359,21.4580909 14.1534091,21.2163743 C14.2556823,20.9746577 14.3958324,20.7641335 14.5738636,20.5847953 C14.7518948,20.4054572 14.96212,20.2631584 15.2045455,20.1578947 C15.4469709,20.0526311 15.7121198,20 16,20 C16.2803044,20 16.5416655,20.0526311 16.7840909,20.1578947 C17.0265164,20.2631584 17.2386355,20.4054572 17.4204545,20.5847953 C17.6022736,20.7641335 17.7443177,20.9746577 17.8465909,21.2163743 C17.9488641,21.4580909 18,21.7192968 18,22 C18,22.2807032 17.9488641,22.5438584 17.8465909,22.7894737 C17.7443177,23.0350889 17.6022736,23.2475625 17.4204545,23.4269006 C17.2386355,23.6062387 17.0265164,23.7465882 16.7840909,23.8479532 C16.5416655,23.9493182 16.2803044,24 16,24 C15.7121198,24 15.4469709,23.9493182 15.2045455,23.8479532 C14.96212,23.7465882 14.7518948,23.6062387 14.5738636,23.4269006 C14.3958324,23.2475625 14.2556823,23.0350889 14.1534091,22.7894737 C14.0511359,22.5438584 14,22.2807032 14,22 Z",
-    return:'M15.3040432,11.8500793 C22.1434689,13.0450349 27.291257,18.2496116 27.291257,24.4890512 C27.291257,25.7084278 27.0946472,26.8882798 26.7272246,28.0064033 L26.7272246,28.0064033 C25.214579,22.4825472 20.8068367,18.2141694 15.3040432,17.0604596 L15.3040432,25.1841972 L4.70874296,14.5888969 L15.3040432,3.99359668 L15.3040432,3.99359668 L15.3040432,11.8500793 Z',
-    reference: {
-        path: 'M32 27.5V2a2 2 0 0 0-2-2H4a4 4 0 0 0-4 4v26a2 2 0 0 0 2 2h22V8H4V6a2 2 0 0 1 2-2h20a2 2 0 0 1 2 2v19h4v2.5z',
-        attrs: { fillRule: 'evenodd' }
-    },
-    refresh: 'M16 2 A14 14 0 0 0 2 16 A14 14 0 0 0 16 30 A14 14 0 0 0 26 26 L 23.25 23 A10 10 0 0 1 16 26 A10 10 0 0 1 6 16 A10 10 0 0 1 16 6 A10 10 0 0 1 23.25 9 L19 13 L30 13 L30 2 L26 6 A14 14 0 0 0 16 2',
-    right: "M9,0 L25,16 L9,32 L9,5.47117907e-13 L9,0 Z",
-    ruler: 'M0.595961814,24.9588734 C-0.196619577,24.166292 -0.200005392,22.8846495 0.593926984,22.0907171 L22.0908075,0.593836573 C22.8822651,-0.197621013 24.1641442,-0.198948234 24.9589638,0.595871404 L31.4040382,7.04094576 C32.1966196,7.83352715 32.2000054,9.11516967 31.406073,9.90910205 L9.90919246,31.4059826 C9.11773487,32.1974402 7.83585581,32.1987674 7.04103617,31.4039478 L0.595961814,24.9588734 Z M17.8319792,7.8001489 L16.3988585,9.23326963 L18.548673,11.3830842 C18.9443414,11.7787526 18.9470387,12.4175604 18.5485351,12.816064 C18.1527906,13.2118086 17.5140271,13.2146738 17.1155553,12.816202 L14.9657407,10.6663874 L13.5326229,12.0995052 L15.6824375,14.2493198 C16.0781059,14.6449881 16.0808032,15.283796 15.6822996,15.6822996 C15.286555,16.0780441 14.6477916,16.0809094 14.2493198,15.6824375 L12.0995052,13.5326229 L10.6663845,14.9657436 C10.6670858,14.9664411 10.6677865,14.9671398 10.6684864,14.9678397 L14.2470828,18.5464361 C14.6439866,18.9433399 14.6476854,19.5831493 14.2491818,19.9816529 C13.8534373,20.3773974 13.2188552,20.384444 12.813965,19.9795538 L9.23536867,16.4009575 C9.23466875,16.4002576 9.23397006,16.3995569 9.2332726,16.3988555 L7.80015186,17.8319762 L9.94996646,19.9817908 C10.3456348,20.3774592 10.3483321,21.016267 9.94982851,21.4147706 C9.55408397,21.8105152 8.91532053,21.8133804 8.51684869,21.4149086 L6.3670341,19.265094 L4.93391633,20.6982118 L7.08373093,22.8480263 C7.47939928,23.2436947 7.48209658,23.8825026 7.08359298,24.2810062 C6.68784844,24.6767507 6.049085,24.6796159 5.65061316,24.2811441 L3.50079857,22.1313295 L2.02673458,23.6053935 L8.47576453,30.0544235 L30.0544235,8.47576453 L23.6053935,2.02673458 L22.1313295,3.50079857 L24.2811441,5.65061316 C24.6768125,6.04628152 24.6795098,6.68508938 24.2810062,7.08359298 C23.8852616,7.47933752 23.2464982,7.48220276 22.8480263,7.08373093 L20.6982118,4.93391633 L19.2650911,6.36703697 C19.2657924,6.36773446 19.2664931,6.36843318 19.267193,6.36913314 L22.8457894,9.94772948 C23.2426932,10.3446333 23.246392,10.9844427 22.8478884,11.3829463 C22.4521439,11.7786908 21.8175617,11.7857374 21.4126716,11.3808472 L17.8340753,7.8022509 C17.8333753,7.80155099 17.8326767,7.80085032 17.8319792,7.8001489 Z',
-    search: 'M12 0 A12 12 0 0 0 0 12 A12 12 0 0 0 12 24 A12 12 0 0 0 18.5 22.25 L28 32 L32 28 L22.25 18.5 A12 12 0 0 0 24 12 A12 12 0 0 0 12 0 M12 4 A8 8 0 0 1 12 20 A8 8 0 0 1 12 4  ',
-    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',
-    slackIcon: 'M11.432 13.934l1.949 5.998 5.573-1.864-1.949-6-5.573 1.866zm-5.266 1.762l-2.722.91a2.776 2.776 0 1 1-1.762-5.265l2.768-.926-.956-2.943a2.776 2.776 0 0 1 5.28-1.716l.942 2.897 5.573-1.865-1.023-3.151a2.776 2.776 0 1 1 5.28-1.716l1.009 3.105 3.67-1.228a2.776 2.776 0 1 1 1.762 5.265l-3.716 1.244 1.949 5.999 3.336-1.117a2.776 2.776 0 0 1 1.762 5.266l-3.382 1.131.956 2.942a2.776 2.776 0 0 1-5.28 1.716l-.942-2.896-5.573 1.865 1.023 3.15a2.776 2.776 0 0 1-5.28 1.716L9.83 26.975l-3.056 1.022a2.776 2.776 0 1 1-1.762-5.265l3.102-1.038-1.949-5.998z',
-    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",
-    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'}
-    },
-    sun: 'M18.2857143,27.1999586 L18.2857143,29.7130168 C18.2857143,30.9760827 17.2711661,32 16,32 C14.7376349,32 13.7142857,30.9797942 13.7142857,29.7130168 L13.7142857,27.1999586 C14.4528227,27.3498737 15.2172209,27.4285714 16,27.4285714 C16.7827791,27.4285714 17.5471773,27.3498737 18.2857143,27.1999586 Z M13.7142857,4.80004141 L13.7142857,2.28698322 C13.7142857,1.02391726 14.7288339,0 16,0 C17.2623651,0 18.2857143,1.02020582 18.2857143,2.28698322 L18.2857143,4.80004141 C17.5471773,4.65012631 16.7827791,4.57142857 16,4.57142857 C15.2172209,4.57142857 14.4528227,4.65012631 13.7142857,4.80004141 Z M10.5518048,26.0488463 L8.93640145,27.9740091 C8.1245183,28.9415738 6.68916799,29.0738009 5.71539825,28.2567111 C4.74837044,27.4452784 4.62021518,26.0059593 5.43448399,25.0355515 L7.05102836,23.1090289 C8.00526005,24.3086326 9.1956215,25.3120077 10.5518048,26.0488463 Z M21.4481952,5.95115366 L23.0635986,4.02599087 C23.8754817,3.05842622 25.310832,2.92619908 26.2846018,3.74328891 C27.2516296,4.55472158 27.3797848,5.99404073 26.565516,6.96444852 L24.9489716,8.89097108 C23.9947399,7.69136735 22.8043785,6.68799226 21.4481952,5.95115366 Z M7.05102836,8.89097108 L5.43448399,6.96444852 C4.62260085,5.99688386 4.7416285,4.56037874 5.71539825,3.74328891 C6.68242605,2.93185624 8.12213263,3.05558308 8.93640145,4.02599087 L10.5518048,5.95115366 C9.1956215,6.68799226 8.00526005,7.69136735 7.05102836,8.89097108 Z M24.9489716,23.1090289 L26.565516,25.0355515 C27.3773992,26.0031161 27.2583715,27.4396213 26.2846018,28.2567111 C25.317574,29.0681438 23.8778674,28.9444169 23.0635986,27.9740091 L21.4481952,26.0488463 C22.8043785,25.3120077 23.9947399,24.3086326 24.9489716,23.1090289 Z M27.1999586,13.7142857 L29.7130168,13.7142857 C30.9760827,13.7142857 32,14.7288339 32,16 C32,17.2623651 30.9797942,18.2857143 29.7130168,18.2857143 L27.1999586,18.2857143 C27.3498737,17.5471773 27.4285714,16.7827791 27.4285714,16 C27.4285714,15.2172209 27.3498737,14.4528227 27.1999586,13.7142857 Z M4.80004141,18.2857143 L2.28698322,18.2857143 C1.02391726,18.2857143 2.7533531e-14,17.2711661 2.84217094e-14,16 C2.84217094e-14,14.7376349 1.02020582,13.7142857 2.28698322,13.7142857 L4.80004141,13.7142857 C4.65012631,14.4528227 4.57142857,15.2172209 4.57142857,16 C4.57142857,16.7827791 4.65012631,17.5471773 4.80004141,18.2857143 Z M16,22.8571429 C19.7870954,22.8571429 22.8571429,19.7870954 22.8571429,16 C22.8571429,12.2129046 19.7870954,9.14285714 16,9.14285714 C12.2129046,9.14285714 9.14285714,12.2129046 9.14285714,16 C9.14285714,19.7870954 12.2129046,22.8571429 16,22.8571429 Z',
-    table: 'M11.077 11.077h9.846v9.846h-9.846v-9.846zm11.077 11.077H32V32h-9.846v-9.846zm-11.077 0h9.846V32h-9.846v-9.846zM0 22.154h9.846V32H0v-9.846zM0 0h9.846v9.846H0V0zm0 11.077h9.846v9.846H0v-9.846zM22.154 0H32v9.846h-9.846V0zm0 11.077H32v9.846h-9.846v-9.846zM11.077 0h9.846v9.846h-9.846V0z',
-    table2: {
-        svg: '<g fill="currentcolor" fill-rule="evenodd"><path d="M10,19 L10,15 L3,15 L3,13 L10,13 L10,9 L12,9 L12,13 L20,13 L20,9 L22,9 L22,13 L29,13 L29,15 L22,15 L22,19 L29,19 L29,21 L22,21 L22,25 L20,25 L20,21 L12,21 L12,25 L10,25 L10,21 L3,21 L3,19 L10,19 L10,19 Z M12,19 L12,15 L20,15 L20,19 L12,19 Z M30.5,0 L32,0 L32,28 L30.5,28 L1.5,28 L0,28 L0,0 L1.5,0 L30.5,0 Z M29,3 L29,25 L3,25 L3,3 L29,3 Z M3,7 L29,7 L29,9 L3,9 L3,7 Z"></path></g>',
-        attrs: { viewBox: '0 0 32 28' }
-    },
-    tilde: 'M.018 22.856s-.627-7.417 5.456-10.293c6.416-3.033 12.638 2.01 15.885 2.01 2.09 0 4.067-1.105 4.067-4.483 0-.118 6.563-.086 6.563-.086s.338 5.151-2.756 8.403c-3.095 3.251-7.314 2.899-7.314 2.899s-2.686 0-6.353-1.543c-4.922-2.07-6.494-1.348-7.095-.969-.6.38-1.863 1.04-1.863 4.062H.018z',
-    trash: 'M4.31904507,29.7285487 C4.45843264,30.9830366 5.59537721,32 6.85726914,32 L20.5713023,32 C21.8337371,32 22.9701016,30.9833707 23.1095264,29.7285487 L25.1428571,11.4285714 L2.28571429,11.4285714 L4.31904507,29.7285487 L4.31904507,29.7285487 Z M6.85714286,4.57142857 L8.57142857,0 L18.8571429,0 L20.5714286,4.57142857 L25.1428571,4.57142857 C27.4285714,4.57142857 27.4285714,9.14285714 27.4285714,9.14285714 L13.7142857,9.14285714 L-1.0658141e-14,9.14285714 C-1.0658141e-14,9.14285714 -1.0658141e-14,4.57142857 2.28571429,4.57142857 L6.85714286,4.57142857 L6.85714286,4.57142857 Z M9.14285714,4.57142857 L18.2857143,4.57142857 L17.1428571,2.28571429 L10.2857143,2.28571429 L9.14285714,4.57142857 L9.14285714,4.57142857 Z',
-    unarchive: 'M18,7.95916837 L22.98085,7.97386236 L15.9779702,-0.00230793315 L9.02202984,7.93268248 L14,7.94736798 L14,22.8635899 L18,22.8635899 L18,7.95916837 Z M7,12.1176568 L0,12.1176568 L0,17.0882426 L3,17.0882426 L3,32 L29,32 L29,17.0882426 L32,17.0882426 L32,12.1176568 L25,12.1176568 L25,27.8341757 L7,27.8341757 L7,12.1176568 Z',
-    unknown: 'M16.5,26.5 C22.0228475,26.5 26.5,22.0228475 26.5,16.5 C26.5,10.9771525 22.0228475,6.5 16.5,6.5 C10.9771525,6.5 6.5,10.9771525 6.5,16.5 C6.5,22.0228475 10.9771525,26.5 16.5,26.5 L16.5,26.5 Z M16.5,23.5 C12.6340068,23.5 9.5,20.3659932 9.5,16.5 C9.5,12.6340068 12.6340068,9.5 16.5,9.5 C20.3659932,9.5 23.5,12.6340068 23.5,16.5 C23.5,20.3659932 20.3659932,23.5 16.5,23.5 L16.5,23.5 Z',
-    variable: 'M32,3.85760518 C32,5.35923081 31.5210404,6.55447236 30.5631068,7.4433657 C29.6051732,8.33225903 28.4358214,8.77669903 27.0550162,8.77669903 C26.2265331,8.77669903 25.4110072,8.67314019 24.6084142,8.46601942 C23.8058212,8.25889864 23.111114,8.05178097 22.5242718,7.84466019 C22.2481108,8.03452091 21.8425054,8.44875625 21.3074434,9.08737864 C20.7723814,9.72600104 20.1682882,10.5026923 19.4951456,11.4174757 C20.116508,14.0582656 20.6170423,15.9352695 20.9967638,17.0485437 C21.3764852,18.1618179 21.7389411,19.2880202 22.0841424,20.4271845 C22.3775635,21.3419679 22.8090586,22.0582498 23.3786408,22.5760518 C23.9482229,23.0938537 24.8457328,23.3527508 26.0711974,23.3527508 C26.5199591,23.3527508 27.0809028,23.2664518 27.7540453,23.0938511 C28.4271878,22.9212505 28.9795016,22.7486524 29.4110032,22.5760518 L28.8414239,24.9061489 C27.1326775,25.6310716 25.6397043,26.1574957 24.3624595,26.4854369 C23.0852148,26.8133781 21.9460676,26.9773463 20.9449838,26.9773463 C20.2200611,26.9773463 19.5037792,26.9083071 18.7961165,26.7702265 C18.0884539,26.632146 17.4412111,26.3818788 16.8543689,26.0194175 C16.2157465,25.6396961 15.6763776,25.1650514 15.236246,24.5954693 C14.7961143,24.0258871 14.4207135,23.2319361 14.1100324,22.2135922 C13.9029116,21.5749698 13.7130537,20.850058 13.5404531,20.038835 C13.3678524,19.2276119 13.1952544,18.51133 13.0226537,17.8899676 C12.5221118,18.6321504 12.1596559,19.1844642 11.9352751,19.5469256 C11.7108942,19.9093869 11.3829579,20.4185512 10.9514563,21.0744337 C9.5879112,23.1629015 8.4056145,24.6515597 7.40453074,25.5404531 C6.40344699,26.4293464 5.20389049,26.8737864 3.80582524,26.8737864 C2.75296129,26.8737864 1.85545139,26.5199604 1.11326861,25.8122977 C0.371085825,25.1046351 0,24.1812355 0,23.0420712 C0,21.5059254 0.478959612,20.2934241 1.4368932,19.4045307 C2.3948268,18.5156374 3.56417864,18.0711974 4.94498382,18.0711974 C5.77346693,18.0711974 6.56741799,18.1704413 7.32686084,18.368932 C8.08630369,18.5674228 8.80258563,18.7874853 9.47572816,19.0291262 C9.73462913,18.8220054 10.1359196,18.4164 10.6796117,17.8122977 C11.2233037,17.2081955 11.814452,16.4573939 12.4530744,15.5598706 C11.8834923,13.2470219 11.4174775,11.5037815 11.0550162,10.3300971 C10.6925548,9.15641269 10.321469,7.99137579 9.94174757,6.83495146 C9.63106641,5.90290796 9.18231146,5.18231107 8.59546926,4.67313916 C8.00862706,4.16396725 7.12837696,3.90938511 5.95469256,3.90938511 C5.43689061,3.90938511 4.85868712,3.99999909 4.22006472,4.18122977 C3.58144233,4.36246045 3.04638835,4.53074356 2.61488673,4.68608414 L3.18446602,2.35598706 C4.73787184,1.66558447 6.20927029,1.14779029 7.5987055,0.802588997 C8.98814071,0.457387702 10.1488627,0.284789644 11.0809061,0.284789644 C11.9266493,0.284789644 12.6515612,0.345198964 13.2556634,0.466019417 C13.8597657,0.586839871 14.4983785,0.845736958 15.171521,1.24271845 C15.7928834,1.62243987 16.3322523,2.10139948 16.789644,2.67961165 C17.2470357,3.25782382 17.6224365,4.04745994 17.9158576,5.04854369 C18.1229784,5.73894628 18.3128362,6.45522822 18.4854369,7.197411 C18.6580375,7.93959379 18.8047459,8.5782066 18.9255663,9.11326861 C19.2880277,8.56094654 19.6634285,7.99137294 20.0517799,7.40453074 C20.4401314,6.81768854 20.7723827,6.29989437 21.0485437,5.85113269 C22.3775687,3.76266485 23.5684953,2.2653767 24.6213592,1.3592233 C25.6742232,0.453069903 26.8651498,0 28.1941748,0 C29.2815588,0 30.1876986,0.358140971 30.9126214,1.07443366 C31.6375441,1.79072634 32,2.71844091 32,3.85760518 L32,3.85760518 Z',
-    viewArchive: {
-        path: 'M2.783 12.8h26.434V29H2.783V12.8zm6.956 3.4h12.522v2.6H9.739v-2.6zM0 4h32v6.4H0V4z',
-        attrs: { fillRule: "evenodd" }
-    },
-    warning: {
-        svg: '<g fill="currentcolor" fill-rule="evenodd"><path d="M10.9665007,4.7224988 C11.5372866,3.77118898 12.455761,3.75960159 13.0334993,4.7224988 L22.9665007,21.2775012 C23.5372866,22.228811 23.1029738,23 21.9950534,23 L2.00494659,23 C0.897645164,23 0.455760956,22.2403984 1.03349928,21.2775012 L10.9665007,4.7224988 Z M13.0184348,11.258 L13.0184348,14.69 C13.0184348,15.0580018 12.996435,15.4229982 12.9524348,15.785 C12.9084346,16.1470018 12.8504351,16.5159981 12.7784348,16.892 L11.5184348,16.892 C11.4464344,16.5159981 11.388435,16.1470018 11.3444348,15.785 C11.3004346,15.4229982 11.2784348,15.0580018 11.2784348,14.69 L11.2784348,11.258 L13.0184348,11.258 Z M11.0744348,19.058 C11.0744348,18.9139993 11.1014345,18.7800006 11.1554348,18.656 C11.2094351,18.5319994 11.2834343,18.4240005 11.3774348,18.332 C11.4714353,18.2399995 11.5824341,18.1670003 11.7104348,18.113 C11.8384354,18.0589997 11.978434,18.032 12.1304348,18.032 C12.2784355,18.032 12.4164341,18.0589997 12.5444348,18.113 C12.6724354,18.1670003 12.7844343,18.2399995 12.8804348,18.332 C12.9764353,18.4240005 13.0514345,18.5319994 13.1054348,18.656 C13.1594351,18.7800006 13.1864348,18.9139993 13.1864348,19.058 C13.1864348,19.2020007 13.1594351,19.3369994 13.1054348,19.463 C13.0514345,19.5890006 12.9764353,19.6979995 12.8804348,19.79 C12.7844343,19.8820005 12.6724354,19.9539997 12.5444348,20.006 C12.4164341,20.0580003 12.2784355,20.084 12.1304348,20.084 C11.978434,20.084 11.8384354,20.0580003 11.7104348,20.006 C11.5824341,19.9539997 11.4714353,19.8820005 11.3774348,19.79 C11.2834343,19.6979995 11.2094351,19.5890006 11.1554348,19.463 C11.1014345,19.3369994 11.0744348,19.2020007 11.0744348,19.058 Z"></path></g>',
-        attrs: { viewBox: '0 0 23 23' }
+  add:
+    "M19,13 L19,2 L14,2 L14,13 L2,13 L2,18 L14,18 L14,30 L19,30 L19,18 L30,18 L30,13 L19,13 Z",
+  addtodash: {
+    path:
+      "M21,23 L16,23 L16,27 L21,27 L21,32 L25,32 L25,27 L30,27 L30,23 L25,23 L25,18 L21,18 L21,23 Z M32,7 L32,14 L28,14 L28,8 L0,8 L0,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 L4,0 L28,0 C30.209139,-4.05812251e-16 32,1.790861 32,4 L32,7 Z M0,8 L4,8 L4,28 L0,28 L0,8 Z M0,28 L12,28 L12,32 L4,32 C1.790861,32 2.705415e-16,30.209139 0,28 Z",
+    attrs: { fillRule: "evenodd" },
+  },
+  alert: {
+    path:
+      "M14.677 7.339c-4.77.562-5.23 4.75-5.23 7.149 0 2.576 0 3.606-.53 4.121-.352.344-1.058.515-2.117.515V21.7h18v-2.576c-1.059 0-1.588 0-2.118-.515-.353-.343-.53-2.06-.53-5.151-.316-3.705-2.06-5.745-5.23-6.12a1.52 1.52 0 0 0 .466-1.093c0-.853-.71-1.545-1.588-1.545-.877 0-1.588.692-1.588 1.545 0 .427.178.814.465 1.094zM16.05 0c2.473 0 5.57 1.851 6.22 4.12 3.057 1.58 4.868 4.503 5.223 8.706l.013.158v.157c0 .905.014 1.682.042 2.327H30.6V25.73H1.5V15.468h3.091c.002-.326.003-.725.003-1.222 0-2.308.316-4.322 1.26-6.233.881-1.784 2.223-2.988 3.976-3.893C10.48 1.85 13.576 0 16.05 0zM13.1 25.8c.25 1.6 1.166 2.4 2.75 2.4s2.5-.8 2.75-2.4h-5.5zm-4.35-3.16h14.191l-.586 3.261c-.497 3.607-2.919 6.001-6.51 6.001-3.59 0-6.012-2.394-6.508-6L8.75 22.64z",
+    attrs: { fillRule: "nonzero" },
+  },
+  alertConfirm: {
+    path:
+      "M24.326 7.184a9.604 9.604 0 0 0-.021-.034c-.876-1.39-2.056-2.47-3.518-3.19-.509-2.269-2.51-3.96-4.9-3.96-2.361 0-4.344 1.652-4.881 3.88C7.113 5.63 5.68 9.55 5.68 14.424c0 .88-.003 1.473-.01 1.902H2.8v9.605h26.175v-9.602h-3.297v6.257H6.097V19.67c1.152 0 1.92-.194 2.304-.583.576-.583.576-1.75.576-4.664 0-2.716.5-7.456 5.69-8.091a1.754 1.754 0 0 1-.507-1.238c0-.966.773-1.749 1.727-1.749.955 0 1.728.783 1.728 1.75 0 .483-.194.92-.507 1.237 2.2.27 3.768 1.308 4.705 3.112.037-.04.874-.793 2.513-2.26zm-11.312 18.7H9.741C10.214 29.398 12.48 32 15.887 32c3.409 0 5.674-2.602 6.147-6.116H18.76c-.27 1.911-1.228 2.77-2.874 2.77-1.645 0-2.603-.859-2.873-2.77zm.297-12.466l2.504-2.707 3.819 4.106 7.653-8.254L29.8 9.38 19.636 20.295l-6.325-6.877z",
+    attrs: { fillRule: "nonzero" },
+  },
+  all:
+    "M30.595 13.536c1.85.755 1.879 2.05.053 2.9l-11.377 5.287c-1.82.846-4.763.858-6.583.022L1.344 16.532c-1.815-.835-1.785-2.131.05-2.89l1.637-.677 8.977 4.125c2.194 1.009 5.74.994 7.934-.026l9.022-4.193 1.63.665zm-1.63 7.684l1.63.666c1.85.755 1.879 2.05.053 2.898l-11.377 5.288c-1.82.847-4.763.859-6.583.022L1.344 24.881c-1.815-.834-1.785-2.131.05-2.89l1.637-.677 8.977 4.126c2.194 1.008 5.74.993 7.934-.026l9.022-4.194zM12.686 1.576c1.843-.762 4.834-.77 6.687-.013l11.22 4.578c1.85.755 1.88 2.05.054 2.899l-11.377 5.288c-1.82.846-4.763.858-6.583.022L1.344 9.136c-1.815-.834-1.785-2.13.05-2.89l11.293-4.67z",
+  archive: {
+    path:
+      "M27.557 1v2.356a1 1 0 0 1-1 1H5.443a1 1 0 0 1-1-1V1a1 1 0 0 1 1-1h21.114a1 1 0 0 1 1 1zM4.356 26.644h23.288v-15.57H4.356v15.57zM32 8.718V29a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V8.718a2 2 0 0 1 2-2h28a2 2 0 0 1 2 2zM16.116 25.076l5.974-6.57h-3.983V12.93h-3.982v5.575h-3.982l5.973 6.571z",
+    attrs: { fillRule: "evenodd" },
+  },
+  area:
+    "M31.154 28.846l.852.004V8.64l-1.15 2.138-6.818 6.37c-.13.122-9.148 1.622-9.148 1.622l-.545.096-.383.4-7.93 8.31-1.016 1.146 2.227.017 23.91.107L7.25 28.74l7.93-8.31 9.615-1.684 7.211-6.737v15.984a.855.855 0 0 1-.852.854zM0 28.74l11.79-13.362 11.788-3.369 8.077-8.07c.194-.193.351-.128.351.15V28.85L0 28.74z",
+  attachment: {
+    path:
+      "M22.162 8.704c.029 8.782-.038 14.123-.194 15.926-.184 2.114-2.922 4.322-5.9 4.322-3.06 0-5.542-1.98-5.836-4.376-.294-2.392-.195-14.266.01-18.699.077-1.661 1.422-2.83 3.548-2.83 2.067 0 3.488 1.335 3.594 3.164.06 1.052.074 3.49.053 7.107-.006.928-.013 1.891-.023 3.072l-.023 2.527c-.006.824-.01 1.358-.01 1.718 0 1.547-.39 2.011-1.475 2.011-.804 0-1.202-.522-1.202-1.38V8.699a1.524 1.524 0 0 0-3.048 0v12.567c0 2.389 1.554 4.428 4.25 4.428 2.897 0 4.523-1.934 4.523-5.06 0-.348.003-.875.01-1.691l.022-2.526c.01-1.184.018-2.15.024-3.082.021-3.697.008-6.155-.058-7.3C20.227 2.592 17.469 0 13.79 0c-3.695 0-6.438 2.382-6.593 5.737-.213 4.613-.312 16.585.01 19.21C7.697 28.94 11.53 32 16.067 32c4.482 0 8.61-3.327 8.937-7.106.168-1.935.235-7.302.206-16.2a1.524 1.524 0 0 0-3.048.01z",
+    attrs: { fillRule: "nonzero" },
+  },
+  backArrow:
+    "M11.7416687,19.0096 L18.8461178,26.4181004 L14.2696969,30.568 L0.38960831,16.093881 L0,15.6875985 L0.49145276,15.241949 L14.6347557,1 L19.136,5.22693467 L11.3214393,13.096 L32,13.096 L32,19.0096 L11.7416687,19.0096 Z",
+  bar:
+    "M2 23.467h6.4V32H2v-8.533zm10.667-12.8h6.4V32h-6.4V10.667zM23.333 0h6.4v32h-6.4V0z",
+  beaker:
+    "M4.31736354,31.1631075 C3.93810558,30.6054137 3.89343681,29.6635358 4.20559962,29.0817181 L11.806982,14.9140486 L11.8069821,10.5816524 L10.7015144,10.4653256 C10.0309495,10.394763 9.48734928,9.78799739 9.48734928,9.12166999 L9.48734928,7.34972895 C9.48734928,6.67821106 10.0368737,6.13383825 10.7172248,6.13383825 L21.8462005,6.13383825 C22.525442,6.13383825 23.0760761,6.68340155 23.0760761,7.34972895 L23.0760761,9.12166999 C23.0760761,9.79318788 22.5250158,10.3375607 21.856025,10.3375607 L20.9787023,10.3375607 L20.9787024,14.9281806 L28.77277,29.0827118 C29.0983515,29.6739888 29.0709073,30.6193105 28.7174156,31.1846409 L28.852457,30.9686726 C28.4963041,31.538259 27.6541076,32 26.9865771,32 L6.10749779,32 C5.43315365,32 4.58248747,31.5529687 4.19978245,30.9902061 L4.31736354,31.1631075 Z M15.5771418,17.6040443 C16.5170398,17.6040443 17.2789777,16.8377777 17.2789777,15.89254 C17.2789777,14.9473023 16.5170398,14.1810358 15.5771418,14.1810358 C14.6372438,14.1810358 13.8753059,14.9473023 13.8753059,15.89254 C13.8753059,16.8377777 14.6372438,17.6040443 15.5771418,17.6040443 Z M16.5496195,12.8974079 C17.8587633,12.8974079 18.9200339,11.830108 18.9200339,10.5135268 C18.9200339,9.1969457 17.8587633,8.1296458 16.5496195,8.1296458 C15.2404758,8.1296458 14.1792052,9.1969457 14.1792052,10.5135268 C14.1792052,11.830108 15.2404758,12.8974079 16.5496195,12.8974079 Z M5.71098553,30.2209651 L10.9595331,20.5151267 C10.9595331,20.5151267 12.6834557,21.2672852 14.3734184,21.2672852 C16.0633811,21.2672852 16.8198616,19.2872624 17.588452,18.6901539 C18.3570425,18.0930453 19.9467191,17.1113296 19.9467191,17.1113296 L27.0506095,30.1110325 L5.71098553,30.2209651 Z M13.6608671,4.37817079 C14.4114211,4.37817079 15.0198654,3.78121712 15.0198654,3.04483745 C15.0198654,2.30845779 14.4114211,1.71150412 13.6608671,1.71150412 C12.9103132,1.71150412 12.3018689,2.30845779 12.3018689,3.04483745 C12.3018689,3.78121712 12.9103132,4.37817079 13.6608671,4.37817079 Z M17.9214578,2.45333328 C18.6119674,2.45333328 19.1717361,1.90413592 19.1717361,1.22666664 C19.1717361,0.549197362 18.6119674,0 17.9214578,0 C17.2309481,0 16.6711794,0.549197362 16.6711794,1.22666664 C16.6711794,1.90413592 17.2309481,2.45333328 17.9214578,2.45333328 Z",
+  breakout:
+    "M24.47 1H32v7.53h-7.53V1zm0 11.294H32v7.53h-7.53v-7.53zm0 11.294H32v7.53h-7.53v-7.53zM0 1h9.412v30.118H0V1zm11.731 13.714c.166-.183.452-.177.452-.177h6.475s-1.601-2.053-2.07-2.806c-.469-.753-.604-1.368 0-1.905.603-.536 1.226-.281 1.878.497.652.779 2.772 3.485 3.355 4.214.583.73.65 1.965 0 2.835-.65.87-2.65 4.043-3.163 4.65-.514.607-1.123.713-1.732.295-.609-.419-.838-1.187-.338-1.872.5-.684 2.07-3.073 2.07-3.073h-6.475s-.27 0-.46-.312-.151-.612-.151-.612l.007-1.246s-.014-.306.152-.488z",
+  bubble:
+    "M18.155 20.882c-5.178-.638-9.187-5.051-9.187-10.402C8.968 4.692 13.66 0 19.448 0c5.789 0 10.48 4.692 10.48 10.48 0 3.05-1.302 5.797-3.383 7.712a7.127 7.127 0 1 1-8.39 2.69zm-6.392 10.14a2.795 2.795 0 1 1 0-5.59 2.795 2.795 0 0 1 0 5.59zm-6.079-6.288a4.541 4.541 0 1 1 0-9.083 4.541 4.541 0 0 1 0 9.083z",
+  burger:
+    "M2.5 3.6h27a2.5 2.5 0 1 1 0 5h-27a2.5 2.5 0 0 1 0-5zm0 9.931h27a2.5 2.5 0 1 1 0 5h-27a2.5 2.5 0 1 1 0-5zm0 9.931h27a2.5 2.5 0 1 1 0 5h-27a2.5 2.5 0 0 1 0-5z",
+  calendar: {
+    path:
+      "M21,2 L21,0 L18,0 L18,2 L6,2 L6,0 L3,0 L3,2 L2.99109042,2 C1.34177063,2 0,3.34314575 0,5 L0,6.99502651 L0,20.009947 C0,22.2157067 1.78640758,24 3.99005301,24 L20.009947,24 C22.2157067,24 24,22.2135924 24,20.009947 L24,6.99502651 L24,5 C24,3.34651712 22.6608432,2 21.0089096,2 L21,2 L21,2 Z M22,8 L22,20.009947 C22,21.1099173 21.1102431,22 20.009947,22 L3.99005301,22 C2.89008272,22 2,21.1102431 2,20.009947 L2,8 L22,8 L22,8 Z M6,12 L10,12 L10,16 L6,16 L6,12 Z",
+    attrs: { viewBox: "0 0 24 24" },
+  },
+  check: "M1 14 L5 10 L13 18 L27 4 L31 8 L13 26 z ",
+  chevrondown: "M1 12 L16 26 L31 12 L27 8 L16 18 L5 8 z ",
+  chevronleft: "M20 1 L24 5 L14 16 L24 27 L20 31 L6 16 z",
+  chevronright: "M12 1 L26 16 L12 31 L8 27 L18 16 L8 5 z ",
+  chevronup: "M1 20 L16 6 L31 20 L27 24 L16 14 L5 24 z",
+  clipboard:
+    "M8.54667751,5.50894675 L6.00494659,5.50894675 C4.89702623,5.50894675 4,6.40070914 4,7.50075379 L4,30.0171397 C4,31.1120596 4.89764516,32.0089468 6.00494659,32.0089468 L25.9950534,32.0089468 C27.1029738,32.0089468 28,31.1171844 28,30.0171397 L28,7.50075379 C28,6.40583387 27.1023548,5.50894675 25.9950534,5.50894675 L23.5373296,5.50894675 L23.5373296,3.0446713 L19.9106557,3.0446713 C19.9106557,3.0446713 19.6485834,8.05825522e-08 16.0837607,0 C12.518938,-8.05825523e-08 12.1644547,3.04776207 12.1644547,3.04776207 L8.57253264,3.04776207 L8.54667751,5.50894675 Z M23.5373296,7.50894675 L26,7.50894675 L26,30.0089468 L6,30.0089468 L6,7.50894675 L8.52566721,7.50894675 L8.4996301,9.98745456 L23.5373296,9.98745456 L23.5373296,7.50894675 Z M10.573037,5.01478303 L13.9861608,5.01478303 L13.9861608,3.76128231 C13.9861608,3.76128231 14.0254332,1.94834752 16.0135743,1.94834752 C18.0017155,1.94834752 18.0017156,3.7055821 18.0017156,3.7055821 L18.0017156,4.94060459 L21.4955568,4.94060459 L21.4955568,8.03924122 L10.5173901,8.03924122 L10.573037,5.01478303 Z M16,5.00894675 C16.5522847,5.00894675 17,4.5612315 17,4.00894675 C17,3.456662 16.5522847,3.00894675 16,3.00894675 C15.4477153,3.00894675 15,3.456662 15,4.00894675 C15,4.5612315 15.4477153,5.00894675 16,5.00894675 Z M8.5,18.0089468 L8.5,21.0082323 L11.5,21.0082323 L11.5,18.0446111 L8.5,18.0089468 Z M8.5,23.0089468 L8.5,26.0082323 L11.5,26.0082323 L11.5,23.0446111 L8.5,23.0089468 Z M8.5,13.0089468 L8.5,16.0082323 L11.5,16.0082323 L11.5,13.0446111 L8.5,13.0089468 Z M13.5,13.0193041 L13.5,16 L23.5,16 L23.5,13 L13.5,13.0193041 Z M13.5,23.0193041 L13.5,26 L23.5,26 L23.5,23 L13.5,23.0193041 Z M13.5,18.0193041 L13.5,21 L23.5,21 L23.5,18 L13.5,18.0193041 Z",
+  clock:
+    "M16 0 A16 16 0 0 0 0 16 A16 16 0 0 0 16 32 A16 16 0 0 0 32 16 A16 16 0 0 0 16 0 M16 4 A12 12 0 0 1 28 16 A12 12 0 0 1 16 28 A12 12 0 0 1 4 16 A12 12 0 0 1 16 4 M14 6 L14 17.25 L22 22 L24.25 18.5 L18 14.75 L18 6z",
+  clone: {
+    path:
+      "M12,11 L16,11 L16,0 L5,0 L5,3 L12,3 L12,11 L12,11 Z M0,4 L11,4 L11,15 L0,15 L0,4 Z",
+    attrs: { viewBox: "0 0 16 15" },
+  },
+  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",
+  compare: {
+    path:
+      "M8.514 23.486C3.587 21.992 0 17.416 0 12 0 5.373 5.373 0 12 0c5.415 0 9.992 3.587 11.486 8.514C28.413 10.008 32 14.584 32 20c0 6.627-5.373 12-12 12-5.415 0-9.992-3.587-11.486-8.514zm2.293.455A10.003 10.003 0 0 0 20 30c5.523 0 10-4.477 10-10 0-4.123-2.496-7.664-6.059-9.193.04.392.059.79.059 1.193 0 6.627-5.373 12-12 12-.403 0-.8-.02-1.193-.059z",
+    attrs: {
+      fillRule: "nonzero",
     },
-    warning2: {
-        path: 'M12.3069589,4.52260192 C14.3462632,1.2440969 17.653446,1.24541073 19.691933,4.52260192 L31.2249413,23.0637415 C33.2642456,26.3422466 31.7889628,29 27.9115531,29 L4.08733885,29 C0.218100769,29 -1.26453645,26.3409327 0.77395061,23.0637415 L12.3069589,4.52260192 Z M18.0499318,23.0163223 C18.0499772,23.0222378 18.05,23.0281606 18.05,23.0340907 C18.05,23.3266209 17.9947172,23.6030345 17.8840476,23.8612637 C17.7737568,24.1186089 17.6195847,24.3426723 17.4224081,24.5316332 C17.2266259,24.7192578 16.998292,24.8660439 16.7389806,24.9713892 C16.4782454,25.0773129 16.1979962,25.1301134 15.9,25.1301134 C15.5950083,25.1301134 15.3111795,25.0774024 15.0502239,24.9713892 C14.7901813,24.8657469 14.5629613,24.7183609 14.3703047,24.5298034 C14.177545,24.3411449 14.0258626,24.1177208 13.9159524,23.8612637 C13.8052827,23.6030345 13.75,23.3266209 13.75,23.0340907 C13.75,22.7411889 13.8054281,22.4661013 13.9165299,22.2109786 C14.0264627,21.9585404 14.1779817,21.7374046 14.3703047,21.5491736 C14.5621821,21.3613786 14.7883231,21.2126553 15.047143,21.1034656 C15.3089445,20.9930181 15.593871,20.938068 15.9,20.938068 C16.1991423,20.938068 16.4804862,20.9931136 16.7420615,21.1034656 C17.0001525,21.2123478 17.2274115,21.360472 17.4224081,21.5473437 C17.6191428,21.7358811 17.7731504,21.957652 17.88347,22.2109786 C17.9124619,22.2775526 17.9376628,22.3454862 17.9590769,22.414741 C18.0181943,22.5998533 18.05,22.7963729 18.05,23 C18.05,23.0054459 18.0499772,23.0108867 18.0499318,23.0163223 L18.0499318,23.0163223 Z M17.7477272,14.1749999 L17.7477272,8.75 L14.1170454,8.75 L14.1170454,14.1749999 C14.1170454,14.8471841 14.1572355,15.5139742 14.2376219,16.1753351 C14.3174838,16.8323805 14.4227217,17.5019113 14.5533248,18.1839498 L14.5921937,18.3869317 L17.272579,18.3869317 L17.3114479,18.1839498 C17.442051,17.5019113 17.5472889,16.8323805 17.6271507,16.1753351 C17.7075371,15.5139742 17.7477272,14.8471841 17.7477272,14.1749999 Z',
-        attrs: { fillRule: "evenodd" }
+  },
+  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",
+  },
+  connections: {
+    path:
+      "M5.37815706,11.5570815 C5.55061975,11.1918363 5.64705882,10.783651 5.64705882,10.3529412 C5.64705882,9.93118218 5.55458641,9.53102128 5.38881053,9.1716274 L11.1846365,4.82475792 C11.6952189,5.33295842 12.3991637,5.64705882 13.1764706,5.64705882 C14.7358628,5.64705882 16,4.38292165 16,2.82352941 C16,1.26413718 14.7358628,0 13.1764706,0 C11.6170784,0 10.3529412,1.26413718 10.3529412,2.82352941 C10.3529412,3.2452884 10.4454136,3.64544931 10.6111895,4.00484319 L10.6111895,4.00484319 L4.81536351,8.35171266 C4.3047811,7.84351217 3.60083629,7.52941176 2.82352941,7.52941176 C1.26413718,7.52941176 0,8.79354894 0,10.3529412 C0,11.9123334 1.26413718,13.1764706 2.82352941,13.1764706 C3.59147157,13.1764706 4.28780867,12.8698929 4.79682555,12.3724528 L10.510616,16.0085013 C10.408473,16.3004758 10.3529412,16.6143411 10.3529412,16.9411765 C10.3529412,18.5005687 11.6170784,19.7647059 13.1764706,19.7647059 C14.7358628,19.7647059 16,18.5005687 16,16.9411765 C16,15.3817842 14.7358628,14.1176471 13.1764706,14.1176471 C12.3029783,14.1176471 11.5221273,14.5142917 11.0042049,15.1372938 L5.37815706,11.5570815 Z",
+    attrs: { viewBox: "0 0 16 19.7647" },
+  },
+  contract:
+    "M18.0015892,0.327942852 L18.0015892,14 L31.6736463,14 L26.6544389,8.98079262 L32,3.63523156 L28.3647684,0 L23.0192074,5.34556106 L18.0015892,0.327942852 Z M14,31.6720571 L14,18 L0.327942852,18 L5.34715023,23.0192074 L0.00158917013,28.3647684 L3.63682073,32 L8.98238179,26.6544389 L14,31.6720571 Z",
+  copy: {
+    path:
+      "M10.329 6.4h-3.33c-.95 0-1.72.77-1.72 1.72v.413h17.118V8.12c0-.941-.77-1.719-1.72-1.719h-3.31l-1.432-1.705a2.137 2.137 0 0 0-2.097-2.562 2.137 2.137 0 0 0-2.054 2.73L10.329 6.4zm12.808 4.267h1.4v8.557h-4.42l.111-4.188-5.981 6.064 5.805 6.264v-3.966h4.485v6.469H3.14v-19.2h19.997zm3.54 12.731v6.888c0 .947-.769 1.714-1.725 1.714H2.725C1.772 32 1 31.23 1 30.286V5.981c0-.947.768-1.714 1.725-1.714h6.834A4.273 4.273 0 0 1 13.839 0c2.363 0 4.279 1.91 4.279 4.267h6.834c.953 0 1.725.77 1.725 1.714v13.243H31v4.174h-4.323zM5.279 12.8h10.7v2.133h-10.7V12.8zm0 4.267h5.564V19.2H5.279v-2.133zm0 4.266h5.564v2.134H5.279v-2.134zm0 4.267h8.56v2.133h-8.56V25.6z",
+    attrs: { fillRule: "evenodd" },
+  },
+  cursor_move:
+    "M14.8235294,14.8235294 L14.8235294,6.58823529 L17.1764706,6.58823529 L17.1764706,14.8235294 L25.4117647,14.8235294 L25.4117647,17.1764706 L17.1764706,17.1764706 L17.1764706,25.4117647 L14.8235294,25.4117647 L14.8235294,17.1764706 L6.58823529,17.1764706 L6.58823529,14.8235294 L14.8235294,14.8235294 L14.8235294,14.8235294 Z M16,0 L20.1176471,6.58823529 L11.8823529,6.58823529 L16,0 Z M11.8823529,25.4117647 L20.1176471,25.4117647 L16,32 L11.8823529,25.4117647 Z M32,16 L25.4117647,20.1176471 L25.4117647,11.8823529 L32,16 Z M6.58823529,11.8823529 L6.58823529,20.1176471 L0,16 L6.58823529,11.8823529 Z",
+  cursor_resize:
+    "M17.4017952,6.81355995 L15.0488541,6.81355995 L15.0488541,25.6370894 L17.4017952,25.6370894 L17.4017952,6.81355995 Z M16.2253247,0.225324657 L20.3429717,6.81355995 L12.1076776,6.81355995 L16.2253247,0.225324657 Z M12.1076776,25.6370894 L20.3429717,25.6370894 L16.2253247,32.2253247 L12.1076776,25.6370894 Z",
+  costapproximate:
+    "M27 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM16 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 22a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM5 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6z",
+  costexact:
+    "M27 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM16 8a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm0 22a3 3 0 1 1 0-6 3 3 0 0 1 0 6zM5 19a3 3 0 1 1 0-6 3 3 0 0 1 0 6zm11 0a3 3 0 1 1 0-6 3 3 0 0 1 0 6z",
+  costextended:
+    "M27,19 C25.3431458,19 24,17.6568542 24,16 C24,14.3431458 25.3431458,13 27,13 C28.6568542,13 30,14.3431458 30,16 C30,17.6568542 28.6568542,19 27,19 Z M16,8 C14.3431458,8 13,6.65685425 13,5 C13,3.34314575 14.3431458,2 16,2 C17.6568542,2 19,3.34314575 19,5 C19,6.65685425 17.6568542,8 16,8 Z M16,30 C14.3431458,30 13,28.6568542 13,27 C13,25.3431458 14.3431458,24 16,24 C17.6568542,24 19,25.3431458 19,27 C19,28.6568542 17.6568542,30 16,30 Z M5,19 C3.34314575,19 2,17.6568542 2,16 C2,14.3431458 3.34314575,13 5,13 C6.65685425,13 8,14.3431458 8,16 C8,17.6568542 6.65685425,19 5,19 Z M16,19 C14.3431458,19 13,17.6568542 13,16 C13,14.3431458 14.3431458,13 16,13 C17.6568542,13 19,14.3431458 19,16 C19,17.6568542 17.6568542,19 16,19 Z M10,12 C8.8954305,12 8,11.1045695 8,10 C8,8.8954305 8.8954305,8 10,8 C11.1045695,8 12,8.8954305 12,10 C12,11.1045695 11.1045695,12 10,12 Z M22,12 C20.8954305,12 20,11.1045695 20,10 C20,8.8954305 20.8954305,8 22,8 C23.1045695,8 24,8.8954305 24,10 C24,11.1045695 23.1045695,12 22,12 Z M22,24 C20.8954305,24 20,23.1045695 20,22 C20,20.8954305 20.8954305,20 22,20 C23.1045695,20 24,20.8954305 24,22 C24,23.1045695 23.1045695,24 22,24 Z M10,24 C8.8954305,24 8,23.1045695 8,22 C8,20.8954305 8.8954305,20 10,20 C11.1045695,20 12,20.8954305 12,22 C12,23.1045695 11.1045695,24 10,24 Z",
+  database:
+    "M1.18285296e-08,10.5127919 C-1.47856568e-08,7.95412848 1.18285298e-08,4.57337284 1.18285298e-08,4.57337284 C1.18285298e-08,4.57337284 1.58371041,5.75351864e-10 15.6571342,0 C29.730558,-5.7535027e-10 31.8900148,4.13849684 31.8900148,4.57337284 L31.8900148,10.4843058 C31.8900148,10.4843058 30.4448001,15.1365942 16.4659751,15.1365944 C2.48715012,15.1365947 2.14244494e-08,11.4353349 1.18285296e-08,10.5127919 Z M0.305419478,21.1290071 C0.305419478,21.1290071 0.0405133833,21.2033291 0.0405133833,21.8492606 L0.0405133833,27.3032816 C0.0405133833,27.3032816 1.46515486,31.941655 15.9641228,31.941655 C30.4630908,31.941655 32,27.3446712 32,27.3446712 C32,27.3446712 32,21.7986104 32,21.7986105 C32,21.2073557 31.6620557,21.0987647 31.6620557,21.0987647 C31.6620557,21.0987647 29.7146434,25.22314 16.0318829,25.22314 C2.34912233,25.22314 0.305419478,21.1290071 0.305419478,21.1290071 Z M0.305419478,12.656577 C0.305419478,12.656577 0.0405133833,12.730899 0.0405133833,13.3768305 L0.0405133833,18.8308514 C0.0405133833,18.8308514 1.46515486,23.4692249 15.9641228,23.4692249 C30.4630908,23.4692249 32,18.8722411 32,18.8722411 C32,18.8722411 32,13.3261803 32,13.3261803 C32,12.7349256 31.6620557,12.6263346 31.6620557,12.6263346 C31.6620557,12.6263346 29.7146434,16.7507099 16.0318829,16.7507099 C2.34912233,16.7507099 0.305419478,12.656577 0.305419478,12.656577 Z",
+  dashboard:
+    "M32,29 L32,4 L32,0 L0,0 L0,8 L28,8 L28,28 L4,28 L4,8 L0,8 L0,29.5 L0,32 L32,32 L32,29 Z M7.27272727,18.9090909 L17.4545455,18.9090909 L17.4545455,23.2727273 L7.27272727,23.2727273 L7.27272727,18.9090909 Z M7.27272727,12.0909091 L24.7272727,12.0909091 L24.7272727,16.4545455 L7.27272727,16.4545455 L7.27272727,12.0909091 Z M20.3636364,18.9090909 L24.7272727,18.9090909 L24.7272727,23.2727273 L20.3636364,23.2727273 L20.3636364,18.9090909 Z",
+  curve:
+    "M3.033 3.791v22.211H31.09c.403 0 .882.872.882 1.59 0 .717-.48 1.408-.882 1.408H0V3.791c0-.403.875-.914 1.487-.914.612 0 1.546.511 1.546.914zm3.804 17.912C5.714 21.495 5 20.318 5 19.355c0-.963.831-2.296 1.837-2.296 2.093 0 2.965-1.207 4.204-5.242l.148-.482C12.798 6.077 14.18 3 17.968 3c3.792 0 5.17 3.08 6.765 8.343l.145.478c1.227 4.034 2.093 5.238 4.181 5.238 1.006 0 1.875 1.29 1.875 2.296 0 1.007-.898 2.184-1.875 2.348-3.656.612-6.004-2.364-7.665-7.821l-.146-.482c-1.14-3.76-1.8-6.754-3.28-6.754-1.483 0-2.147 2.995-3.297 6.754l-.148.486c-1.675 5.454-3.93 8.514-7.686 7.817z",
+  document:
+    "M29,10.1052632 L29,28.8325291 C29,30.581875 27.5842615,32 25.8337327,32 L7.16626728,32 C5.41758615,32 4,30.5837102 4,28.8441405 L4,3.15585953 C4,1.41292644 5.42339685,9.39605581e-15 7.15970573,8.42009882e-15 L20.713352,8.01767853e-16 L20.713352,8.42105263 L22.3846872,8.42105263 L22.3846872,0.310375032 L28.7849894,8.42105263 L20.713352,8.42105263 L20.713352,10.1052632 L29,10.1052632 Z M7.3426704,12.8000006 L25.7273576,12.8000006 L25.7273576,14.4842112 L7.3426704,14.4842112 L7.3426704,12.8000006 Z M7.3426704,17.3473687 L25.7273576,17.3473687 L25.7273576,19.0315793 L7.3426704,19.0315793 L7.3426704,17.3473687 Z M7.3426704,21.8947352 L25.7273576,21.8947352 L25.7273576,23.5789458 L7.3426704,23.5789458 L7.3426704,21.8947352 Z M7.43137255,26.2736849 L16.535014,26.2736849 L16.535014,27.9578954 L7.43137255,27.9578954 L7.43137255,26.2736849 Z",
+  downarrow:
+    "M12.2782161,19.3207547 L12.2782161,0 L19.5564322,0 L19.5564322,19.3207547 L26.8346484,19.3207547 L15.9173242,32 L5,19.3207547 L12.2782161,19.3207547 Z",
+  download:
+    "M19.3636364,15.0588235 L19.3636364,0 L13.6363636,0 L13.6363636,15.0588235 L7.90909091,15.0588235 L16.5,24.9411765 L25.0909091,15.0588235 L19.3636364,15.0588235 Z M27,26.3529412 L27,32 L6,32 L6,26.3529412 L27,26.3529412 Z",
+  editdocument:
+    "M19.27 20.255l-5.642 2.173 1.75-6.085L28.108 3.45 32 7.363 19.27 20.255zM20.442 6.9l-2.044-2.049H4.79v23.29h18.711v-6.577l4.787-4.83V31a2 2 0 0 1-2 2H2a2 2 0 0 1-2-2V2a2 2 0 0 1 2-2h18.024a1 1 0 0 1 .711.297L23.85 3.45 20.442 6.9z",
+  ellipsis: {
+    path:
+      "M26.1111111,19 C27.7066004,19 29,17.6568542 29,16 C29,14.3431458 27.7066004,13 26.1111111,13 C24.5156218,13 23.2222222,14.3431458 23.2222222,16 C23.2222222,17.6568542 24.5156218,19 26.1111111,19 Z M5.88888889,19 C7.48437817,19 8.77777778,17.6568542 8.77777778,16 C8.77777778,14.3431458 7.48437817,13 5.88888889,13 C4.29339961,13 3,14.3431458 3,16 C3,17.6568542 4.29339961,19 5.88888889,19 Z M16,19 C17.5954893,19 18.8888889,17.6568542 18.8888889,16 C18.8888889,14.3431458 17.5954893,13 16,13 C14.4045107,13 13.1111111,14.3431458 13.1111111,16 C13.1111111,17.6568542 14.4045107,19 16,19 Z",
+    attrs: { width: 32, height: 32 },
+  },
+  embed:
+    "M12.734 9.333L6.099 16l6.635 6.667a2.547 2.547 0 0 1 0 3.59 2.518 2.518 0 0 1-3.573 0L.74 17.795a2.547 2.547 0 0 1 0-3.59L9.16 5.743a2.518 2.518 0 0 1 3.573 0 2.547 2.547 0 0 1 0 3.59zm6.527 13.339l6.64-6.71-6.63-6.623a2.547 2.547 0 0 1-.01-3.59 2.518 2.518 0 0 1 3.573-.01l8.42 8.412c.99.988.995 2.596.011 3.59l-8.42 8.51a2.518 2.518 0 0 1-3.574.01 2.547 2.547 0 0 1-.01-3.59z",
+  emojiactivity:
+    "M4.58360576,27.9163942 C7.44354038,30.7763289 17.5452495,30.2077351 24.1264923,23.6264923 C30.7077351,17.0452495 31.2763289,6.94354038 28.4163942,4.08360576 C25.5564596,1.22367115 15.4547505,1.79226488 8.87350769,8.37350769 C2.29226488,14.9547505 1.72367115,25.0564596 4.58360576,27.9163942 Z M18.0478143,6.51491123 C17.0327353,6.95492647 16.0168474,7.462507 15.0611336,8.03487206 C13.9504884,8.70002358 12.9907793,9.41185633 12.2241295,10.1785061 C11.6753609,10.7272747 11.1524326,11.3411471 10.6544469,12.0086598 C9.95174829,12.950575 9.33131183,13.9528185 8.79588947,14.9547475 C8.47309082,15.5587964 8.24755055,16.0346972 8.12501633,16.3206104 L10.6033735,17.3827634 C10.6970997,17.1640688 10.8900635,16.7569059 11.1739957,16.2255873 C11.6497914,15.335237 12.2007659,14.4452012 12.8156543,13.6209891 C13.2395954,13.0527276 13.6791325,12.5367492 14.1307526,12.0851292 C14.7228522,11.4930296 15.5113715,10.9081712 16.4465129,10.3481268 C17.2918299,9.84187694 18.205618,9.38530978 19.1202149,8.98885142 C19.6674377,8.75164195 20.0881481,8.58915552 20.3167167,8.50897949 L19.4242138,5.96460106 C19.1382021,6.06492664 18.6583971,6.25023653 18.0478143,6.51491123 Z",
+  emojifood:
+    "M14.9166667,8.71296296 L14.9166667,4.85648148 L14.9636504,4.84865086 L18.8123012,1 L21.149682,3.33738075 L18.2222222,6.26484052 L18.2222222,8.71296296 L27.037037,8.71296296 L27.037037,10.9166667 L24.6968811,10.9166667 L22.2407407,30.75 L10.3148148,30.75 L7.36744639,10.9166667 L5,10.9166667 L5,8.71296296 L14.9166667,8.71296296 Z",
+  emojiflags:
+    "M14.4,17.8888889 L7.9,17.8888889 L7.9,28.9485494 C7.9,30.0201693 7.0344636,30.8888889 5.95,30.8888889 C4.87304474,30.8888889 4,30.0243018 4,28.9485494 L4,2.99442095 C4,2.44521742 4.44737959,2 5.00434691,2 L7.25,2 L19.1921631,2 C20.8138216,2 22.135741,3.27793211 22.1977269,4.88888889 L29.0004187,4.88888889 C29.5524722,4.88888889 30,5.33043204 30,5.88281005 L30,17.7705198 C30,19.4313825 28.6564509,20.7777778 26.9921631,20.7777778 L14.4,20.7777778 L14.4,17.8888889 Z",
+  emojinature:
+    "M19.0364085,24.2897898 L29.4593537,24.2897898 C30.0142087,24.2897898 30.2190588,23.9075249 29.9310364,23.4359772 L17.0209996,2.29978531 C16.7300287,1.82341061 16.2660004,1.82823762 15.9779779,2.29978531 L3.06794114,23.4359772 C2.77697033,23.9123519 2.9910982,24.2897898 3.53962381,24.2897898 L13.962569,24.2897898 L13.962569,31.5582771 L16.5093674,31.5582771 L19.0364085,31.5582771 L19.0364085,24.2897898 Z",
+  mojiobjects:
+    "M10.4444444,25.2307692 L10.4444444,20.0205203 C7.76447613,18.1576095 6,14.9850955 6,11.3846154 C6,5.64935068 10.4771525,1 16,1 C21.5228475,1 26,5.64935068 26,11.3846154 C26,14.9850955 24.2355239,18.1576095 21.5555556,20.0205203 L21.5555556,25.2307692 C21.5555556,28.4170274 19.0682486,31 16,31 C12.9317514,31 10.4444444,28.4170274 10.4444444,25.2307692 Z",
+  emojipeople:
+    "M16,31 C24.2842712,31 31,24.2842712 31,16 C31,7.71572875 24.2842712,1 16,1 C7.71572875,1 1,7.71572875 1,16 C1,24.2842712 7.71572875,31 16,31 Z M22.9642857,19.2142857 C22.9642857,22.3818012 19.8462688,24.9495798 16,24.9495798 C12.1537312,24.9495798 9.03571429,22.3818012 9.03571429,19.2142857 L22.9642857,19.2142857 Z M19.75,14.9285714 C20.6376005,14.9285714 21.3571429,13.2496392 21.3571429,11.1785714 C21.3571429,9.10750362 20.6376005,7.42857143 19.75,7.42857143 C18.8623995,7.42857143 18.1428571,9.10750362 18.1428571,11.1785714 C18.1428571,13.2496392 18.8623995,14.9285714 19.75,14.9285714 Z M12.25,14.9285714 C13.1376005,14.9285714 13.8571429,13.2496392 13.8571429,11.1785714 C13.8571429,9.10750362 13.1376005,7.42857143 12.25,7.42857143 C11.3623995,7.42857143 10.6428571,9.10750362 10.6428571,11.1785714 C10.6428571,13.2496392 11.3623995,14.9285714 12.25,14.9285714 Z",
+  emojisymbols:
+    "M15.915,6.8426232 L13.8540602,4.78168347 C10.8078936,1.73551684 5.86858639,1.73495294 2.82246208,4.78107725 C-0.217463984,7.82100332 -0.223390824,12.7662163 2.82306829,15.8126754 L15.6919527,28.6815598 L15.915,28.4585125 L16.1380473,28.6815598 L29.0069316,15.8126754 C32.0533907,12.7662163 32.0474639,7.82100332 29.0075378,4.78107725 C25.9614135,1.73495294 21.0221063,1.73551684 17.9759397,4.78168347 L15.915,6.8426232 Z",
+  emojitravel:
+    "M21.5273864,25.0150018 L21.5273864,1.91741484 C21.5273864,0.0857778237 20.049926,-1.38499821 18.2273864,-1.38499821 C16.4085552,-1.38499821 14.9273864,0.0935424793 14.9273864,1.91741484 L14.9273864,25.0150018 L11.6273864,28.3150018 L24.8273864,28.3150018 L21.5273864,25.0150018 Z M1.72738636,18.4150018 L14.9273864,5.21500179 L14.9273864,15.7750018 L1.72738636,23.6950018 L1.72738636,18.4150018 Z M34.7273864,18.4150018 L21.5273864,5.21500179 L21.5273864,15.7750018 L34.7273864,23.6950018 L34.7273864,18.4150018 Z",
+  empty: " ",
+  enterorreturn:
+    "M6.81 16.784l6.14-4.694a1.789 1.789 0 0 0 .341-2.49 1.748 1.748 0 0 0-2.464-.344L.697 17a1.788 1.788 0 0 0-.01 2.826l10.058 7.806c.77.598 1.875.452 2.467-.326a1.79 1.79 0 0 0-.323-2.492l-5.766-4.475h23.118c.971 0 1.759-.796 1.759-1.777V6.777C32 5.796 31.212 5 30.24 5c-.971 0-1.759.796-1.759 1.777v10.007H6.811z",
+  expand:
+    "M29,13.6720571 L29,8.26132482e-16 L15.3279429,8.64083276e-16 L20.3471502,5.01920738 L15.0015892,10.3647684 L18.6368207,14 L23.9823818,8.65443894 L29,13.6720571 Z M0.00158917013,15.3279429 L0.00158917013,29 L13.6736463,29 L8.65443894,23.9807926 L14,18.6352316 L10.3647684,15 L5.01920738,20.3455611 L0.00158917013,15.3279429 Z",
+  expandarrow: "M16.429 28.429L.429 5.57h32z",
+  external:
+    "M13.7780693,4.44451732 L5.1588494,4.44451732 C2.32615959,4.44451732 0,6.75504816 0,9.60367661 L0,25.1192379 C0,27.9699171 2.30950226,30.2783972 5.1588494,30.2783972 L18.9527718,30.2783972 C21.7854617,30.2783972 24.1116212,27.9678664 24.1116212,25.1192379 L24.1116212,19.9448453 L20.6671039,19.9448453 L20.6671039,25.1192379 C20.6671039,26.0662085 19.882332,26.8338799 18.9527718,26.8338799 L5.1588494,26.8338799 C4.21204994,26.8338799 3.44451732,26.0677556 3.44451732,25.1192379 L3.44451732,9.60367661 C3.44451732,8.656706 4.22928927,7.88903464 5.1588494,7.88903464 L13.7780693,7.88903464 L13.7780693,4.44451732 L13.7780693,4.44451732 Z M30.9990919,14.455325 L30.9990919,1 L17.5437669,1 L22.4834088,5.93964193 L17.2225866,11.2004641 L20.8001918,14.7780693 L26.061014,9.51724709 L30.9990919,14.455325 L30.9990919,14.455325 L30.9990919,14.455325 Z",
+  everything: {
+    path:
+      "M26.656 17.273a400.336 400.336 0 0 1 2.632.017l.895.008-2.824-9.56c-.122-.411.1-.848.495-.975a.743.743 0 0 1 .936.516l3.177 10.752.033.23V31H0V17.994L3.303 7.212a.743.743 0 0 1 .94-.507c.394.132.611.57.486.982L1.763 17.37h3.486V9.758c0-.865.669-1.558 1.505-1.558h18.398c.83 0 1.504.69 1.504 1.56v7.513zm-1.497 0h-.176c-.362.002-.648.006-.853.012a7.264 7.264 0 0 0-.272.011c-.056.004-.056.004-.117.011-.058-.001-.058-.001-.355.135-.233.164-.344.37-.452.68-.05.147-.236.807-.287.97-.132.419-.279.775-.469 1.108-.43.752-1.048 1.322-1.959 1.693-.15.062-2.005.093-4.356.056-1.561-.024-4.056-.096-4.053-.095-.92-.185-1.634-.826-2.173-1.818a7.023 7.023 0 0 1-.566-1.4 5.72 5.72 0 0 1-.147-.614.758.758 0 0 0-.738-.652h-1.44V9.76l18.406.002c.003 0 .006 4.98.007 7.51zM1.497 18.931v10.507h29.006V18.863a780.193 780.193 0 0 0-3.023-.025 233.572 233.572 0 0 0-2.489-.003c-.287.001-.524.004-.706.008-.067.23-.17.59-.216.736-.164.52-.352.977-.605 1.42-.594 1.042-1.468 1.846-2.7 2.349-.681.278-8.668.153-9.236.04-1.407-.283-2.456-1.225-3.194-2.582a8.596 8.596 0 0 1-.74-1.874H1.498zM6.982 5.828h18.074c.375 0 .679-.35.679-.78 0-.432-.304-.782-.679-.782H6.982c-.375 0-.678.35-.678.781 0 .431.303.781.678.781zm1.38-3.267h15.28c.369 0 .668-.35.668-.78 0-.431-.299-.781-.668-.781H8.362c-.37 0-.668.35-.668.78 0 .432.299.781.668.781z",
+    attrs: { fillRule: "evenodd" },
+  },
+  eye:
+    "M30.622 18.49c-.549.769-1.46 1.86-2.737 3.273-1.276 1.414-2.564 2.614-3.866 3.602-2.297 1.757-4.963 2.635-8 2.635-3.062 0-5.741-.878-8.038-2.635-1.302-.988-2.59-2.188-3.866-3.602-1.276-1.413-2.188-2.504-2.737-3.272-.549-.769-.9-1.277-1.053-1.524-.433-.63-.433-1.276 0-1.934.128-.247.472-.755 1.034-1.524.561-.768 1.48-1.852 2.756-3.252 1.276-1.4 2.564-2.593 3.866-3.581C10.303 4.892 12.982 4 16.019 4c3.011 0 5.678.892 8 2.676 1.302.988 2.59 2.182 3.866 3.581 1.276 1.4 2.195 2.484 2.756 3.252.562.769.906 1.277 1.034 1.524.433.63.433 1.276 0 1.934-.153.247-.504.755-1.053 1.524zm-1.516-3.214c-.248.376-.248 1.089.034 1.499l-.11-.16-.088-.17a21.93 21.93 0 0 0-.784-1.121c-.483-.66-1.338-1.67-2.546-2.995-1.154-1.266-2.306-2.333-3.466-3.214-1.781-1.368-3.788-2.04-6.127-2.04-2.365 0-4.385.673-6.179 2.05-1.146.87-2.298 1.938-3.452 3.204-1.208 1.325-2.063 2.334-2.546 2.995a21.93 21.93 0 0 0-.784 1.12l-.075.145-.09.135c.249-.376.249-1.089-.033-1.499l.08.122c.105.17.432.644.941 1.356.466.653 1.313 1.666 2.517 3 1.152 1.275 2.3 2.346 3.451 3.22 1.752 1.339 3.773 2.001 6.17 2.001 2.37 0 4.379-.661 6.14-2.008 1.143-.867 2.291-1.938 3.443-3.214 1.204-1.333 2.05-2.346 2.517-2.999.509-.712.836-1.186.942-1.356l.045-.071zm-17.353 5.663C10.584 19.709 10 18.237 10 16.522c0-1.744.584-3.224 1.753-4.439 1.168-1.215 2.59-1.822 4.268-1.822 1.65 0 3.058.607 4.226 1.822C21.416 13.298 22 14.778 22 16.522c0 1.715-.584 3.187-1.753 4.417-1.168 1.229-2.577 1.844-4.226 1.844-1.677 0-3.1-.615-4.268-1.844zm6.265-2.12c.624-.655.906-1.368.906-2.297 0-.957-.281-1.67-.893-2.307-.592-.616-1.203-.879-2.01-.879-.84 0-1.462.266-2.052.88-.612.636-.893 1.35-.893 2.306 0 .929.282 1.642.906 2.298.59.62 1.207.887 2.039.887.8 0 1.405-.264 1.997-.887z",
+  field:
+    "M10,1 L22,1 L22,4 L10,4 L10,1 Z M10,6 L22,6 L22,12 L10,12 L10,6 Z M10,14 L22,14 L22,20 L10,20 L10,14 Z M10,22 L22,22 L22,28 L16.1421494,32 L10,28 L10,22 Z",
+  fields:
+    "M0,0 L7.51851852,0 L7.51851852,3.2 L0,3.2 L0,0 Z M10.7407407,0 L18.2592593,0 L18.2592593,3.2 L10.7407407,3.2 L10.7407407,0 Z M21.4814815,0 L29,0 L29,3.2 L21.4814815,3.2 L21.4814815,0 Z M0,5.33333333 L7.51851852,5.33333333 L7.51851852,29.8666667 L3.85540334,32 L0,29.8666667 L0,5.33333333 Z M10.7407407,5.33333333 L18.2592593,5.33333333 L18.2592593,29.8666667 L14.5690136,32 L10.7407407,29.8666667 L10.7407407,5.33333333 Z M21.4814815,5.33333333 L29,5.33333333 L29,29.8666667 L25.2114718,32 L21.4814815,29.8666667 L21.4814815,5.33333333 Z",
+  filter: {
+    svg:
+      '<g><path d="M1,12 L17,12 L17,14 L1,14 L1,12 Z M1,7 L17,7 L17,9 L1,9 L1,7 Z M1,2 L17,2 L17,4 L1,4 L1,2 Z" fill="currentcolor"></path><path d="M9,15.5 C10.3807119,15.5 11.5,14.3807119 11.5,13 C11.5,11.6192881 10.3807119,10.5 9,10.5 C7.61928813,10.5 6.5,11.6192881 6.5,13 C6.5,14.3807119 7.61928813,15.5 9,15.5 Z M13,5.5 C14.3807119,5.5 15.5,4.38071187 15.5,3 C15.5,1.61928813 14.3807119,0.5 13,0.5 C11.6192881,0.5 10.5,1.61928813 10.5,3 C10.5,4.38071187 11.6192881,5.5 13,5.5 Z M3,10.5 C4.38071187,10.5 5.5,9.38071187 5.5,8 C5.5,6.61928813 4.38071187,5.5 3,5.5 C1.61928813,5.5 0.5,6.61928813 0.5,8 C0.5,9.38071187 1.61928813,10.5 3,10.5 Z" id="Path" fill="currentcolor"></path><path d="M13,4.5 C12.1715729,4.5 11.5,3.82842712 11.5,3 C11.5,2.17157288 12.1715729,1.5 13,1.5 C13.8284271,1.5 14.5,2.17157288 14.5,3 C14.5,3.82842712 13.8284271,4.5 13,4.5 Z M9,14.5 C8.17157288,14.5 7.5,13.8284271 7.5,13 C7.5,12.1715729 8.17157288,11.5 9,11.5 C9.82842712,11.5 10.5,12.1715729 10.5,13 C10.5,13.8284271 9.82842712,14.5 9,14.5 Z M3,9.5 C2.17157288,9.5 1.5,8.82842712 1.5,8 C1.5,7.17157288 2.17157288,6.5 3,6.5 C3.82842712,6.5 4.5,7.17157288 4.5,8 C4.5,8.82842712 3.82842712,9.5 3,9.5 Z" fill="#FFFFFF"></path></g>',
+    attrs: { viewBox: "0 0 19 16" },
+  },
+  funnel:
+    "M3.18586974,3.64621479 C2.93075885,3.28932022 3.08031197,3 3.5066208,3 L28.3780937,3 C28.9190521,3 29.0903676,3.34981042 28.7617813,3.77995708 L18.969764,16.5985181 L18.969764,24.3460671 C18.969764,24.8899179 18.5885804,25.5564176 18.133063,25.8254534 C18.133063,25.8254534 12.5698889,29.1260709 12.5673818,28.9963552 C12.4993555,25.4767507 12.5749031,16.7812673 12.5749031,16.7812673 L3.18586974,3.64621479 Z",
+  funneladd:
+    "M22.5185184,5.27947653 L17.2510286,5.27947653 L17.2510286,9.50305775 L22.5185184,9.50305775 L22.5185184,14.7825343 L26.7325102,14.7825343 L26.7325102,9.50305775 L32,9.50305775 L32,5.27947653 L26.7325102,5.27947653 L26.7325102,0 L22.5185184,0 L22.5185184,5.27947653 Z M14.9369872,0.791920724 C14.9369872,0.791920724 2.77552871,0.83493892 1.86648164,0.83493892 C0.957434558,0.83493892 0.45215388,1.50534608 0.284450368,1.77831828 C0.116746855,2.05129048 -0.317642562,2.91298361 0.398382661,3.9688628 C1.11440788,5.024742 9.74577378,17.8573356 9.74577378,17.8573356 C9.74577378,17.8573356 9.74577394,28.8183645 9.74577378,29.6867194 C9.74577362,30.5550744 9.83306175,31.1834301 10.7557323,31.6997692 C11.6784029,32.2161084 12.4343349,31.9564284 12.7764933,31.7333621 C13.1186517,31.5102958 19.6904355,27.7639669 20.095528,27.4682772 C20.5006204,27.1725875 20.7969652,26.5522071 20.7969651,25.7441659 C20.7969649,24.9361247 20.7969651,18.2224765 20.7969651,18.2224765 L21.6163131,16.9859755 L18.152048,15.0670739 C18.152048,15.0670739 17.3822517,16.199685 17.2562629,16.4000338 C17.1302741,16.6003826 16.8393552,16.9992676 16.8393551,17.7062886 C16.8393549,18.4133095 16.8393551,24.9049733 16.8393551,24.9049733 L13.7519708,26.8089871 C13.7519708,26.8089871 13.7318369,18.3502323 13.7318367,17.820601 C13.7318366,17.2909696 13.8484216,16.6759061 13.2410236,15.87149 C12.6336257,15.0670739 5.59381579,4.76288686 5.59381579,4.76288686 L14.9359238,4.76288686 L14.9369872,0.791920724 Z",
+  funneloutline: {
+    path:
+      "M3.186 3.646C2.93 3.29 3.08 3 3.506 3h24.872c.541 0 .712.35.384.78L18.97 16.599v7.747c0 .544-.381 1.21-.837 1.48 0 0-5.563 3.3-5.566 3.17-.068-3.52.008-12.215.008-12.215L3.185 3.646z",
+    attrs: {
+      stroke: "currentcolor",
+      strokeWidth: "4",
+      fill: "none",
+      fillRule: "evenodd",
     },
-    x: 'm11.271709,16 l-3.19744231e-13,4.728291 l4.728291,0 l16,11.271709 l27.271709,2.39808173e-13 l32,4.728291 l20.728291,16 l31.1615012,26.4332102 l26.4332102,31.1615012 l16,20.728291 l5.56678976,31.1615012 l0.838498756,26.4332102 l11.271709,16 z',
-    zoom: 'M12.416 12.454V8.37h3.256v4.083h4.07v3.266h-4.07v4.083h-3.256V15.72h-4.07v-3.266h4.07zm10.389 13.28c-5.582 4.178-13.543 3.718-18.632-1.37-5.58-5.581-5.595-14.615-.031-20.179 5.563-5.563 14.597-5.55 20.178.031 5.068 5.068 5.545 12.985 1.422 18.563l5.661 5.661a2.08 2.08 0 0 1 .003 2.949 2.085 2.085 0 0 1-2.95-.003l-5.651-5.652zm-1.486-4.371c3.895-3.895 3.885-10.218-.021-14.125-3.906-3.906-10.23-3.916-14.125-.021-3.894 3.894-3.885 10.218.022 14.124 3.906 3.907 10.23 3.916 14.124.022z',
-    slack: {
-        img: "app/assets/img/slack.png"
-    }
+  },
+  folder:
+    "M3.96901618e-15,5.41206355 L0.00949677904,29 L31.8821132,29 L31.8821132,10.8928571 L18.2224205,10.8928571 L15.0267944,5.41206355 L3.96901618e-15,5.41206355 Z M16.8832349,5.42402804 L16.8832349,4.52140947 C16.8832349,3.68115822 17.5639241,3 18.4024298,3 L27.7543992,3 L30.36417,3 C31.2031259,3 31.8832341,3.67669375 31.8832341,4.51317691 L31.8832341,7.86669975 L31.8832349,8.5999999 L18.793039,8.5999999 L16.8832349,5.42402804 Z",
+  gear:
+    "M14 0 H18 L19 6 L20.707 6.707 L26 3.293 L28.707 6 L25.293 11.293 L26 13 L32 14 V18 L26 19 L25.293 20.707 L28.707 26 L26 28.707 L20.707 25.293 L19 26 L18 32 L14 32 L13 26 L11.293 25.293 L6 28.707 L3.293 26 L6.707 20.707 L6 19 L0 18 L0 14 L6 13 L6.707 11.293 L3.293 6 L6 3.293 L11.293 6.707 L13 6 L14 0 z M16 10 A6 6 0 0 0 16 22 A6 6 0 0 0 16 10",
+  grabber:
+    "M0,5 L32,5 L32,9.26666667 L0,9.26666667 L0,5 Z M0,13.5333333 L32,13.5333333 L32,17.8 L0,17.8 L0,13.5333333 Z M0,22.0666667 L32,22.0666667 L32,26.3333333 L0,26.3333333 L0,22.0666667 Z",
+  grid:
+    "M2 2 L10 2 L10 10 L2 10z M12 2 L20 2 L20 10 L12 10z M22 2 L30 2 L30 10 L22 10z M2 12 L10 12 L10 20 L2 20z M12 12 L20 12 L20 20 L12 20z M22 12 L30 12 L30 20 L22 20z M2 22 L10 22 L10 30 L2 30z M12 22 L20 22 L20 30 L12 30z M22 22 L30 22 L30 30 L22 30z",
+  google: {
+    svg:
+      '<g fill="none" fill-rule="evenodd"><path d="M16 32c4.32 0 7.947-1.422 10.596-3.876l-5.05-3.91c-1.35.942-3.164 1.6-5.546 1.6-4.231 0-7.822-2.792-9.102-6.65l-5.174 4.018C4.356 28.41 9.742 32 16 32z" fill="#34A853"/><path d="M6.898 19.164A9.85 9.85 0 0 1 6.364 16c0-1.102.196-2.169.516-3.164L1.707 8.818A16.014 16.014 0 0 0 0 16c0 2.578.622 5.013 1.707 7.182l5.19-4.018z" fill="#FBBC05"/><path d="M31.36 16.356c0-1.316-.107-2.276-.338-3.272H16v5.938h8.818c-.178 1.476-1.138 3.698-3.271 5.191l5.049 3.911c3.022-2.79 4.764-6.897 4.764-11.768z" fill="#4285F4"/><path d="M16 6.187c3.004 0 5.031 1.297 6.187 2.382l4.515-4.409C23.93 1.582 20.32 0 16 0 9.742 0 4.338 3.591 1.707 8.818l5.173 4.018c1.298-3.858 4.889-6.65 9.12-6.65z" fill="#EA4335"/></g>',
+  },
+  history: {
+    path:
+      "M4.03074198,15 C4.54693838,6.62927028 11.4992947,0 20,0 C28.836556,0 36,7.163444 36,16 C36,24.836556 28.836556,32 20,32 C16.9814511,32 14.1581361,31.164104 11.7489039,29.7111608 L14.1120194,26.4586113 C15.8515127,27.4400159 17.8603607,28 20,28 C26.627417,28 32,22.627417 32,16 C32,9.372583 26.627417,4 20,4 C13.7093362,4 8.54922468,8.84046948 8.04107378,15 L11,15 L6,22 L1.34313965,15 L4.03074198,15 Z M22,15.2218254 L24.5913352,17.8131606 L24.5913352,17.8131606 C25.3723838,18.5942092 25.3723838,19.8605392 24.5913352,20.6415878 C23.8176686,21.4152544 22.5633071,21.4152544 21.7896404,20.6415878 C21.7852062,20.6371536 21.7807931,20.6326983 21.7764012,20.6282222 L18.8194549,17.6145768 C18.3226272,17.2506894 18,16.6630215 18,16 L18,10 C18,8.8954305 18.8954305,8 20,8 C21.1045695,8 22,8.8954305 22,10 L22,15.2218254 Z",
+    attrs: { viewBox: "0 0 36 33" },
+  },
+  info:
+    "M16 0 A16 16 0 0 1 16 32 A16 16 0 0 1 16 0 M19 15 L13 15 L13 26 L19 26 z M16 6 A3 3 0 0 0 16 12 A3 3 0 0 0 16 6",
+  infooutlined:
+    "M16 29c7.18 0 13-5.82 13-13S23.18 3 16 3 3 8.82 3 16s5.82 13 13 13zm0 3C7.163 32 0 24.837 0 16S7.163 0 16 0s16 7.163 16 16-7.163 16-16 16zm1.697-20h-4.185v14h4.185V12zm.432-3.834c0-.342-.067-.661-.203-.958a2.527 2.527 0 0 0-1.37-1.31 2.613 2.613 0 0 0-.992-.188c-.342 0-.661.062-.959.189a2.529 2.529 0 0 0-1.33 1.309c-.13.297-.195.616-.195.958 0 .334.065.646.196.939.13.292.31.549.54.77.23.22.492.395.79.526.297.13.616.196.958.196.351 0 .682-.066.992-.196.31-.13.583-.306.817-.527a2.47 2.47 0 0 0 .553-.77c.136-.292.203-.604.203-.938z",
+  insight:
+    "M12.6325203 19.3674797 0 16 12.6325203 12.6325203 16 0 19.3674797 12.6325203 32 16 19.3674797 19.3674797 16 32z",
+  int: {
+    path:
+      "M15.141,15.512 L14.294,20 L13.051,20 C12.8309989,20 12.6403341,19.9120009 12.479,19.736 C12.3176659,19.5599991 12.237,19.343668 12.237,19.087 C12.237,19.0503332 12.2388333,19.0155002 12.2425,18.9825 C12.2461667,18.9494998 12.2516666,18.9146668 12.259,18.878 L12.908,15.512 L10.653,15.512 L10.015,19.01 C9.94899967,19.3620018 9.79866784,19.6149992 9.564,19.769 C9.32933216,19.9230008 9.06900143,20 8.783,20 L7.584,20 L8.42,15.512 L7.155,15.512 C6.92033216,15.512 6.74066729,15.4551672 6.616,15.3415 C6.49133271,15.2278328 6.429,15.0390013 6.429,14.775 C6.429,14.6723328 6.43999989,14.5550007 6.462,14.423 L6.605,13.554 L8.695,13.554 L9.267,10.518 L6.913,10.518 L7.122,9.385 C7.17333359,9.10633194 7.28699912,8.89916734 7.463,8.7635 C7.63900088,8.62783266 7.92499802,8.56 8.321,8.56 L9.542,8.56 L10.224,5.018 C10.282667,4.7246652 10.4183323,4.49733414 10.631,4.336 C10.8436677,4.17466586 11.0929986,4.094 11.379,4.094 L12.611,4.094 L11.775,8.56 L14.019,8.56 L14.866,4.094 L16.076,4.094 C16.3326679,4.094 16.5416659,4.1673326 16.703,4.314 C16.8643341,4.4606674 16.945,4.64766553 16.945,4.875 C16.945,4.9483337 16.9413334,5.00333315 16.934,5.04 L16.252,8.56 L18.485,8.56 L18.276,9.693 C18.2246664,9.97166806 18.1091676,10.1788327 17.9295,10.3145 C17.7498324,10.4501673 17.4656686,10.518 17.077,10.518 L15.977,10.518 L15.416,13.554 L16.978,13.554 C17.2126678,13.554 17.3904994,13.6108328 17.5115,13.7245 C17.6325006,13.8381672 17.693,14.0306653 17.693,14.302 C17.693,14.4046672 17.6820001,14.5219993 17.66,14.654 L17.528,15.512 L15.141,15.512 Z M10.928,13.554 L13.183,13.554 L13.744,10.518 L11.5,10.518 L10.928,13.554 Z",
+    attrs: { viewBox: "0 0 24 24" },
+  },
+  io:
+    "M1,9 L6,9 L6,24 L1,24 L1,9 Z M31,16 C31,11.581722 27.418278,8 23,8 C18.581722,8 15,11.581722 15,16 C15,20.418278 18.581722,24 23,24 C27.418278,24 31,20.418278 31,16 Z M19,16 C19,13.790861 20.790861,12 23,12 C25.209139,12 27,13.790861 27,16 C27,18.209139 25.209139,20 23,20 C20.790861,20 19,18.209139 19,16 Z M15.3815029,9 L13.4537572,9 L7,23.5 L8.92774566,23.5 L15.3815029,9 Z",
+  key: {
+    path:
+      "M11.5035746,7.9975248 C10.8617389,5.26208051 13.0105798,1.44695394 16.9897081,1.44695394 C20.919315,1.44695394 23.1811258,5.37076315 22.2565255,8.42469226 C21.3223229,7.86427598 20.2283376,7.54198814 19.0589133,7.54198814 C17.3567818,7.54198814 15.8144729,8.22477622 14.6920713,9.33083544 C14.4930673,9.31165867 14.2913185,9.30184676 14.087273,9.30184676 C10.654935,9.30184676 7.87247532,12.0782325 7.87247532,15.5030779 C7.87247532,17.1058665 8.48187104,18.5666337 9.48208198,19.6672763 L8.98356958,20.658345 L9.19925633,22.7713505 L7.5350473,23.4587525 C7.37507672,23.5248284 7.30219953,23.707739 7.37031308,23.8681037 L7.95501877,25.2447188 L6.28291833,25.7863476 C6.10329817,25.8445303 6.01548404,26.0233452 6.06755757,26.1919683 L6.54426059,27.7356153 L5.02460911,28.2609385 C4.86686602,28.3154681 4.7743984,28.501653 4.83652351,28.6704172 L6.04508836,31.95351 C6.10987939,32.1295162 6.29662279,32.2151174 6.46814592,32.160881 L9.48965349,31.2054672 C9.66187554,31.1510098 9.86840241,30.9790422 9.95250524,30.8208731 L14.8228902,21.6613229 C15.8820565,21.5366928 16.8596786,21.1462953 17.6869404,20.558796 C17.5652123,20.567429 17.4424042,20.5718139 17.318643,20.5718139 C14.2753735,20.5718139 11.8083161,17.9204625 11.8083161,14.6498548 C11.8083161,12.518229 12.8562751,10.6496514 14.428709,9.60671162 C13.4433608,10.7041074 12.8441157,12.1538355 12.8441157,13.7432193 C12.8441157,16.9974306 15.3562245,19.6661883 18.5509945,19.9240384 L19.1273026,21.5699573 L20.7971002,22.8826221 L20.1355191,24.5572635 C20.0719252,24.7182369 20.1528753,24.8977207 20.3155476,24.9601226 L21.7119724,25.4957977 L20.9400489,27.0748531 C20.8571275,27.2444782 20.9247553,27.4318616 21.082226,27.5115385 L22.5237784,28.2409344 L21.8460256,29.6990003 C21.7756734,29.8503507 21.8453702,30.0462011 22.0099247,30.1187455 L25.2111237,31.5300046 C25.3827397,31.6056621 25.5740388,31.5307937 25.6541745,31.3697345 L27.0658228,28.5325576 C27.1462849,28.3708422 27.1660474,28.1028205 27.1106928,27.9324485 L23.8023823,17.7500271 C24.7201964,16.6692906 25.273711,15.270754 25.273711,13.7432193 C25.273711,12.0364592 24.582689,10.4907436 23.4645818,9.36943333 C25.0880384,5.38579616 22.187534,0 16.9897081,0 C12.1196563,0 9.42801686,4.46934651 10.0266074,7.9975248 L11.5035746,7.9975248 Z M19.0589133,14.7767578 C20.203026,14.7767578 21.1305126,13.8512959 21.1305126,12.7096808 C21.1305126,11.5680656 20.203026,10.6426037 19.0589133,10.6426037 C17.9148007,10.6426037 16.9873141,11.5680656 16.9873141,12.7096808 C16.9873141,13.8512959 17.9148007,14.7767578 19.0589133,14.7767578 Z",
+    attrs: { fillRule: "evenodd" },
+  },
+  label:
+    "M14.577 31.042a2.005 2.005 0 0 1-2.738-.733L1.707 12.759c-.277-.477-.298-1.265-.049-1.757L6.45 1.537C6.7 1.044 7.35.67 7.9.7l10.593.582c.551.03 1.22.44 1.498.921l10.132 17.55a2.002 2.002 0 0 1-.734 2.737l-14.812 8.552zm.215-22.763a3.016 3.016 0 1 0-5.224 3.016 3.016 3.016 0 0 0 5.224-3.016z",
+  ldap: {
+    path:
+      "M1.006 3h13.702c.554 0 1.178.41 1.39.915l.363.874c.21.504.827.915 1.376.915h13.169c.54 0 .994.448.994 1.001v20.952a.99.99 0 0 1-.992 1H1.002c-.54 0-.993-.45-.993-1.005l-.01-23.646C0 3.446.45 3 1.007 3zM16.5 19.164c1.944 0 3.52-1.828 3.52-4.082 0-2.254-1.576-4.082-3.52-4.082-1.945 0-3.52 1.828-3.52 4.082 0 2.254 1.575 4.082 3.52 4.082zm6.5 4.665c0-1.872-1.157-3.521-2.913-4.484-.927.97-2.192 1.568-3.587 1.568s-2.66-.597-3.587-1.568C11.157 20.308 10 21.957 10 23.83h13z",
+    attrs: { fillRule: "evenodd" },
+  },
+  left: "M21,0 L5,16 L21,32 L21,5.47117907e-13 L21,0 Z",
+  lightbulb:
+    "M16.1 11.594L18.756 8.9a1.03 1.03 0 0 1 1.446-.018c.404.39.412 1.03.018 1.43l-3.193 3.24v4.975c0 .559-.458 1.011-1.022 1.011a1.017 1.017 0 0 1-1.023-1.01v-5.17l-3.003-3.046c-.394-.4-.386-1.04.018-1.43a1.03 1.03 0 0 1 1.446.018l2.657 2.695zM11.03 28.815h9.938a1.01 1.01 0 1 1 0 2.02 376.72 376.72 0 0 0-2.964.002C18.005 31.857 16.767 32 16 32c-.767 0-1.993-.139-1.993-1.163H11.03a1.011 1.011 0 0 1 0-2.022zm0-3.033h9.938a1.011 1.011 0 0 1 0 2.022H11.03a1.011 1.011 0 1 1 0-2.022zM8.487 20.43A11.659 11.659 0 0 1 4.5 11.627C4.5 5.214 9.64 0 16 0s11.5 5.214 11.5 11.627c0 3.43-1.481 6.617-3.987 8.803v1.308c0 1.954-1.601 3.538-3.577 3.538h-7.872c-1.976 0-3.577-1.584-3.577-3.538V20.43zm2.469-1.915l.597.455v2.768c0 .279.23.505.511.505h7.872a.508.508 0 0 0 .51-.505V18.97l.598-.455a8.632 8.632 0 0 0 3.39-6.888c0-4.755-3.785-8.594-8.434-8.594-4.649 0-8.433 3.84-8.433 8.594a8.632 8.632 0 0 0 3.389 6.888z",
+  link:
+    "M12.56 17.04c-1.08 1.384-1.303 1.963 1.755 4.04 3.058 2.076 7.29.143 8.587-1.062 1.404-1.304 4.81-4.697 7.567-7.842 2.758-3.144 1.338-8.238-.715-9.987-5.531-4.71-9.5-.554-11.088.773-2.606 2.176-5.207 5.144-5.207 5.144s1.747-.36 2.784 0c1.036.36 2.102.926 2.102.926l4.003-3.969s2.367-1.907 4.575 0 .674 4.404 0 5.189c-.674.784-6.668 6.742-6.668 6.742s-1.52.811-2.37.811c-.85 0-2.582-.528-2.582-.932 0-.405-1.665-1.22-2.744.166zm7.88-2.08c1.08-1.384 1.303-1.963-1.755-4.04-3.058-2.076-7.29-.143-8.587 1.062-1.404 1.304-4.81 4.697-7.567 7.842-2.758 3.144-1.338 8.238.715 9.987 5.531 4.71 9.5.554 11.088-.773 2.606-2.176 5.207-5.144 5.207-5.144s-1.747.36-2.784 0a17.379 17.379 0 0 1-2.102-.926l-4.003 3.969s-2.367 1.907-4.575 0-.674-4.404 0-5.189c.674-.784 6.668-6.742 6.668-6.742s1.52-.811 2.37-.811c.85 0 2.582.528 2.582.932 0 .405 1.665 1.22 2.744-.166z",
+  line:
+    "M18.867 16.377l-3.074-3.184-.08.077-.002-.002.01-.01-.53-.528-.066-.07-.001.002-2.071-2.072L-.002 23.645l2.668 2.668 10.377-10.377 3.074 3.183.08-.076.001.003-.008.008.5.501.094.097.002-.001 2.072 2.072L31.912 8.669 29.244 6 18.867 16.377z",
+  list:
+    "M3 8 A3 3 0 0 0 9 8 A3 3 0 0 0 3 8 M12 6 L28 6 L28 10 L12 10z M3 16 A3 3 0 0 0 9 16 A3 3 0 0 0 3 16 M12 14 L28 14 L28 18 L12 18z M3 24 A3 3 0 0 0 9 24 A3 3 0 0 0 3 24 M12 22 L28 22 L28 26 L12 26z",
+  location: {
+    path:
+      "M19.4917776,13.9890373 C20.4445763,12.5611169 21,10.8454215 21,9 C21,4.02943725 16.9705627,0 12,0 C7.02943725,0 3,4.02943725 3,9 C3,10.8454215 3.5554237,12.5611168 4.50822232,13.9890371 L4.49999986,14.0000004 L4.58010869,14.0951296 C4.91305602,14.5790657 5.29212089,15.0288088 5.71096065,15.4380163 L12.5,23.5 L19.4999993,13.9999996 L19.4917776,13.9890373 L19.4917776,13.9890373 Z M12,12 C13.6568542,12 15,10.6568542 15,9 C15,7.34314575 13.6568542,6 12,6 C10.3431458,6 9,7.34314575 9,9 C9,10.6568542 10.3431458,12 12,12 Z",
+    attrs: { viewBox: "0 0 24 24" },
+  },
+  lock: {
+    path:
+      "M7.30894737,12.4444444 L4.91725192,12.4444444 C3.2943422,12.4444444 2,13.7457504 2,15.3509926 L2,29.0934518 C2,30.7017608 3.30609817,32 4.91725192,32 L27.0827481,32 C28.7056578,32 30,30.6986941 30,29.0934518 L30,15.3509926 C30,13.7426837 28.6939018,12.4444444 27.0827481,12.4444444 L24.6910526,12.4444444 L24.6910526,7.44176009 C24.6910526,3.33441301 21.3568185,0 17.2438323,0 L14.7561677,0 C10.6398254,0 7.30894737,3.33178948 7.30894737,7.44176009 L7.30894737,12.4444444 Z M10.8678947,8.21027479 C10.8678947,5.65010176 12.9450109,3.57467145 15.5045167,3.57467145 L16.4954833,3.57467145 C19.0562189,3.57467145 21.1321053,5.65531119 21.1321053,8.21027479 L21.1321053,12.8458781 L10.8678947,12.8458781 L10.8678947,8.21027479 Z M16,26.6666667 C17.9329966,26.6666667 19.5,25.0747902 19.5,23.1111111 C19.5,21.147432 17.9329966,19.5555556 16,19.5555556 C14.0670034,19.5555556 12.5,21.147432 12.5,23.1111111 C12.5,25.0747902 14.0670034,26.6666667 16,26.6666667 Z",
+    attrs: { fillRule: "evenodd" },
+  },
+  lockoutline:
+    "M7 12H5.546A3.548 3.548 0 0 0 2 15.553v12.894A3.547 3.547 0 0 0 5.546 32h20.908C28.414 32 30 30.41 30 28.447V15.553A3.547 3.547 0 0 0 26.454 12H25V8.99C25 4.029 20.97 0 16 0c-4.972 0-9 4.025-9 8.99V12zm4-3.766c0-2.338 1.89-4.413 4.219-4.634L16 3.525l.781.075C19.111 3.82 21 5.896 21 8.234V12H11V8.234zm-5 9.537C6 16.793 6.796 16 7.775 16h16.45c.98 0 1.775.787 1.775 1.77v8.46c0 .977-.796 1.77-1.775 1.77H7.775A1.77 1.77 0 0 1 6 26.23v-8.46zM16 25a3 3 0 1 0 0-6 3 3 0 0 0 0 6z",
+  mail:
+    "M1.503 6h28.994C31.327 6 32 6.673 32 7.503v16.06A3.436 3.436 0 0 1 28.564 27H3.436A3.436 3.436 0 0 1 0 23.564V7.504C0 6.673.673 6 1.503 6zm4.403 2.938l10.63 8.052 10.31-8.052H5.906zm-2.9 1.632v11.989c0 .83.674 1.503 1.504 1.503h23.087c.83 0 1.504-.673 1.504-1.503V11.005l-11.666 8.891a1.503 1.503 0 0 1-1.806.013l-12.622-9.34z",
+  mine:
+    "M28.4907419,50 C25.5584999,53.6578499 21.0527692,56 16,56 C10.9472308,56 6.44150015,53.6578499 3.50925809,50 L28.4907419,50 Z M29.8594823,31.9999955 C27.0930063,27.217587 21.922257,24 16,24 C10.077743,24 4.9069937,27.217587 2.1405177,31.9999955 L29.8594849,32 Z M16,21 C19.8659932,21 23,17.1944204 23,12.5 C23,7.80557963 22,3 16,3 C10,3 9,7.80557963 9,12.5 C9,17.1944204 12.1340068,21 16,21 Z",
+  moon:
+    "M11.6291702,1.84239429e-11 C19.1234093,1.22958025 24.8413559,7.73631246 24.8413559,15.5785426 C24.8413559,24.2977683 17.7730269,31.3660972 9.05380131,31.3660972 C7.28632096,31.3660972 5.58667863,31.0756481 4,30.5398754 C11.5007933,28.2096945 16.9475786,21.2145715 16.9475786,12.9472835 C16.9475786,7.90001143 14.9174312,3.32690564 11.6291702,1.70246039e-11 L11.6291702,1.84239429e-11 Z",
+  move:
+    "M23 27h-8v-5h8v-4l8 6-8 7v-4zM17.266 0h.86a2 2 0 0 1 1.42.592L27.49 8.61a2 2 0 0 1 .58 1.407v6h-5.01v-5.065L17.133 5H0V2a2 2 0 0 1 2-2h15.266zM5 27h7v5H2a2 2 0 0 1-2-2V5h5v22z",
+  number:
+    "M0 .503A.5.5 0 0 1 .503 0h30.994A.5.5 0 0 1 32 .503v30.994a.5.5 0 0 1-.503.503H.503A.5.5 0 0 1 0 31.497V.503zM8.272 22V10.8H6.464c-.064.427-.197.784-.4 1.072-.203.288-.45.52-.744.696a2.984 2.984 0 0 1-.992.368c-.368.07-.75.099-1.144.088v1.712H6V22h2.272zm2.96-5.648c0 1.12.11 2.056.328 2.808.219.752.515 1.352.888 1.8.373.448.808.768 1.304.96a4.327 4.327 0 0 0 1.576.288c.565 0 1.096-.096 1.592-.288a3.243 3.243 0 0 0 1.312-.96c.379-.448.677-1.048.896-1.8.219-.752.328-1.688.328-2.808 0-1.088-.11-2.003-.328-2.744-.219-.741-.517-1.336-.896-1.784a3.243 3.243 0 0 0-1.312-.96 4.371 4.371 0 0 0-1.592-.288c-.555 0-1.08.096-1.576.288-.496.192-.93.512-1.304.96-.373.448-.67 1.043-.888 1.784-.219.741-.328 1.656-.328 2.744zm2.272 0c0-.192.003-.424.008-.696.005-.272.024-.552.056-.84.032-.288.085-.573.16-.856a2.95 2.95 0 0 1 .312-.76 1.67 1.67 0 0 1 .512-.544c.208-.139.467-.208.776-.208.31 0 .57.07.784.208.213.139.39.32.528.544.139.224.243.477.312.76a7.8 7.8 0 0 1 .224 1.696 25.247 25.247 0 0 1-.024 1.856c-.021.453-.088.89-.2 1.312a2.754 2.754 0 0 1-.544 1.08c-.25.299-.61.448-1.08.448-.459 0-.81-.15-1.056-.448a2.815 2.815 0 0 1-.536-1.08 6.233 6.233 0 0 1-.2-1.312c-.021-.453-.032-.84-.032-1.16zm6.624 0c0 1.12.11 2.056.328 2.808.219.752.515 1.352.888 1.8.373.448.808.768 1.304.96a4.327 4.327 0 0 0 1.576.288c.565 0 1.096-.096 1.592-.288a3.243 3.243 0 0 0 1.312-.96c.379-.448.677-1.048.896-1.8.219-.752.328-1.688.328-2.808 0-1.088-.11-2.003-.328-2.744-.219-.741-.517-1.336-.896-1.784a3.243 3.243 0 0 0-1.312-.96 4.371 4.371 0 0 0-1.592-.288c-.555 0-1.08.096-1.576.288-.496.192-.93.512-1.304.96-.373.448-.67 1.043-.888 1.784-.219.741-.328 1.656-.328 2.744zm2.272 0c0-.192.003-.424.008-.696.005-.272.024-.552.056-.84.032-.288.085-.573.16-.856a2.95 2.95 0 0 1 .312-.76 1.67 1.67 0 0 1 .512-.544c.208-.139.467-.208.776-.208.31 0 .57.07.784.208.213.139.39.32.528.544.139.224.243.477.312.76a7.8 7.8 0 0 1 .224 1.696 25.247 25.247 0 0 1-.024 1.856c-.021.453-.088.89-.2 1.312a2.754 2.754 0 0 1-.544 1.08c-.25.299-.61.448-1.08.448-.459 0-.81-.15-1.056-.448a2.815 2.815 0 0 1-.536-1.08 6.233 6.233 0 0 1-.2-1.312c-.021-.453-.032-.84-.032-1.16z",
+  pencil:
+    "M11.603 29.428a2.114 2.114 0 0 1-.345.111l-8.58 2.381c-1.606.446-3.073-1.047-2.582-2.627l2.482-7.974c.06-.424.25-.834.575-1.164L22.279.633A2.12 2.12 0 0 1 25.257.59l6.103 5.872c.835.803.855 2.124.046 2.952l-18.983 19.41c-.21.256-.486.467-.82.604zm-6.424-2.562l4.605-1.015L21.04 14.185l-3.172-3.043-11.46 11.666-1.229 4.058zM20.733 8.2l3.07 2.942 3.07-3.145-3.07-2.942-3.07 3.145z",
+  permissionsLimited:
+    "M0,16 C0,7.163444 7.163444,0 16,0 C24.836556,0 32,7.163444 32,16 C32,24.836556 24.836556,32 16,32 C7.163444,32 0,24.836556 0,16 Z M29,16 C29,8.82029825 23.1797017,3 16,3 C8.82029825,3 3,8.82029825 3,16 C3,23.1797017 8.82029825,29 16,29 C23.1797017,29 29,23.1797017 29,16 Z M16,5 C11.0100706,5.11743299 5.14533409,7.90852303 5,15.5 C4.85466591,23.091477 11.0100706,26.882567 16,27 L16,5 Z",
+  pie:
+    "M15.181 15.435V.021a15.94 15.94 0 0 1 11.42 3.995l-11.42 11.42zm1.131 1.384H31.98a15.941 15.941 0 0 0-4.114-11.553L16.312 16.819zm15.438 2.013H13.168V.25C5.682 1.587 0 8.13 0 16c0 8.837 7.163 16 16 16 7.87 0 14.413-5.682 15.75-13.168z",
+  pinmap:
+    "M13.4 18.987v8.746L15.533 32l2.134-4.25v-8.763a10.716 10.716 0 0 1-4.267 0zm2.133-1.92a8.533 8.533 0 1 0 0-17.067 8.533 8.533 0 0 0 0 17.067z",
+  popular:
+    "M23.29 11.224l-7.067 7.067-2.658-2.752.007-.007-.386-.385-.126-.131-.003.002-1.789-1.79L.705 23.793A.994.994 0 0 0 .704 25.2l.896.897a1 1 0 0 0 1.408-.002l8.253-8.252 2.654 2.748.226-.218-.161.161 1.152 1.152c.64.64 1.668.636 2.304 0l8.158-8.159L32 19.933V5H17.067l6.223 6.224z",
+  pulse:
+    "M16.9862306,27.387699 C17.4904976,29.2137955 20.0148505,29.3806482 20.7550803,27.6368095 L24.8588086,17.9692172 L31.7352165,17.9692172 L31.7352165,13.9692172 L23.5350474,13.9692172 C22.7324769,13.9692172 22.0076375,14.4489743 21.6940431,15.1877423 L19.314793,20.7927967 L14.8067319,4.4678059 C14.3010535,2.63659841 11.7668377,2.47581319 11.033781,4.22842821 L6.99549907,13.8832799 L0,13.8832799 L0,17.8832799 L8.32686781,17.8832799 C9.13327931,17.8832799 9.86080237,17.3989791 10.1719732,16.655022 L12.491241,11.1100437 L16.9862306,27.387699 Z",
+  recents:
+    "M15.689 17.292l-.689.344V6.992c0-.55.448-.992 1.001-.992h.907c.547 0 1.001.445 1.001.995v9.187l-.372.186 4.362 5.198a1.454 1.454 0 1 1-2.228 1.87L15 17.87l.689-.578zM16 32c8.837 0 16-7.163 16-16S24.837 0 16 0 0 7.163 0 16s7.163 16 16 16z",
+  share: {
+    path:
+      "M13.714 8.96H9.143L16 0l6.857 8.96h-4.571v19.16h-4.572V8.96zM4.571 32.52a4 4 0 0 0 4 4H23.43a4 4 0 0 0 4-4V20.36a4 4 0 0 0-4-4h-.572v-4.48H28a4 4 0 0 1 4 4V37a4 4 0 0 1-4 4H4a4 4 0 0 1-4-4V15.88a4 4 0 0 1 4-4h5.143v4.48H8.57a4 4 0 0 0-4 4v12.16z",
+    attrs: { fillRule: "evenodd", viewBox: "0 0 32 41" },
+  },
+  sql: {
+    path:
+      "M4,0 L28,0 C30.209139,-4.05812251e-16 32,1.790861 32,4 L32,28 C32,30.209139 30.209139,32 28,32 L4,32 C1.790861,32 2.705415e-16,30.209139 0,28 L0,4 L0,4 C-2.705415e-16,1.790861 1.790861,4.05812251e-16 4,0 L4,0 Z M6,6 C4.8954305,6 4,6.8954305 4,8 L4,26 C4,27.1045695 4.8954305,28 6,28 L26,28 C27.1045695,28 28,27.1045695 28,26 L28,8 C28,6.8954305 27.1045695,6 26,6 L6,6 Z M14,20 L25,20 L25,24 L14,24 L14,20 Z M14,13.5 L8,17 L8,10 L14,13.5 Z",
+    attrs: { fillRule: "evenodd" },
+  },
+  progress: {
+    path:
+      "M0 11.996A3.998 3.998 0 0 1 4.004 8h23.992A4 4 0 0 1 32 11.996v8.008A3.998 3.998 0 0 1 27.996 24H4.004A4 4 0 0 1 0 20.004v-8.008zM22 11h3.99A3.008 3.008 0 0 1 29 14v4c0 1.657-1.35 3-3.01 3H22V11z",
+    attrs: { fillRule: "evenodd" },
+  },
+  sort:
+    "M14.615.683c.765-.926 2.002-.93 2.77 0L26.39 11.59c.765.927.419 1.678-.788 1.678H6.398c-1.2 0-1.557-.747-.788-1.678L14.615.683zm2.472 30.774c-.6.727-1.578.721-2.174 0l-9.602-11.63c-.6-.727-.303-1.316.645-1.316h20.088c.956 0 1.24.595.645 1.316l-9.602 11.63z",
+  sum:
+    "M3 27.41l1.984 4.422L27.895 32l.04-5.33-17.086-.125 8.296-9.457-.08-3.602L11.25 5.33H27.43V0H5.003L3.08 4.51l10.448 10.9z",
+  sync:
+    "M16 2 A14 14 0 0 0 2 16 A14 14 0 0 0 16 30 A14 14 0 0 0 26 26 L 23.25 23 A10 10 0 0 1 16 26 A10 10 0 0 1 6 16 A10 10 0 0 1 16 6 A10 10 0 0 1 23.25 9 L19 13 L30 13 L30 2 L26 6 A14 14 0 0 0 16 2",
+  question:
+    "M16,32 C24.836556,32 32,24.836556 32,16 C32,7.163444 24.836556,0 16,0 C7.163444,0 0,7.163444 0,16 C0,24.836556 7.163444,32 16,32 L16,32 Z M16,29.0909091 C8.77009055,29.0909091 2.90909091,23.2299095 2.90909091,16 C2.90909091,8.77009055 8.77009055,2.90909091 16,2.90909091 C23.2299095,2.90909091 29.0909091,8.77009055 29.0909091,16 C29.0909091,23.2299095 23.2299095,29.0909091 16,29.0909091 Z M12,9.56020942 C12.2727286,9.34380346 12.5694087,9.1413622 12.8900491,8.95287958 C13.2106896,8.76439696 13.5552807,8.59860455 13.9238329,8.45549738 C14.2923851,8.31239021 14.6885728,8.20069848 15.1124079,8.12041885 C15.5362429,8.04013921 15.9950835,8 16.4889435,8 C17.1818216,8 17.8065083,8.08725916 18.3630221,8.2617801 C18.919536,8.43630105 19.3931184,8.68586225 19.7837838,9.0104712 C20.1744491,9.33508016 20.4748147,9.7260012 20.6848894,10.1832461 C20.8949642,10.6404909 21,11.1483393 21,11.7068063 C21,12.2373499 20.9226052,12.6963331 20.7678133,13.0837696 C20.6130213,13.4712061 20.4176916,13.8080265 20.1818182,14.0942408 C19.9459448,14.3804552 19.6861194,14.6282712 19.4023342,14.8376963 C19.1185489,15.0471215 18.8495099,15.2408368 18.5952088,15.4188482 C18.3409078,15.5968595 18.1197798,15.773123 17.9318182,15.947644 C17.7438566,16.1221649 17.6240789,16.3176254 17.5724816,16.5340314 L17.2628993,18 L14.9189189,18 L14.6756757,16.3141361 C14.6167073,15.9720751 14.653562,15.6736487 14.7862408,15.4188482 C14.9189196,15.1640476 15.1013502,14.9336834 15.3335381,14.7277487 C15.565726,14.521814 15.8255514,14.3263535 16.1130221,14.1413613 C16.4004928,13.9563691 16.6695319,13.7574182 16.9201474,13.5445026 C17.1707629,13.3315871 17.3826773,13.0942421 17.5558968,12.8324607 C17.7291163,12.5706793 17.8157248,12.2582915 17.8157248,11.895288 C17.8157248,11.4764377 17.6701489,11.1431077 17.3789926,10.895288 C17.0878364,10.6474682 16.6879632,10.5235602 16.1793612,10.5235602 C15.7886958,10.5235602 15.462532,10.5619542 15.20086,10.6387435 C14.9391879,10.7155327 14.7143744,10.8010466 14.5264128,10.895288 C14.3384511,10.9895293 14.1744479,11.0750432 14.034398,11.1518325 C13.8943482,11.2286217 13.7543005,11.2670157 13.6142506,11.2670157 C13.2972957,11.2670157 13.0614258,11.1378721 12.9066339,10.8795812 L12,9.56020942 Z M14,22 C14,21.7192968 14.0511359,21.4580909 14.1534091,21.2163743 C14.2556823,20.9746577 14.3958324,20.7641335 14.5738636,20.5847953 C14.7518948,20.4054572 14.96212,20.2631584 15.2045455,20.1578947 C15.4469709,20.0526311 15.7121198,20 16,20 C16.2803044,20 16.5416655,20.0526311 16.7840909,20.1578947 C17.0265164,20.2631584 17.2386355,20.4054572 17.4204545,20.5847953 C17.6022736,20.7641335 17.7443177,20.9746577 17.8465909,21.2163743 C17.9488641,21.4580909 18,21.7192968 18,22 C18,22.2807032 17.9488641,22.5438584 17.8465909,22.7894737 C17.7443177,23.0350889 17.6022736,23.2475625 17.4204545,23.4269006 C17.2386355,23.6062387 17.0265164,23.7465882 16.7840909,23.8479532 C16.5416655,23.9493182 16.2803044,24 16,24 C15.7121198,24 15.4469709,23.9493182 15.2045455,23.8479532 C14.96212,23.7465882 14.7518948,23.6062387 14.5738636,23.4269006 C14.3958324,23.2475625 14.2556823,23.0350889 14.1534091,22.7894737 C14.0511359,22.5438584 14,22.2807032 14,22 Z",
+  return:
+    "M15.3040432,11.8500793 C22.1434689,13.0450349 27.291257,18.2496116 27.291257,24.4890512 C27.291257,25.7084278 27.0946472,26.8882798 26.7272246,28.0064033 L26.7272246,28.0064033 C25.214579,22.4825472 20.8068367,18.2141694 15.3040432,17.0604596 L15.3040432,25.1841972 L4.70874296,14.5888969 L15.3040432,3.99359668 L15.3040432,3.99359668 L15.3040432,11.8500793 Z",
+  reference: {
+    path:
+      "M32 27.5V2a2 2 0 0 0-2-2H4a4 4 0 0 0-4 4v26a2 2 0 0 0 2 2h22V8H4V6a2 2 0 0 1 2-2h20a2 2 0 0 1 2 2v19h4v2.5z",
+    attrs: { fillRule: "evenodd" },
+  },
+  refresh:
+    "M16 2 A14 14 0 0 0 2 16 A14 14 0 0 0 16 30 A14 14 0 0 0 26 26 L 23.25 23 A10 10 0 0 1 16 26 A10 10 0 0 1 6 16 A10 10 0 0 1 16 6 A10 10 0 0 1 23.25 9 L19 13 L30 13 L30 2 L26 6 A14 14 0 0 0 16 2",
+  right: "M9,0 L25,16 L9,32 L9,5.47117907e-13 L9,0 Z",
+  ruler:
+    "M0.595961814,24.9588734 C-0.196619577,24.166292 -0.200005392,22.8846495 0.593926984,22.0907171 L22.0908075,0.593836573 C22.8822651,-0.197621013 24.1641442,-0.198948234 24.9589638,0.595871404 L31.4040382,7.04094576 C32.1966196,7.83352715 32.2000054,9.11516967 31.406073,9.90910205 L9.90919246,31.4059826 C9.11773487,32.1974402 7.83585581,32.1987674 7.04103617,31.4039478 L0.595961814,24.9588734 Z M17.8319792,7.8001489 L16.3988585,9.23326963 L18.548673,11.3830842 C18.9443414,11.7787526 18.9470387,12.4175604 18.5485351,12.816064 C18.1527906,13.2118086 17.5140271,13.2146738 17.1155553,12.816202 L14.9657407,10.6663874 L13.5326229,12.0995052 L15.6824375,14.2493198 C16.0781059,14.6449881 16.0808032,15.283796 15.6822996,15.6822996 C15.286555,16.0780441 14.6477916,16.0809094 14.2493198,15.6824375 L12.0995052,13.5326229 L10.6663845,14.9657436 C10.6670858,14.9664411 10.6677865,14.9671398 10.6684864,14.9678397 L14.2470828,18.5464361 C14.6439866,18.9433399 14.6476854,19.5831493 14.2491818,19.9816529 C13.8534373,20.3773974 13.2188552,20.384444 12.813965,19.9795538 L9.23536867,16.4009575 C9.23466875,16.4002576 9.23397006,16.3995569 9.2332726,16.3988555 L7.80015186,17.8319762 L9.94996646,19.9817908 C10.3456348,20.3774592 10.3483321,21.016267 9.94982851,21.4147706 C9.55408397,21.8105152 8.91532053,21.8133804 8.51684869,21.4149086 L6.3670341,19.265094 L4.93391633,20.6982118 L7.08373093,22.8480263 C7.47939928,23.2436947 7.48209658,23.8825026 7.08359298,24.2810062 C6.68784844,24.6767507 6.049085,24.6796159 5.65061316,24.2811441 L3.50079857,22.1313295 L2.02673458,23.6053935 L8.47576453,30.0544235 L30.0544235,8.47576453 L23.6053935,2.02673458 L22.1313295,3.50079857 L24.2811441,5.65061316 C24.6768125,6.04628152 24.6795098,6.68508938 24.2810062,7.08359298 C23.8852616,7.47933752 23.2464982,7.48220276 22.8480263,7.08373093 L20.6982118,4.93391633 L19.2650911,6.36703697 C19.2657924,6.36773446 19.2664931,6.36843318 19.267193,6.36913314 L22.8457894,9.94772948 C23.2426932,10.3446333 23.246392,10.9844427 22.8478884,11.3829463 C22.4521439,11.7786908 21.8175617,11.7857374 21.4126716,11.3808472 L17.8340753,7.8022509 C17.8333753,7.80155099 17.8326767,7.80085032 17.8319792,7.8001489 Z",
+  search:
+    "M22.805 25.734c-5.582 4.178-13.543 3.718-18.632-1.37-5.58-5.581-5.595-14.615-.031-20.179 5.563-5.563 14.597-5.55 20.178.031 5.068 5.068 5.545 12.985 1.422 18.563l5.661 5.661a2.08 2.08 0 0 1 .003 2.949 2.085 2.085 0 0 1-2.95-.003l-5.651-5.652zm-1.486-4.371c3.895-3.895 3.885-10.218-.021-14.125-3.906-3.906-10.23-3.916-14.125-.021-3.894 3.894-3.885 10.218.022 14.124 3.906 3.907 10.23 3.916 14.124.022z",
+  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",
+  slackIcon:
+    "M11.432 13.934l1.949 5.998 5.573-1.864-1.949-6-5.573 1.866zm-5.266 1.762l-2.722.91a2.776 2.776 0 1 1-1.762-5.265l2.768-.926-.956-2.943a2.776 2.776 0 0 1 5.28-1.716l.942 2.897 5.573-1.865-1.023-3.151a2.776 2.776 0 1 1 5.28-1.716l1.009 3.105 3.67-1.228a2.776 2.776 0 1 1 1.762 5.265l-3.716 1.244 1.949 5.999 3.336-1.117a2.776 2.776 0 0 1 1.762 5.266l-3.382 1.131.956 2.942a2.776 2.776 0 0 1-5.28 1.716l-.942-2.896-5.573 1.865 1.023 3.15a2.776 2.776 0 0 1-5.28 1.716L9.83 26.975l-3.056 1.022a2.776 2.776 0 1 1-1.762-5.265l3.102-1.038-1.949-5.998z",
+  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",
+  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" },
+  },
+  sun:
+    "M18.2857143,27.1999586 L18.2857143,29.7130168 C18.2857143,30.9760827 17.2711661,32 16,32 C14.7376349,32 13.7142857,30.9797942 13.7142857,29.7130168 L13.7142857,27.1999586 C14.4528227,27.3498737 15.2172209,27.4285714 16,27.4285714 C16.7827791,27.4285714 17.5471773,27.3498737 18.2857143,27.1999586 Z M13.7142857,4.80004141 L13.7142857,2.28698322 C13.7142857,1.02391726 14.7288339,0 16,0 C17.2623651,0 18.2857143,1.02020582 18.2857143,2.28698322 L18.2857143,4.80004141 C17.5471773,4.65012631 16.7827791,4.57142857 16,4.57142857 C15.2172209,4.57142857 14.4528227,4.65012631 13.7142857,4.80004141 Z M10.5518048,26.0488463 L8.93640145,27.9740091 C8.1245183,28.9415738 6.68916799,29.0738009 5.71539825,28.2567111 C4.74837044,27.4452784 4.62021518,26.0059593 5.43448399,25.0355515 L7.05102836,23.1090289 C8.00526005,24.3086326 9.1956215,25.3120077 10.5518048,26.0488463 Z M21.4481952,5.95115366 L23.0635986,4.02599087 C23.8754817,3.05842622 25.310832,2.92619908 26.2846018,3.74328891 C27.2516296,4.55472158 27.3797848,5.99404073 26.565516,6.96444852 L24.9489716,8.89097108 C23.9947399,7.69136735 22.8043785,6.68799226 21.4481952,5.95115366 Z M7.05102836,8.89097108 L5.43448399,6.96444852 C4.62260085,5.99688386 4.7416285,4.56037874 5.71539825,3.74328891 C6.68242605,2.93185624 8.12213263,3.05558308 8.93640145,4.02599087 L10.5518048,5.95115366 C9.1956215,6.68799226 8.00526005,7.69136735 7.05102836,8.89097108 Z M24.9489716,23.1090289 L26.565516,25.0355515 C27.3773992,26.0031161 27.2583715,27.4396213 26.2846018,28.2567111 C25.317574,29.0681438 23.8778674,28.9444169 23.0635986,27.9740091 L21.4481952,26.0488463 C22.8043785,25.3120077 23.9947399,24.3086326 24.9489716,23.1090289 Z M27.1999586,13.7142857 L29.7130168,13.7142857 C30.9760827,13.7142857 32,14.7288339 32,16 C32,17.2623651 30.9797942,18.2857143 29.7130168,18.2857143 L27.1999586,18.2857143 C27.3498737,17.5471773 27.4285714,16.7827791 27.4285714,16 C27.4285714,15.2172209 27.3498737,14.4528227 27.1999586,13.7142857 Z M4.80004141,18.2857143 L2.28698322,18.2857143 C1.02391726,18.2857143 2.7533531e-14,17.2711661 2.84217094e-14,16 C2.84217094e-14,14.7376349 1.02020582,13.7142857 2.28698322,13.7142857 L4.80004141,13.7142857 C4.65012631,14.4528227 4.57142857,15.2172209 4.57142857,16 C4.57142857,16.7827791 4.65012631,17.5471773 4.80004141,18.2857143 Z M16,22.8571429 C19.7870954,22.8571429 22.8571429,19.7870954 22.8571429,16 C22.8571429,12.2129046 19.7870954,9.14285714 16,9.14285714 C12.2129046,9.14285714 9.14285714,12.2129046 9.14285714,16 C9.14285714,19.7870954 12.2129046,22.8571429 16,22.8571429 Z",
+  table:
+    "M11.077 11.077h9.846v9.846h-9.846v-9.846zm11.077 11.077H32V32h-9.846v-9.846zm-11.077 0h9.846V32h-9.846v-9.846zM0 22.154h9.846V32H0v-9.846zM0 0h9.846v9.846H0V0zm0 11.077h9.846v9.846H0v-9.846zM22.154 0H32v9.846h-9.846V0zm0 11.077H32v9.846h-9.846v-9.846zM11.077 0h9.846v9.846h-9.846V0z",
+  table2: {
+    svg:
+      '<g fill="currentcolor" fill-rule="evenodd"><path d="M10,19 L10,15 L3,15 L3,13 L10,13 L10,9 L12,9 L12,13 L20,13 L20,9 L22,9 L22,13 L29,13 L29,15 L22,15 L22,19 L29,19 L29,21 L22,21 L22,25 L20,25 L20,21 L12,21 L12,25 L10,25 L10,21 L3,21 L3,19 L10,19 L10,19 Z M12,19 L12,15 L20,15 L20,19 L12,19 Z M30.5,0 L32,0 L32,28 L30.5,28 L1.5,28 L0,28 L0,0 L1.5,0 L30.5,0 Z M29,3 L29,25 L3,25 L3,3 L29,3 Z M3,7 L29,7 L29,9 L3,9 L3,7 Z"></path></g>',
+    attrs: { viewBox: "0 0 32 28" },
+  },
+  tilde:
+    "M.018 22.856s-.627-7.417 5.456-10.293c6.416-3.033 12.638 2.01 15.885 2.01 2.09 0 4.067-1.105 4.067-4.483 0-.118 6.563-.086 6.563-.086s.338 5.151-2.756 8.403c-3.095 3.251-7.314 2.899-7.314 2.899s-2.686 0-6.353-1.543c-4.922-2.07-6.494-1.348-7.095-.969-.6.38-1.863 1.04-1.863 4.062H.018z",
+  trash:
+    "M4.31904507,29.7285487 C4.45843264,30.9830366 5.59537721,32 6.85726914,32 L20.5713023,32 C21.8337371,32 22.9701016,30.9833707 23.1095264,29.7285487 L25.1428571,11.4285714 L2.28571429,11.4285714 L4.31904507,29.7285487 L4.31904507,29.7285487 Z M6.85714286,4.57142857 L8.57142857,0 L18.8571429,0 L20.5714286,4.57142857 L25.1428571,4.57142857 C27.4285714,4.57142857 27.4285714,9.14285714 27.4285714,9.14285714 L13.7142857,9.14285714 L-1.0658141e-14,9.14285714 C-1.0658141e-14,9.14285714 -1.0658141e-14,4.57142857 2.28571429,4.57142857 L6.85714286,4.57142857 L6.85714286,4.57142857 Z M9.14285714,4.57142857 L18.2857143,4.57142857 L17.1428571,2.28571429 L10.2857143,2.28571429 L9.14285714,4.57142857 L9.14285714,4.57142857 Z",
+  unarchive:
+    "M18,7.95916837 L22.98085,7.97386236 L15.9779702,-0.00230793315 L9.02202984,7.93268248 L14,7.94736798 L14,22.8635899 L18,22.8635899 L18,7.95916837 Z M7,12.1176568 L0,12.1176568 L0,17.0882426 L3,17.0882426 L3,32 L29,32 L29,17.0882426 L32,17.0882426 L32,12.1176568 L25,12.1176568 L25,27.8341757 L7,27.8341757 L7,12.1176568 Z",
+  unknown:
+    "M16.5,26.5 C22.0228475,26.5 26.5,22.0228475 26.5,16.5 C26.5,10.9771525 22.0228475,6.5 16.5,6.5 C10.9771525,6.5 6.5,10.9771525 6.5,16.5 C6.5,22.0228475 10.9771525,26.5 16.5,26.5 L16.5,26.5 Z M16.5,23.5 C12.6340068,23.5 9.5,20.3659932 9.5,16.5 C9.5,12.6340068 12.6340068,9.5 16.5,9.5 C20.3659932,9.5 23.5,12.6340068 23.5,16.5 C23.5,20.3659932 20.3659932,23.5 16.5,23.5 L16.5,23.5 Z",
+  variable:
+    "M32,3.85760518 C32,5.35923081 31.5210404,6.55447236 30.5631068,7.4433657 C29.6051732,8.33225903 28.4358214,8.77669903 27.0550162,8.77669903 C26.2265331,8.77669903 25.4110072,8.67314019 24.6084142,8.46601942 C23.8058212,8.25889864 23.111114,8.05178097 22.5242718,7.84466019 C22.2481108,8.03452091 21.8425054,8.44875625 21.3074434,9.08737864 C20.7723814,9.72600104 20.1682882,10.5026923 19.4951456,11.4174757 C20.116508,14.0582656 20.6170423,15.9352695 20.9967638,17.0485437 C21.3764852,18.1618179 21.7389411,19.2880202 22.0841424,20.4271845 C22.3775635,21.3419679 22.8090586,22.0582498 23.3786408,22.5760518 C23.9482229,23.0938537 24.8457328,23.3527508 26.0711974,23.3527508 C26.5199591,23.3527508 27.0809028,23.2664518 27.7540453,23.0938511 C28.4271878,22.9212505 28.9795016,22.7486524 29.4110032,22.5760518 L28.8414239,24.9061489 C27.1326775,25.6310716 25.6397043,26.1574957 24.3624595,26.4854369 C23.0852148,26.8133781 21.9460676,26.9773463 20.9449838,26.9773463 C20.2200611,26.9773463 19.5037792,26.9083071 18.7961165,26.7702265 C18.0884539,26.632146 17.4412111,26.3818788 16.8543689,26.0194175 C16.2157465,25.6396961 15.6763776,25.1650514 15.236246,24.5954693 C14.7961143,24.0258871 14.4207135,23.2319361 14.1100324,22.2135922 C13.9029116,21.5749698 13.7130537,20.850058 13.5404531,20.038835 C13.3678524,19.2276119 13.1952544,18.51133 13.0226537,17.8899676 C12.5221118,18.6321504 12.1596559,19.1844642 11.9352751,19.5469256 C11.7108942,19.9093869 11.3829579,20.4185512 10.9514563,21.0744337 C9.5879112,23.1629015 8.4056145,24.6515597 7.40453074,25.5404531 C6.40344699,26.4293464 5.20389049,26.8737864 3.80582524,26.8737864 C2.75296129,26.8737864 1.85545139,26.5199604 1.11326861,25.8122977 C0.371085825,25.1046351 0,24.1812355 0,23.0420712 C0,21.5059254 0.478959612,20.2934241 1.4368932,19.4045307 C2.3948268,18.5156374 3.56417864,18.0711974 4.94498382,18.0711974 C5.77346693,18.0711974 6.56741799,18.1704413 7.32686084,18.368932 C8.08630369,18.5674228 8.80258563,18.7874853 9.47572816,19.0291262 C9.73462913,18.8220054 10.1359196,18.4164 10.6796117,17.8122977 C11.2233037,17.2081955 11.814452,16.4573939 12.4530744,15.5598706 C11.8834923,13.2470219 11.4174775,11.5037815 11.0550162,10.3300971 C10.6925548,9.15641269 10.321469,7.99137579 9.94174757,6.83495146 C9.63106641,5.90290796 9.18231146,5.18231107 8.59546926,4.67313916 C8.00862706,4.16396725 7.12837696,3.90938511 5.95469256,3.90938511 C5.43689061,3.90938511 4.85868712,3.99999909 4.22006472,4.18122977 C3.58144233,4.36246045 3.04638835,4.53074356 2.61488673,4.68608414 L3.18446602,2.35598706 C4.73787184,1.66558447 6.20927029,1.14779029 7.5987055,0.802588997 C8.98814071,0.457387702 10.1488627,0.284789644 11.0809061,0.284789644 C11.9266493,0.284789644 12.6515612,0.345198964 13.2556634,0.466019417 C13.8597657,0.586839871 14.4983785,0.845736958 15.171521,1.24271845 C15.7928834,1.62243987 16.3322523,2.10139948 16.789644,2.67961165 C17.2470357,3.25782382 17.6224365,4.04745994 17.9158576,5.04854369 C18.1229784,5.73894628 18.3128362,6.45522822 18.4854369,7.197411 C18.6580375,7.93959379 18.8047459,8.5782066 18.9255663,9.11326861 C19.2880277,8.56094654 19.6634285,7.99137294 20.0517799,7.40453074 C20.4401314,6.81768854 20.7723827,6.29989437 21.0485437,5.85113269 C22.3775687,3.76266485 23.5684953,2.2653767 24.6213592,1.3592233 C25.6742232,0.453069903 26.8651498,0 28.1941748,0 C29.2815588,0 30.1876986,0.358140971 30.9126214,1.07443366 C31.6375441,1.79072634 32,2.71844091 32,3.85760518 L32,3.85760518 Z",
+  viewArchive: {
+    path:
+      "M2.783 12.8h26.434V29H2.783V12.8zm6.956 3.4h12.522v2.6H9.739v-2.6zM0 4h32v6.4H0V4z",
+    attrs: { fillRule: "evenodd" },
+  },
+  warning: {
+    svg:
+      '<g fill="currentcolor" fill-rule="evenodd"><path d="M10.9665007,4.7224988 C11.5372866,3.77118898 12.455761,3.75960159 13.0334993,4.7224988 L22.9665007,21.2775012 C23.5372866,22.228811 23.1029738,23 21.9950534,23 L2.00494659,23 C0.897645164,23 0.455760956,22.2403984 1.03349928,21.2775012 L10.9665007,4.7224988 Z M13.0184348,11.258 L13.0184348,14.69 C13.0184348,15.0580018 12.996435,15.4229982 12.9524348,15.785 C12.9084346,16.1470018 12.8504351,16.5159981 12.7784348,16.892 L11.5184348,16.892 C11.4464344,16.5159981 11.388435,16.1470018 11.3444348,15.785 C11.3004346,15.4229982 11.2784348,15.0580018 11.2784348,14.69 L11.2784348,11.258 L13.0184348,11.258 Z M11.0744348,19.058 C11.0744348,18.9139993 11.1014345,18.7800006 11.1554348,18.656 C11.2094351,18.5319994 11.2834343,18.4240005 11.3774348,18.332 C11.4714353,18.2399995 11.5824341,18.1670003 11.7104348,18.113 C11.8384354,18.0589997 11.978434,18.032 12.1304348,18.032 C12.2784355,18.032 12.4164341,18.0589997 12.5444348,18.113 C12.6724354,18.1670003 12.7844343,18.2399995 12.8804348,18.332 C12.9764353,18.4240005 13.0514345,18.5319994 13.1054348,18.656 C13.1594351,18.7800006 13.1864348,18.9139993 13.1864348,19.058 C13.1864348,19.2020007 13.1594351,19.3369994 13.1054348,19.463 C13.0514345,19.5890006 12.9764353,19.6979995 12.8804348,19.79 C12.7844343,19.8820005 12.6724354,19.9539997 12.5444348,20.006 C12.4164341,20.0580003 12.2784355,20.084 12.1304348,20.084 C11.978434,20.084 11.8384354,20.0580003 11.7104348,20.006 C11.5824341,19.9539997 11.4714353,19.8820005 11.3774348,19.79 C11.2834343,19.6979995 11.2094351,19.5890006 11.1554348,19.463 C11.1014345,19.3369994 11.0744348,19.2020007 11.0744348,19.058 Z"></path></g>',
+    attrs: { viewBox: "0 0 23 23" },
+  },
+  warning2: {
+    path:
+      "M12.3069589,4.52260192 C14.3462632,1.2440969 17.653446,1.24541073 19.691933,4.52260192 L31.2249413,23.0637415 C33.2642456,26.3422466 31.7889628,29 27.9115531,29 L4.08733885,29 C0.218100769,29 -1.26453645,26.3409327 0.77395061,23.0637415 L12.3069589,4.52260192 Z M18.0499318,23.0163223 C18.0499772,23.0222378 18.05,23.0281606 18.05,23.0340907 C18.05,23.3266209 17.9947172,23.6030345 17.8840476,23.8612637 C17.7737568,24.1186089 17.6195847,24.3426723 17.4224081,24.5316332 C17.2266259,24.7192578 16.998292,24.8660439 16.7389806,24.9713892 C16.4782454,25.0773129 16.1979962,25.1301134 15.9,25.1301134 C15.5950083,25.1301134 15.3111795,25.0774024 15.0502239,24.9713892 C14.7901813,24.8657469 14.5629613,24.7183609 14.3703047,24.5298034 C14.177545,24.3411449 14.0258626,24.1177208 13.9159524,23.8612637 C13.8052827,23.6030345 13.75,23.3266209 13.75,23.0340907 C13.75,22.7411889 13.8054281,22.4661013 13.9165299,22.2109786 C14.0264627,21.9585404 14.1779817,21.7374046 14.3703047,21.5491736 C14.5621821,21.3613786 14.7883231,21.2126553 15.047143,21.1034656 C15.3089445,20.9930181 15.593871,20.938068 15.9,20.938068 C16.1991423,20.938068 16.4804862,20.9931136 16.7420615,21.1034656 C17.0001525,21.2123478 17.2274115,21.360472 17.4224081,21.5473437 C17.6191428,21.7358811 17.7731504,21.957652 17.88347,22.2109786 C17.9124619,22.2775526 17.9376628,22.3454862 17.9590769,22.414741 C18.0181943,22.5998533 18.05,22.7963729 18.05,23 C18.05,23.0054459 18.0499772,23.0108867 18.0499318,23.0163223 L18.0499318,23.0163223 Z M17.7477272,14.1749999 L17.7477272,8.75 L14.1170454,8.75 L14.1170454,14.1749999 C14.1170454,14.8471841 14.1572355,15.5139742 14.2376219,16.1753351 C14.3174838,16.8323805 14.4227217,17.5019113 14.5533248,18.1839498 L14.5921937,18.3869317 L17.272579,18.3869317 L17.3114479,18.1839498 C17.442051,17.5019113 17.5472889,16.8323805 17.6271507,16.1753351 C17.7075371,15.5139742 17.7477272,14.8471841 17.7477272,14.1749999 Z",
+    attrs: { fillRule: "evenodd" },
+  },
+  x:
+    "m11.271709,16 l-3.19744231e-13,4.728291 l4.728291,0 l16,11.271709 l27.271709,2.39808173e-13 l32,4.728291 l20.728291,16 l31.1615012,26.4332102 l26.4332102,31.1615012 l16,20.728291 l5.56678976,31.1615012 l0.838498756,26.4332102 l11.271709,16 z",
+  zoom:
+    "M12.416 12.454V8.37h3.256v4.083h4.07v3.266h-4.07v4.083h-3.256V15.72h-4.07v-3.266h4.07zm10.389 13.28c-5.582 4.178-13.543 3.718-18.632-1.37-5.58-5.581-5.595-14.615-.031-20.179 5.563-5.563 14.597-5.55 20.178.031 5.068 5.068 5.545 12.985 1.422 18.563l5.661 5.661a2.08 2.08 0 0 1 .003 2.949 2.085 2.085 0 0 1-2.95-.003l-5.651-5.652zm-1.486-4.371c3.895-3.895 3.885-10.218-.021-14.125-3.906-3.906-10.23-3.916-14.125-.021-3.894 3.894-3.885 10.218.022 14.124 3.906 3.907 10.23 3.916 14.124.022z",
+  slack: {
+    img: "app/assets/img/slack.png",
+  },
 };
 
 // $FlowFixMe
 ICON_PATHS["horizontal_bar"] = {
-    path: ICON_PATHS["bar"],
-    attrs: {
-        style: { transform: "rotate(90deg) scaleX(-1)" }
-    }
+  path: ICON_PATHS["bar"],
+  attrs: {
+    style: { transform: "rotate(90deg) scaleX(-1)" },
+  },
 };
 
 // $FlowFixMe
 ICON_PATHS["forwardArrow"] = {
-    path: ICON_PATHS["backArrow"],
-    attrs: {
-        style: { transform: "rotate(-180deg)" }
-    }
+  path: ICON_PATHS["backArrow"],
+  attrs: {
+    style: { transform: "rotate(-180deg)" },
+  },
 };
 
 // $FlowFixMe
 ICON_PATHS["scalar"] = ICON_PATHS["number"];
 
 export function parseViewBox(viewBox: string): Array<number> {
-    // a viewBox is a string that takes the form 'min-x, min-y, width, height'
-    // grab the values and return just width and height since we currently don't
-    // tend to card about min-x or min-y
+  // a viewBox is a string that takes the form 'min-x, min-y, width, height'
+  // grab the values and return just width and height since we currently don't
+  // tend to card about min-x or min-y
 
-    // we cast to numbers so we can do math-y stuff with the width and height
-    return viewBox.split(' ').map(v => Number(v)).slice(2, 4)
+  // we cast to numbers so we can do math-y stuff with the width and height
+  return viewBox
+    .split(" ")
+    .map(v => Number(v))
+    .slice(2, 4);
 }
 
-export function loadIcon(name:string) {
-    var def = ICON_PATHS[name];
-    if (!def) {
-        console.warn('Icon "' + name + '" does not exist.');
-        return;
-    }
+export function loadIcon(name: string) {
+  var def = ICON_PATHS[name];
+  if (!def) {
+    console.warn('Icon "' + name + '" does not exist.');
+    return;
+  }
 
-    if (def.img) {
-        return { ...def, attrs: { ...def.attrs, className: 'Icon Icon-' + name } };
-    }
+  if (def.img) {
+    return { ...def, attrs: { ...def.attrs, className: "Icon Icon-" + name } };
+  }
 
-    var icon = {
-        attrs: {
-            className: 'Icon Icon-' + name,
-            viewBox: '0 0 32 32',
-            width: '16px',
-            height: '16px',
-            fill: 'currentcolor'
-        },
-        svg: undefined,
-        path: undefined
-    };
+  var icon = {
+    attrs: {
+      className: "Icon Icon-" + name,
+      viewBox: "0 0 32 32",
+      width: "16px",
+      height: "16px",
+      fill: "currentcolor",
+    },
+    svg: undefined,
+    path: undefined,
+  };
 
-    if (typeof def === 'string') {
-        icon.path = def;
-    } else if (def != null) {
-        var { svg, path, attrs } = def;
-        for (var attr in attrs) {
-            icon.attrs[attr] = attrs[attr];
-        }
+  if (typeof def === "string") {
+    icon.path = def;
+  } else if (def != null) {
+    var { svg, path, attrs } = def;
+    for (var attr in attrs) {
+      icon.attrs[attr] = attrs[attr];
+    }
 
-        // Note - @kdoh 10/13/2017
-        // in the case of a custom viewBox, we need to set the width and height
-        // of the icon def based on the view box since we're scaling all icons
-        // down by half currently
-        if(attrs && attrs.viewBox) {
-            const [width, height] = parseViewBox(attrs.viewBox)
-            icon.attrs.width = `${width / 2}px`
-            icon.attrs.height = `${height / 2}px`
-        }
-        icon.path = path;
-        icon.svg = svg;
+    // Note - @kdoh 10/13/2017
+    // in the case of a custom viewBox, we need to set the width and height
+    // of the icon def based on the view box since we're scaling all icons
+    // down by half currently
+    if (attrs && attrs.viewBox) {
+      const [width, height] = parseViewBox(attrs.viewBox);
+      icon.attrs.width = `${width / 2}px`;
+      icon.attrs.height = `${height / 2}px`;
     }
+    icon.path = path;
+    icon.svg = svg;
+  }
 
-    return icon;
+  return icon;
 }
diff --git a/frontend/src/metabase/internal/components/ColorsApp.jsx b/frontend/src/metabase/internal/components/ColorsApp.jsx
index 8e641e1c3d968d2f408ad5b4a393ec8acd7bc442..aa1e941aa12f187a3e6ef51930a42acd83ac84f0 100644
--- a/frontend/src/metabase/internal/components/ColorsApp.jsx
+++ b/frontend/src/metabase/internal/components/ColorsApp.jsx
@@ -5,16 +5,21 @@ import cx from "classnames";
 // eslint-disable-next-line import/no-commonjs
 let colorStyles = require("!style-loader!css-loader?modules!postcss-loader!metabase/css/core/colors.css");
 
-const ColorsApp = () =>
-    <div className="p2">
-        {Object.entries(colorStyles).map(([name, className]) =>
-            <div
-                className={cx(className, "rounded px1")}
-                style={{ paddingTop: "0.25em", paddingBottom: "0.25em", marginBottom: "0.25em" }}
-            >
-                {name}
-            </div>
-        )}
-    </div>
+const ColorsApp = () => (
+  <div className="p2">
+    {Object.entries(colorStyles).map(([name, className]) => (
+      <div
+        className={cx(className, "rounded px1")}
+        style={{
+          paddingTop: "0.25em",
+          paddingBottom: "0.25em",
+          marginBottom: "0.25em",
+        }}
+      >
+        {name}
+      </div>
+    ))}
+  </div>
+);
 
 export default ColorsApp;
diff --git a/frontend/src/metabase/internal/components/ComponentsApp.jsx b/frontend/src/metabase/internal/components/ComponentsApp.jsx
index 07a228f856617a35c88088e620d19a3bcbfad04d..a503a0f25831e62822912208d79d1c429bc1542c 100644
--- a/frontend/src/metabase/internal/components/ComponentsApp.jsx
+++ b/frontend/src/metabase/internal/components/ComponentsApp.jsx
@@ -5,83 +5,86 @@ import { slugify } from "metabase/lib/formatting";
 import reactElementToJSXString from "react-element-to-jsx-string";
 import COMPONENTS from "../lib/components-webpack";
 
-const Section = ({ title, children }) =>
-    <div className="mb2">
-        <h3 className="my2">{title}</h3>
-        {children}
-    </div>
+const Section = ({ title, children }) => (
+  <div className="mb2">
+    <h3 className="my2">{title}</h3>
+    {children}
+  </div>
+);
 
 export default class ComponentsApp extends Component {
-    render() {
-        const componentName = slugify(this.props.params.componentName);
-        const exampleName = slugify(this.props.params.exampleName);
-        return (
-            <div className="wrapper p4">
-                {COMPONENTS
-                    .filter(({ component, description, examples }) =>
-                        !componentName || componentName === slugify(component.name)
-                    )
-                    .map(({ component, description, examples }) => (
+  render() {
+    const componentName = slugify(this.props.params.componentName);
+    const exampleName = slugify(this.props.params.exampleName);
+    return (
+      <div className="wrapper p4">
+        {COMPONENTS.filter(
+          ({ component, description, examples }) =>
+            !componentName || componentName === slugify(component.name),
+        ).map(({ component, description, examples }) => (
+          <div>
+            <h2>
+              <Link
+                to={`_internal/components/${slugify(component.name)}`}
+                className="no-decoration"
+              >
+                {component.name}
+              </Link>
+            </h2>
+            {description && <p className="my2">{description}</p>}
+            {component.propTypes && (
+              <Section title="Props">
+                <div className="border-left border-right border-bottom text-code">
+                  {Object.keys(component.propTypes).map(prop => (
                     <div>
-                        <h2>
-                            <Link
-                                to={`_internal/components/${slugify(component.name)}`}
-                                className="no-decoration"
-                            >
-                                {component.name}
-                            </Link>
-                        </h2>
-                        { description &&
-                            <p className="my2">{description}</p>
-                        }
-                        { component.propTypes &&
-                            <Section title="Props">
-                                <div className="border-left border-right border-bottom text-code">
-                                    {Object.keys(component.propTypes).map(prop =>
-                                        <div>{prop} {component.defaultProps[prop] !== undefined ? "(default: " + JSON.stringify(component.defaultProps[prop]) + ")" : ""}</div>
-                                    )}
-                                </div>
-                            </Section>
-                        }
-                        { examples &&
-                            <Section title="Examples">
-                                {Object.entries(examples)
-                                    .filter(([name, element]) =>
-                                        !exampleName || exampleName === slugify(name)
-                                    )
-                                    .map(([name, element]) => (
-                                    <div className="my2">
-                                        <h4 className="my1">
-                                            <Link
-                                                to={`_internal/components/${slugify(component.name)}/${slugify(name)}`}
-                                                className="no-decoration"
-                                            >
-                                                {name}
-                                            </Link>
-                                        </h4>
-                                        <div className="flex flex-column">
-                                            <div
-                                                className="p2 bordered flex align-center flex-full"
-                                            >
-                                                <div className="full">
-                                                    {element}
-                                                </div>
-                                            </div>
-                                            <div
-                                                className="border-left border-right border-bottom text-code"
-                                            >
-                                                <div className="p1">
-                                                    {reactElementToJSXString(element)}
-                                                </div>
-                                            </div>
-                                        </div>
-                                    </div>
-                                ))}
-                            </Section>
-                        }
+                      {prop}{" "}
+                      {component.defaultProps &&
+                      component.defaultProps[prop] !== undefined
+                        ? "(default: " +
+                          JSON.stringify(component.defaultProps[prop]) +
+                          ")"
+                        : ""}
                     </div>
-                ))}
-            </div>
-        );
-    }
+                  ))}
+                </div>
+              </Section>
+            )}
+            {examples && (
+              <Section title="Examples">
+                {Object.entries(examples)
+                  .filter(
+                    ([name, element]) =>
+                      !exampleName || exampleName === slugify(name),
+                  )
+                  .map(([name, element]) => (
+                    <div className="my2">
+                      <h4 className="my1">
+                        <Link
+                          to={`_internal/components/${slugify(
+                            component.name,
+                          )}/${slugify(name)}`}
+                          className="no-decoration"
+                        >
+                          {name}
+                        </Link>
+                      </h4>
+                      <div className="flex flex-column">
+                        <div className="p2 bordered flex align-center flex-full">
+                          <div className="full">{element}</div>
+                        </div>
+                        <div className="border-left border-right border-bottom text-code">
+                          <div className="p1">
+                            {reactElementToJSXString(element)}
+                          </div>
+                        </div>
+                      </div>
+                    </div>
+                  ))}
+              </Section>
+            )}
+          </div>
+        ))}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/internal/components/IconsApp.jsx b/frontend/src/metabase/internal/components/IconsApp.jsx
index b362ba28450f619d1f9df1ef50c29ea0d193be2a..1e688eccfc15db8793c29d26a13f311668bfc64c 100644
--- a/frontend/src/metabase/internal/components/IconsApp.jsx
+++ b/frontend/src/metabase/internal/components/IconsApp.jsx
@@ -5,46 +5,48 @@ import Icon from "metabase/components/Icon.jsx";
 const SIZES = [12, 16];
 
 export default class IconsApp extends Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-            size: 32
-        }
-    }
-    render() {
-        let sizes = SIZES.concat(this.state.size)
-        return (
-            <table className="Table m4" style={{ width: "inherit" }}>
-                <thead>
-                    <tr>
-                        <th>Name</th>
-                        {sizes.map((size, index) =>
-                            <th>
-                                <div>{size}px</div>
-                                { index === SIZES.length &&
-                                    <input
-                                        style={{ width: 60 }}
-                                        type="range"
-                                        value={this.state.size}
-                                        max={64}
-                                        onChange={(e) => this.setState({ size: e.target.value })}
-                                    />
-                                }
-                            </th>
-                        )}
-                    </tr>
-                </thead>
-                <tbody>
-                { Object.keys(require("metabase/icon_paths").ICON_PATHS).map(name =>
-                    <tr>
-                        <td>{name}</td>
-                        {sizes.map(size =>
-                            <td><Icon name={name} size={size} /></td>
-                        )}
-                    </tr>
+  constructor(props) {
+    super(props);
+    this.state = {
+      size: 32,
+    };
+  }
+  render() {
+    let sizes = SIZES.concat(this.state.size);
+    return (
+      <table className="Table m4" style={{ width: "inherit" }}>
+        <thead>
+          <tr>
+            <th>Name</th>
+            {sizes.map((size, index) => (
+              <th>
+                <div>{size}px</div>
+                {index === SIZES.length && (
+                  <input
+                    style={{ width: 60 }}
+                    type="range"
+                    value={this.state.size}
+                    max={64}
+                    onChange={e => this.setState({ size: e.target.value })}
+                  />
                 )}
-                </tbody>
-            </table>
-        )
-    }
+              </th>
+            ))}
+          </tr>
+        </thead>
+        <tbody>
+          {Object.keys(require("metabase/icon_paths").ICON_PATHS).map(name => (
+            <tr>
+              <td>{name}</td>
+              {sizes.map(size => (
+                <td>
+                  <Icon name={name} size={size} />
+                </td>
+              ))}
+            </tr>
+          ))}
+        </tbody>
+      </table>
+    );
+  }
 }
diff --git a/frontend/src/metabase/internal/lib/components-node.js b/frontend/src/metabase/internal/lib/components-node.js
index b4b243aff6c979627838aa6c08e1feaa92913499..fe1af8974bda4d8af497659217fc346b1e36278e 100644
--- a/frontend/src/metabase/internal/lib/components-node.js
+++ b/frontend/src/metabase/internal/lib/components-node.js
@@ -6,6 +6,6 @@ import fs from "fs";
 var normalizedPath = path.join(__dirname, "..", "..", "components");
 
 export default fs
-    .readdirSync(normalizedPath)
-    .filter(file => /\.info\.js$/.test(file))
-    .map(file => require(path.join(normalizedPath, file)));
+  .readdirSync(normalizedPath)
+  .filter(file => /\.info\.js$/.test(file))
+  .map(file => require(path.join(normalizedPath, file)));
diff --git a/frontend/src/metabase/internal/lib/components-webpack.js b/frontend/src/metabase/internal/lib/components-webpack.js
index 126af0567ed3212928d4583dd82ec5d39d6179b0..2fd81307fa18970328c836efd70b967b9a8711b5 100644
--- a/frontend/src/metabase/internal/lib/components-webpack.js
+++ b/frontend/src/metabase/internal/lib/components-webpack.js
@@ -1,8 +1,10 @@
 // import all modules in this directory (http://stackoverflow.com/a/31770875)
 const req = require.context(
-    "metabase/components",
-    true,
-    /^(.*\.info\.(js$))[^.]*$/im
+  "metabase/components",
+  true,
+  /^(.*\.info\.(js$))[^.]*$/im,
 );
 
-export default req.keys().map(key => Object.assign({}, req(key), { showExample: true }));
+export default req
+  .keys()
+  .map(key => Object.assign({}, req(key), { showExample: true }));
diff --git a/frontend/src/metabase/internal/routes.js b/frontend/src/metabase/internal/routes.js
index b1e8626916dbccad7c7c43b69a2259bd44adaa85..c0b6be00dbe4ee4b38336cfa8a01a6044cd2be9a 100644
--- a/frontend/src/metabase/internal/routes.js
+++ b/frontend/src/metabase/internal/routes.js
@@ -6,27 +6,31 @@ import ColorsApp from "metabase/internal/components/ColorsApp";
 import ComponentsApp from "metabase/internal/components/ComponentsApp";
 
 const PAGES = {
-    "Icons": IconsApp,
-    "Colors": ColorsApp,
-    "Components": ComponentsApp,
-}
-
-const ListApp = () =>
-    <ul>
-        { Object.keys(PAGES).map((name) =>
-            <li><a href={"/_internal/"+name.toLowerCase()}>{name}</a></li>
-        )}
-    </ul>
-
+  Icons: IconsApp,
+  Colors: ColorsApp,
+  Components: ComponentsApp,
+};
 
+const ListApp = () => (
+  <ul>
+    {Object.keys(PAGES).map(name => (
+      <li>
+        <a href={"/_internal/" + name.toLowerCase()}>{name}</a>
+      </li>
+    ))}
+  </ul>
+);
 
 export default (
-    <Route>
-        <IndexRoute component={ListApp} />
-        { Object.entries(PAGES).map(([name, Component]) =>
-            <Route path={name.toLowerCase()} component={Component} />
-        )}
-        <Route path="components/:componentName" component={ComponentsApp} />
-        <Route path="components/:componentName/:exampleName" component={ComponentsApp} />
-    </Route>
+  <Route>
+    <IndexRoute component={ListApp} />
+    {Object.entries(PAGES).map(([name, Component]) => (
+      <Route path={name.toLowerCase()} component={Component} />
+    ))}
+    <Route path="components/:componentName" component={ComponentsApp} />
+    <Route
+      path="components/:componentName/:exampleName"
+      component={ComponentsApp}
+    />
+  </Route>
 );
diff --git a/frontend/src/metabase/lib/ace/sql_behaviour.js b/frontend/src/metabase/lib/ace/sql_behaviour.js
index 2f612aa04027a7be20c23f761c09317df070ee80..b5297c3731bd9943bab24c7e2c6ac35ef6e808bb 100644
--- a/frontend/src/metabase/lib/ace/sql_behaviour.js
+++ b/frontend/src/metabase/lib/ace/sql_behaviour.js
@@ -4,7 +4,7 @@
 
 // Modified from https://github.com/ajaxorg/ace/blob/b8804b1e9db1f7f02337ca884f4780f3579cc41b/lib/ace/mode/behaviour/cstyle.js
 
-/* ***** BEGIN LICENSE BLOCK *****
+/****** BEGIN LICENSE BLOCK *****
  * Distributed under the BSD license:
  *
  * Copyright (c) 2010, Ajax.org B.V.
@@ -34,242 +34,316 @@
  *
  * ***** END LICENSE BLOCK ***** */
 
-ace.require(["ace/lib/oop", "ace/mode/behaviour", "ace/token_iterator", "ace/lib/lang"],
-function(oop, { Behaviour }, { TokenIterator }, lang) {
-
-    var SAFE_INSERT_IN_TOKENS =
-        ["text", "paren.rparen", "punctuation.operator"];
-    var SAFE_INSERT_BEFORE_TOKENS =
-        ["text", "paren.rparen", "punctuation.operator", "comment"];
+ace.require(
+  ["ace/lib/oop", "ace/mode/behaviour", "ace/token_iterator", "ace/lib/lang"],
+  function(oop, { Behaviour }, { TokenIterator }, lang) {
+    var SAFE_INSERT_IN_TOKENS = [
+      "text",
+      "paren.rparen",
+      "punctuation.operator",
+    ];
+    var SAFE_INSERT_BEFORE_TOKENS = [
+      "text",
+      "paren.rparen",
+      "punctuation.operator",
+      "comment",
+    ];
 
     var context;
     var contextCache = {};
     var initContext = function(editor) {
-        var id = -1;
-        if (editor.multiSelect) {
-            id = editor.selection.index;
-            if (contextCache.rangeCount != editor.multiSelect.rangeCount)
-                contextCache = {rangeCount: editor.multiSelect.rangeCount};
-        }
-        if (contextCache[id])
-            return context = contextCache[id];
-        context = contextCache[id] = {
-            autoInsertedBrackets: 0,
-            autoInsertedRow: -1,
-            autoInsertedLineEnd: "",
-            maybeInsertedBrackets: 0,
-            maybeInsertedRow: -1,
-            maybeInsertedLineStart: "",
-            maybeInsertedLineEnd: ""
-        };
+      var id = -1;
+      if (editor.multiSelect) {
+        id = editor.selection.index;
+        if (contextCache.rangeCount != editor.multiSelect.rangeCount)
+          contextCache = { rangeCount: editor.multiSelect.rangeCount };
+      }
+      if (contextCache[id]) return (context = contextCache[id]);
+      context = contextCache[id] = {
+        autoInsertedBrackets: 0,
+        autoInsertedRow: -1,
+        autoInsertedLineEnd: "",
+        maybeInsertedBrackets: 0,
+        maybeInsertedRow: -1,
+        maybeInsertedLineStart: "",
+        maybeInsertedLineEnd: "",
+      };
     };
 
     var getWrapped = function(selection, selected, opening, closing) {
-        var rowDiff = selection.end.row - selection.start.row;
-        return {
-            text: opening + selected + closing,
-            selection: [
-                    0,
-                    selection.start.column + 1,
-                    rowDiff,
-                    selection.end.column + (rowDiff ? 0 : 1)
-                ]
-        };
+      var rowDiff = selection.end.row - selection.start.row;
+      return {
+        text: opening + selected + closing,
+        selection: [
+          0,
+          selection.start.column + 1,
+          rowDiff,
+          selection.end.column + (rowDiff ? 0 : 1),
+        ],
+      };
     };
 
     var SQLBehaviour = function() {
-        function createInsertDeletePair(name, opening, closing) {
-            this.add(name, "insertion", function(state, action, editor, session, text) {
-                if (text == opening) {
-                    initContext(editor);
-                    var selection = editor.getSelectionRange();
-                    var selected = session.doc.getTextRange(selection);
-                    if (selected !== "" && editor.getWrapBehavioursEnabled()) {
-                        return getWrapped(selection, selected, opening, closing);
-                    } else if (SQLBehaviour.isSaneInsertion(editor, session)) {
-                        SQLBehaviour.recordAutoInsert(editor, session, closing);
-                        return {
-                            text: opening + closing,
-                            selection: [1, 1]
-                        };
-                    }
-                } else if (text == closing) {
-                    initContext(editor);
-                    var cursor = editor.getCursorPosition();
-                    var line = session.doc.getLine(cursor.row);
-                    var rightChar = line.substring(cursor.column, cursor.column + 1);
-                    if (rightChar == closing) {
-                        var matching = session.$findOpeningBracket(closing, {column: cursor.column + 1, row: cursor.row});
-                        if (matching !== null && SQLBehaviour.isAutoInsertedClosing(cursor, line, text)) {
-                            SQLBehaviour.popAutoInsertedClosing();
-                            return {
-                                text: '',
-                                selection: [1, 1]
-                            };
-                        }
-                    }
-                }
-            });
+      function createInsertDeletePair(name, opening, closing) {
+        this.add(name, "insertion", function(
+          state,
+          action,
+          editor,
+          session,
+          text,
+        ) {
+          if (text == opening) {
+            initContext(editor);
+            var selection = editor.getSelectionRange();
+            var selected = session.doc.getTextRange(selection);
+            if (selected !== "" && editor.getWrapBehavioursEnabled()) {
+              return getWrapped(selection, selected, opening, closing);
+            } else if (SQLBehaviour.isSaneInsertion(editor, session)) {
+              SQLBehaviour.recordAutoInsert(editor, session, closing);
+              return {
+                text: opening + closing,
+                selection: [1, 1],
+              };
+            }
+          } else if (text == closing) {
+            initContext(editor);
+            var cursor = editor.getCursorPosition();
+            var line = session.doc.getLine(cursor.row);
+            var rightChar = line.substring(cursor.column, cursor.column + 1);
+            if (rightChar == closing) {
+              var matching = session.$findOpeningBracket(closing, {
+                column: cursor.column + 1,
+                row: cursor.row,
+              });
+              if (
+                matching !== null &&
+                SQLBehaviour.isAutoInsertedClosing(cursor, line, text)
+              ) {
+                SQLBehaviour.popAutoInsertedClosing();
+                return {
+                  text: "",
+                  selection: [1, 1],
+                };
+              }
+            }
+          }
+        });
 
-            this.add(name, "deletion", function(state, action, editor, session, range) {
-                var selected = session.doc.getTextRange(range);
-                if (!range.isMultiLine() && selected == opening) {
-                    initContext(editor);
-                    var line = session.doc.getLine(range.start.row);
-                    var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
-                    if (rightChar == closing) {
-                        range.end.column++;
-                        return range;
-                    }
-                }
-            });
-        }
+        this.add(name, "deletion", function(
+          state,
+          action,
+          editor,
+          session,
+          range,
+        ) {
+          var selected = session.doc.getTextRange(range);
+          if (!range.isMultiLine() && selected == opening) {
+            initContext(editor);
+            var line = session.doc.getLine(range.start.row);
+            var rightChar = line.substring(
+              range.start.column + 1,
+              range.start.column + 2,
+            );
+            if (rightChar == closing) {
+              range.end.column++;
+              return range;
+            }
+          }
+        });
+      }
 
-        createInsertDeletePair.call(this, "braces", "{", "}");
-        createInsertDeletePair.call(this, "parens", "(", ")");
-        createInsertDeletePair.call(this, "brackets", "[", "]");
+      createInsertDeletePair.call(this, "braces", "{", "}");
+      createInsertDeletePair.call(this, "parens", "(", ")");
+      createInsertDeletePair.call(this, "brackets", "[", "]");
 
-        this.add("string_dquotes", "insertion", function(state, action, editor, session, text) {
-            if (text == '"' || text == "'") {
-                if (this.lineCommentStart && this.lineCommentStart.indexOf(text) != -1)
-                    return;
-                initContext(editor);
-                var quote = text;
-                var selection = editor.getSelectionRange();
-                var selected = session.doc.getTextRange(selection);
-                if (selected !== "" && selected !== "'" && selected != '"' && editor.getWrapBehavioursEnabled()) {
-                    return getWrapped(selection, selected, quote, quote);
-                } else if (!selected) {
-                    var cursor = editor.getCursorPosition();
-                    var line = session.doc.getLine(cursor.row);
-                    var leftChar = line.substring(cursor.column-1, cursor.column);
-                    var rightChar = line.substring(cursor.column, cursor.column + 1);
+      this.add("string_dquotes", "insertion", function(
+        state,
+        action,
+        editor,
+        session,
+        text,
+      ) {
+        if (text == '"' || text == "'") {
+          if (
+            this.lineCommentStart &&
+            this.lineCommentStart.indexOf(text) != -1
+          )
+            return;
+          initContext(editor);
+          var quote = text;
+          var selection = editor.getSelectionRange();
+          var selected = session.doc.getTextRange(selection);
+          if (
+            selected !== "" &&
+            selected !== "'" &&
+            selected != '"' &&
+            editor.getWrapBehavioursEnabled()
+          ) {
+            return getWrapped(selection, selected, quote, quote);
+          } else if (!selected) {
+            var cursor = editor.getCursorPosition();
+            var line = session.doc.getLine(cursor.row);
+            var leftChar = line.substring(cursor.column - 1, cursor.column);
+            var rightChar = line.substring(cursor.column, cursor.column + 1);
 
-                    var token = session.getTokenAt(cursor.row, cursor.column);
-                    var rightToken = session.getTokenAt(cursor.row, cursor.column + 1);
-                    // We're escaped.
-                    if (leftChar == "\\" && token && /escape/.test(token.type))
-                        return null;
+            var token = session.getTokenAt(cursor.row, cursor.column);
+            var rightToken = session.getTokenAt(cursor.row, cursor.column + 1);
+            // We're escaped.
+            if (leftChar == "\\" && token && /escape/.test(token.type))
+              return null;
 
-                    var stringBefore = token && /string|escape/.test(token.type);
-                    var stringAfter = !rightToken || /string|escape/.test(rightToken.type);
+            var stringBefore = token && /string|escape/.test(token.type);
+            var stringAfter =
+              !rightToken || /string|escape/.test(rightToken.type);
 
-                    var pair;
-                    if (rightChar == quote) {
-                        pair = stringBefore !== stringAfter;
-                        if (pair && /string\.end/.test(rightToken.type))
-                            pair = false;
-                    } else {
-                        if (stringBefore && !stringAfter)
-                            return null; // wrap string with different quote
-                        if (stringBefore && stringAfter)
-                            return null; // do not pair quotes inside strings
-                        var wordRe = session.$mode.tokenRe;
-                        wordRe.lastIndex = 0;
-                        var isWordBefore = wordRe.test(leftChar);
-                        wordRe.lastIndex = 0;
-                        var isWordAfter = wordRe.test(leftChar);
-                        if (isWordBefore || isWordAfter)
-                            return null; // before or after alphanumeric
-                        if (rightChar && !/[\s;,.})\]\\]/.test(rightChar))
-                            return null; // there is rightChar and it isn't closing
-                        pair = true;
-                    }
-                    return {
-                        text: pair ? quote + quote : "",
-                        selection: [1,1]
-                    };
-                }
+            var pair;
+            if (rightChar == quote) {
+              pair = stringBefore !== stringAfter;
+              if (pair && /string\.end/.test(rightToken.type)) pair = false;
+            } else {
+              if (stringBefore && !stringAfter) return null; // wrap string with different quote
+              if (stringBefore && stringAfter) return null; // do not pair quotes inside strings
+              var wordRe = session.$mode.tokenRe;
+              wordRe.lastIndex = 0;
+              var isWordBefore = wordRe.test(leftChar);
+              wordRe.lastIndex = 0;
+              var isWordAfter = wordRe.test(leftChar);
+              if (isWordBefore || isWordAfter) return null; // before or after alphanumeric
+              if (rightChar && !/[\s;,.})\]\\]/.test(rightChar)) return null; // there is rightChar and it isn't closing
+              pair = true;
             }
-        });
-
-        this.add("string_dquotes", "deletion", function(state, action, editor, session, range) {
-            var selected = session.doc.getTextRange(range);
-            if (!range.isMultiLine() && (selected == '"' || selected == "'")) {
-                initContext(editor);
-                var line = session.doc.getLine(range.start.row);
-                var rightChar = line.substring(range.start.column + 1, range.start.column + 2);
-                if (rightChar == selected) {
-                    range.end.column++;
-                    return range;
-                }
-            }
-        });
+            return {
+              text: pair ? quote + quote : "",
+              selection: [1, 1],
+            };
+          }
+        }
+      });
 
+      this.add("string_dquotes", "deletion", function(
+        state,
+        action,
+        editor,
+        session,
+        range,
+      ) {
+        var selected = session.doc.getTextRange(range);
+        if (!range.isMultiLine() && (selected == '"' || selected == "'")) {
+          initContext(editor);
+          var line = session.doc.getLine(range.start.row);
+          var rightChar = line.substring(
+            range.start.column + 1,
+            range.start.column + 2,
+          );
+          if (rightChar == selected) {
+            range.end.column++;
+            return range;
+          }
+        }
+      });
     };
 
-
     SQLBehaviour.isSaneInsertion = function(editor, session) {
-        var cursor = editor.getCursorPosition();
-        var iterator = new TokenIterator(session, cursor.row, cursor.column);
+      var cursor = editor.getCursorPosition();
+      var iterator = new TokenIterator(session, cursor.row, cursor.column);
 
-        // Don't insert in the middle of a keyword/identifier/lexical
-        if (!this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS)) {
-            // Look ahead in case we're at the end of a token
-            var iterator2 = new TokenIterator(session, cursor.row, cursor.column + 1);
-            if (!this.$matchTokenType(iterator2.getCurrentToken() || "text", SAFE_INSERT_IN_TOKENS))
-                return false;
-        }
+      // Don't insert in the middle of a keyword/identifier/lexical
+      if (
+        !this.$matchTokenType(
+          iterator.getCurrentToken() || "text",
+          SAFE_INSERT_IN_TOKENS,
+        )
+      ) {
+        // Look ahead in case we're at the end of a token
+        var iterator2 = new TokenIterator(
+          session,
+          cursor.row,
+          cursor.column + 1,
+        );
+        if (
+          !this.$matchTokenType(
+            iterator2.getCurrentToken() || "text",
+            SAFE_INSERT_IN_TOKENS,
+          )
+        )
+          return false;
+      }
 
-        // Only insert in front of whitespace/comments
-        iterator.stepForward();
-        return iterator.getCurrentTokenRow() !== cursor.row ||
-            this.$matchTokenType(iterator.getCurrentToken() || "text", SAFE_INSERT_BEFORE_TOKENS);
+      // Only insert in front of whitespace/comments
+      iterator.stepForward();
+      return (
+        iterator.getCurrentTokenRow() !== cursor.row ||
+        this.$matchTokenType(
+          iterator.getCurrentToken() || "text",
+          SAFE_INSERT_BEFORE_TOKENS,
+        )
+      );
     };
 
     SQLBehaviour.$matchTokenType = function(token, types) {
-        return types.indexOf(token.type || token) > -1;
+      return types.indexOf(token.type || token) > -1;
     };
 
     SQLBehaviour.recordAutoInsert = function(editor, session, bracket) {
-        var cursor = editor.getCursorPosition();
-        var line = session.doc.getLine(cursor.row);
-        // Reset previous state if text or context changed too much
-        if (!this.isAutoInsertedClosing(cursor, line, context.autoInsertedLineEnd[0]))
-            context.autoInsertedBrackets = 0;
-        context.autoInsertedRow = cursor.row;
-        context.autoInsertedLineEnd = bracket + line.substr(cursor.column);
-        context.autoInsertedBrackets++;
+      var cursor = editor.getCursorPosition();
+      var line = session.doc.getLine(cursor.row);
+      // Reset previous state if text or context changed too much
+      if (
+        !this.isAutoInsertedClosing(
+          cursor,
+          line,
+          context.autoInsertedLineEnd[0],
+        )
+      )
+        context.autoInsertedBrackets = 0;
+      context.autoInsertedRow = cursor.row;
+      context.autoInsertedLineEnd = bracket + line.substr(cursor.column);
+      context.autoInsertedBrackets++;
     };
 
     SQLBehaviour.recordMaybeInsert = function(editor, session, bracket) {
-        var cursor = editor.getCursorPosition();
-        var line = session.doc.getLine(cursor.row);
-        if (!this.isMaybeInsertedClosing(cursor, line))
-            context.maybeInsertedBrackets = 0;
-        context.maybeInsertedRow = cursor.row;
-        context.maybeInsertedLineStart = line.substr(0, cursor.column) + bracket;
-        context.maybeInsertedLineEnd = line.substr(cursor.column);
-        context.maybeInsertedBrackets++;
+      var cursor = editor.getCursorPosition();
+      var line = session.doc.getLine(cursor.row);
+      if (!this.isMaybeInsertedClosing(cursor, line))
+        context.maybeInsertedBrackets = 0;
+      context.maybeInsertedRow = cursor.row;
+      context.maybeInsertedLineStart = line.substr(0, cursor.column) + bracket;
+      context.maybeInsertedLineEnd = line.substr(cursor.column);
+      context.maybeInsertedBrackets++;
     };
 
     SQLBehaviour.isAutoInsertedClosing = function(cursor, line, bracket) {
-        return context.autoInsertedBrackets > 0 &&
-            cursor.row === context.autoInsertedRow &&
-            bracket === context.autoInsertedLineEnd[0] &&
-            line.substr(cursor.column) === context.autoInsertedLineEnd;
+      return (
+        context.autoInsertedBrackets > 0 &&
+        cursor.row === context.autoInsertedRow &&
+        bracket === context.autoInsertedLineEnd[0] &&
+        line.substr(cursor.column) === context.autoInsertedLineEnd
+      );
     };
 
     SQLBehaviour.isMaybeInsertedClosing = function(cursor, line) {
-        return context.maybeInsertedBrackets > 0 &&
-            cursor.row === context.maybeInsertedRow &&
-            line.substr(cursor.column) === context.maybeInsertedLineEnd &&
-            line.substr(0, cursor.column) == context.maybeInsertedLineStart;
+      return (
+        context.maybeInsertedBrackets > 0 &&
+        cursor.row === context.maybeInsertedRow &&
+        line.substr(cursor.column) === context.maybeInsertedLineEnd &&
+        line.substr(0, cursor.column) == context.maybeInsertedLineStart
+      );
     };
 
     SQLBehaviour.popAutoInsertedClosing = function() {
-        context.autoInsertedLineEnd = context.autoInsertedLineEnd.substr(1);
-        context.autoInsertedBrackets--;
+      context.autoInsertedLineEnd = context.autoInsertedLineEnd.substr(1);
+      context.autoInsertedBrackets--;
     };
 
     SQLBehaviour.clearMaybeInsertedClosing = function() {
-        if (context) {
-            context.maybeInsertedBrackets = 0;
-            context.maybeInsertedRow = -1;
-        }
+      if (context) {
+        context.maybeInsertedBrackets = 0;
+        context.maybeInsertedRow = -1;
+      }
     };
 
     oop.inherits(SQLBehaviour, Behaviour);
 
     exports.SQLBehaviour = SQLBehaviour;
-});
+  },
+);
diff --git a/frontend/src/metabase/lib/ace/theme-metabase.js b/frontend/src/metabase/lib/ace/theme-metabase.js
index dd2b89004f88d85be92bea49c87d5dce784a73a5..1738e4a5e43ebb1db3491ae23fbbef49bfd2fda4 100644
--- a/frontend/src/metabase/lib/ace/theme-metabase.js
+++ b/frontend/src/metabase/lib/ace/theme-metabase.js
@@ -1,10 +1,13 @@
 /*global ace*/
 /* eslint "import/no-commonjs": 0 */
-ace.define("ace/theme/metabase",["require","exports","module","ace/lib/dom"], function(require, exports, module) {
-
-exports.isDark = false;
-exports.cssClass = "ace-metabase";
-exports.cssText = "\
+ace.define(
+  "ace/theme/metabase",
+  ["require", "exports", "module", "ace/lib/dom"],
+  function(require, exports, module) {
+    exports.isDark = false;
+    exports.cssClass = "ace-metabase";
+    exports.cssText =
+      '\
 .ace-metabase .ace_gutter {\
 background: rgb(220,236,249);\
 color: #509EE3;\
@@ -99,9 +102,10 @@ width: 1px;\
 background: #e8e8e8;\
 }\
 .ace-metabase .ace_indent-guide {\
-background: url(\"data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==\") right repeat-y;\
-}";
+background: url("data:image/png;base64,iVBORw0KGgoAAAANSUhEUgAAAAEAAAACCAYAAACZgbYnAAAAE0lEQVQImWP4////f4bLly//BwAmVgd1/w11/gAAAABJRU5ErkJggg==") right repeat-y;\
+}';
 
     var dom = require("../lib/dom");
     dom.importCssString(exports.cssText, exports.cssClass);
-});
+  },
+);
diff --git a/frontend/src/metabase/lib/analytics.js b/frontend/src/metabase/lib/analytics.js
index 2db380cdd85ae78f7ca6dc35fb846d22690d47a9..0378b683ae0addbdd55cf248a7b66e66264c6605 100644
--- a/frontend/src/metabase/lib/analytics.js
+++ b/frontend/src/metabase/lib/analytics.js
@@ -5,56 +5,65 @@ import MetabaseSettings from "metabase/lib/settings";
 
 import { DEBUG } from "metabase/lib/debug";
 
-
 // Simple module for in-app analytics.  Currently sends data to GA but could be extended to anything else.
 const MetabaseAnalytics = {
-    // track a pageview (a.k.a. route change)
-    trackPageView: function(url: string) {
-        if (url) {
-            // scrub query builder urls to remove serialized json queries from path
-            url = (url.lastIndexOf('/q/', 0) === 0) ? '/q/' : url;
-
-            const { tag } = MetabaseSettings.get('version');
-
-            // $FlowFixMe
-            ga('set', 'dimension1', tag);
-            ga('set', 'page', url);
-            ga('send', 'pageview', url);
-        }
-    },
+  // track a pageview (a.k.a. route change)
+  trackPageView: function(url: string) {
+    if (url) {
+      // scrub query builder urls to remove serialized json queries from path
+      url = url.lastIndexOf("/q/", 0) === 0 ? "/q/" : url;
 
-    // track an event
-    trackEvent: function(category: string, action?: string, label?: string|number|boolean, value?: number) {
-        const { tag } = MetabaseSettings.get('version');
+      const { tag } = MetabaseSettings.get("version");
 
-        // category & action are required, rest are optional
-        if (category && action) {
-            // $FlowFixMe
-            ga('set', 'dimension1', tag);
-            ga('send', 'event', category, action, label, value);
-        }
-        if (DEBUG) {
-            console.log("trackEvent", { category, action, label, value });
-        }
+      // $FlowFixMe
+      ga("set", "dimension1", tag);
+      ga("set", "page", url);
+      ga("send", "pageview", url);
     }
-}
+  },
 
-export default MetabaseAnalytics;
+  // track an event
+  trackEvent: function(
+    category: string,
+    action?: string,
+    label?: string | number | boolean,
+    value?: number,
+  ) {
+    const { tag } = MetabaseSettings.get("version");
 
+    // category & action are required, rest are optional
+    if (category && action) {
+      // $FlowFixMe
+      ga("set", "dimension1", tag);
+      ga("send", "event", category, action, label, value);
+    }
+    if (DEBUG) {
+      console.log("trackEvent", { category, action, label, value });
+    }
+  },
+};
+
+export default MetabaseAnalytics;
 
 export function registerAnalyticsClickListener() {
-    // $FlowFixMe
-    document.body.addEventListener("click", function(e) {
-        var node = e.target;
-
-        // check the target and all parent elements
-        while (node) {
-            if (node.dataset && node.dataset.metabaseEvent) {
-                // we expect our event to be a semicolon delimited string
-                const parts = node.dataset.metabaseEvent.split(";").map(p => p.trim());
-                MetabaseAnalytics.trackEvent(...parts);
-            }
-            node = node.parentNode;
+  // $FlowFixMe
+  document.body.addEventListener(
+    "click",
+    function(e) {
+      var node = e.target;
+
+      // check the target and all parent elements
+      while (node) {
+        if (node.dataset && node.dataset.metabaseEvent) {
+          // we expect our event to be a semicolon delimited string
+          const parts = node.dataset.metabaseEvent
+            .split(";")
+            .map(p => p.trim());
+          MetabaseAnalytics.trackEvent(...parts);
         }
-    }, true);
+        node = node.parentNode;
+      }
+    },
+    true,
+  );
 }
diff --git a/frontend/src/metabase/lib/api.js b/frontend/src/metabase/lib/api.js
index ea661c351c4dc4053ee553b15525db6c6fcca066..78c4a0dd53a8d0cbf7ba7f8f410760a13fcfde1b 100644
--- a/frontend/src/metabase/lib/api.js
+++ b/frontend/src/metabase/lib/api.js
@@ -7,122 +7,133 @@ import EventEmitter from "events";
 type TransformFn = (o: any) => any;
 
 type Options = {
-    noEvent?: boolean,
-    transformResponse?: TransformFn,
-    cancelled?: Promise<any>
-}
+  noEvent?: boolean,
+  transformResponse?: TransformFn,
+  cancelled?: Promise<any>,
+};
 type Data = {
-    [key:string]: any
+  [key: string]: any,
 };
 
 const DEFAULT_OPTIONS: Options = {
-    noEvent: false,
-    transformResponse: (o) => o
-}
+  noEvent: false,
+  transformResponse: o => o,
+};
 
 class Api extends EventEmitter {
-    basename: "";
-
-    GET:    (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
-    POST:   (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
-    PUT:    (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
-    DELETE: (t: string, o?: Options|TransformFn) => (d?: Data, o?: Options) => Promise<any>;
+  basename: "";
 
-    constructor() {
-        super();
-        this.GET = this._makeMethod("GET").bind(this);
-        this.DELETE = this._makeMethod("DELETE").bind(this);
-        this.POST = this._makeMethod("POST", true).bind(this);
-        this.PUT = this._makeMethod("PUT", true).bind(this);
-    }
+  GET: (
+    t: string,
+    o?: Options | TransformFn,
+  ) => (d?: Data, o?: Options) => Promise<any>;
+  POST: (
+    t: string,
+    o?: Options | TransformFn,
+  ) => (d?: Data, o?: Options) => Promise<any>;
+  PUT: (
+    t: string,
+    o?: Options | TransformFn,
+  ) => (d?: Data, o?: Options) => Promise<any>;
+  DELETE: (
+    t: string,
+    o?: Options | TransformFn,
+  ) => (d?: Data, o?: Options) => Promise<any>;
 
-    _makeMethod(method: string, hasBody: boolean = false) {
-        return (
-            urlTemplate: string,
-            methodOptions?: Options|TransformFn = {}
-        ) => {
-            if (typeof methodOptions === "function") {
-                methodOptions = { transformResponse: methodOptions };
-            }
-            const defaultOptions = { ...DEFAULT_OPTIONS, ...methodOptions };
-            return (
-                data?: Data,
-                invocationOptions?: Options = {}
-            ): Promise<any> => {
-                const options: Options = { ...defaultOptions, ...invocationOptions };
-                let url = urlTemplate;
-                data = { ...data };
-                for (let tag of (url.match(/:\w+/g) || [])) {
-                    let value = data[tag.slice(1)];
-                    if (value === undefined) {
-                        console.warn("Warning: calling", method, "without", tag);
-                        value = "";
-                    }
-                    url = url.replace(tag, encodeURIComponent(data[tag.slice(1)]))
-                    delete data[tag.slice(1)];
-                }
+  constructor() {
+    super();
+    this.GET = this._makeMethod("GET").bind(this);
+    this.DELETE = this._makeMethod("DELETE").bind(this);
+    this.POST = this._makeMethod("POST", true).bind(this);
+    this.PUT = this._makeMethod("PUT", true).bind(this);
+  }
 
-                let headers: { [key:string]: string } = {
-                    "Accept": "application/json",
-                };
+  _makeMethod(method: string, hasBody: boolean = false) {
+    return (
+      urlTemplate: string,
+      methodOptions?: Options | TransformFn = {},
+    ) => {
+      if (typeof methodOptions === "function") {
+        methodOptions = { transformResponse: methodOptions };
+      }
+      const defaultOptions = { ...DEFAULT_OPTIONS, ...methodOptions };
+      return (data?: Data, invocationOptions?: Options = {}): Promise<any> => {
+        const options: Options = { ...defaultOptions, ...invocationOptions };
+        let url = urlTemplate;
+        data = { ...data };
+        for (let tag of url.match(/:\w+/g) || []) {
+          let value = data[tag.slice(1)];
+          if (value === undefined) {
+            console.warn("Warning: calling", method, "without", tag);
+            value = "";
+          }
+          url = url.replace(tag, encodeURIComponent(data[tag.slice(1)]));
+          delete data[tag.slice(1)];
+        }
 
-                let body;
-                if (hasBody) {
-                    headers["Content-Type"] = "application/json";
-                    body = JSON.stringify(data);
-                } else {
-                    let qs = querystring.stringify(data);
-                    if (qs) {
-                        url += (url.indexOf("?") >= 0 ? "&" : "?") + qs;
-                    }
-                }
+        let headers: { [key: string]: string } = {
+          Accept: "application/json",
+        };
 
-                return this._makeRequest(method, url, headers, body, data, options);
-            }
+        let body;
+        if (hasBody) {
+          headers["Content-Type"] = "application/json";
+          body = JSON.stringify(data);
+        } else {
+          let qs = querystring.stringify(data);
+          if (qs) {
+            url += (url.indexOf("?") >= 0 ? "&" : "?") + qs;
+          }
         }
-    }
 
-    // TODO Atte Keinänen 6/26/17: Replacing this with isomorphic-fetch could simplify the implementation
-    _makeRequest(method, url, headers, body, data, options) {
-        return new Promise((resolve, reject) => {
-            let isCancelled = false;
-            let xhr = new XMLHttpRequest();
-            xhr.open(method, this.basename + url);
-            for (let headerName in headers) {
-                xhr.setRequestHeader(headerName, headers[headerName])
-            }
-            xhr.onreadystatechange = () => {
-                // $FlowFixMe
-                if (xhr.readyState === XMLHttpRequest.DONE) {
-                    let body = xhr.responseText;
-                    try { body = JSON.parse(body); } catch (e) {}
-                    if (xhr.status >= 200 && xhr.status <= 299) {
-                        if (options.transformResponse) {
-                            body = options.transformResponse(body, { data });
-                        }
-                        resolve(body);
-                    } else {
-                        reject({
-                            status: xhr.status,
-                            data: body,
-                            isCancelled: isCancelled
-                        });
-                    }
-                    if (!options.noEvent) {
-                        this.emit(xhr.status, url);
-                    }
-                }
-            }
-            xhr.send(body);
+        return this._makeRequest(method, url, headers, body, data, options);
+      };
+    };
+  }
 
-            if (options.cancelled) {
-                options.cancelled.then(() => {
-                    isCancelled = true;
-                    xhr.abort()
-                });
+  // TODO Atte Keinänen 6/26/17: Replacing this with isomorphic-fetch could simplify the implementation
+  _makeRequest(method, url, headers, body, data, options) {
+    return new Promise((resolve, reject) => {
+      let isCancelled = false;
+      let xhr = new XMLHttpRequest();
+      xhr.open(method, this.basename + url);
+      for (let headerName in headers) {
+        xhr.setRequestHeader(headerName, headers[headerName]);
+      }
+      xhr.onreadystatechange = () => {
+        // $FlowFixMe
+        if (xhr.readyState === XMLHttpRequest.DONE) {
+          let body = xhr.responseText;
+          try {
+            body = JSON.parse(body);
+          } catch (e) {}
+          if (xhr.status >= 200 && xhr.status <= 299) {
+            if (options.transformResponse) {
+              body = options.transformResponse(body, { data });
             }
+            resolve(body);
+          } else {
+            reject({
+              status: xhr.status,
+              data: body,
+              isCancelled: isCancelled,
+            });
+          }
+          if (!options.noEvent) {
+            this.emit(xhr.status, url);
+          }
+        }
+      };
+      xhr.send(body);
+
+      if (options.cancelled) {
+        options.cancelled.then(() => {
+          isCancelled = true;
+          xhr.abort();
         });
-    }
+      }
+    });
+  }
 }
 
-export default new Api();
\ No newline at end of file
+export default new Api();
diff --git a/frontend/src/metabase/lib/auth.js b/frontend/src/metabase/lib/auth.js
index c3fdd444ac3fba2ca16be7d3e0002d5aa19e13e9..92f1d263ab6ed24f85f7d43c679a6632da793178 100644
--- a/frontend/src/metabase/lib/auth.js
+++ b/frontend/src/metabase/lib/auth.js
@@ -2,14 +2,17 @@
 
 /// clear out Google Auth credentials in browser if present
 export function clearGoogleAuthCredentials() {
-    let googleAuth = typeof gapi !== 'undefined' && gapi && gapi.auth2 ? gapi.auth2.getAuthInstance() : undefined;
-    if (!googleAuth) return;
+  let googleAuth =
+    typeof gapi !== "undefined" && gapi && gapi.auth2
+      ? gapi.auth2.getAuthInstance()
+      : undefined;
+  if (!googleAuth) return;
 
-    try {
-        googleAuth.signOut().then(function() {
-            console.log('Cleared Google Auth credentials.');
-        });
-    } catch (error) {
-        console.error('Problem clearing Google Auth credentials', error);
-    }
+  try {
+    googleAuth.signOut().then(function() {
+      console.log("Cleared Google Auth credentials.");
+    });
+  } catch (error) {
+    console.error("Problem clearing Google Auth credentials", error);
+  }
 }
diff --git a/frontend/src/metabase/lib/browser.js b/frontend/src/metabase/lib/browser.js
index c6a5ff8c9971f72e45fbbd9e60a3b12cf9820419..6a3cfb7c94fd632a516329b4ec05c6fa604f490f 100644
--- a/frontend/src/metabase/lib/browser.js
+++ b/frontend/src/metabase/lib/browser.js
@@ -1,28 +1,28 @@
 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]);
-        }
+  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;
+  }
+  return options;
 }
 
 export function stringifyHashOptions(options) {
-    return querystring.stringify(options).replace(/=true\b/g, "");
+  return querystring.stringify(options).replace(/=true\b/g, "");
 }
 
 export function updateQueryString(location, optionsUpdater) {
-    const currentOptions = parseHashOptions(location.search.substring(1))
-    const queryString = stringifyHashOptions(optionsUpdater(currentOptions))
+  const currentOptions = parseHashOptions(location.search.substring(1));
+  const queryString = stringifyHashOptions(optionsUpdater(currentOptions));
 
-    return {
-        pathname: location.pathname,
-        hash: location.hash,
-        search: queryString ? `?${queryString}` : null
-    };
+  return {
+    pathname: location.pathname,
+    hash: location.hash,
+    search: queryString ? `?${queryString}` : null,
+  };
 }
diff --git a/frontend/src/metabase/lib/card.js b/frontend/src/metabase/lib/card.js
index c4a4906d439a2d032dc243e502718c863ace0ffc..4f668ba089be7131b1405bd3deb1a96219e9bd10 100644
--- a/frontend/src/metabase/lib/card.js
+++ b/frontend/src/metabase/lib/card.js
@@ -5,130 +5,140 @@ import * as Urls from "metabase/lib/urls";
 
 import { CardApi } from "metabase/services";
 
-
 export function createCard(name = null) {
-    return {
-        name: name,
-        display: "table",
-        visualization_settings: {},
-        dataset_query: {}
-    };
+  return {
+    name: name,
+    display: "table",
+    visualization_settings: {},
+    dataset_query: {},
+  };
 }
 
 // start a new card using the given query type and optional database and table selections
 export function startNewCard(type, databaseId, tableId) {
-    // create a brand new card to work from
-    let card = createCard();
-    card.dataset_query = createQuery(type, databaseId, tableId);
+  // create a brand new card to work from
+  let card = createCard();
+  card.dataset_query = createQuery(type, databaseId, tableId);
 
-    return card;
+  return card;
 }
 
 // load a card either by ID or from a base64 serialization.  if both are present then they are merged, which the serialized version taking precedence
 // TODO: move to redux
 export async function loadCard(cardId) {
-    try {
-        return await CardApi.get({ "cardId": cardId });
-    } catch (error) {
-        console.log("error loading card", error);
-        throw error;
-    }
+  try {
+    return await CardApi.get({ cardId: cardId });
+  } catch (error) {
+    console.log("error loading card", error);
+    throw error;
+  }
 }
 
 // TODO Atte Keinänen 5/31/17 Deprecated, we should migrate existing references to this method to `question.isCardDirty`
 // predicate function that dermines if a given card is "dirty" compared to the last known version of the card
 export function isCardDirty(card, originalCard) {
-    // The rules:
-    //   - if it's new, then it's dirty when
-    //       1) there is a database/table chosen or
-    //       2) when there is any content on the native query
-    //   - if it's saved, then it's dirty when
-    //       1) the current card doesn't match the last saved version
-
-    if (!card) {
-        return false;
-    } else if (!card.id) {
-        if (card.dataset_query.query && card.dataset_query.query.source_table) {
-            return true;
-        } else if (card.dataset_query.native && !_.isEmpty(card.dataset_query.native.query)) {
-            return true;
-        } else {
-            return false;
-        }
+  // The rules:
+  //   - if it's new, then it's dirty when
+  //       1) there is a database/table chosen or
+  //       2) when there is any content on the native query
+  //   - if it's saved, then it's dirty when
+  //       1) the current card doesn't match the last saved version
+
+  if (!card) {
+    return false;
+  } else if (!card.id) {
+    if (card.dataset_query.query && card.dataset_query.query.source_table) {
+      return true;
+    } else if (
+      card.dataset_query.native &&
+      !_.isEmpty(card.dataset_query.native.query)
+    ) {
+      return true;
     } else {
-        const origCardSerialized = originalCard ? serializeCardForUrl(originalCard) : null;
-        const newCardSerialized = card ? serializeCardForUrl(_.omit(card, 'original_card_id')) : null;
-        return (newCardSerialized !== origCardSerialized);
+      return false;
     }
+  } else {
+    const origCardSerialized = originalCard
+      ? serializeCardForUrl(originalCard)
+      : null;
+    const newCardSerialized = card
+      ? serializeCardForUrl(_.omit(card, "original_card_id"))
+      : null;
+    return newCardSerialized !== origCardSerialized;
+  }
 }
 
 export function isCardRunnable(card, tableMetadata) {
-    if (!card) {
-        return false;
-    }
-    const datasetQuery = card.dataset_query;
-    if (datasetQuery.query) {
-        return Query.canRun(datasetQuery.query, tableMetadata);
-    } else {
-        return (datasetQuery.database != undefined && datasetQuery.native.query !== "");
-    }
+  if (!card) {
+    return false;
+  }
+  const datasetQuery = card.dataset_query;
+  if (datasetQuery.query) {
+    return Query.canRun(datasetQuery.query, tableMetadata);
+  } else {
+    return (
+      datasetQuery.database != undefined && datasetQuery.native.query !== ""
+    );
+  }
 }
 
 // TODO Atte Keinänen 5/31/17 Deprecated, we should move tests to Questions.spec.js
 export function serializeCardForUrl(card) {
-    var dataset_query = Utils.copy(card.dataset_query);
-    if (dataset_query.query) {
-        dataset_query.query = Query.cleanQuery(dataset_query.query);
-    }
-
-    var cardCopy = {
-        name: card.name,
-        description: card.description,
-        dataset_query: dataset_query,
-        display: card.display,
-        parameters: card.parameters,
-        visualization_settings: card.visualization_settings,
-        original_card_id: card.original_card_id
-    };
-
-    return utf8_to_b64url(JSON.stringify(cardCopy));
+  var dataset_query = Utils.copy(card.dataset_query);
+  if (dataset_query.query) {
+    dataset_query.query = Query.cleanQuery(dataset_query.query);
+  }
+
+  var cardCopy = {
+    name: card.name,
+    description: card.description,
+    dataset_query: dataset_query,
+    display: card.display,
+    parameters: card.parameters,
+    visualization_settings: card.visualization_settings,
+    original_card_id: card.original_card_id,
+  };
+
+  return utf8_to_b64url(JSON.stringify(cardCopy));
 }
 
 export function deserializeCardFromUrl(serialized) {
-    serialized = serialized.replace(/^#/, "");
-    return JSON.parse(b64url_to_utf8(serialized));
+  serialized = serialized.replace(/^#/, "");
+  return JSON.parse(b64url_to_utf8(serialized));
 }
 
 // escaping before base64 encoding is necessary for non-ASCII characters
 // https://developer.mozilla.org/en-US/docs/Web/API/WindowBase64/btoa
 export function utf8_to_b64(str) {
-    return window.btoa(unescape(encodeURIComponent(str)));
+  return window.btoa(unescape(encodeURIComponent(str)));
 }
 export function b64_to_utf8(b64) {
-    return decodeURIComponent(escape(window.atob(b64)));
+  return decodeURIComponent(escape(window.atob(b64)));
 }
 
 // for "URL safe" base64, replace "+" with "-" and "/" with "_" as per RFC 4648
 export function utf8_to_b64url(str) {
-    return utf8_to_b64(str).replace(/\+/g, "-").replace(/\//g, "_");
+  return utf8_to_b64(str)
+    .replace(/\+/g, "-")
+    .replace(/\//g, "_");
 }
 export function b64url_to_utf8(b64url) {
-    return b64_to_utf8(b64url.replace(/-/g, "+").replace(/_/g, "/"))
+  return b64_to_utf8(b64url.replace(/-/g, "+").replace(/_/g, "/"));
 }
 
 export function urlForCardState(state, dirty) {
-    return Urls.question(
-        state.cardId,
-        (state.serializedCard && dirty) ? state.serializedCard : ""
-    );
+  return Urls.question(
+    state.cardId,
+    state.serializedCard && dirty ? state.serializedCard : "",
+  );
 }
 
 export function cleanCopyCard(card) {
-    var cardCopy = {};
-    for (var name in card) {
-        if (name.charAt(0) !== "$") {
-            cardCopy[name] = card[name];
-        }
+  var cardCopy = {};
+  for (var name in card) {
+    if (name.charAt(0) !== "$") {
+      cardCopy[name] = card[name];
     }
-    return cardCopy;
+  }
+  return cardCopy;
 }
diff --git a/frontend/src/metabase/lib/colors.js b/frontend/src/metabase/lib/colors.js
index dc43b44b322f4b052b1b28bdbc26b6cac4fc3a7e..0f5bfc26c48c789e90c7f5116bc7ca8be9cfb89d 100644
--- a/frontend/src/metabase/lib/colors.js
+++ b/frontend/src/metabase/lib/colors.js
@@ -1,71 +1,71 @@
 // @flow
 
 type ColorName = string;
-type Color = string
+type Color = string;
 type ColorFamily = { [name: ColorName]: Color };
 
 export const normal = {
-    blue: '#509EE3',
-    green: '#9CC177',
-    purple: '#A989C5',
-    red: '#EF8C8C',
-    yellow: '#f9d45c',
-    orange: '#F1B556',
-    teal: '#A6E7F3',
-    indigo: '#7172AD',
-    gray: '#7B8797'
-}
+  blue: "#509EE3",
+  green: "#9CC177",
+  purple: "#A989C5",
+  red: "#EF8C8C",
+  yellow: "#f9d45c",
+  orange: "#F1B556",
+  teal: "#A6E7F3",
+  indigo: "#7172AD",
+  gray: "#7B8797",
+};
 
 export const saturated = {
-    blue: '#2D86D4',
-    green: '#84BB4C',
-    purple: '#885AB1',
-    red: '#ED6E6E',
-    yellow: '#F9CF48',
-}
+  blue: "#2D86D4",
+  green: "#84BB4C",
+  purple: "#885AB1",
+  red: "#ED6E6E",
+  yellow: "#F9CF48",
+};
 
 export const desaturated = {
-    blue: '#72AFE5',
-    green: '#A8C987',
-    purple: '#B8A2CC',
-    red: '#EEA5A5',
-    yellow: '#F7D97B',
-}
+  blue: "#72AFE5",
+  green: "#A8C987",
+  purple: "#B8A2CC",
+  red: "#EEA5A5",
+  yellow: "#F7D97B",
+};
 
 export const harmony = [
-    '#509ee3',
-    '#9cc177',
-    '#a989c5',
-    '#ef8c8c',
-    '#f9d45c',
-    '#F1B556',
-    '#A6E7F3',
-    '#7172AD',
-    '#7B8797',
-    '#6450e3',
-    '#55e350',
-    '#e35850',
-    '#77c183',
-    '#7d77c1',
-    '#c589b9',
-    '#bec589',
-    '#89c3c5',
-    '#c17777',
-    '#899bc5',
-    '#efce8c',
-    '#50e3ae',
-    '#be8cef',
-    '#8cefc6',
-    '#ef8cde',
-    '#b5f95c',
-    '#5cc2f9',
-    '#f95cd0',
-    '#c1a877',
-    '#f95c67',
-]
+  "#509ee3",
+  "#9cc177",
+  "#a989c5",
+  "#ef8c8c",
+  "#f9d45c",
+  "#F1B556",
+  "#A6E7F3",
+  "#7172AD",
+  "#7B8797",
+  "#6450e3",
+  "#55e350",
+  "#e35850",
+  "#77c183",
+  "#7d77c1",
+  "#c589b9",
+  "#bec589",
+  "#89c3c5",
+  "#c17777",
+  "#899bc5",
+  "#efce8c",
+  "#50e3ae",
+  "#be8cef",
+  "#8cefc6",
+  "#ef8cde",
+  "#b5f95c",
+  "#5cc2f9",
+  "#f95cd0",
+  "#c1a877",
+  "#f95c67",
+];
 
 export const getRandomColor = (family: ColorFamily): Color => {
-    // $FlowFixMe: Object.values doesn't preserve the type :-/
-    const colors: Color[] = Object.values(family)
-    return colors[Math.floor(Math.random() * colors.length)]
-}
+  // $FlowFixMe: Object.values doesn't preserve the type :-/
+  const colors: Color[] = Object.values(family);
+  return colors[Math.floor(Math.random() * colors.length)];
+};
diff --git a/frontend/src/metabase/lib/cookies.js b/frontend/src/metabase/lib/cookies.js
index eb1b777d7d06682346f5309ecd877ab108dd50ab..33a78587665ba777ceb983a105df197935faef4a 100644
--- a/frontend/src/metabase/lib/cookies.js
+++ b/frontend/src/metabase/lib/cookies.js
@@ -1,61 +1,60 @@
-
 import { clearGoogleAuthCredentials } from "metabase/lib/auth";
 
 import Cookies from "js-cookie";
 
-export const METABASE_SESSION_COOKIE = 'metabase.SESSION_ID';
-export const METABASE_SEEN_ALERT_SPLASH_COOKIE = 'metabase.SEEN_ALERT_SPLASH'
+export const METABASE_SESSION_COOKIE = "metabase.SESSION_ID";
+export const METABASE_SEEN_ALERT_SPLASH_COOKIE = "metabase.SEEN_ALERT_SPLASH";
 
 // Handles management of Metabase cookie work
 var MetabaseCookies = {
-    // set the session cookie.  if sessionId is null, clears the cookie
-    setSessionCookie: function(sessionId) {
-        const options = {
-            path: window.MetabaseRoot || '/',
-            expires: 14,
-            secure: window.location.protocol === "https:"
-        };
-
-        try {
-            if (sessionId) {
-                // set a session cookie
-                Cookies.set(METABASE_SESSION_COOKIE, sessionId, options);
-            } else {
-                sessionId = Cookies.get(METABASE_SESSION_COOKIE);
-
-                // delete the current session cookie and Google Auth creds
-                Cookies.remove(METABASE_SESSION_COOKIE);
-                clearGoogleAuthCredentials();
-
-                return sessionId;
-            }
-        } catch (e) {
-            console.error("setSessionCookie:", e);
-        }
-    },
-
-    setHasSeenAlertSplash: (hasSeen) => {
-        const options = {
-            path: window.MetabaseRoot || '/',
-            expires: 365,
-            secure: window.location.protocol === "https:"
-        };
-
-        try {
-            Cookies.set(METABASE_SEEN_ALERT_SPLASH_COOKIE, hasSeen, options);
-        } catch (e) {
-            console.error("setSeenAlertSplash:", e);
-        }
-    },
-
-    getHasSeenAlertSplash: () => {
-        try {
-            return Cookies.get(METABASE_SEEN_ALERT_SPLASH_COOKIE) || false;
-        } catch(e) {
-            console.error("getSeenAlertSplash:", e);
-            return false;
-        }
+  // set the session cookie.  if sessionId is null, clears the cookie
+  setSessionCookie: function(sessionId) {
+    const options = {
+      path: window.MetabaseRoot || "/",
+      expires: 14,
+      secure: window.location.protocol === "https:",
+    };
+
+    try {
+      if (sessionId) {
+        // set a session cookie
+        Cookies.set(METABASE_SESSION_COOKIE, sessionId, options);
+      } else {
+        sessionId = Cookies.get(METABASE_SESSION_COOKIE);
+
+        // delete the current session cookie and Google Auth creds
+        Cookies.remove(METABASE_SESSION_COOKIE);
+        clearGoogleAuthCredentials();
+
+        return sessionId;
+      }
+    } catch (e) {
+      console.error("setSessionCookie:", e);
+    }
+  },
+
+  setHasSeenAlertSplash: hasSeen => {
+    const options = {
+      path: window.MetabaseRoot || "/",
+      expires: 365,
+      secure: window.location.protocol === "https:",
+    };
+
+    try {
+      Cookies.set(METABASE_SEEN_ALERT_SPLASH_COOKIE, hasSeen, options);
+    } catch (e) {
+      console.error("setSeenAlertSplash:", e);
+    }
+  },
+
+  getHasSeenAlertSplash: () => {
+    try {
+      return Cookies.get(METABASE_SEEN_ALERT_SPLASH_COOKIE) || false;
+    } catch (e) {
+      console.error("getSeenAlertSplash:", e);
+      return false;
     }
-}
+  },
+};
 
 export default MetabaseCookies;
diff --git a/frontend/src/metabase/lib/core.js b/frontend/src/metabase/lib/core.js
index 29e27a008abda784cd3b861555acb71bd996733d..c17d7e32f3d632447e1e66fe2c333a88084a2745 100644
--- a/frontend/src/metabase/lib/core.js
+++ b/frontend/src/metabase/lib/core.js
@@ -1,104 +1,131 @@
 import { TYPE } from "metabase/lib/types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
-export const field_special_types = [{
-    'id': TYPE.PK,
-    'name': t`Entity Key`,
-    'section': 'Overall Row',
-    'description': t`The primary key for this table.`
-}, {
-    'id': TYPE.Name,
-    'name': t`Entity Name`,
-    'section': 'Overall Row',
-    'description': t`The "name" of each record. Usually a column called "name", "title", etc.`
-}, {
-    'id': TYPE.FK,
-    'name': t`Foreign Key`,
-    'section': 'Overall Row',
-    'description': t`Points to another table to make a connection.`
-}, {
-    'id': TYPE.AvatarURL,
-    'name': t`Avatar Image URL`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Category,
-    'name': t`Category`,
-    'section': 'Common'
-}, {
-    'id': TYPE.City,
-    'name': t`City`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Country,
-    'name': t`Country`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Description,
-    'name': t`Description`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Email,
-    'name': t`Email`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Enum,
-    'name': t`Enum`,
-    'section': 'Common'
-}, {
-    'id': TYPE.ImageURL,
-    'name': t`Image URL`,
-    'section': 'Common'
-}, {
-    'id': TYPE.SerializedJSON,
-    'name': t`Field containing JSON`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Latitude,
-    'name': t`Latitude`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Longitude,
-    'name': t`Longitude`,
-    'section': 'Common'
-}, {
-    'id': TYPE.Number,
-    'name': t`Number`,
-    'section': 'Common'
-}, {
-    'id': TYPE.State,
-    'name': t`State`,
-    'section': 'Common'
-}, {
-    'id': TYPE.UNIXTimestampSeconds,
-    'name': t`UNIX Timestamp (Seconds)`,
-    'section': 'Common'
-}, {
-    'id': TYPE.UNIXTimestampMilliseconds,
-    'name': t`UNIX Timestamp (Milliseconds)`,
-    'section': 'Common'
-}, {
-    'id': TYPE.URL,
-    'name': t`URL`,
-    'section': 'Common'
-}, {
-    'id': TYPE.ZipCode,
-    'name': t`Zip Code`,
-    'section': 'Common'
-}];
+export const field_special_types = [
+  {
+    id: TYPE.PK,
+    name: t`Entity Key`,
+    section: "Overall Row",
+    description: t`The primary key for this table.`,
+  },
+  {
+    id: TYPE.Name,
+    name: t`Entity Name`,
+    section: "Overall Row",
+    description: t`The "name" of each record. Usually a column called "name", "title", etc.`,
+  },
+  {
+    id: TYPE.FK,
+    name: t`Foreign Key`,
+    section: "Overall Row",
+    description: t`Points to another table to make a connection.`,
+  },
+  {
+    id: TYPE.AvatarURL,
+    name: t`Avatar Image URL`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Category,
+    name: t`Category`,
+    section: "Common",
+  },
+  {
+    id: TYPE.City,
+    name: t`City`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Country,
+    name: t`Country`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Description,
+    name: t`Description`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Email,
+    name: t`Email`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Enum,
+    name: t`Enum`,
+    section: "Common",
+  },
+  {
+    id: TYPE.ImageURL,
+    name: t`Image URL`,
+    section: "Common",
+  },
+  {
+    id: TYPE.SerializedJSON,
+    name: t`Field containing JSON`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Latitude,
+    name: t`Latitude`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Longitude,
+    name: t`Longitude`,
+    section: "Common",
+  },
+  {
+    id: TYPE.Number,
+    name: t`Number`,
+    section: "Common",
+  },
+  {
+    id: TYPE.State,
+    name: t`State`,
+    section: "Common",
+  },
+  {
+    id: TYPE.UNIXTimestampSeconds,
+    name: t`UNIX Timestamp (Seconds)`,
+    section: "Common",
+  },
+  {
+    id: TYPE.UNIXTimestampMilliseconds,
+    name: t`UNIX Timestamp (Milliseconds)`,
+    section: "Common",
+  },
+  {
+    id: TYPE.URL,
+    name: t`URL`,
+    section: "Common",
+  },
+  {
+    id: TYPE.ZipCode,
+    name: t`Zip Code`,
+    section: "Common",
+  },
+];
 
-export const field_special_types_map = field_special_types
-    .reduce((map, type) => Object.assign({}, map, {[type.id]: type}), {});
+export const field_special_types_map = field_special_types.reduce(
+  (map, type) => Object.assign({}, map, { [type.id]: type }),
+  {},
+);
 
-export const field_visibility_types = [{
-    'id': 'normal',
-    'name': t`Everywhere`,
-    'description': t`The default setting. This field will be displayed normally in tables and charts.`
-}, {
-    'id': 'details-only',
-    'name': t`Only in Detail Views`,
-    'description': t`This field will only be displayed when viewing the details of a single record. Use this for information that's lengthy or that isn't useful in a table or chart.`
-}, {
-    'id': 'sensitive',
-    'name': t`Do Not Include`,
-    'description': t`Metabase will never retrieve this field. Use this for sensitive or irrelevant information.`
-}];
+export const field_visibility_types = [
+  {
+    id: "normal",
+    name: t`Everywhere`,
+    description: t`The default setting. This field will be displayed normally in tables and charts.`,
+  },
+  {
+    id: "details-only",
+    name: t`Only in Detail Views`,
+    description: t`This field will only be displayed when viewing the details of a single record. Use this for information that's lengthy or that isn't useful in a table or chart.`,
+  },
+  {
+    id: "sensitive",
+    name: t`Do Not Include`,
+    description: t`Metabase will never retrieve this field. Use this for sensitive or irrelevant information.`,
+  },
+];
diff --git a/frontend/src/metabase/lib/dashboard_grid.js b/frontend/src/metabase/lib/dashboard_grid.js
index c44980020a576e7001e48857a49188b79c1e5f78..14cfff368fa72f2153fcb244ed3c5eaa6dcf11b9 100644
--- a/frontend/src/metabase/lib/dashboard_grid.js
+++ b/frontend/src/metabase/lib/dashboard_grid.js
@@ -9,65 +9,70 @@ export const GRID_MARGIN = 6;
 export const DEFAULT_CARD_SIZE = { width: 4, height: 4 };
 
 type DashCardPosition = {
-    col: number,
-    row: number,
-    sizeY: number,
-    sizeX: number
-}
+  col: number,
+  row: number,
+  sizeY: number,
+  sizeX: number,
+};
 
 // returns the first available position from left to right, top to bottom,
 // based on the existing cards,  item size, and grid width
 export function getPositionForNewDashCard(
-    cards: Array<DashCard>,
-    sizeX: number = DEFAULT_CARD_SIZE.width,
-    sizeY: number = DEFAULT_CARD_SIZE.height,
-    width: number = GRID_WIDTH
+  cards: Array<DashCard>,
+  sizeX: number = DEFAULT_CARD_SIZE.width,
+  sizeY: number = DEFAULT_CARD_SIZE.height,
+  width: number = GRID_WIDTH,
 ): DashCardPosition {
-    let row = 0;
-    let col = 0;
-    while (row < 1000) {
-        while (col <= width - sizeX) {
-            let good = true;
-            let position = { col, row, sizeX, sizeY };
-            for (let card of cards) {
-                if (intersects(card, position)) {
-                    good = false;
-                    break;
-                }
-            }
-            if (good) {
-                return position;
-            }
-            col++;
+  let row = 0;
+  let col = 0;
+  while (row < 1000) {
+    while (col <= width - sizeX) {
+      let good = true;
+      let position = { col, row, sizeX, sizeY };
+      for (let card of cards) {
+        if (intersects(card, position)) {
+          good = false;
+          break;
         }
-        col = 0;
-        row++;
+      }
+      if (good) {
+        return position;
+      }
+      col++;
     }
-    // this should never happen but flow complains if we return undefined
-    return { col, row, sizeX, sizeY };
+    col = 0;
+    row++;
+  }
+  // this should never happen but flow complains if we return undefined
+  return { col, row, sizeX, sizeY };
 }
 
 function intersects(a: DashCardPosition, b: DashCardPosition): boolean {
-    return !(
-        b.col >= a.col + a.sizeX ||
-        b.col + b.sizeX <= a.col ||
-        b.row >= a.row + a.sizeY ||
-        b.row + b.sizeY <= a.row
-    );
+  return !(
+    b.col >= a.col + a.sizeX ||
+    b.col + b.sizeX <= a.col ||
+    b.row >= a.row + a.sizeY ||
+    b.row + b.sizeY <= a.row
+  );
 }
 
 // for debugging
 /*eslint-disable */
 function printGrid(cards, width) {
-    let grid = [];
-    for (let card of cards) {
-        for (let col = card.col; col < card.col + card.sizeX; col++) {
-            for (let row = card.row; row < card.row + card.sizeY; row++) {
-                grid[row] = grid[row] || Array(width).join(".").split(".").map(() => 0);
-                grid[row][col]++;
-            }
-        }
+  let grid = [];
+  for (let card of cards) {
+    for (let col = card.col; col < card.col + card.sizeX; col++) {
+      for (let row = card.row; row < card.row + card.sizeY; row++) {
+        grid[row] =
+          grid[row] ||
+          Array(width)
+            .join(".")
+            .split(".")
+            .map(() => 0);
+        grid[row][col]++;
+      }
     }
-    console.log("\n"+grid.map(row => row.join(".")).join("\n")+"\n");
+  }
+  console.log("\n" + grid.map(row => row.join(".")).join("\n") + "\n");
 }
 /*eslint-enable */
diff --git a/frontend/src/metabase/lib/data_grid.js b/frontend/src/metabase/lib/data_grid.js
index 55895e556c7dab165b039eb01faf75280db95a4e..dbb9f541fd6d775549ac63b61ced58e709d4252b 100644
--- a/frontend/src/metabase/lib/data_grid.js
+++ b/frontend/src/metabase/lib/data_grid.js
@@ -4,96 +4,99 @@ import * as SchemaMetadata from "metabase/lib/schema_metadata";
 import { formatValue } from "metabase/lib/formatting";
 
 function compareNumbers(a, b) {
-    return a - b;
+  return a - b;
 }
 
 export function pivot(data) {
-    // find the lowest cardinality dimension and make it our "pivoted" column
-    // TODO: we assume dimensions are in the first 2 columns, which is less than ideal
-    var pivotCol = 0,
-        normalCol = 1,
-        cellCol = 2,
-        pivotColValues = distinctValues(data, pivotCol),
-        normalColValues = distinctValues(data, normalCol);
-    if (normalColValues.length <= pivotColValues.length) {
-        pivotCol = 1;
-        normalCol = 0;
-
-        var tmp = pivotColValues;
-        pivotColValues = normalColValues;
-        normalColValues = tmp;
-    }
-
-    // sort the column values sensibly
-    if (SchemaMetadata.isNumeric(data.cols[pivotCol])) {
-        pivotColValues.sort(compareNumbers);
-    } else {
-        pivotColValues.sort();
-    }
-
-    if (SchemaMetadata.isNumeric(data.cols[normalCol])) {
-        normalColValues.sort(compareNumbers);
-    } else {
-        normalColValues.sort();
-    }
-
-    // make sure that the first element in the pivoted column list is null which makes room for the label of the other column
-    pivotColValues.unshift(data.cols[normalCol].display_name);
-
-    // start with an empty grid that we'll fill with the appropriate values
-    const pivotedRows = normalColValues.map((normalColValues, index) => {
-        const row = pivotColValues.map(() => null);
-        // for onVisualizationClick:
-        row._dimension = {
-            value: normalColValues,
-            column: data.cols[normalCol]
-        };
-        return row;
-    })
-
-    // fill it up with the data
-    for (var j=0; j < data.rows.length; j++) {
-        var normalColIdx = normalColValues.lastIndexOf(data.rows[j][normalCol]);
-        var pivotColIdx = pivotColValues.lastIndexOf(data.rows[j][pivotCol]);
-
-        pivotedRows[normalColIdx][0] = data.rows[j][normalCol];
-        // NOTE: we are hard coding the expectation that the metric is in the 3rd column
-        pivotedRows[normalColIdx][pivotColIdx] = data.rows[j][2];
+  // find the lowest cardinality dimension and make it our "pivoted" column
+  // TODO: we assume dimensions are in the first 2 columns, which is less than ideal
+  var pivotCol = 0,
+    normalCol = 1,
+    cellCol = 2,
+    pivotColValues = distinctValues(data, pivotCol),
+    normalColValues = distinctValues(data, normalCol);
+  if (normalColValues.length <= pivotColValues.length) {
+    pivotCol = 1;
+    normalCol = 0;
+
+    var tmp = pivotColValues;
+    pivotColValues = normalColValues;
+    normalColValues = tmp;
+  }
+
+  // sort the column values sensibly
+  if (SchemaMetadata.isNumeric(data.cols[pivotCol])) {
+    pivotColValues.sort(compareNumbers);
+  } else {
+    pivotColValues.sort();
+  }
+
+  if (SchemaMetadata.isNumeric(data.cols[normalCol])) {
+    normalColValues.sort(compareNumbers);
+  } else {
+    normalColValues.sort();
+  }
+
+  // make sure that the first element in the pivoted column list is null which makes room for the label of the other column
+  pivotColValues.unshift(data.cols[normalCol].display_name);
+
+  // start with an empty grid that we'll fill with the appropriate values
+  const pivotedRows = normalColValues.map((normalColValues, index) => {
+    const row = pivotColValues.map(() => null);
+    // for onVisualizationClick:
+    row._dimension = {
+      value: normalColValues,
+      column: data.cols[normalCol],
+    };
+    return row;
+  });
+
+  // fill it up with the data
+  for (var j = 0; j < data.rows.length; j++) {
+    var normalColIdx = normalColValues.lastIndexOf(data.rows[j][normalCol]);
+    var pivotColIdx = pivotColValues.lastIndexOf(data.rows[j][pivotCol]);
+
+    pivotedRows[normalColIdx][0] = data.rows[j][normalCol];
+    // NOTE: we are hard coding the expectation that the metric is in the 3rd column
+    pivotedRows[normalColIdx][pivotColIdx] = data.rows[j][2];
+  }
+
+  // provide some column metadata to maintain consistency
+  const cols = pivotColValues.map(function(value, idx) {
+    if (idx === 0) {
+      // first column is always the coldef of the normal column
+      return data.cols[normalCol];
     }
 
-    // provide some column metadata to maintain consistency
-    const cols = pivotColValues.map(function(value, idx) {
-        if (idx === 0) {
-            // first column is always the coldef of the normal column
-            return data.cols[normalCol];
-        }
-
-        var colDef = _.clone(data.cols[cellCol]);
-        colDef.name = colDef.display_name = formatValue(value, { column: data.cols[pivotCol] }) || "";
-        // for onVisualizationClick:
-        colDef._dimension = {
-            value: value,
-            column: data.cols[pivotCol]
-        };
-        // delete colDef.id
-        return colDef;
-    });
-
-    return {
-        cols: cols,
-        columns: pivotColValues,
-        rows: pivotedRows
+    var colDef = _.clone(data.cols[cellCol]);
+    colDef.name = colDef.display_name =
+      formatValue(value, { column: data.cols[pivotCol] }) || "";
+    // for onVisualizationClick:
+    colDef._dimension = {
+      value: value,
+      column: data.cols[pivotCol],
     };
+    // delete colDef.id
+    return colDef;
+  });
+
+  return {
+    cols: cols,
+    columns: pivotColValues,
+    rows: pivotedRows,
+  };
 }
 
 export function distinctValues(data, colIdx) {
-    var vals = data.rows.map(function(r) {
-        return r[colIdx];
-    });
+  var vals = data.rows.map(function(r) {
+    return r[colIdx];
+  });
 
-    return vals.filter(function(v, i) { return i==vals.lastIndexOf(v); });
+  return vals.filter(function(v, i) {
+    return i == vals.lastIndexOf(v);
+  });
 }
 
 export function cardinality(data, colIdx) {
-    return distinctValues(data, colIdx).length;
+  return distinctValues(data, colIdx).length;
 }
diff --git a/frontend/src/metabase/lib/dataset.js b/frontend/src/metabase/lib/dataset.js
index 41faf1fcb145df73993e2849e6e308c92352539b..02e819c304123f3eef00b43b22a83ac45869e8e2 100644
--- a/frontend/src/metabase/lib/dataset.js
+++ b/frontend/src/metabase/lib/dataset.js
@@ -4,16 +4,16 @@ import type { Value, Column, DatasetData } from "metabase/meta/types/Dataset";
 
 // Many aggregations result in [[null]] if there are no rows to aggregate after filters
 export const datasetContainsNoResults = (data: DatasetData): boolean =>
-    data.rows.length === 0 || _.isEqual(data.rows, [[null]]);
+  data.rows.length === 0 || _.isEqual(data.rows, [[null]]);
 
 /**
  * @returns min and max for a value in a column
  */
 export const rangeForValue = (
-    value: Value,
-    column: Column
+  value: Value,
+  column: Column,
 ): ?[number, number] => {
-    if (column && column.binning_info && column.binning_info.bin_width) {
-        return [value, value + column.binning_info.bin_width];
-    }
+  if (column && column.binning_info && column.binning_info.bin_width) {
+    return [value, value + column.binning_info.bin_width];
+  }
 };
diff --git a/frontend/src/metabase/lib/debug.js b/frontend/src/metabase/lib/debug.js
index c2feac194121fd462a11e90a8ee4906b59f34741..fcacf91b693abb3d8b9f1d07f4e218c7071231ef 100644
--- a/frontend/src/metabase/lib/debug.js
+++ b/frontend/src/metabase/lib/debug.js
@@ -1,11 +1,12 @@
 let debug;
-if (typeof window === "object" && (
-    (window.location && window.location.hash === "#debug") ||
-    (window.localStorage && window.localStorage.getItem("debug"))
-)) {
-    debug = true;
+if (
+  typeof window === "object" &&
+  ((window.location && window.location.hash === "#debug") ||
+    (window.localStorage && window.localStorage.getItem("debug")))
+) {
+  debug = true;
 } else {
-    debug = false;
+  debug = false;
 }
 
 export const DEBUG = debug;
diff --git a/frontend/src/metabase/lib/dom.js b/frontend/src/metabase/lib/dom.js
index 8ff4c73b2607438aa58ce70654fc3e7a544c8bf2..e93b2c238a332ec64517e45d45a58cc3478b9b88 100644
--- a/frontend/src/metabase/lib/dom.js
+++ b/frontend/src/metabase/lib/dom.js
@@ -1,15 +1,16 @@
-
 // IE doesn't support scrollX/scrollY:
-export const getScrollX = () => typeof window.scrollX === "undefined" ? window.pageXOffset : window.scrollX;
-export const getScrollY = () => typeof window.scrollY === "undefined" ? window.pageYOffset : window.scrollY;
+export const getScrollX = () =>
+  typeof window.scrollX === "undefined" ? window.pageXOffset : window.scrollX;
+export const getScrollY = () =>
+  typeof window.scrollY === "undefined" ? window.pageYOffset : window.scrollY;
 
 // denotes whether the current page is loaded in an iframe or not
 export const IFRAMED = (function() {
-    try {
-        return window.self !== window.top;
-    } catch (e) {
-        return true;
-    }
+  try {
+    return window.self !== window.top;
+  } catch (e) {
+    return true;
+  }
 })();
 
 // add a global so we can check if the parent iframe is Metabase
@@ -18,226 +19,238 @@ window.METABASE = true;
 // check that we're both iframed, and the parent is a Metabase instance
 // used for detecting if we're previewing an embed
 export const IFRAMED_IN_SELF = (function() {
-    try {
-        return window.self !== window.top && window.top.METABASE;
-    } catch (e) {
-        return false;
-    }
+  try {
+    return window.self !== window.top && window.top.METABASE;
+  } catch (e) {
+    return false;
+  }
 })();
 
 export function isObscured(element, offset) {
-    // default to the center of the element
-    offset = offset || {
-        top: Math.round(element.offsetHeight / 2),
-        left: Math.round(element.offsetWidth / 2)
-    };
-    let position = findPosition(element, true);
-    let elem = document.elementFromPoint(position.left + offset.left, position.top + offset.top);
-    return !element.contains(elem)
+  if (!document.elementFromPoint) {
+    return false;
+  }
+  // default to the center of the element
+  offset = offset || {
+    top: Math.round(element.offsetHeight / 2),
+    left: Math.round(element.offsetWidth / 2),
+  };
+  let position = findPosition(element, true);
+  let elem = document.elementFromPoint(
+    position.left + offset.left,
+    position.top + offset.top,
+  );
+  return !element.contains(elem);
 }
 
 // get the position of an element on the page
 export function findPosition(element, excludeScroll = false) {
-	var offset = { top: 0, left: 0 };
-	var scroll = { top: 0, left: 0 };
-    let offsetParent = element;
-    while (offsetParent) {
-        // we need to check every element for scrollTop/scrollLeft
-        scroll.left += element.scrollLeft || 0;
-        scroll.top += element.scrollTop || 0;
-        // but only the original element and offsetParents for offsetTop/offsetLeft
-        if (offsetParent === element) {
-    		offset.left += element.offsetLeft;
-    		offset.top += element.offsetTop;
-            offsetParent = element.offsetParent;
-        }
-        element = element.parentNode;
+  var offset = { top: 0, left: 0 };
+  var scroll = { top: 0, left: 0 };
+  let offsetParent = element;
+  while (offsetParent) {
+    // we need to check every element for scrollTop/scrollLeft
+    scroll.left += element.scrollLeft || 0;
+    scroll.top += element.scrollTop || 0;
+    // but only the original element and offsetParents for offsetTop/offsetLeft
+    if (offsetParent === element) {
+      offset.left += element.offsetLeft;
+      offset.top += element.offsetTop;
+      offsetParent = element.offsetParent;
     }
-    if (excludeScroll) {
-        offset.left -= scroll.left;
-        offset.top -= scroll.top;
-    }
-    return offset;
+    element = element.parentNode;
+  }
+  if (excludeScroll) {
+    offset.left -= scroll.left;
+    offset.top -= scroll.top;
+  }
+  return offset;
 }
 
 // based on http://stackoverflow.com/a/38039019/113
 export function elementIsInView(element, percentX = 1, percentY = 1) {
-    const tolerance = 0.01;   //needed because the rects returned by getBoundingClientRect provide the position up to 10 decimals
+  const tolerance = 0.01; //needed because the rects returned by getBoundingClientRect provide the position up to 10 decimals
 
-    const elementRect = element.getBoundingClientRect();
-    const parentRects = [];
+  const elementRect = element.getBoundingClientRect();
+  const parentRects = [];
 
-    while (element.parentElement != null) {
-        parentRects.push(element.parentElement.getBoundingClientRect());
-        element = element.parentElement;
-    }
+  while (element.parentElement != null) {
+    parentRects.push(element.parentElement.getBoundingClientRect());
+    element = element.parentElement;
+  }
 
-    return parentRects.every((parentRect) => {
-        const visiblePixelX = Math.min(elementRect.right, parentRect.right) - Math.max(elementRect.left, parentRect.left);
-        const visiblePixelY = Math.min(elementRect.bottom, parentRect.bottom) - Math.max(elementRect.top, parentRect.top);
-        const visiblePercentageX = visiblePixelX / elementRect.width;
-        const visiblePercentageY = visiblePixelY / elementRect.height;
-        return visiblePercentageX + tolerance > percentX && visiblePercentageY + tolerance > percentY;
-    });
+  return parentRects.every(parentRect => {
+    const visiblePixelX =
+      Math.min(elementRect.right, parentRect.right) -
+      Math.max(elementRect.left, parentRect.left);
+    const visiblePixelY =
+      Math.min(elementRect.bottom, parentRect.bottom) -
+      Math.max(elementRect.top, parentRect.top);
+    const visiblePercentageX = visiblePixelX / elementRect.width;
+    const visiblePercentageY = visiblePixelY / elementRect.height;
+    return (
+      visiblePercentageX + tolerance > percentX &&
+      visiblePercentageY + tolerance > percentY
+    );
+  });
 }
 
 export function getSelectionPosition(element) {
-    // input, textarea, IE
-    if (element.setSelectionRange || element.createTextRange) {
-        return [element.selectionStart, element.selectionEnd];
-    }
+  // input, textarea, IE
+  if (element.setSelectionRange || element.createTextRange) {
+    return [element.selectionStart, element.selectionEnd];
+  } else {
     // contenteditable
-    else {
-        try {
-            const selection = window.getSelection();
-            // Clone the Range otherwise setStart/setEnd will mutate the actual selection in Chrome 58+ and Firefox!
-            const range = selection.getRangeAt(0).cloneRange();
-            const { startContainer, startOffset } = range;
-            range.setStart(element, 0);
-            const end = range.toString().length;
-            range.setEnd(startContainer, startOffset);
-            const start = range.toString().length;
-
-            return [start, end];
-        } catch (e) {
-            return [0, 0];
-        }
+    try {
+      const selection = window.getSelection();
+      // Clone the Range otherwise setStart/setEnd will mutate the actual selection in Chrome 58+ and Firefox!
+      const range = selection.getRangeAt(0).cloneRange();
+      const { startContainer, startOffset } = range;
+      range.setStart(element, 0);
+      const end = range.toString().length;
+      range.setEnd(startContainer, startOffset);
+      const start = range.toString().length;
+
+      return [start, end];
+    } catch (e) {
+      return [0, 0];
     }
+  }
 }
 
 export function setSelectionPosition(element, [start, end]) {
-    // input, textarea
-    if (element.setSelectionRange) {
-        element.focus();
-        element.setSelectionRange(start, end);
-    }
+  // input, textarea
+  if (element.setSelectionRange) {
+    element.focus();
+    element.setSelectionRange(start, end);
+  } else if (element.createTextRange) {
     // IE
-    else if (element.createTextRange) {
-        const range = element.createTextRange();
-        range.collapse(true);
-        range.moveEnd("character", end);
-        range.moveStart("character", start);
-        range.select();
-    }
+    const range = element.createTextRange();
+    range.collapse(true);
+    range.moveEnd("character", end);
+    range.moveStart("character", start);
+    range.select();
+  } else {
     // contenteditable
-    else {
-        const selection = window.getSelection();
-        const startPos = getTextNodeAtPosition(element, start);
-        const endPos = getTextNodeAtPosition(element, end);
-        selection.removeAllRanges();
-        const range = new Range();
-        range.setStart(startPos.node, startPos.position);
-        range.setEnd(endPos.node, endPos.position);
-        selection.addRange(range);
-    }
+    const selection = window.getSelection();
+    const startPos = getTextNodeAtPosition(element, start);
+    const endPos = getTextNodeAtPosition(element, end);
+    selection.removeAllRanges();
+    const range = new Range();
+    range.setStart(startPos.node, startPos.position);
+    range.setEnd(endPos.node, endPos.position);
+    selection.addRange(range);
+  }
 }
 
 export function saveSelection(element) {
-    let range = getSelectionPosition(element);
-    return () => setSelectionPosition(element, range);
+  let range = getSelectionPosition(element);
+  return () => setSelectionPosition(element, range);
 }
 
 export function getCaretPosition(element) {
-    return getSelectionPosition(element)[1];
+  return getSelectionPosition(element)[1];
 }
 
 export function setCaretPosition(element, position) {
-    setSelectionPosition(element, [position, position]);
+  setSelectionPosition(element, [position, position]);
 }
 
 export function saveCaretPosition(element) {
-    let position = getCaretPosition(element);
-    return () => setCaretPosition(element, position);
+  let position = getCaretPosition(element);
+  return () => setCaretPosition(element, position);
 }
 
 function getTextNodeAtPosition(root, index) {
-    let treeWalker = document.createTreeWalker(root, NodeFilter.SHOW_TEXT, (elem) => {
-        if (index > elem.textContent.length){
-            index -= elem.textContent.length;
-            return NodeFilter.FILTER_REJECT
-        }
-        return NodeFilter.FILTER_ACCEPT;
-    });
-    var c = treeWalker.nextNode();
-    return {
-        node: c ? c : root,
-        position: c ? index :  0
-    };
+  let treeWalker = document.createTreeWalker(
+    root,
+    NodeFilter.SHOW_TEXT,
+    elem => {
+      if (index > elem.textContent.length) {
+        index -= elem.textContent.length;
+        return NodeFilter.FILTER_REJECT;
+      }
+      return NodeFilter.FILTER_ACCEPT;
+    },
+  );
+  var c = treeWalker.nextNode();
+  return {
+    node: c ? c : root,
+    position: c ? index : 0,
+  };
 }
 
 // https://davidwalsh.name/add-rules-stylesheets
 var STYLE_SHEET = (function() {
-    // Create the <style> tag
-    var style = document.createElement("style");
+  // Create the <style> tag
+  var style = document.createElement("style");
 
-    // WebKit hack :(
-    style.appendChild(document.createTextNode("/* dynamic stylesheet */"));
+  // WebKit hack :(
+  style.appendChild(document.createTextNode("/* dynamic stylesheet */"));
 
-    // Add the <style> element to the page
-    document.head.appendChild(style);
+  // Add the <style> element to the page
+  document.head.appendChild(style);
 
-    return style.sheet;
+  return style.sheet;
 })();
 
 export function addCSSRule(selector, rules, index = 0) {
-    if("insertRule" in STYLE_SHEET) {
-        STYLE_SHEET.insertRule(selector + "{" + rules + "}", index);
-    }
-    else if("addRule" in STYLE_SHEET) {
-        STYLE_SHEET.addRule(selector, rules, index);
-    }
+  if ("insertRule" in STYLE_SHEET) {
+    STYLE_SHEET.insertRule(selector + "{" + rules + "}", index);
+  } else if ("addRule" in STYLE_SHEET) {
+    STYLE_SHEET.addRule(selector, rules, index);
+  }
 }
 
 export function constrainToScreen(element, direction, padding) {
-    if (direction === "bottom") {
-        let screenBottom = window.innerHeight + getScrollY();
-        let overflowY = element.getBoundingClientRect().bottom - screenBottom;
-        if (overflowY + padding > 0) {
-            element.style.maxHeight = (element.getBoundingClientRect().height - overflowY - padding) + "px";
-            return true;
-        }
-    } else if (direction === "top") {
-        let screenTop = getScrollY();
-        let overflowY = screenTop - element.getBoundingClientRect().top;
-        if (overflowY + padding > 0) {
-            element.style.maxHeight = (element.getBoundingClientRect().height - overflowY - padding) + "px";
-            return true;
-        }
-    } else {
-        throw new Error("Direction " + direction + " not implemented");
+  if (direction === "bottom") {
+    let screenBottom = window.innerHeight + getScrollY();
+    let overflowY = element.getBoundingClientRect().bottom - screenBottom;
+    if (overflowY + padding > 0) {
+      element.style.maxHeight =
+        element.getBoundingClientRect().height - overflowY - padding + "px";
+      return true;
     }
-    return false;
+  } else if (direction === "top") {
+    let screenTop = getScrollY();
+    let overflowY = screenTop - element.getBoundingClientRect().top;
+    if (overflowY + padding > 0) {
+      element.style.maxHeight =
+        element.getBoundingClientRect().height - overflowY - padding + "px";
+      return true;
+    }
+  } else {
+    throw new Error("Direction " + direction + " not implemented");
+  }
+  return false;
 }
 
 // Used for tackling Safari rendering issues
 // http://stackoverflow.com/a/3485654
 export function forceRedraw(domNode) {
-    domNode.style.display='none';
-    domNode.offsetHeight;
-    domNode.style.display='';
+  domNode.style.display = "none";
+  domNode.offsetHeight;
+  domNode.style.display = "";
 }
 
 export function moveToBack(element) {
-    if (element && element.parentNode) {
-        element.parentNode.insertBefore(
-            element,
-            element.parentNode.firstChild
-        );
-    }
+  if (element && element.parentNode) {
+    element.parentNode.insertBefore(element, element.parentNode.firstChild);
+  }
 }
 
 export function moveToFront(element) {
-    if (element && element.parentNode) {
-        element.parentNode.appendChild(element);
-    }
+  if (element && element.parentNode) {
+    element.parentNode.appendChild(element);
+  }
 }
 
 /**
  * @returns the clip-path CSS property referencing the clip path in the current document, taking into account the <base> tag.
  */
 export function clipPathReference(id: string): string {
-    // add the current page URL (with fragment removed) to support pages with <base> tag.
-    // https://stackoverflow.com/questions/18259032/using-base-tag-on-a-page-that-contains-svg-marker-elements-fails-to-render-marke
-    const url = window.location.href.replace(/#.*$/, "") + "#" + id;
-    return `url(${url})`;
+  // add the current page URL (with fragment removed) to support pages with <base> tag.
+  // https://stackoverflow.com/questions/18259032/using-base-tag-on-a-page-that-contains-svg-marker-elements-fails-to-render-marke
+  const url = window.location.href.replace(/#.*$/, "") + "#" + id;
+  return `url(${url})`;
 }
diff --git a/frontend/src/metabase/lib/emoji.js b/frontend/src/metabase/lib/emoji.js
index ab4be4694677734c1943de1f57b173ad91fd5b0f..454e09b09c9d88189c1c5c3739dc327975100eca 100644
--- a/frontend/src/metabase/lib/emoji.js
+++ b/frontend/src/metabase/lib/emoji.js
@@ -1,13 +1,13 @@
-import EMOJI from "./emoji.json"
+import EMOJI from "./emoji.json";
 
 export const emoji = {};
 export const categories = EMOJI.categories;
 
 for (let shortcode in EMOJI.emoji) {
-    let e = EMOJI.emoji[shortcode];
-    emoji[shortcode] = {
-        codepoint:  e,
-        str:        String.fromCodePoint(e),
-        react:      String.fromCodePoint(e)
-    };
+  let e = EMOJI.emoji[shortcode];
+  emoji[shortcode] = {
+    codepoint: e,
+    str: String.fromCodePoint(e),
+    react: String.fromCodePoint(e),
+  };
 }
diff --git a/frontend/src/metabase/lib/engine.js b/frontend/src/metabase/lib/engine.js
index 9d42c15783c1f8ba6e8df229aca470a0dcd6287c..332681bf37a687c1c54560a26de41e029454806d 100644
--- a/frontend/src/metabase/lib/engine.js
+++ b/frontend/src/metabase/lib/engine.js
@@ -1,53 +1,65 @@
-
 export function getEngineNativeType(engine) {
-    switch (engine) {
-        case "mongo":
-        case "druid":
-        case "googleanalytics":
-            return "json";
-        default:
-            return "sql";
-    }
+  switch (engine) {
+    case "mongo":
+    case "druid":
+    case "googleanalytics":
+      return "json";
+    default:
+      return "sql";
+  }
 }
 
 export function getEngineNativeAceMode(engine) {
-    switch (engine) {
-        case "mongo":
-        case "druid":
-        case "googleanalytics":
-            return "ace/mode/json";
-        case "mysql":
-            return "ace/mode/mysql";
-        case "postgres":
-            return "ace/mode/pgsql";
-        case "sqlserver":
-            return "ace/mode/sqlserver";
-        default:
-            return "ace/mode/sql";
-    }
+  switch (engine) {
+    case "mongo":
+    case "druid":
+    case "googleanalytics":
+      return "ace/mode/json";
+    case "mysql":
+      return "ace/mode/mysql";
+    case "postgres":
+      return "ace/mode/pgsql";
+    case "sqlserver":
+      return "ace/mode/sqlserver";
+    default:
+      return "ace/mode/sql";
+  }
 }
 
 export function getEngineNativeRequiresTable(engine) {
-    return engine === "mongo";
+  return engine === "mongo";
 }
 
 export function formatJsonQuery(query, engine) {
-    if (engine === "googleanalytics") {
-        return formatGAQuery(query);
-    } else {
-        return JSON.stringify(query);
-    }
+  if (engine === "googleanalytics") {
+    return formatGAQuery(query);
+  } else {
+    return JSON.stringify(query);
+  }
 }
 
-const GA_ORDERED_PARAMS = ["ids", "start-date", "end-date", "metrics", "dimensions", "sort", "filters", "segment", "samplingLevel", "include-empty-rows", "start-index", "max-results"];
+const GA_ORDERED_PARAMS = [
+  "ids",
+  "start-date",
+  "end-date",
+  "metrics",
+  "dimensions",
+  "sort",
+  "filters",
+  "segment",
+  "samplingLevel",
+  "include-empty-rows",
+  "start-index",
+  "max-results",
+];
 
 // does 3 things: removes null values, sorts the keys by the order in the documentation, and formats with 2 space indents
 function formatGAQuery(query) {
-    const object = {};
-    for (const param of GA_ORDERED_PARAMS) {
-        if (query[param] != null) {
-            object[param] = query[param];
-        }
+  const object = {};
+  for (const param of GA_ORDERED_PARAMS) {
+    if (query[param] != null) {
+      object[param] = query[param];
     }
-    return JSON.stringify(object, null, 2);
+  }
+  return JSON.stringify(object, null, 2);
 }
diff --git a/frontend/src/metabase/lib/expressions/config.js b/frontend/src/metabase/lib/expressions/config.js
index 3d90af26351eb4d50bac4961da7174b9b21c653c..a84b5333aa43520509df0be60879eec3605b6656 100644
--- a/frontend/src/metabase/lib/expressions/config.js
+++ b/frontend/src/metabase/lib/expressions/config.js
@@ -1,23 +1,28 @@
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
-export const VALID_OPERATORS = new Set([
-    '+',
-    '-',
-    '*',
-    '/'
-]);
+export const VALID_OPERATORS = new Set(["+", "-", "*", "/"]);
 
-export const VALID_AGGREGATIONS = new Map(Object.entries({
-    "count": t`Count`,
-    "cum_count": t`CumulativeCount`,
-    "sum": t`Sum`,
-    "cum_sum": t`CumulativeSum`,
-    "distinct": t`Distinct`,
-    "stddev": t`StandardDeviation`,
-    "avg": t`Average`,
-    "min": t`Min`,
-    "max": t`Max`
-}));
+export const VALID_AGGREGATIONS = new Map(
+  Object.entries({
+    count: t`Count`,
+    cum_count: t`CumulativeCount`,
+    sum: t`Sum`,
+    cum_sum: t`CumulativeSum`,
+    distinct: t`Distinct`,
+    stddev: t`StandardDeviation`,
+    avg: t`Average`,
+    min: t`Min`,
+    max: t`Max`,
+  }),
+);
 
 export const NULLARY_AGGREGATIONS = ["count", "cum_count"];
-export const UNARY_AGGREGATIONS = ["sum", "cum_sum", "distinct", "stddev", "avg", "min", "max"];
+export const UNARY_AGGREGATIONS = [
+  "sum",
+  "cum_sum",
+  "distinct",
+  "stddev",
+  "avg",
+  "min",
+  "max",
+];
diff --git a/frontend/src/metabase/lib/expressions/formatter.js b/frontend/src/metabase/lib/expressions/formatter.js
index c089d8cd1d1a3b32d0194417f591897fee9b8847..c98b80aab1a61a0091fb2d799f7478a1fceb530f 100644
--- a/frontend/src/metabase/lib/expressions/formatter.js
+++ b/frontend/src/metabase/lib/expressions/formatter.js
@@ -1,76 +1,90 @@
-
 import _ from "underscore";
 
 import {
-    VALID_OPERATORS, VALID_AGGREGATIONS,
-    isField, isMath, isMetric, isAggregation, isExpressionReference,
-    formatMetricName, formatFieldName, formatExpressionName
+  VALID_OPERATORS,
+  VALID_AGGREGATIONS,
+  isField,
+  isMath,
+  isMetric,
+  isAggregation,
+  isExpressionReference,
+  formatMetricName,
+  formatFieldName,
+  formatExpressionName,
 } from "../expressions";
 
 // convert a MBQL expression back into an expression string
-export function format(expr, {
+export function format(
+  expr,
+  {
     tableMetadata = {},
     customFields = {},
     operators = VALID_OPERATORS,
-    aggregations = VALID_AGGREGATIONS
-}, parens = false) {
-    const info = { tableMetadata, customFields, operators, aggregations };
-    if (expr == null || _.isEqual(expr, [])) {
-        return "";
-    }
-    if (typeof expr === "number") {
-        return formatLiteral(expr);
-    }
-    if (isField(expr)) {
-        return formatField(expr, info);
-    }
-    if (isMetric(expr)) {
-        return formatMetric(expr, info);
-    }
-    if (isMath(expr)) {
-        return formatMath(expr, info, parens);
-    }
-    if (isAggregation(expr)) {
-        return formatAggregation(expr, info);
-    }
-    if (isExpressionReference(expr)) {
-        return formatExpressionReference(expr, info);
-    }
-    throw new Error("Unknown expression " + JSON.stringify(expr));
+    aggregations = VALID_AGGREGATIONS,
+  },
+  parens = false,
+) {
+  const info = { tableMetadata, customFields, operators, aggregations };
+  if (expr == null || _.isEqual(expr, [])) {
+    return "";
+  }
+  if (typeof expr === "number") {
+    return formatLiteral(expr);
+  }
+  if (isField(expr)) {
+    return formatField(expr, info);
+  }
+  if (isMetric(expr)) {
+    return formatMetric(expr, info);
+  }
+  if (isMath(expr)) {
+    return formatMath(expr, info, parens);
+  }
+  if (isAggregation(expr)) {
+    return formatAggregation(expr, info);
+  }
+  if (isExpressionReference(expr)) {
+    return formatExpressionReference(expr, info);
+  }
+  throw new Error("Unknown expression " + JSON.stringify(expr));
 }
 
 function formatLiteral(expr) {
-    return JSON.stringify(expr);
+  return JSON.stringify(expr);
 }
 
 function formatField([, fieldId], { tableMetadata: { fields } }) {
-    const field = _.findWhere(fields, { id: fieldId });
-    if (!field) {
-        throw 'field with ID does not exist: ' + fieldId;
-    }
-    return formatFieldName(field);
+  const field = _.findWhere(fields, { id: fieldId });
+  if (!field) {
+    throw "field with ID does not exist: " + fieldId;
+  }
+  return formatFieldName(field);
 }
 
 function formatMetric([, metricId], { tableMetadata: { metrics } }) {
-    const metric = _.findWhere(metrics, { id: metricId });
-    if (!metric) {
-        throw 'metric with ID does not exist: ' + metricId;
-    }
-    return formatMetricName(metric);
+  const metric = _.findWhere(metrics, { id: metricId });
+  if (!metric) {
+    throw "metric with ID does not exist: " + metricId;
+  }
+  return formatMetricName(metric);
 }
 
 function formatExpressionReference([, expressionName]) {
-    return formatExpressionName(expressionName);
+  return formatExpressionName(expressionName);
 }
 
 function formatMath([operator, ...args], info, parens) {
-    let formatted = args.map(arg => format(arg, info, true)).join(` ${operator} `)
-    return parens ? `(${formatted})` : formatted;
+  let formatted = args
+    .map(arg => format(arg, info, true))
+    .join(` ${operator} `);
+  return parens ? `(${formatted})` : formatted;
 }
 
 function formatAggregation([aggregation, ...args], info) {
-    const { aggregations } = info;
-    return args.length === 0 ?
-        aggregations.get(aggregation) :
-        `${aggregations.get(aggregation)}(${args.map(arg => format(arg, info)).join(", ")})`;
+  const { aggregations } = info;
+  return args.length === 0
+    ? aggregations.get(aggregation)
+    : `${aggregations.get(aggregation)}(${args
+        .map(arg => format(arg, info))
+        .join(", ")})`;
 }
diff --git a/frontend/src/metabase/lib/expressions/index.js b/frontend/src/metabase/lib/expressions/index.js
index f99271c4f6aed1d87701c890edb48963cbef99ba..73300e09dcce8c207be785e1bf1f8acefbcbf7b0 100644
--- a/frontend/src/metabase/lib/expressions/index.js
+++ b/frontend/src/metabase/lib/expressions/index.js
@@ -1,73 +1,103 @@
-
 import _ from "underscore";
 import { mbqlEq } from "../query/util";
 
 import { VALID_OPERATORS, VALID_AGGREGATIONS } from "./config";
 export { VALID_OPERATORS, VALID_AGGREGATIONS } from "./config";
 
-const AGG_NAMES_MAP = new Map(Array.from(VALID_AGGREGATIONS).map(([short, displayName]) =>
+const AGG_NAMES_MAP = new Map(
+  Array.from(VALID_AGGREGATIONS).map(([short, displayName]) =>
     // case-insensitive
-    [displayName.toLowerCase(), short]
-));
+    [displayName.toLowerCase(), short],
+  ),
+);
 
 export function getAggregationFromName(name) {
-    // case-insensitive
-    return AGG_NAMES_MAP.get(name.toLowerCase());
+  // case-insensitive
+  return AGG_NAMES_MAP.get(name.toLowerCase());
 }
 
 export function isReservedWord(word) {
-    return !!getAggregationFromName(word);
+  return !!getAggregationFromName(word);
 }
 
 export function formatAggregationName(aggregationOption) {
-    return VALID_AGGREGATIONS.get(aggregationOption.short);
+  return VALID_AGGREGATIONS.get(aggregationOption.short);
 }
 
 function formatIdentifier(name) {
-    return /^\w+$/.test(name) && !isReservedWord(name) ?
-        name :
-        JSON.stringify(name);
+  return /^\w+$/.test(name) && !isReservedWord(name)
+    ? name
+    : JSON.stringify(name);
 }
 
 export function formatMetricName(metric) {
-    return formatIdentifier(metric.name);
+  return formatIdentifier(metric.name);
 }
 
 export function formatFieldName(field) {
-    return formatIdentifier(field.display_name);
+  return formatIdentifier(field.display_name);
 }
 
 export function formatExpressionName(name) {
-    return formatIdentifier(name);
+  return formatIdentifier(name);
 }
 
 // move to query lib
 
 export function isExpression(expr) {
-    return isMath(expr) || isAggregation(expr) || isField(expr) || isMetric(expr) || isExpressionReference(expr);
+  return (
+    isMath(expr) ||
+    isAggregation(expr) ||
+    isField(expr) ||
+    isMetric(expr) ||
+    isExpressionReference(expr)
+  );
 }
 
 export function isField(expr) {
-    return Array.isArray(expr) && expr.length === 2 && mbqlEq(expr[0], 'field-id') && typeof expr[1] === 'number';
+  return (
+    Array.isArray(expr) &&
+    expr.length === 2 &&
+    mbqlEq(expr[0], "field-id") &&
+    typeof expr[1] === "number"
+  );
 }
 
 export function isMetric(expr) {
-    // case sensitive, unlike most mbql
-    return Array.isArray(expr) && expr.length === 2 && expr[0] === "METRIC" && typeof expr[1] === 'number';
+  // case sensitive, unlike most mbql
+  return (
+    Array.isArray(expr) &&
+    expr.length === 2 &&
+    expr[0] === "METRIC" &&
+    typeof expr[1] === "number"
+  );
 }
 
 export function isMath(expr) {
-    return Array.isArray(expr) && VALID_OPERATORS.has(expr[0]) && _.all(expr.slice(1), isValidArg);
+  return (
+    Array.isArray(expr) &&
+    VALID_OPERATORS.has(expr[0]) &&
+    _.all(expr.slice(1), isValidArg)
+  );
 }
 
 export function isAggregation(expr) {
-    return Array.isArray(expr) && VALID_AGGREGATIONS.has(expr[0]) && _.all(expr.slice(1), isValidArg);
+  return (
+    Array.isArray(expr) &&
+    VALID_AGGREGATIONS.has(expr[0]) &&
+    _.all(expr.slice(1), isValidArg)
+  );
 }
 
 export function isExpressionReference(expr) {
-    return Array.isArray(expr) && expr.length === 2 && mbqlEq(expr[0], 'expression') && typeof expr[1] === 'string';
+  return (
+    Array.isArray(expr) &&
+    expr.length === 2 &&
+    mbqlEq(expr[0], "expression") &&
+    typeof expr[1] === "string"
+  );
 }
 
 export function isValidArg(arg) {
-    return isExpression(arg) || isField(arg) || typeof arg === 'number';
+  return isExpression(arg) || isField(arg) || typeof arg === "number";
 }
diff --git a/frontend/src/metabase/lib/expressions/parser.js b/frontend/src/metabase/lib/expressions/parser.js
index 2c9d57b21e678b94bc48bb6ff87e9d4cc740ea14..2cd918ad3fe0d140095542e2a620388a032132d5 100644
--- a/frontend/src/metabase/lib/expressions/parser.js
+++ b/frontend/src/metabase/lib/expressions/parser.js
@@ -1,472 +1,563 @@
 import { Lexer, Parser, getImage } from "chevrotain";
 
 import _ from "underscore";
-import { t } from 'c-3po';
-import { formatFieldName, formatExpressionName, formatAggregationName, getAggregationFromName } from "../expressions";
+import { t } from "c-3po";
+import {
+  formatFieldName,
+  formatExpressionName,
+  formatAggregationName,
+  getAggregationFromName,
+} from "../expressions";
 import { isNumeric } from "metabase/lib/schema_metadata";
 
 import {
-    allTokens,
-    LParen, RParen,
-    AdditiveOperator, MultiplicativeOperator,
-    Aggregation, NullaryAggregation, UnaryAggregation,
-    StringLiteral, NumberLiteral, Minus,
-    Identifier
+  allTokens,
+  LParen,
+  RParen,
+  AdditiveOperator,
+  MultiplicativeOperator,
+  Aggregation,
+  NullaryAggregation,
+  UnaryAggregation,
+  StringLiteral,
+  NumberLiteral,
+  Minus,
+  Identifier,
 } from "./tokens";
 
 const ExpressionsLexer = new Lexer(allTokens);
 
 class ExpressionsParser extends Parser {
-    constructor(input, options = {}) {
-        const parserOptions = {
-            // recoveryEnabled: false,
-            ignoredIssues: {
-                // uses GATE to disambiguate fieldName and metricName
-                atomicExpression: { OR1: true }
-            }
-        };
-        super(input, allTokens, parserOptions);
-
-        let $ = this;
-
-        this._options = options;
-
-        // an expression without aggregations in it
-        $.RULE("expression", function (outsideAggregation = false) {
-            return $.SUBRULE($.additionExpression, [outsideAggregation])
-        });
-
-        // an expression with aggregations in it
-        $.RULE("aggregation", function () {
-            return $.SUBRULE($.additionExpression, [true])
-        });
-
-        // Lowest precedence thus it is first in the rule chain
-        // The precedence of binary expressions is determined by
-        // how far down the Parse Tree the binary expression appears.
-        $.RULE("additionExpression", (outsideAggregation) => {
-            let initial = $.SUBRULE($.multiplicationExpression, [outsideAggregation]);
-            let operations = $.MANY(() => {
-                const op = $.CONSUME(AdditiveOperator);
-                const rhsVal = $.SUBRULE2($.multiplicationExpression, [outsideAggregation]);
-                return [op, rhsVal];
-            });
-            return this._math(initial, operations);
-        });
-
-        $.RULE("multiplicationExpression", (outsideAggregation) => {
-            let initial = $.SUBRULE($.atomicExpression, [outsideAggregation]);
-            let operations = $.MANY(() => {
-                const op = $.CONSUME(MultiplicativeOperator);
-                const rhsVal = $.SUBRULE2($.atomicExpression, [outsideAggregation]);
-                return [op, rhsVal];
-            });
-            return this._math(initial, operations);
-        });
-
-        $.RULE("nullaryCall", () => {
-            return {
-                lParen: $.CONSUME(LParen),
-                rParen: $.CONSUME(RParen)
-            }
-        })
-        $.RULE("unaryCall", () => {
-            return {
-                lParen: $.CONSUME(LParen),
-                arg:    $.SUBRULE($.expression, [false]),
-                rParen: $.CONSUME(RParen)
-            }
-        })
-
-        $.RULE("aggregationExpression", (outsideAggregation) => {
-            const { aggregation, lParen, arg, rParen } = $.OR([
-                {ALT: () => ({
-                    aggregation: $.CONSUME(NullaryAggregation),
-                    ...$.OPTION(() => $.SUBRULE($.nullaryCall))
-                })},
-                {ALT: () => ({
-                    aggregation: $.CONSUME(UnaryAggregation),
-                    ...$.SUBRULE($.unaryCall)
-                })}
-            ]);
-            return this._aggregation(aggregation, lParen, arg, rParen);
-        });
-
-        $.RULE("metricExpression", () => {
-            const metricName = $.OR([
-                {ALT: () => $.SUBRULE($.stringLiteral) },
-                {ALT: () => $.SUBRULE($.identifier) }
-            ]);
-
-            const metric = this.getMetricForName(this._toString(metricName));
-            if (metric != null) {
-                return this._metricReference(metricName, metric.id);
-            }
-            return this._unknownMetric(metricName);
-        });
-
-        $.RULE("fieldExpression", () => {
-            const fieldName = $.OR([
-                {ALT: () => $.SUBRULE($.stringLiteral) },
-                {ALT: () => $.SUBRULE($.identifier) }
-            ]);
-
-            const field = this.getFieldForName(this._toString(fieldName));
-            if (field != null) {
-                return this._fieldReference(fieldName, field.id);
-            }
-            const expression = this.getExpressionForName(this._toString(fieldName));
-            if (expression != null) {
-                return this._expressionReference(fieldName, expression);
-            }
-            return this._unknownField(fieldName);
-        });
-
-        $.RULE("identifier", () => {
-            const identifier = $.CONSUME(Identifier);
-            return this._identifier(identifier);
-        })
-
-        $.RULE("stringLiteral", () => {
-            const stringLiteral = $.CONSUME(StringLiteral);
-            return this._stringLiteral(stringLiteral);
-        })
-
-        $.RULE("numberLiteral", () => {
-            const minus = $.OPTION(() => $.CONSUME(Minus));
-            const numberLiteral = $.CONSUME(NumberLiteral);
-            return this._numberLiteral(minus, numberLiteral);
-        })
-
-        $.RULE("atomicExpression", (outsideAggregation) => {
-            return $.OR([
-                // aggregations are not allowed inside other aggregations
-                {GATE: () => outsideAggregation, ALT: () => $.SUBRULE($.aggregationExpression, [false]) },
-
-                // NOTE: DISABLE METRICS
-                // {GATE: () => outsideAggregation, ALT: () => $.SUBRULE($.metricExpression) },
-
-                // fields are not allowed outside aggregations
-                {GATE: () => !outsideAggregation, ALT: () => $.SUBRULE($.fieldExpression) },
-
-                {ALT: () => $.SUBRULE($.parenthesisExpression, [outsideAggregation]) },
-                {ALT: () => $.SUBRULE($.numberLiteral) }
-            ], (outsideAggregation ? "aggregation" : "field name") + ", number, or expression");
-        });
-
-        $.RULE("parenthesisExpression", (outsideAggregation) => {
-            let lParen = $.CONSUME(LParen);
-            let expValue = $.SUBRULE($.expression, [outsideAggregation]);
-            let rParen = $.CONSUME(RParen);
-            return this._parens(lParen, expValue, rParen);
-        });
-
-        Parser.performSelfAnalysis(this);
-    }
-
-    getFieldForName(fieldName) {
-        const fields = this._options.tableMetadata && this._options.tableMetadata.fields;
-        return _.findWhere(fields, { display_name: fieldName });
-    }
-
-    getExpressionForName(expressionName) {
-        const customFields = this._options && this._options.customFields;
-        return customFields[expressionName];
-    }
-
-    getMetricForName(metricName) {
-        const metrics = this._options.tableMetadata && this._options.tableMetadata.metrics;
-        return _.find(metrics, (metric) => metric.name.toLowerCase() === metricName.toLowerCase());
-    }
+  constructor(input, options = {}) {
+    const parserOptions = {
+      // recoveryEnabled: false,
+      ignoredIssues: {
+        // uses GATE to disambiguate fieldName and metricName
+        atomicExpression: { OR1: true },
+      },
+    };
+    super(input, allTokens, parserOptions);
+
+    let $ = this;
+
+    this._options = options;
+
+    // an expression without aggregations in it
+    $.RULE("expression", function(outsideAggregation = false) {
+      return $.SUBRULE($.additionExpression, [outsideAggregation]);
+    });
+
+    // an expression with aggregations in it
+    $.RULE("aggregation", function() {
+      return $.SUBRULE($.additionExpression, [true]);
+    });
+
+    // Lowest precedence thus it is first in the rule chain
+    // The precedence of binary expressions is determined by
+    // how far down the Parse Tree the binary expression appears.
+    $.RULE("additionExpression", outsideAggregation => {
+      let initial = $.SUBRULE($.multiplicationExpression, [outsideAggregation]);
+      let operations = $.MANY(() => {
+        const op = $.CONSUME(AdditiveOperator);
+        const rhsVal = $.SUBRULE2($.multiplicationExpression, [
+          outsideAggregation,
+        ]);
+        return [op, rhsVal];
+      });
+      return this._math(initial, operations);
+    });
+
+    $.RULE("multiplicationExpression", outsideAggregation => {
+      let initial = $.SUBRULE($.atomicExpression, [outsideAggregation]);
+      let operations = $.MANY(() => {
+        const op = $.CONSUME(MultiplicativeOperator);
+        const rhsVal = $.SUBRULE2($.atomicExpression, [outsideAggregation]);
+        return [op, rhsVal];
+      });
+      return this._math(initial, operations);
+    });
+
+    $.RULE("nullaryCall", () => {
+      return {
+        lParen: $.CONSUME(LParen),
+        rParen: $.CONSUME(RParen),
+      };
+    });
+    $.RULE("unaryCall", () => {
+      return {
+        lParen: $.CONSUME(LParen),
+        arg: $.SUBRULE($.expression, [false]),
+        rParen: $.CONSUME(RParen),
+      };
+    });
+
+    $.RULE("aggregationExpression", outsideAggregation => {
+      const { aggregation, lParen, arg, rParen } = $.OR([
+        {
+          ALT: () => ({
+            aggregation: $.CONSUME(NullaryAggregation),
+            ...$.OPTION(() => $.SUBRULE($.nullaryCall)),
+          }),
+        },
+        {
+          ALT: () => ({
+            aggregation: $.CONSUME(UnaryAggregation),
+            ...$.SUBRULE($.unaryCall),
+          }),
+        },
+      ]);
+      return this._aggregation(aggregation, lParen, arg, rParen);
+    });
+
+    $.RULE("metricExpression", () => {
+      const metricName = $.OR([
+        { ALT: () => $.SUBRULE($.stringLiteral) },
+        { ALT: () => $.SUBRULE($.identifier) },
+      ]);
+
+      const metric = this.getMetricForName(this._toString(metricName));
+      if (metric != null) {
+        return this._metricReference(metricName, metric.id);
+      }
+      return this._unknownMetric(metricName);
+    });
+
+    $.RULE("fieldExpression", () => {
+      const fieldName = $.OR([
+        { ALT: () => $.SUBRULE($.stringLiteral) },
+        { ALT: () => $.SUBRULE($.identifier) },
+      ]);
+
+      const field = this.getFieldForName(this._toString(fieldName));
+      if (field != null) {
+        return this._fieldReference(fieldName, field.id);
+      }
+      const expression = this.getExpressionForName(this._toString(fieldName));
+      if (expression != null) {
+        return this._expressionReference(fieldName, expression);
+      }
+      return this._unknownField(fieldName);
+    });
+
+    $.RULE("identifier", () => {
+      const identifier = $.CONSUME(Identifier);
+      return this._identifier(identifier);
+    });
+
+    $.RULE("stringLiteral", () => {
+      const stringLiteral = $.CONSUME(StringLiteral);
+      return this._stringLiteral(stringLiteral);
+    });
+
+    $.RULE("numberLiteral", () => {
+      const minus = $.OPTION(() => $.CONSUME(Minus));
+      const numberLiteral = $.CONSUME(NumberLiteral);
+      return this._numberLiteral(minus, numberLiteral);
+    });
+
+    $.RULE("atomicExpression", outsideAggregation => {
+      return $.OR(
+        [
+          // aggregations are not allowed inside other aggregations
+          {
+            GATE: () => outsideAggregation,
+            ALT: () => $.SUBRULE($.aggregationExpression, [false]),
+          },
+
+          // NOTE: DISABLE METRICS
+          // {GATE: () => outsideAggregation, ALT: () => $.SUBRULE($.metricExpression) },
+
+          // fields are not allowed outside aggregations
+          {
+            GATE: () => !outsideAggregation,
+            ALT: () => $.SUBRULE($.fieldExpression),
+          },
+
+          {
+            ALT: () => $.SUBRULE($.parenthesisExpression, [outsideAggregation]),
+          },
+          { ALT: () => $.SUBRULE($.numberLiteral) },
+        ],
+        (outsideAggregation ? "aggregation" : "field name") +
+          ", number, or expression",
+      );
+    });
+
+    $.RULE("parenthesisExpression", outsideAggregation => {
+      let lParen = $.CONSUME(LParen);
+      let expValue = $.SUBRULE($.expression, [outsideAggregation]);
+      let rParen = $.CONSUME(RParen);
+      return this._parens(lParen, expValue, rParen);
+    });
+
+    Parser.performSelfAnalysis(this);
+  }
+
+  getFieldForName(fieldName) {
+    const fields =
+      this._options.tableMetadata && this._options.tableMetadata.fields;
+    return _.findWhere(fields, { display_name: fieldName });
+  }
+
+  getExpressionForName(expressionName) {
+    const customFields = this._options && this._options.customFields;
+    return customFields[expressionName];
+  }
+
+  getMetricForName(metricName) {
+    const metrics =
+      this._options.tableMetadata && this._options.tableMetadata.metrics;
+    return _.find(
+      metrics,
+      metric => metric.name.toLowerCase() === metricName.toLowerCase(),
+    );
+  }
 }
 
 class ExpressionsParserMBQL extends ExpressionsParser {
-    _math(initial, operations) {
-        for (const [op, rhsVal] of operations) {
-            // collapse multiple consecutive operators into a single MBQL statement
-            if (Array.isArray(initial) && initial[0] === op.image) {
-                initial.push(rhsVal);
-            } else {
-                initial = [op.image, initial, rhsVal]
-            }
-        }
-        return initial;
-    }
-    _aggregation(aggregation, lParen, arg, rParen) {
-        const agg = getAggregationFromName(getImage(aggregation));
-        return arg == null ? [agg] : [agg, arg];
-    }
-    _metricReference(metricName, metricId) {
-        return ["METRIC", metricId];
-    }
-    _fieldReference(fieldName, fieldId) {
-        return ["field-id", fieldId];
-    }
-    _expressionReference(fieldName) {
-        return ["expression", fieldName];
-    }
-    _unknownField(fieldName) {
-        throw new Error("Unknown field \"" + fieldName + "\"");
-    }
-    _unknownMetric(metricName) {
-        throw new Error("Unknown metric \"" + metricName + "\"");
-    }
-
-    _identifier(identifier) {
-        return identifier.image;
-    }
-    _stringLiteral(stringLiteral) {
-        return JSON.parse(stringLiteral.image);
-    }
-    _numberLiteral(minus, numberLiteral) {
-        return parseFloat(numberLiteral.image) * (minus ? -1 : 1);
-    }
-    _parens(lParen, expValue, rParen) {
-        return expValue;
-    }
-    _toString(x) {
-        return x;
-    }
+  _math(initial, operations) {
+    for (const [op, rhsVal] of operations) {
+      // collapse multiple consecutive operators into a single MBQL statement
+      if (Array.isArray(initial) && initial[0] === op.image) {
+        initial.push(rhsVal);
+      } else {
+        initial = [op.image, initial, rhsVal];
+      }
+    }
+    return initial;
+  }
+  _aggregation(aggregation, lParen, arg, rParen) {
+    const agg = getAggregationFromName(getImage(aggregation));
+    return arg == null ? [agg] : [agg, arg];
+  }
+  _metricReference(metricName, metricId) {
+    return ["METRIC", metricId];
+  }
+  _fieldReference(fieldName, fieldId) {
+    return ["field-id", fieldId];
+  }
+  _expressionReference(fieldName) {
+    return ["expression", fieldName];
+  }
+  _unknownField(fieldName) {
+    throw new Error('Unknown field "' + fieldName + '"');
+  }
+  _unknownMetric(metricName) {
+    throw new Error('Unknown metric "' + metricName + '"');
+  }
+
+  _identifier(identifier) {
+    return identifier.image;
+  }
+  _stringLiteral(stringLiteral) {
+    return JSON.parse(stringLiteral.image);
+  }
+  _numberLiteral(minus, numberLiteral) {
+    return parseFloat(numberLiteral.image) * (minus ? -1 : 1);
+  }
+  _parens(lParen, expValue, rParen) {
+    return expValue;
+  }
+  _toString(x) {
+    return x;
+  }
 }
 
 const syntax = (type, ...children) => ({
-    type: type,
-    children: children.filter(child => child)
-})
-const token = (token) => token && ({
+  type: type,
+  children: children.filter(child => child),
+});
+const token = token =>
+  token && {
     type: "token",
     text: token.image,
     start: token.startOffset,
     end: token.endOffset,
-});
+  };
 
 class ExpressionsParserSyntax extends ExpressionsParser {
-    _math(initial, operations) {
-        return syntax("math", ...[initial].concat(...operations.map(([op, arg]) => [token(op), arg])));
-    }
-    _aggregation(aggregation, lParen, arg, rParen) {
-        return syntax("aggregation", token(aggregation), token(lParen), arg, token(rParen));
-    }
-    _metricReference(metricName, metricId) {
-        return syntax("metric", metricName);
-    }
-    _fieldReference(fieldName, fieldId) {
-        return syntax("field", fieldName);
-    }
-    _expressionReference(fieldName) {
-        return syntax("expression-reference", token(fieldName));
-    }
-    _unknownField(fieldName) {
-        return syntax("unknown", fieldName);
-    }
-    _unknownMetric(metricName) {
-        return syntax("unknown", metricName);
-    }
-
-    _identifier(identifier) {
-        return syntax("identifier", token(identifier));
-    }
-    _stringLiteral(stringLiteral) {
-        return syntax("string", token(stringLiteral));
-    }
-    _numberLiteral(minus, numberLiteral) {
-        return syntax("number", token(minus), token(numberLiteral));
-    }
-    _parens(lParen, expValue, rParen) {
-        return syntax("group", token(lParen), expValue, token(rParen));
-    }
-    _toString(x) {
-        if (typeof x === "string") {
-            return x;
-        } else if (x.type === "string") {
-            return JSON.parse(x.children[0].text);
-        } else if (x.type === "identifier") {
-            return x.children[0].text;
-        }
-    }
+  _math(initial, operations) {
+    return syntax(
+      "math",
+      ...[initial].concat(...operations.map(([op, arg]) => [token(op), arg])),
+    );
+  }
+  _aggregation(aggregation, lParen, arg, rParen) {
+    return syntax(
+      "aggregation",
+      token(aggregation),
+      token(lParen),
+      arg,
+      token(rParen),
+    );
+  }
+  _metricReference(metricName, metricId) {
+    return syntax("metric", metricName);
+  }
+  _fieldReference(fieldName, fieldId) {
+    return syntax("field", fieldName);
+  }
+  _expressionReference(fieldName) {
+    return syntax("expression-reference", token(fieldName));
+  }
+  _unknownField(fieldName) {
+    return syntax("unknown", fieldName);
+  }
+  _unknownMetric(metricName) {
+    return syntax("unknown", metricName);
+  }
+
+  _identifier(identifier) {
+    return syntax("identifier", token(identifier));
+  }
+  _stringLiteral(stringLiteral) {
+    return syntax("string", token(stringLiteral));
+  }
+  _numberLiteral(minus, numberLiteral) {
+    return syntax("number", token(minus), token(numberLiteral));
+  }
+  _parens(lParen, expValue, rParen) {
+    return syntax("group", token(lParen), expValue, token(rParen));
+  }
+  _toString(x) {
+    if (typeof x === "string") {
+      return x;
+    } else if (x.type === "string") {
+      return JSON.parse(x.children[0].text);
+    } else if (x.type === "identifier") {
+      return x.children[0].text;
+    }
+  }
 }
 
 function getSubTokenTypes(TokenClass) {
-    return TokenClass.extendingTokenTypes.map(tokenType => _.findWhere(allTokens, { tokenType }));
+  return TokenClass.extendingTokenTypes.map(tokenType =>
+    _.findWhere(allTokens, { tokenType }),
+  );
 }
 
 function getTokenSource(TokenClass) {
-    // strip regex escaping, e.x. "\+" -> "+"
-    return TokenClass.PATTERN.source.replace(/^\\/, "");
+  // strip regex escaping, e.x. "\+" -> "+"
+  return TokenClass.PATTERN.source.replace(/^\\/, "");
 }
 
 function run(Parser, source, options) {
-    if (!source) {
-        return [];
-    }
-    const { startRule } = options;
-    const parser = new Parser(ExpressionsLexer.tokenize(source).tokens, options);
-    const expression = parser[startRule]();
-    if (parser.errors.length > 0) {
-        for (const error of parser.errors) {
-            // clean up error messages
-            error.message = error.message && error.message
-                .replace(/^Expecting:?\s+/, "Expected ")
-                .replace(/--> (.*?) <--/g, "$1")
-                .replace(/(\n|\s)*but found:?/, " but found ")
-                .replace(/\s*but found\s+''$/, "");
-        }
-        throw parser.errors;
-    }
-    return expression;
+  if (!source) {
+    return [];
+  }
+  const { startRule } = options;
+  const parser = new Parser(ExpressionsLexer.tokenize(source).tokens, options);
+  const expression = parser[startRule]();
+  if (parser.errors.length > 0) {
+    for (const error of parser.errors) {
+      // clean up error messages
+      error.message =
+        error.message &&
+        error.message
+          .replace(/^Expecting:?\s+/, "Expected ")
+          .replace(/--> (.*?) <--/g, "$1")
+          .replace(/(\n|\s)*but found:?/, " but found ")
+          .replace(/\s*but found\s+''$/, "");
+    }
+    throw parser.errors;
+  }
+  return expression;
 }
 
 export function compile(source, options = {}) {
-    return run(ExpressionsParserMBQL, source, options);
+  return run(ExpressionsParserMBQL, source, options);
 }
 
 export function parse(source, options = {}) {
-    return run(ExpressionsParserSyntax, source, options);
+  return run(ExpressionsParserSyntax, source, options);
 }
 
 // No need for more than one instance.
-const parserInstance = new ExpressionsParser([])
-export function suggest(source, {
-    tableMetadata,
-    customFields,
+const parserInstance = new ExpressionsParser([]);
+export function suggest(
+  source,
+  { tableMetadata, customFields, startRule, index = source.length } = {},
+) {
+  const partialSource = source.slice(0, index);
+  const lexResult = ExpressionsLexer.tokenize(partialSource);
+  if (lexResult.errors.length > 0) {
+    throw new Error(t`sad sad panda, lexing errors detected`);
+  }
+
+  const lastInputToken = _.last(lexResult.tokens);
+  let partialSuggestionMode = false;
+  let assistanceTokenVector = lexResult.tokens;
+
+  // we have requested assistance while inside an Identifier
+  if (
+    lastInputToken instanceof Identifier &&
+    /\w/.test(partialSource[partialSource.length - 1])
+  ) {
+    assistanceTokenVector = assistanceTokenVector.slice(0, -1);
+    partialSuggestionMode = true;
+  }
+
+  let finalSuggestions = [];
+
+  // TODO: is there a better way to figure out which aggregation we're inside of?
+  const currentAggregationToken = _.find(
+    assistanceTokenVector.slice().reverse(),
+    t => t instanceof Aggregation,
+  );
+
+  const syntacticSuggestions = parserInstance.computeContentAssist(
     startRule,
-    index = source.length
-} = {}) {
-    const partialSource = source.slice(0, index);
-    const lexResult = ExpressionsLexer.tokenize(partialSource);
-    if (lexResult.errors.length > 0) {
-        throw new Error(t`sad sad panda, lexing errors detected`);
-    }
-
-    const lastInputToken = _.last(lexResult.tokens)
-    let partialSuggestionMode = false
-    let assistanceTokenVector = lexResult.tokens
-
-    // we have requested assistance while inside an Identifier
-    if ((lastInputToken instanceof Identifier) &&
-        /\w/.test(partialSource[partialSource.length - 1])) {
-        assistanceTokenVector = assistanceTokenVector.slice(0, -1);
-        partialSuggestionMode = true
-    }
-
-
-    let finalSuggestions = []
-
-    // TODO: is there a better way to figure out which aggregation we're inside of?
-    const currentAggregationToken = _.find(assistanceTokenVector.slice().reverse(), (t) => t instanceof Aggregation);
-
-    const syntacticSuggestions = parserInstance.computeContentAssist(startRule, assistanceTokenVector)
-    for (const suggestion of syntacticSuggestions) {
-        const { nextTokenType, ruleStack } = suggestion;
-        // no nesting of aggregations or field references outside of aggregations
-        // we have a predicate in the grammar to prevent nested aggregations but chevrotain
-        // doesn't support predicates in content-assist mode, so we need this extra check
-        const outsideAggregation = startRule === "aggregation" && ruleStack.slice(0, -1).indexOf("aggregationExpression") < 0;
-
-        if (nextTokenType === MultiplicativeOperator || nextTokenType === AdditiveOperator) {
-            let tokens = getSubTokenTypes(nextTokenType);
-            finalSuggestions.push(...tokens.map(token => ({
-                type: "operators",
-                name: getTokenSource(token),
-                text: " " + getTokenSource(token) + " ",
-                prefixTrim: /\s*$/,
-                postfixTrim: /^\s*[*/+-]?\s*/
-            })))
-        } else if (nextTokenType === LParen) {
-            finalSuggestions.push({
-                type: "other",
-                name: "(",
-                text: " (",
-                postfixText: ")",
-                prefixTrim: /\s*$/,
-                postfixTrim: /^\s*\(?\s*/
-            });
-        } else if (nextTokenType === RParen) {
-            finalSuggestions.push({
-                type: "other",
-                name: ")",
-                text: ") ",
-                prefixTrim: /\s*$/,
-                postfixTrim: /^\s*\)?\s*/
-            });
-        } else if (nextTokenType === Identifier || nextTokenType === StringLiteral) {
-            if (!outsideAggregation) {
-                let fields = [];
-                if (startRule === "aggregation" && currentAggregationToken) {
-                    let aggregationShort = getAggregationFromName(getImage(currentAggregationToken));
-                    let aggregationOption = _.findWhere(tableMetadata.aggregation_options, { short: aggregationShort });
-                    fields = aggregationOption && aggregationOption.fields && aggregationOption.fields[0] || []
-                } else if (startRule === "expression") {
-                    fields = tableMetadata.fields.filter(isNumeric);
-                }
-                finalSuggestions.push(...fields.map(field => ({
-                    type: "fields",
-                    name: field.display_name,
-                    text: formatFieldName(field) + " ",
-                    prefixTrim: /\w+$/,
-                    postfixTrim: /^\w+\s*/
-                })));
-                finalSuggestions.push(...Object.keys(customFields || {}).map(expressionName => ({
-                    type: "fields",
-                    name: expressionName,
-                    text: formatExpressionName(expressionName) + " ",
-                    prefixTrim: /\w+$/,
-                    postfixTrim: /^\w+\s*/
-                })));
-            }
-        } else if (nextTokenType === Aggregation || nextTokenType === NullaryAggregation || nextTokenType === UnaryAggregation || nextTokenType === Identifier || nextTokenType === StringLiteral) {
-            if (outsideAggregation) {
-                finalSuggestions.push(...tableMetadata.aggregation_options.filter(a => formatAggregationName(a)).map(aggregationOption => {
-                    const arity = aggregationOption.fields.length;
-                    return {
-                        type: "aggregations",
-                        name: formatAggregationName(aggregationOption),
-                        text: formatAggregationName(aggregationOption) + (arity > 0 ? "(" : " "),
-                        postfixText: (arity > 0 ? ")" : " "),
-                        prefixTrim: /\w+$/,
-                        postfixTrim: (arity > 0 ? /^\w+(\(\)?|$)/ : /^\w+\s*/)
-                    };
-                }));
-                // NOTE: DISABLE METRICS
-                // finalSuggestions.push(...tableMetadata.metrics.map(metric => ({
-                //     type: "metrics",
-                //     name: metric.name,
-                //     text: formatMetricName(metric),
-                //     prefixTrim: /\w+$/,
-                //     postfixTrim: /^\w+\s*/
-                // })))
-            }
-        } else if (nextTokenType === NumberLiteral) {
-            // skip number literal
-        } else {
-            console.warn("non exhaustive match", nextTokenType.name, suggestion)
+    assistanceTokenVector,
+  );
+  for (const suggestion of syntacticSuggestions) {
+    const { nextTokenType, ruleStack } = suggestion;
+    // no nesting of aggregations or field references outside of aggregations
+    // we have a predicate in the grammar to prevent nested aggregations but chevrotain
+    // doesn't support predicates in content-assist mode, so we need this extra check
+    const outsideAggregation =
+      startRule === "aggregation" &&
+      ruleStack.slice(0, -1).indexOf("aggregationExpression") < 0;
+
+    if (
+      nextTokenType === MultiplicativeOperator ||
+      nextTokenType === AdditiveOperator
+    ) {
+      let tokens = getSubTokenTypes(nextTokenType);
+      finalSuggestions.push(
+        ...tokens.map(token => ({
+          type: "operators",
+          name: getTokenSource(token),
+          text: " " + getTokenSource(token) + " ",
+          prefixTrim: /\s*$/,
+          postfixTrim: /^\s*[*/+-]?\s*/,
+        })),
+      );
+    } else if (nextTokenType === LParen) {
+      finalSuggestions.push({
+        type: "other",
+        name: "(",
+        text: " (",
+        postfixText: ")",
+        prefixTrim: /\s*$/,
+        postfixTrim: /^\s*\(?\s*/,
+      });
+    } else if (nextTokenType === RParen) {
+      finalSuggestions.push({
+        type: "other",
+        name: ")",
+        text: ") ",
+        prefixTrim: /\s*$/,
+        postfixTrim: /^\s*\)?\s*/,
+      });
+    } else if (
+      nextTokenType === Identifier ||
+      nextTokenType === StringLiteral
+    ) {
+      if (!outsideAggregation) {
+        let fields = [];
+        if (startRule === "aggregation" && currentAggregationToken) {
+          let aggregationShort = getAggregationFromName(
+            getImage(currentAggregationToken),
+          );
+          let aggregationOption = _.findWhere(
+            tableMetadata.aggregation_options,
+            { short: aggregationShort },
+          );
+          fields =
+            (aggregationOption &&
+              aggregationOption.fields &&
+              aggregationOption.fields[0]) ||
+            [];
+        } else if (startRule === "expression") {
+          fields = tableMetadata.fields.filter(isNumeric);
         }
-    }
-
-    // throw away any suggestion that is not a suffix of the last partialToken.
-    if (partialSuggestionMode) {
-        const partial = getImage(lastInputToken).toLowerCase();
-        finalSuggestions = _.filter(finalSuggestions, (suggestion) =>
-            (suggestion.text && suggestion.text.toLowerCase().startsWith(partial)) ||
-            (suggestion.name && suggestion.name.toLowerCase().startsWith(partial))
+        finalSuggestions.push(
+          ...fields.map(field => ({
+            type: "fields",
+            name: field.display_name,
+            text: formatFieldName(field) + " ",
+            prefixTrim: /\w+$/,
+            postfixTrim: /^\w+\s*/,
+          })),
         );
-
-        let prefixLength = partial.length;
-        for (const suggestion of finalSuggestions) {
-            suggestion.prefixLength = prefixLength;
-        }
-    }
+        finalSuggestions.push(
+          ...Object.keys(customFields || {}).map(expressionName => ({
+            type: "fields",
+            name: expressionName,
+            text: formatExpressionName(expressionName) + " ",
+            prefixTrim: /\w+$/,
+            postfixTrim: /^\w+\s*/,
+          })),
+        );
+      }
+    } else if (
+      nextTokenType === Aggregation ||
+      nextTokenType === NullaryAggregation ||
+      nextTokenType === UnaryAggregation ||
+      nextTokenType === Identifier ||
+      nextTokenType === StringLiteral
+    ) {
+      if (outsideAggregation) {
+        finalSuggestions.push(
+          ...tableMetadata.aggregation_options
+            .filter(a => formatAggregationName(a))
+            .map(aggregationOption => {
+              const arity = aggregationOption.fields.length;
+              return {
+                type: "aggregations",
+                name: formatAggregationName(aggregationOption),
+                text:
+                  formatAggregationName(aggregationOption) +
+                  (arity > 0 ? "(" : " "),
+                postfixText: arity > 0 ? ")" : " ",
+                prefixTrim: /\w+$/,
+                postfixTrim: arity > 0 ? /^\w+(\(\)?|$)/ : /^\w+\s*/,
+              };
+            }),
+        );
+        // NOTE: DISABLE METRICS
+        // finalSuggestions.push(...tableMetadata.metrics.map(metric => ({
+        //     type: "metrics",
+        //     name: metric.name,
+        //     text: formatMetricName(metric),
+        //     prefixTrim: /\w+$/,
+        //     postfixTrim: /^\w+\s*/
+        // })))
+      }
+    } else if (nextTokenType === NumberLiteral) {
+      // skip number literal
+    } else {
+      console.warn("non exhaustive match", nextTokenType.name, suggestion);
+    }
+  }
+
+  // throw away any suggestion that is not a suffix of the last partialToken.
+  if (partialSuggestionMode) {
+    const partial = getImage(lastInputToken).toLowerCase();
+    finalSuggestions = _.filter(
+      finalSuggestions,
+      suggestion =>
+        (suggestion.text &&
+          suggestion.text.toLowerCase().startsWith(partial)) ||
+        (suggestion.name && suggestion.name.toLowerCase().startsWith(partial)),
+    );
+
+    let prefixLength = partial.length;
     for (const suggestion of finalSuggestions) {
-        suggestion.index = index;
-        if (!suggestion.name) {
-            suggestion.name = suggestion.text;
-        }
-    }
-
-    // deduplicate suggestions and sort by type then name
-    return _.chain(finalSuggestions)
-        .uniq(suggestion => suggestion.text)
-        .sortBy("name")
-        .sortBy("type")
-        .value();
+      suggestion.prefixLength = prefixLength;
+    }
+  }
+  for (const suggestion of finalSuggestions) {
+    suggestion.index = index;
+    if (!suggestion.name) {
+      suggestion.name = suggestion.text;
+    }
+  }
+
+  // deduplicate suggestions and sort by type then name
+  return _.chain(finalSuggestions)
+    .uniq(suggestion => suggestion.text)
+    .sortBy("name")
+    .sortBy("type")
+    .value();
 }
diff --git a/frontend/src/metabase/lib/expressions/tokens.js b/frontend/src/metabase/lib/expressions/tokens.js
index ce111c229c3cc5e6ed36956becba48b55430a74e..7f92f558208c1eb8c1cc07a0e3a16a0da7a657ed 100644
--- a/frontend/src/metabase/lib/expressions/tokens.js
+++ b/frontend/src/metabase/lib/expressions/tokens.js
@@ -3,42 +3,62 @@
 import { Lexer, extendToken } from "chevrotain";
 
 import {
-    VALID_AGGREGATIONS,
-    NULLARY_AGGREGATIONS,
-    UNARY_AGGREGATIONS
-} from "./config"
+  VALID_AGGREGATIONS,
+  NULLARY_AGGREGATIONS,
+  UNARY_AGGREGATIONS,
+} from "./config";
 
 export const AdditiveOperator = extendToken("AdditiveOperator", Lexer.NA);
 export const Plus = extendToken("Plus", /\+/, AdditiveOperator);
 export const Minus = extendToken("Minus", /-/, AdditiveOperator);
 
-export const MultiplicativeOperator = extendToken("MultiplicativeOperator", Lexer.NA);
+export const MultiplicativeOperator = extendToken(
+  "MultiplicativeOperator",
+  Lexer.NA,
+);
 export const Multi = extendToken("Multi", /\*/, MultiplicativeOperator);
 export const Div = extendToken("Div", /\//, MultiplicativeOperator);
 
 export const Aggregation = extendToken("Aggregation", Lexer.NA);
 
-export const NullaryAggregation = extendToken("NullaryAggregation", Aggregation);
-const nullaryAggregationTokens = NULLARY_AGGREGATIONS.map((short) =>
-    extendToken(VALID_AGGREGATIONS.get(short), new RegExp(VALID_AGGREGATIONS.get(short), "i"), NullaryAggregation)
+export const NullaryAggregation = extendToken(
+  "NullaryAggregation",
+  Aggregation,
+);
+const nullaryAggregationTokens = NULLARY_AGGREGATIONS.map(short =>
+  extendToken(
+    VALID_AGGREGATIONS.get(short),
+    new RegExp(VALID_AGGREGATIONS.get(short), "i"),
+    NullaryAggregation,
+  ),
 );
 
 export const UnaryAggregation = extendToken("UnaryAggregation", Aggregation);
-const unaryAggregationTokens = UNARY_AGGREGATIONS.map((short) =>
-    extendToken(VALID_AGGREGATIONS.get(short), new RegExp(VALID_AGGREGATIONS.get(short), "i"), UnaryAggregation)
+const unaryAggregationTokens = UNARY_AGGREGATIONS.map(short =>
+  extendToken(
+    VALID_AGGREGATIONS.get(short),
+    new RegExp(VALID_AGGREGATIONS.get(short), "i"),
+    UnaryAggregation,
+  ),
 );
 
-export const Identifier = extendToken('Identifier', /\w+/);
-export const NumberLiteral = extendToken("NumberLiteral", /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/);
-export const StringLiteral = extendToken("StringLiteral", /"(?:[^\\"]+|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/);
+export const Identifier = extendToken("Identifier", /\w+/);
+export const NumberLiteral = extendToken(
+  "NumberLiteral",
+  /-?(0|[1-9]\d*)(\.\d+)?([eE][+-]?\d+)?/,
+);
+export const StringLiteral = extendToken(
+  "StringLiteral",
+  /"(?:[^\\"]+|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"/,
+);
 
-export const Comma = extendToken('Comma', /,/);
+export const Comma = extendToken("Comma", /,/);
 Comma.LABEL = "comma";
 
-export const LParen = extendToken('LParen', /\(/);
+export const LParen = extendToken("LParen", /\(/);
 LParen.LABEL = "opening parenthesis";
 
-export const RParen = extendToken('RParen', /\)/);
+export const RParen = extendToken("RParen", /\)/);
 RParen.LABEL = "closing parenthesis";
 
 export const WhiteSpace = extendToken("WhiteSpace", /\s+/);
@@ -46,12 +66,22 @@ WhiteSpace.GROUP = Lexer.SKIPPED;
 
 // whitespace is normally very common so it is placed first to speed up the lexer
 export const allTokens = [
-    WhiteSpace, LParen, RParen, Comma,
-    Plus, Minus, Multi, Div,
-    AdditiveOperator, MultiplicativeOperator,
-    Aggregation,
-    NullaryAggregation, ...nullaryAggregationTokens,
-    UnaryAggregation, ...unaryAggregationTokens,
-    StringLiteral, NumberLiteral,
-    Identifier
+  WhiteSpace,
+  LParen,
+  RParen,
+  Comma,
+  Plus,
+  Minus,
+  Multi,
+  Div,
+  AdditiveOperator,
+  MultiplicativeOperator,
+  Aggregation,
+  NullaryAggregation,
+  ...nullaryAggregationTokens,
+  UnaryAggregation,
+  ...unaryAggregationTokens,
+  StringLiteral,
+  NumberLiteral,
+  Identifier,
 ];
diff --git a/frontend/src/metabase/lib/formatting.js b/frontend/src/metabase/lib/formatting.js
index 7ef945bcc72e2d72a8047a19f6a9bb9373ed1937..7720718d7867d62c5e9b41f24c1223202939c556 100644
--- a/frontend/src/metabase/lib/formatting.js
+++ b/frontend/src/metabase/lib/formatting.js
@@ -8,401 +8,497 @@ import React from "react";
 
 import ExternalLink from "metabase/components/ExternalLink.jsx";
 
-import { isDate, isNumber, isCoordinate, isLatitude, isLongitude } from "metabase/lib/schema_metadata";
+import {
+  isDate,
+  isNumber,
+  isCoordinate,
+  isLatitude,
+  isLongitude,
+} from "metabase/lib/schema_metadata";
 import { isa, TYPE } from "metabase/lib/types";
-import { parseTimestamp,parseTime } from "metabase/lib/time";
+import { parseTimestamp, parseTime } from "metabase/lib/time";
 import { rangeForValue } from "metabase/lib/dataset";
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
 import { decimalCount } from "metabase/visualizations/lib/numeric";
 
+import Field from "metabase-lib/lib/metadata/Field";
 import type { Column, Value } from "metabase/meta/types/Dataset";
-import type { Field } from "metabase/meta/types/Field";
 import type { DatetimeUnit } from "metabase/meta/types/Query";
 import type { Moment } from "metabase/meta/types";
 
 export type FormattingOptions = {
-    column?: Column,
-    majorWidth?: number,
-    type?: "axis"|"cell"|"tooltip",
-    comma?: boolean,
-    jsx?: boolean,
-    compact?: boolean,
-}
-
-const PRECISION_NUMBER_FORMATTER      = d3.format(".2r");
-const FIXED_NUMBER_FORMATTER          = d3.format(",.f");
+  column?: Column | Field,
+  majorWidth?: number,
+  type?: "axis" | "cell" | "tooltip",
+  jsx?: boolean,
+  // number options:
+  comma?: boolean,
+  compact?: boolean,
+  round?: boolean,
+};
+
+const DEFAULT_NUMBER_OPTIONS: FormattingOptions = {
+  comma: true,
+  compact: false,
+  round: true,
+};
+
+const PRECISION_NUMBER_FORMATTER = d3.format(".2r");
+const FIXED_NUMBER_FORMATTER = d3.format(",.f");
 const FIXED_NUMBER_FORMATTER_NO_COMMA = d3.format(".f");
-const DECIMAL_DEGREES_FORMATTER       = d3.format(".08f");
-const BINNING_DEGREES_FORMATTER       = (value, binWidth) => {
-    return d3.format(`.0${decimalCount(binWidth)}f`)(value)
-}
+const DECIMAL_DEGREES_FORMATTER = d3.format(".08f");
+const BINNING_DEGREES_FORMATTER = (value, binWidth) => {
+  return d3.format(`.0${decimalCount(binWidth)}f`)(value);
+};
 
 // use en dashes, for Maz
 const RANGE_SEPARATOR = ` – `;
 
 export function formatNumber(number: number, options: FormattingOptions = {}) {
-    options = { comma: true, ...options};
-    if (options.compact) {
-        if (number === 0) {
-            // 0 => 0
-            return "0"
-        } else if (number >= -0.01 && number <= 0.01) {
-            // 0.01 => ~0
-            return "~ 0";
-        } else if (number > -1 && number < 1) {
-            // 0.1 => 0.1
-            return PRECISION_NUMBER_FORMATTER(number).replace(/\.?0+$/, "");
-        } else {
-            // 1 => 1
-            // 1000 => 1K
-            return Humanize.compactInteger(number, 1);
-        }
+  options = { ...DEFAULT_NUMBER_OPTIONS, ...options };
+  if (options.compact) {
+    if (number === 0) {
+      // 0 => 0
+      return "0";
+    } else if (number >= -0.01 && number <= 0.01) {
+      // 0.01 => ~0
+      return "~ 0";
     } else if (number > -1 && number < 1) {
-        // numbers between 1 and -1 round to 2 significant digits with extra 0s stripped off
-        return PRECISION_NUMBER_FORMATTER(number).replace(/\.?0+$/, "");
+      // 0.1 => 0.1
+      return PRECISION_NUMBER_FORMATTER(number).replace(/\.?0+$/, "");
     } else {
-        // anything else rounds to at most 2 decimal points
-        if (options.comma) {
-            return FIXED_NUMBER_FORMATTER(d3.round(number, 2));
-        } else {
-            return FIXED_NUMBER_FORMATTER_NO_COMMA(d3.round(number, 2));
-        }
+      // 1 => 1
+      // 1000 => 1K
+      return Humanize.compactInteger(number, 1);
     }
-}
-
-export function formatCoordinate(value: number, options: FormattingOptions = {}) {
-    const binWidth = options.column && options.column.binning_info && options.column.binning_info.bin_width;
-    let direction = "";
-    if (isLatitude(options.column)) {
-        direction = " " + (value < 0 ? "S" : "N");
-        value = Math.abs(value);
-    } else if (isLongitude(options.column)) {
-        direction = " " + (value < 0 ? "W" : "E");
-        value = Math.abs(value);
+  } else if (number > -1 && number < 1) {
+    // numbers between 1 and -1 round to 2 significant digits with extra 0s stripped off
+    return PRECISION_NUMBER_FORMATTER(number).replace(/\.?0+$/, "");
+  } else {
+    // anything else rounds to at most 2 decimal points, unless disabled
+    if (options.round) {
+      number = d3.round(number, 2);
+    }
+    if (options.comma) {
+      return FIXED_NUMBER_FORMATTER(number);
+    } else {
+      return FIXED_NUMBER_FORMATTER_NO_COMMA(number);
     }
+  }
+}
 
-    const formattedValue = binWidth ? BINNING_DEGREES_FORMATTER(value, binWidth) : DECIMAL_DEGREES_FORMATTER(value)
-    return formattedValue + "°" + direction;
+export function formatCoordinate(
+  value: number,
+  options: FormattingOptions = {},
+) {
+  const binWidth =
+    options.column &&
+    options.column.binning_info &&
+    options.column.binning_info.bin_width;
+  let direction = "";
+  if (isLatitude(options.column)) {
+    direction = " " + (value < 0 ? "S" : "N");
+    value = Math.abs(value);
+  } else if (isLongitude(options.column)) {
+    direction = " " + (value < 0 ? "W" : "E");
+    value = Math.abs(value);
+  }
+
+  const formattedValue = binWidth
+    ? BINNING_DEGREES_FORMATTER(value, binWidth)
+    : DECIMAL_DEGREES_FORMATTER(value);
+  return formattedValue + "°" + direction;
 }
 
-export function formatRange(range: [number, number], formatter: (value: number) => string, options: FormattingOptions = {}) {
-    return range.map(value => formatter(value, options)).join(` ${RANGE_SEPARATOR} `);
+export function formatRange(
+  range: [number, number],
+  formatter: (value: number) => string,
+  options: FormattingOptions = {},
+) {
+  return range
+    .map(value => formatter(value, options))
+    .join(` ${RANGE_SEPARATOR} `);
 }
 
 function formatMajorMinor(major, minor, options = {}) {
-    options = {
-        jsx: false,
-        majorWidth: 3,
-        ...options
-    };
-    if (options.jsx) {
-        return (
-            <span>
-                <span style={{ minWidth: options.majorWidth + "em" }} className="inline-block text-right text-bold">{major}</span>
-                {" - "}
-                <span>{minor}</span>
-            </span>
-        );
-    } else {
-        return `${major} - ${minor}`;
-    }
+  options = {
+    jsx: false,
+    majorWidth: 3,
+    ...options,
+  };
+  if (options.jsx) {
+    return (
+      <span>
+        <span
+          style={{ minWidth: options.majorWidth + "em" }}
+          className="inline-block text-right text-bold"
+        >
+          {major}
+        </span>
+        {" - "}
+        <span>{minor}</span>
+      </span>
+    );
+  } else {
+    return `${major} - ${minor}`;
+  }
 }
 
 /** This formats a time with unit as a date range */
-export function formatTimeRangeWithUnit(value: Value, unit: DatetimeUnit, options: FormattingOptions = {}) {
-    let m = parseTimestamp(value, unit);
-    if (!m.isValid()) {
-        return String(value);
-    }
-
-    // Tooltips should show full month name, but condense "MMMM D, YYYY - MMMM D, YYYY" to "MMMM D - D, YYYY" etc
-    const monthFormat = options.type === "tooltip" ? "MMMM" : "MMM";
-    const condensed = options.type === "tooltip";
-
-    const start = m.clone().startOf(unit);
-    const end = m.clone().endOf(unit);
-    if (start.isValid() && end.isValid()) {
-        if (!condensed || start.year() !== end.year()) {
-            return start.format(`${monthFormat} D, YYYY`) + RANGE_SEPARATOR + end.format(`${monthFormat} D, YYYY`);
-        } else if (start.month() !== end.month()) {
-            return start.format(`${monthFormat} D`) + RANGE_SEPARATOR + end.format(`${monthFormat} D, YYYY`);
-        } else {
-            return start.format(`${monthFormat} D`) + RANGE_SEPARATOR + end.format(`D, YYYY`);
-        }
+export function formatTimeRangeWithUnit(
+  value: Value,
+  unit: DatetimeUnit,
+  options: FormattingOptions = {},
+) {
+  let m = parseTimestamp(value, unit);
+  if (!m.isValid()) {
+    return String(value);
+  }
+
+  // Tooltips should show full month name, but condense "MMMM D, YYYY - MMMM D, YYYY" to "MMMM D - D, YYYY" etc
+  const monthFormat = options.type === "tooltip" ? "MMMM" : "MMM";
+  const condensed = options.type === "tooltip";
+
+  const start = m.clone().startOf(unit);
+  const end = m.clone().endOf(unit);
+  if (start.isValid() && end.isValid()) {
+    if (!condensed || start.year() !== end.year()) {
+      return (
+        start.format(`${monthFormat} D, YYYY`) +
+        RANGE_SEPARATOR +
+        end.format(`${monthFormat} D, YYYY`)
+      );
+    } else if (start.month() !== end.month()) {
+      return (
+        start.format(`${monthFormat} D`) +
+        RANGE_SEPARATOR +
+        end.format(`${monthFormat} D, YYYY`)
+      );
     } else {
-        return formatWeek(m, options);
+      return (
+        start.format(`${monthFormat} D`) +
+        RANGE_SEPARATOR +
+        end.format(`D, YYYY`)
+      );
     }
+  } else {
+    return formatWeek(m, options);
+  }
 }
 
 function formatWeek(m: Moment, options: FormattingOptions = {}) {
-    // force 'en' locale for now since our weeks currently always start on Sundays
-    m = m.locale("en");
-    return formatMajorMinor(m.format("wo"), m.format("gggg"), options);
+  // force 'en' locale for now since our weeks currently always start on Sundays
+  m = m.locale("en");
+  return formatMajorMinor(m.format("wo"), m.format("gggg"), options);
 }
 
-export function formatTimeWithUnit(value: Value, unit: DatetimeUnit, options: FormattingOptions = {}) {
-    let m = parseTimestamp(value, unit);
-    if (!m.isValid()) {
-        return String(value);
-    }
-
-    switch (unit) {
-        case "hour": // 12 AM - January 1, 2015
-            return formatMajorMinor(m.format("h A"), m.format("MMMM D, YYYY"), options);
-        case "day": // January 1, 2015
-            return m.format("MMMM D, YYYY");
-        case "week": // 1st - 2015
-            if (options.type === "tooltip") {
-                // tooltip show range like "January 1 - 7, 2017"
-                return formatTimeRangeWithUnit(value, unit, options);
-            } else if (options.type === "cell") {
-                // table cells show range like "Jan 1, 2017 - Jan 7, 2017"
-                return formatTimeRangeWithUnit(value, unit, options);
-            } else if (options.type === "axis") {
-                // axis ticks show start of the week as "Jan 1"
-                return m.clone().startOf(unit).format(`MMM D`);
-            } else {
-                return formatWeek(m, options);
-            }
-        case "month": // January 2015
-            return options.jsx ?
-                <div><span className="text-bold">{m.format("MMMM")}</span> {m.format("YYYY")}</div> :
-                m.format("MMMM") + " " + m.format("YYYY");
-        case "year": // 2015
-            return m.format("YYYY");
-        case "quarter": // Q1 - 2015
-            return formatMajorMinor(m.format("[Q]Q"), m.format("YYYY"), { ...options, majorWidth: 0 });
-        case "hour-of-day": // 12 AM
-            return moment().hour(value).format("h A");
-        case "day-of-week": // Sunday
-            // $FlowFixMe:
-            return moment().day(value - 1).format("dddd");
-        case "day-of-month":
-            return moment().date(value).format("D");
-        case "week-of-year": // 1st
-            return moment().week(value).format("wo");
-        case "month-of-year": // January
-            // $FlowFixMe:
-            return moment().month(value - 1).format("MMMM");
-        case "quarter-of-year": // January
-            return moment().quarter(value).format("[Q]Q");
-        default:
-            return m.format("LLLL");
-    }
+export function formatTimeWithUnit(
+  value: Value,
+  unit: DatetimeUnit,
+  options: FormattingOptions = {},
+) {
+  let m = parseTimestamp(value, unit);
+  if (!m.isValid()) {
+    return String(value);
+  }
+
+  switch (unit) {
+    case "hour": // 12 AM - January 1, 2015
+      return formatMajorMinor(
+        m.format("h A"),
+        m.format("MMMM D, YYYY"),
+        options,
+      );
+    case "day": // January 1, 2015
+      return m.format("MMMM D, YYYY");
+    case "week": // 1st - 2015
+      if (options.type === "tooltip") {
+        // tooltip show range like "January 1 - 7, 2017"
+        return formatTimeRangeWithUnit(value, unit, options);
+      } else if (options.type === "cell") {
+        // table cells show range like "Jan 1, 2017 - Jan 7, 2017"
+        return formatTimeRangeWithUnit(value, unit, options);
+      } else if (options.type === "axis") {
+        // axis ticks show start of the week as "Jan 1"
+        return m
+          .clone()
+          .startOf(unit)
+          .format(`MMM D`);
+      } else {
+        return formatWeek(m, options);
+      }
+    case "month": // January 2015
+      return options.jsx ? (
+        <div>
+          <span className="text-bold">{m.format("MMMM")}</span>{" "}
+          {m.format("YYYY")}
+        </div>
+      ) : (
+        m.format("MMMM") + " " + m.format("YYYY")
+      );
+    case "year": // 2015
+      return m.format("YYYY");
+    case "quarter": // Q1 - 2015
+      return formatMajorMinor(m.format("[Q]Q"), m.format("YYYY"), {
+        ...options,
+        majorWidth: 0,
+      });
+    case "hour-of-day": // 12 AM
+      return moment()
+        .hour(value)
+        .format("h A");
+    case "day-of-week": // Sunday
+      return (
+        moment()
+          // $FlowFixMe:
+          .day(value - 1)
+          .format("dddd")
+      );
+    case "day-of-month":
+      return moment()
+        .date(value)
+        .format("D");
+    case "week-of-year": // 1st
+      return moment()
+        .week(value)
+        .format("wo");
+    case "month-of-year": // January
+      return (
+        moment()
+          // $FlowFixMe:
+          .month(value - 1)
+          .format("MMMM")
+      );
+    case "quarter-of-year": // January
+      return moment()
+        .quarter(value)
+        .format("[Q]Q");
+    default:
+      return m.format("LLLL");
+  }
 }
 
 export function formatTimeValue(value: Value) {
-    let m = parseTime(value);
-    if (!m.isValid()){
-        return String(value);
-    } else {
-        return m.format("LT");
-    }
+  let m = parseTime(value);
+  if (!m.isValid()) {
+    return String(value);
+  } else {
+    return m.format("LT");
+  }
 }
 
 // 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: Value, { jsx }: FormattingOptions = {}) {
-    const email = String(value);
-    if (jsx && EMAIL_WHITELIST_REGEX.test(email)) {
-        return <ExternalLink href={"mailto:" + email}>{email}</ExternalLink>;
-    } else {
-        return email;
-    }
+  const email = String(value);
+  if (jsx && EMAIL_WHITELIST_REGEX.test(email)) {
+    return <ExternalLink href={"mailto:" + email}>{email}</ExternalLink>;
+  } else {
+    return email;
+  }
 }
 
 // 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: Value, { jsx }: FormattingOptions = {}) {
-    const url = String(value);
-    if (jsx && URL_WHITELIST_REGEX.test(url)) {
-        return <ExternalLink className="link link--wrappable" href={url}>{url}</ExternalLink>;
-    } else {
-        return url;
-    }
+  const url = String(value);
+  if (jsx && URL_WHITELIST_REGEX.test(url)) {
+    return (
+      <ExternalLink className="link link--wrappable" href={url}>
+        {url}
+      </ExternalLink>
+    );
+  } else {
+    return url;
+  }
 }
 
 // fallback for formatting a string without a column special_type
 function formatStringFallback(value: Value, options: FormattingOptions = {}) {
-    value = formatUrl(value, options);
-    if (typeof value === 'string') {
-        value = formatEmail(value, options);
-    }
-    return value;
+  value = formatUrl(value, options);
+  if (typeof value === "string") {
+    value = formatEmail(value, options);
+  }
+  return value;
 }
 
 export function formatValue(value: Value, options: FormattingOptions = {}) {
-    let column = options.column;
-
-    options = {
-        jsx: false,
-        comma: isNumber(column),
-        ...options
-    };
-
-    // "column" may also be a field object
-    // $FlowFixMe: remapping is a special field added by Visualization.jsx or getMetadata selector
-    if (column && column.remapping && column.remapping.size > 0) {
-        // $FlowFixMe
-        const remappedValueSample = column.remapping.values().next().value
-
-        // Even if the column only has a list of analyzed values without remappings, those values
-        // are keys in `remapping` array with value `undefined`
-        const hasSetRemappings = remappedValueSample !== undefined
-        if (hasSetRemappings) {
-            // $FlowFixMe
-            if (column.remapping.has(value)) {
-                // $FlowFixMe
-                return column.remapping.get(value);
-            }
-
-            const remappedValueIsString = typeof remappedValueSample
-            if (remappedValueIsString) {
-                // A simple way to hide intermediate ticks for a numeral value that has been remapped to a string
-                return null;
-            }
-        }
+  let column = options.column;
+
+  options = {
+    jsx: false,
+    remap: true,
+    comma: isNumber(column),
+    ...options,
+  };
+
+  if (options.remap && column) {
+    // $FlowFixMe: column could be Field or Column
+    if (column.hasRemappedValue && column.hasRemappedValue(value)) {
+      // $FlowFixMe: column could be Field or Column
+      return column.remappedValue(value);
     }
-
-    if (value == undefined) {
-        return null;
-    } else if (column && isa(column.special_type, TYPE.URL)) {
-        return formatUrl(value, options);
-    } else if (column && isa(column.special_type, TYPE.Email)) {
-        return formatEmail(value, options);
-    } else if (column && isa(column.base_type, TYPE.Time)) {
-        return formatTimeValue(value);
-    } else if (column && column.unit != null) {
-        return formatTimeWithUnit(value, column.unit, 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 formatStringFallback(value, options);
-    } else if (typeof value === "number") {
-        const formatter = isCoordinate(column) ?
-            formatCoordinate :
-            formatNumber;
-        const range = rangeForValue(value, options.column);
-        if (range) {
-            return formatRange(range, formatter, options);
-        } else {
-            return formatter(value, options);
-        }
-    } else if (typeof value === "object") {
-        // no extra whitespace for table cells
-        return JSON.stringify(value);
+    // or it may be a raw column object with a "remapping" object
+    if (column.remapping instanceof Map && column.remapping.has(value)) {
+      return column.remapping.get(value);
+    }
+    // TODO: get rid of one of these two code paths?
+  }
+
+  if (value == undefined) {
+    return null;
+  } else if (column && isa(column.special_type, TYPE.URL)) {
+    return formatUrl(value, options);
+  } else if (column && isa(column.special_type, TYPE.Email)) {
+    return formatEmail(value, options);
+  } else if (column && isa(column.base_type, TYPE.Time)) {
+    return formatTimeValue(value);
+  } else if (column && column.unit != null) {
+    return formatTimeWithUnit(value, column.unit, 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 formatStringFallback(value, options);
+  } else if (typeof value === "number") {
+    const formatter = isCoordinate(column) ? formatCoordinate : formatNumber;
+    const range = rangeForValue(value, options.column);
+    if (range) {
+      return formatRange(range, formatter, options);
     } else {
-        return String(value);
+      return formatter(value, options);
     }
+  } else if (typeof value === "object") {
+    // no extra whitespace for table cells
+    return JSON.stringify(value);
+  } else {
+    return String(value);
+  }
 }
 
 export function formatColumn(column: Column): string {
-    if (!column) {
-        return "";
-    } else if (column.remapped_to_column != null) {
-        // $FlowFixMe: remapped_to_column is a special field added by Visualization.jsx
-        return formatColumn(column.remapped_to_column)
-    } else {
-        let columnTitle = getFriendlyName(column);
-        if (column.unit && column.unit !== "default") {
-            columnTitle += ": " + capitalize(column.unit.replace(/-/g, " "))
-        }
-        return columnTitle;
+  if (!column) {
+    return "";
+  } else if (column.remapped_to_column != null) {
+    // $FlowFixMe: remapped_to_column is a special field added by Visualization.jsx
+    return formatColumn(column.remapped_to_column);
+  } else {
+    let columnTitle = getFriendlyName(column);
+    if (column.unit && column.unit !== "default") {
+      columnTitle += ": " + capitalize(column.unit.replace(/-/g, " "));
     }
+    return columnTitle;
+  }
 }
 
 export function formatField(field: Field): string {
-    if (!field) {
-        return "";
-    } else if (field.dimensions && field.dimensions.name) {
-        return field.dimensions.name;
-    } else {
-        return field.display_name || field.name;
-    }
+  if (!field) {
+    return "";
+  } else if (field.dimensions && field.dimensions.name) {
+    return field.dimensions.name;
+  } else {
+    return field.display_name || field.name;
+  }
 }
 
 // $FlowFixMe
 export function singularize(...args) {
-    return inflection.singularize(...args);
+  return inflection.singularize(...args);
 }
 
 // $FlowFixMe
 export function pluralize(...args) {
-    return inflection.pluralize(...args);
+  return inflection.pluralize(...args);
 }
 
 // $FlowFixMe
 export function capitalize(...args) {
-    return inflection.capitalize(...args);
+  return inflection.capitalize(...args);
 }
 
 // $FlowFixMe
 export function inflect(...args) {
-    return inflection.inflect(...args);
+  return inflection.inflect(...args);
 }
 
 // $FlowFixMe
 export function titleize(...args) {
-    return inflection.titleize(...args);
+  return inflection.titleize(...args);
 }
 
 // $FlowFixMe
 export function humanize(...args) {
-    return inflection.humanize(...args);
+  return inflection.humanize(...args);
 }
 
 export function duration(milliseconds: number) {
-    if (milliseconds < 60000) {
-        let seconds = Math.round(milliseconds / 1000);
-        return seconds + " " + inflect("second", seconds);
-    } else {
-        let minutes = Math.round(milliseconds / 1000 / 60);
-        return minutes + " " + inflect("minute", minutes);
-    }
+  if (milliseconds < 60000) {
+    let seconds = Math.round(milliseconds / 1000);
+    return seconds + " " + inflect("second", seconds);
+  } else {
+    let minutes = Math.round(milliseconds / 1000 / 60);
+    return minutes + " " + inflect("minute", minutes);
+  }
 }
 
 // Removes trailing "id" from field names
 export function stripId(name: string) {
-    return name && name.replace(/ id$/i, "");
+  return name && name.replace(/ id$/i, "").trim();
 }
 
 export function slugify(name: string) {
-    return name && name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
+  return name && name.toLowerCase().replace(/[^a-z0-9_]/g, "_");
 }
 
-export function assignUserColors(userIds: number[], currentUserId: number, colorClasses: string[] = ['bg-brand', 'bg-purple', 'bg-error', 'bg-green', 'bg-gold', 'bg-grey-2']) {
-    let assignments = {};
-
-    const currentUserColor = colorClasses[0];
-    const otherUserColors = colorClasses.slice(1);
-    let otherUserColorIndex = 0;
-
-    for (let userId of userIds) {
-        if (!(userId in assignments)) {
-            if (userId === currentUserId) {
-                assignments[userId] = currentUserColor;
-            } else if (userId != null) {
-                assignments[userId] = otherUserColors[otherUserColorIndex++ % otherUserColors.length];
-            }
-        }
+export function assignUserColors(
+  userIds: number[],
+  currentUserId: number,
+  colorClasses: string[] = [
+    "bg-brand",
+    "bg-purple",
+    "bg-error",
+    "bg-green",
+    "bg-gold",
+    "bg-grey-2",
+  ],
+) {
+  let assignments = {};
+
+  const currentUserColor = colorClasses[0];
+  const otherUserColors = colorClasses.slice(1);
+  let otherUserColorIndex = 0;
+
+  for (let userId of userIds) {
+    if (!(userId in assignments)) {
+      if (userId === currentUserId) {
+        assignments[userId] = currentUserColor;
+      } else if (userId != null) {
+        assignments[userId] =
+          otherUserColors[otherUserColorIndex++ % otherUserColors.length];
+      }
     }
+  }
 
-    return assignments;
+  return assignments;
 }
 
 export function formatSQL(sql: string) {
-    if (typeof sql === "string") {
-        sql = sql.replace(/\sFROM/, "\nFROM");
-        sql = sql.replace(/\sLEFT JOIN/, "\nLEFT JOIN");
-        sql = sql.replace(/\sWHERE/, "\nWHERE");
-        sql = sql.replace(/\sGROUP BY/, "\nGROUP BY");
-        sql = sql.replace(/\sORDER BY/, "\nORDER BY");
-        sql = sql.replace(/\sLIMIT/, "\nLIMIT");
-        sql = sql.replace(/\sAND\s/, "\n   AND ");
-        sql = sql.replace(/\sOR\s/, "\n    OR ");
-
-        return sql;
-    }
+  if (typeof sql === "string") {
+    sql = sql.replace(/\sFROM/, "\nFROM");
+    sql = sql.replace(/\sLEFT JOIN/, "\nLEFT JOIN");
+    sql = sql.replace(/\sWHERE/, "\nWHERE");
+    sql = sql.replace(/\sGROUP BY/, "\nGROUP BY");
+    sql = sql.replace(/\sORDER BY/, "\nORDER BY");
+    sql = sql.replace(/\sLIMIT/, "\nLIMIT");
+    sql = sql.replace(/\sAND\s/, "\n   AND ");
+    sql = sql.replace(/\sOR\s/, "\n    OR ");
+
+    return sql;
+  }
 }
diff --git a/frontend/src/metabase/lib/ga-metadata.js b/frontend/src/metabase/lib/ga-metadata.js
index 754a6a2bb7f46f032e0c49439133af49a8800ca5..c856cd3c6bb7d1528a007afef7621c83e58659e0 100644
--- a/frontend/src/metabase/lib/ga-metadata.js
+++ b/frontend/src/metabase/lib/ga-metadata.js
@@ -1,261 +1,1118 @@
-export const fields = { 'ga:userType': { section: 'User', can_filter: true, can_breakout: true },
-  'ga:sessionCount': { section: 'User', can_filter: true, can_breakout: true },
-  'ga:daysSinceLastSession': { section: 'User', can_filter: true, can_breakout: true },
-  'ga:userDefinedValue': { section: 'User', can_filter: true, can_breakout: true },
-  'ga:users': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:newUsers': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:percentNewSessions': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:1dayUsers': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:7dayUsers': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:14dayUsers': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:30dayUsers': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:sessionDurationBucket': { section: 'Session', can_filter: true, can_breakout: true },
-  'ga:sessions': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:bounces': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:bounceRate': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:sessionDuration': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:avgSessionDuration': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:referralPath': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:fullReferrer': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:campaign': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:source': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:medium': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:sourceMedium': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:keyword': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:adContent': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:socialNetwork': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:hasSocialSourceReferral': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:organicSearches': { section: 'Traffic Sources', can_filter: true, can_breakout: false },
-  'ga:adGroup': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adSlot': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adDistributionNetwork': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adMatchType': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adKeywordMatchType': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adMatchedQuery': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adPlacementDomain': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adPlacementUrl': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adFormat': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adTargetingType': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adTargetingOption': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adDisplayUrl': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adDestinationUrl': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adwordsCustomerID': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adwordsCampaignID': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adwordsAdGroupID': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adwordsCreativeID': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:adwordsCriteriaID': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:impressions': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:adClicks': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:adCost': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:CPM': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:CPC': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:CTR': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:costPerTransaction': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:costPerGoalConversion': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:costPerConversion': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:RPC': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:ROAS': { section: 'Adwords', can_filter: true, can_breakout: false },
-  'ga:adQueryWordCount': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:goalCompletionLocation': { section: 'Goal Conversions', can_filter: true, can_breakout: true },
-  'ga:goalPreviousStep1': { section: 'Goal Conversions', can_filter: true, can_breakout: true },
-  'ga:goalPreviousStep2': { section: 'Goal Conversions', can_filter: true, can_breakout: true },
-  'ga:goalPreviousStep3': { section: 'Goal Conversions', can_filter: true, can_breakout: true },
-  'ga:goalXXStarts': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalStartsAll': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalXXCompletions': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalCompletionsAll': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalXXValue': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalValueAll': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalValuePerSession': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalXXConversionRate': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalConversionRateAll': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalXXAbandons': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalAbandonsAll': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalXXAbandonRate': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:goalAbandonRateAll': { section: 'Goal Conversions', can_filter: true, can_breakout: false },
-  'ga:browser': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:browserVersion': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:operatingSystem': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:operatingSystemVersion': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:mobileDeviceBranding': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:mobileDeviceModel': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:mobileInputSelector': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:mobileDeviceInfo': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:mobileDeviceMarketingName': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:deviceCategory': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:continent': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:subContinent': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:country': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:region': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:metro': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:city': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:latitude': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:longitude': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:networkDomain': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:networkLocation': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:flashVersion': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:javaEnabled': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:language': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:screenColors': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:sourcePropertyDisplayName': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:sourcePropertyTrackingId': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:screenResolution': { section: 'System', can_filter: true, can_breakout: true },
-  'ga:hostname': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pagePath': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pagePathLevel1': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pagePathLevel2': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pagePathLevel3': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pagePathLevel4': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pageTitle': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:landingPagePath': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:secondPagePath': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:exitPagePath': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:previousPagePath': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pageDepth': { section: 'Page Tracking', can_filter: true, can_breakout: true },
-  'ga:pageValue': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:entrances': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:entranceRate': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:pageviews': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:pageviewsPerSession': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:contentGroupUniqueViewsXX': { section: 'Content Grouping', can_filter: true, can_breakout: false },
-  'ga:uniquePageviews': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:timeOnPage': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:avgTimeOnPage': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:exits': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:exitRate': { section: 'Page Tracking', can_filter: true, can_breakout: false },
-  'ga:searchUsed': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchKeyword': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchKeywordRefinement': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchCategory': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchStartPage': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchDestinationPage': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchAfterDestinationPage': { section: 'Internal Search', can_filter: true, can_breakout: true },
-  'ga:searchResultViews': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchUniques': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:avgSearchResultViews': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchSessions': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:percentSessionsWithSearch': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchDepth': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:avgSearchDepth': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchRefinements': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:percentSearchRefinements': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchDuration': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:avgSearchDuration': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchExits': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchExitRate': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchGoalXXConversionRate': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:searchGoalConversionRateAll': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:goalValueAllPerSearch': { section: 'Internal Search', can_filter: true, can_breakout: false },
-  'ga:pageLoadTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:pageLoadSample': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgPageLoadTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:domainLookupTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgDomainLookupTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:pageDownloadTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgPageDownloadTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:redirectionTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgRedirectionTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:serverConnectionTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgServerConnectionTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:serverResponseTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgServerResponseTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:speedMetricsSample': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:domInteractiveTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgDomInteractiveTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:domContentLoadedTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:avgDomContentLoadedTime': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:domLatencyMetricsSample': { section: 'Site Speed', can_filter: true, can_breakout: false },
-  'ga:appInstallerId': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:appVersion': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:appName': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:appId': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:screenName': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:screenDepth': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:landingScreenName': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:exitScreenName': { section: 'App Tracking', can_filter: true, can_breakout: true },
-  'ga:screenviews': { section: 'App Tracking', can_filter: true, can_breakout: false },
-  'ga:uniqueScreenviews': { section: 'App Tracking', can_filter: true, can_breakout: false },
-  'ga:screenviewsPerSession': { section: 'App Tracking', can_filter: true, can_breakout: false },
-  'ga:timeOnScreen': { section: 'App Tracking', can_filter: true, can_breakout: false },
-  'ga:avgScreenviewDuration': { section: 'App Tracking', can_filter: true, can_breakout: false },
-  'ga:eventCategory': { section: 'Event Tracking', can_filter: true, can_breakout: true },
-  'ga:eventAction': { section: 'Event Tracking', can_filter: true, can_breakout: true },
-  'ga:eventLabel': { section: 'Event Tracking', can_filter: true, can_breakout: true },
-  'ga:totalEvents': { section: 'Event Tracking', can_filter: true, can_breakout: false },
-  'ga:uniqueDimensionCombinations': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:eventValue': { section: 'Event Tracking', can_filter: true, can_breakout: false },
-  'ga:avgEventValue': { section: 'Event Tracking', can_filter: true, can_breakout: false },
-  'ga:sessionsWithEvent': { section: 'Event Tracking', can_filter: true, can_breakout: false },
-  'ga:eventsPerSessionWithEvent': { section: 'Event Tracking', can_filter: true, can_breakout: false },
-  'ga:transactionId': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:affiliation': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:sessionsToTransaction': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:daysToTransaction': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productSku': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productName': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productCategory': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:currencyCode': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:transactions': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:transactionsPerSession': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:transactionRevenue': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:revenuePerTransaction': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:transactionRevenuePerSession': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:transactionShipping': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:transactionTax': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:totalValue': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:itemQuantity': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:uniquePurchases': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:revenuePerItem': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:itemRevenue': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:itemsPerPurchase': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:localTransactionRevenue': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:localTransactionShipping': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:localTransactionTax': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:localItemRevenue': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:socialInteractionNetwork': { section: 'Social Interactions', can_filter: true, can_breakout: true },
-  'ga:socialInteractionAction': { section: 'Social Interactions', can_filter: true, can_breakout: true },
-  'ga:socialInteractionNetworkAction': { section: 'Social Interactions', can_filter: true, can_breakout: true },
-  'ga:socialInteractionTarget': { section: 'Social Interactions', can_filter: true, can_breakout: true },
-  'ga:socialEngagementType': { section: 'Social Interactions', can_filter: true, can_breakout: true },
-  'ga:socialInteractions': { section: 'Social Interactions', can_filter: true, can_breakout: false },
-  'ga:uniqueSocialInteractions': { section: 'Social Interactions', can_filter: true, can_breakout: false },
-  'ga:socialInteractionsPerSession': { section: 'Social Interactions', can_filter: true, can_breakout: false },
-  'ga:userTimingCategory': { section: 'User Timings', can_filter: true, can_breakout: true },
-  'ga:userTimingLabel': { section: 'User Timings', can_filter: true, can_breakout: true },
-  'ga:userTimingVariable': { section: 'User Timings', can_filter: true, can_breakout: true },
-  'ga:userTimingValue': { section: 'User Timings', can_filter: true, can_breakout: false },
-  'ga:userTimingSample': { section: 'User Timings', can_filter: true, can_breakout: false },
-  'ga:avgUserTimingValue': { section: 'User Timings', can_filter: true, can_breakout: false },
-  'ga:exceptionDescription': { section: 'Exceptions', can_filter: true, can_breakout: true },
-  'ga:exceptions': { section: 'Exceptions', can_filter: true, can_breakout: false },
-  'ga:exceptionsPerScreenview': { section: 'Exceptions', can_filter: true, can_breakout: false },
-  'ga:fatalExceptions': { section: 'Exceptions', can_filter: true, can_breakout: false },
-  'ga:fatalExceptionsPerScreenview': { section: 'Exceptions', can_filter: true, can_breakout: false },
-  'ga:experimentId': { section: 'Content Experiments', can_filter: true, can_breakout: true },
-  'ga:experimentVariant': { section: 'Content Experiments', can_filter: true, can_breakout: true },
-  'ga:dimensionXX': { section: 'Custom Variables or Columns', can_filter: true, can_breakout: true },
-  'ga:customVarNameXX': { section: 'Custom Variables or Columns', can_filter: true, can_breakout: true },
-  'ga:metricXX': { section: 'Custom Variables or Columns', can_filter: true, can_breakout: false },
-  'ga:customVarValueXX': { section: 'Custom Variables or Columns', can_filter: true, can_breakout: true },
-  'ga:date': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:year': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:month': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:week': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:day': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:hour': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:minute': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:nthMonth': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:nthWeek': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:nthDay': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:nthMinute': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:dayOfWeek': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:dayOfWeekName': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:dateHour': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:yearMonth': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:yearWeek': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:isoWeek': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:isoYear': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:isoYearIsoWeek': { section: 'Time', can_filter: true, can_breakout: true },
+export const fields = {
+  "ga:userType": { section: "User", can_filter: true, can_breakout: true },
+  "ga:sessionCount": { section: "User", can_filter: true, can_breakout: true },
+  "ga:daysSinceLastSession": {
+    section: "User",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:userDefinedValue": {
+    section: "User",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:users": { section: "User", can_filter: true, can_breakout: false },
+  "ga:newUsers": { section: "User", can_filter: true, can_breakout: false },
+  "ga:percentNewSessions": {
+    section: "User",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:1dayUsers": { section: "User", can_filter: true, can_breakout: false },
+  "ga:7dayUsers": { section: "User", can_filter: true, can_breakout: false },
+  "ga:14dayUsers": { section: "User", can_filter: true, can_breakout: false },
+  "ga:30dayUsers": { section: "User", can_filter: true, can_breakout: false },
+  "ga:sessionDurationBucket": {
+    section: "Session",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:sessions": { section: "Session", can_filter: true, can_breakout: false },
+  "ga:bounces": { section: "Session", can_filter: true, can_breakout: false },
+  "ga:bounceRate": {
+    section: "Session",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:sessionDuration": {
+    section: "Session",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgSessionDuration": {
+    section: "Session",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:referralPath": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:fullReferrer": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:campaign": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:source": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:medium": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:sourceMedium": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:keyword": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adContent": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:socialNetwork": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:hasSocialSourceReferral": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:organicSearches": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adGroup": { section: "Adwords", can_filter: true, can_breakout: true },
+  "ga:adSlot": { section: "Adwords", can_filter: true, can_breakout: true },
+  "ga:adDistributionNetwork": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adMatchType": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adKeywordMatchType": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adMatchedQuery": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adPlacementDomain": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adPlacementUrl": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adFormat": { section: "Adwords", can_filter: true, can_breakout: true },
+  "ga:adTargetingType": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adTargetingOption": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adDisplayUrl": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adDestinationUrl": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adwordsCustomerID": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adwordsCampaignID": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adwordsAdGroupID": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adwordsCreativeID": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adwordsCriteriaID": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:impressions": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adClicks": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:adCost": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:CPM": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:CPC": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:CTR": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:costPerTransaction": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:costPerGoalConversion": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:costPerConversion": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:RPC": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:ROAS": { section: "Adwords", can_filter: true, can_breakout: false },
+  "ga:adQueryWordCount": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:goalCompletionLocation": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:goalPreviousStep1": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:goalPreviousStep2": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:goalPreviousStep3": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:goalXXStarts": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalStartsAll": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalXXCompletions": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalCompletionsAll": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalXXValue": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalValueAll": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalValuePerSession": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalXXConversionRate": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalConversionRateAll": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalXXAbandons": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalAbandonsAll": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalXXAbandonRate": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalAbandonRateAll": {
+    section: "Goal Conversions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:browser": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:browserVersion": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:operatingSystem": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:operatingSystemVersion": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:mobileDeviceBranding": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:mobileDeviceModel": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:mobileInputSelector": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:mobileDeviceInfo": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:mobileDeviceMarketingName": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:deviceCategory": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:continent": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:subContinent": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:country": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:region": { section: "Geo Network", can_filter: true, can_breakout: true },
+  "ga:metro": { section: "Geo Network", can_filter: true, can_breakout: true },
+  "ga:city": { section: "Geo Network", can_filter: true, can_breakout: true },
+  "ga:latitude": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:longitude": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:networkDomain": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:networkLocation": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:flashVersion": {
+    section: "System",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:javaEnabled": { section: "System", can_filter: true, can_breakout: true },
+  "ga:language": { section: "System", can_filter: true, can_breakout: true },
+  "ga:screenColors": {
+    section: "System",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:sourcePropertyDisplayName": {
+    section: "System",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:sourcePropertyTrackingId": {
+    section: "System",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:screenResolution": {
+    section: "System",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:hostname": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pagePath": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pagePathLevel1": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pagePathLevel2": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pagePathLevel3": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pagePathLevel4": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pageTitle": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:landingPagePath": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:secondPagePath": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:exitPagePath": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:previousPagePath": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pageDepth": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:pageValue": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:entrances": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:entranceRate": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:pageviews": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:pageviewsPerSession": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:contentGroupUniqueViewsXX": {
+    section: "Content Grouping",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:uniquePageviews": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:timeOnPage": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgTimeOnPage": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:exits": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:exitRate": {
+    section: "Page Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchUsed": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchKeyword": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchKeywordRefinement": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchCategory": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchStartPage": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchDestinationPage": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchAfterDestinationPage": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:searchResultViews": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchUniques": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgSearchResultViews": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchSessions": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:percentSessionsWithSearch": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchDepth": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgSearchDepth": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchRefinements": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:percentSearchRefinements": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchDuration": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgSearchDuration": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchExits": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchExitRate": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchGoalXXConversionRate": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:searchGoalConversionRateAll": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:goalValueAllPerSearch": {
+    section: "Internal Search",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:pageLoadTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:pageLoadSample": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgPageLoadTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:domainLookupTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgDomainLookupTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:pageDownloadTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgPageDownloadTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:redirectionTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgRedirectionTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:serverConnectionTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgServerConnectionTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:serverResponseTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgServerResponseTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:speedMetricsSample": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:domInteractiveTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgDomInteractiveTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:domContentLoadedTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgDomContentLoadedTime": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:domLatencyMetricsSample": {
+    section: "Site Speed",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:appInstallerId": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:appVersion": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:appName": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:appId": { section: "App Tracking", can_filter: true, can_breakout: true },
+  "ga:screenName": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:screenDepth": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:landingScreenName": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:exitScreenName": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:screenviews": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:uniqueScreenviews": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:screenviewsPerSession": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:timeOnScreen": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgScreenviewDuration": {
+    section: "App Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:eventCategory": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:eventAction": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:eventLabel": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:totalEvents": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:uniqueDimensionCombinations": {
+    section: "Session",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:eventValue": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgEventValue": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:sessionsWithEvent": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:eventsPerSessionWithEvent": {
+    section: "Event Tracking",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionId": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:affiliation": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:sessionsToTransaction": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:daysToTransaction": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productSku": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productName": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productCategory": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:currencyCode": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:transactions": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionsPerSession": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionRevenue": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:revenuePerTransaction": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionRevenuePerSession": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionShipping": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionTax": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:totalValue": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:itemQuantity": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:uniquePurchases": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:revenuePerItem": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:itemRevenue": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:itemsPerPurchase": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:localTransactionRevenue": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:localTransactionShipping": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:localTransactionTax": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:localItemRevenue": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:socialInteractionNetwork": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:socialInteractionAction": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:socialInteractionNetworkAction": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:socialInteractionTarget": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:socialEngagementType": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:socialInteractions": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:uniqueSocialInteractions": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:socialInteractionsPerSession": {
+    section: "Social Interactions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:userTimingCategory": {
+    section: "User Timings",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:userTimingLabel": {
+    section: "User Timings",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:userTimingVariable": {
+    section: "User Timings",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:userTimingValue": {
+    section: "User Timings",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:userTimingSample": {
+    section: "User Timings",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:avgUserTimingValue": {
+    section: "User Timings",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:exceptionDescription": {
+    section: "Exceptions",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:exceptions": {
+    section: "Exceptions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:exceptionsPerScreenview": {
+    section: "Exceptions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:fatalExceptions": {
+    section: "Exceptions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:fatalExceptionsPerScreenview": {
+    section: "Exceptions",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:experimentId": {
+    section: "Content Experiments",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:experimentVariant": {
+    section: "Content Experiments",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:dimensionXX": {
+    section: "Custom Variables or Columns",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:customVarNameXX": {
+    section: "Custom Variables or Columns",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:metricXX": {
+    section: "Custom Variables or Columns",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:customVarValueXX": {
+    section: "Custom Variables or Columns",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:date": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:year": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:month": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:week": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:day": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:hour": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:minute": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:nthMonth": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:nthWeek": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:nthDay": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:nthMinute": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:dayOfWeek": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:dayOfWeekName": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:dateHour": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:yearMonth": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:yearWeek": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:isoWeek": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:isoYear": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:isoYearIsoWeek": {
+    section: "Time",
+    can_filter: true,
+    can_breakout: true,
+  },
   // 'ga:dcmClickAd': { section: 'DoubleClick Campaign Manager', can_filter: true, can_breakout: true },
   // 'ga:dcmClickAdId': { section: 'DoubleClick Campaign Manager', can_filter: true, can_breakout: true },
   // 'ga:dcmClickAdType': { section: 'DoubleClick Campaign Manager', can_filter: true, can_breakout: true },
@@ -304,24 +1161,96 @@ export const fields = { 'ga:userType': { section: 'User', can_filter: true, can_
   // 'ga:dcmLastEventSpotId': { section: 'DoubleClick Campaign Manager', can_filter: true, can_breakout: true },
   // 'ga:dcmFloodlightQuantity': { section: 'DoubleClick Campaign Manager', can_filter: true, can_breakout: false },
   // 'ga:dcmFloodlightRevenue': { section: 'DoubleClick Campaign Manager', can_filter: true, can_breakout: false },
-  'ga:landingContentGroupXX': { section: 'Content Grouping', can_filter: true, can_breakout: true },
-  'ga:previousContentGroupXX': { section: 'Content Grouping', can_filter: true, can_breakout: true },
-  'ga:contentGroupXX': { section: 'Content Grouping', can_filter: true, can_breakout: true },
-  'ga:userAgeBracket': { section: 'Audience', can_filter: true, can_breakout: true },
-  'ga:userGender': { section: 'Audience', can_filter: true, can_breakout: true },
-  'ga:interestOtherCategory': { section: 'Audience', can_filter: true, can_breakout: true },
-  'ga:interestAffinityCategory': { section: 'Audience', can_filter: true, can_breakout: true },
-  'ga:interestInMarketCategory': { section: 'Audience', can_filter: true, can_breakout: true },
-  'ga:adsenseRevenue': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseAdUnitsViewed': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseAdsViewed': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseAdsClicks': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsensePageImpressions': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseCTR': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseECPM': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseExits': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseViewableImpressionPercent': { section: 'Adsense', can_filter: true, can_breakout: false },
-  'ga:adsenseCoverage': { section: 'Adsense', can_filter: true, can_breakout: false },
+  "ga:landingContentGroupXX": {
+    section: "Content Grouping",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:previousContentGroupXX": {
+    section: "Content Grouping",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:contentGroupXX": {
+    section: "Content Grouping",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:userAgeBracket": {
+    section: "Audience",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:userGender": {
+    section: "Audience",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:interestOtherCategory": {
+    section: "Audience",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:interestAffinityCategory": {
+    section: "Audience",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:interestInMarketCategory": {
+    section: "Audience",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:adsenseRevenue": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseAdUnitsViewed": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseAdsViewed": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseAdsClicks": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsensePageImpressions": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseCTR": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseECPM": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseExits": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseViewableImpressionPercent": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:adsenseCoverage": {
+    section: "Adsense",
+    can_filter: true,
+    can_breakout: false,
+  },
   // 'ga:adxImpressions': { section: 'Ad Exchange', can_filter: true, can_breakout: false },
   // 'ga:adxCoverage': { section: 'Ad Exchange', can_filter: true, can_breakout: false },
   // 'ga:adxMonetizedPageviews': { section: 'Ad Exchange', can_filter: true, can_breakout: false },
@@ -352,24 +1281,92 @@ export const fields = { 'ga:userType': { section: 'User', can_filter: true, can_
   // 'ga:backfillRevenue': { section: 'DoubleClick for Publishers Backfill', can_filter: true, can_breakout: false },
   // 'ga:backfillRevenuePer1000Sessions': { section: 'DoubleClick for Publishers Backfill', can_filter: true, can_breakout: false },
   // 'ga:backfillECPM': { section: 'DoubleClick for Publishers Backfill', can_filter: true, can_breakout: false },
-  'ga:acquisitionCampaign': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:acquisitionMedium': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:acquisitionSource': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:acquisitionSourceMedium': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:acquisitionTrafficChannel': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:browserSize': { section: 'Platform or Device', can_filter: true, can_breakout: true },
-  'ga:campaignCode': { section: 'Traffic Sources', can_filter: true, can_breakout: true },
-  'ga:channelGrouping': { section: 'Channel Grouping', can_filter: true, can_breakout: true },
-  'ga:checkoutOptions': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:cityId': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:cohort': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: true },
-  'ga:cohortNthDay': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: true },
-  'ga:cohortNthMonth': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: true },
-  'ga:cohortNthWeek': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: true },
-  'ga:continentId': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:correlationModelId': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:countryIsoCode': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:dataSource': { section: 'Platform or Device', can_filter: true, can_breakout: true },
+  "ga:acquisitionCampaign": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:acquisitionMedium": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:acquisitionSource": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:acquisitionSourceMedium": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:acquisitionTrafficChannel": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:browserSize": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:campaignCode": {
+    section: "Traffic Sources",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:channelGrouping": {
+    section: "Channel Grouping",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:checkoutOptions": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:cityId": { section: "Geo Network", can_filter: true, can_breakout: true },
+  "ga:cohort": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:cohortNthDay": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:cohortNthMonth": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:cohortNthWeek": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:continentId": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:correlationModelId": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:countryIsoCode": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:dataSource": {
+    section: "Platform or Device",
+    can_filter: true,
+    can_breakout: true,
+  },
   // 'ga:dbmClickAdvertiser': { section: 'DoubleClick Bid Manager', can_filter: true, can_breakout: true },
   // 'ga:dbmClickAdvertiserId': { section: 'DoubleClick Bid Manager', can_filter: true, can_breakout: true },
   // 'ga:dbmClickCreativeId': { section: 'DoubleClick Bid Manager', can_filter: true, can_breakout: true },
@@ -404,51 +1401,227 @@ export const fields = { 'ga:userType': { section: 'User', can_filter: true, can_
   // 'ga:dsEngineAccountId': { section: 'DoubleClick Search', can_filter: true, can_breakout: true },
   // 'ga:dsKeyword': { section: 'DoubleClick Search', can_filter: true, can_breakout: true },
   // 'ga:dsKeywordId': { section: 'DoubleClick Search', can_filter: true, can_breakout: true },
-  'ga:internalPromotionCreative': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:internalPromotionId': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:internalPromotionName': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:internalPromotionPosition': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:isTrueViewVideoAd': { section: 'Adwords', can_filter: true, can_breakout: true },
-  'ga:metroId': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:nthHour': { section: 'Time', can_filter: true, can_breakout: true },
-  'ga:orderCouponCode': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productBrand': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productCategoryHierarchy': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productCategoryLevelXX': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productCouponCode': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productListName': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productListPosition': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:productVariant': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:queryProductId': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:queryProductName': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:queryProductVariation': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:regionId': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:regionIsoCode': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:relatedProductId': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:relatedProductName': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:relatedProductVariation': { section: 'Related Products', can_filter: true, can_breakout: true },
-  'ga:shoppingStage': { section: 'Ecommerce', can_filter: true, can_breakout: true },
-  'ga:subContinentCode': { section: 'Geo Network', can_filter: true, can_breakout: true },
-  'ga:buyToDetailRate': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:calcMetric_<NAME>': { section: 'Custom Variables or Columns', can_filter: true, can_breakout: false },
-  'ga:cartToDetailRate': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:cohortActiveUsers': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortAppviewsPerUser': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortAppviewsPerUserWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortGoalCompletionsPerUser': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortGoalCompletionsPerUserWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortPageviewsPerUser': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortPageviewsPerUserWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortRetentionRate': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortRevenuePerUser': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortRevenuePerUserWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortSessionDurationPerUser': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortSessionDurationPerUserWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortSessionsPerUser': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortSessionsPerUserWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortTotalUsers': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:cohortTotalUsersWithLifetimeCriteria': { section: 'Lifetime Value and Cohorts', can_filter: true, can_breakout: false },
-  'ga:correlationScore': { section: 'Related Products', can_filter: true, can_breakout: false },
+  "ga:internalPromotionCreative": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:internalPromotionId": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:internalPromotionName": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:internalPromotionPosition": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:isTrueViewVideoAd": {
+    section: "Adwords",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:metroId": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:nthHour": { section: "Time", can_filter: true, can_breakout: true },
+  "ga:orderCouponCode": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productBrand": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productCategoryHierarchy": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productCategoryLevelXX": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productCouponCode": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productListName": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productListPosition": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:productVariant": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:queryProductId": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:queryProductName": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:queryProductVariation": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:regionId": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:regionIsoCode": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:relatedProductId": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:relatedProductName": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:relatedProductVariation": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:shoppingStage": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:subContinentCode": {
+    section: "Geo Network",
+    can_filter: true,
+    can_breakout: true,
+  },
+  "ga:buyToDetailRate": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:calcMetric_<NAME>": {
+    section: "Custom Variables or Columns",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cartToDetailRate": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortActiveUsers": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortAppviewsPerUser": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortAppviewsPerUserWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortGoalCompletionsPerUser": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortGoalCompletionsPerUserWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortPageviewsPerUser": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortPageviewsPerUserWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortRetentionRate": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortRevenuePerUser": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortRevenuePerUserWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortSessionDurationPerUser": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortSessionDurationPerUserWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortSessionsPerUser": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortSessionsPerUserWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortTotalUsers": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:cohortTotalUsersWithLifetimeCriteria": {
+    section: "Lifetime Value and Cohorts",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:correlationScore": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: false,
+  },
   // 'ga:dbmCPA': { section: 'DoubleClick Bid Manager', can_filter: true, can_breakout: false },
   // 'ga:dbmCPC': { section: 'DoubleClick Bid Manager', can_filter: true, can_breakout: false },
   // 'ga:dbmCPM': { section: 'DoubleClick Bid Manager', can_filter: true, can_breakout: false },
@@ -473,168 +1646,1146 @@ export const fields = { 'ga:userType': { section: 'User', can_filter: true, can_
   // 'ga:dsProfit': { section: 'DoubleClick Search', can_filter: true, can_breakout: false },
   // 'ga:dsReturnOnAdSpend': { section: 'DoubleClick Search', can_filter: true, can_breakout: false },
   // 'ga:dsRevenuePerClick': { section: 'DoubleClick Search', can_filter: true, can_breakout: false },
-  'ga:hits': { section: 'Session', can_filter: true, can_breakout: false },
-  'ga:internalPromotionCTR': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:internalPromotionClicks': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:internalPromotionViews': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:localProductRefundAmount': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:localRefundAmount': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productAddsToCart': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productCheckouts': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productDetailViews': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productListCTR': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productListClicks': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productListViews': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productRefundAmount': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productRefunds': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productRemovesFromCart': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:productRevenuePerPurchase': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:quantityAddedToCart': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:quantityCheckedOut': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:quantityRefunded': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:quantityRemovedFromCart': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:queryProductQuantity': { section: 'Related Products', can_filter: true, can_breakout: false },
-  'ga:refundAmount': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:relatedProductQuantity': { section: 'Related Products', can_filter: true, can_breakout: false },
-  'ga:revenuePerUser': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:sessionsPerUser': { section: 'User', can_filter: true, can_breakout: false },
-  'ga:totalRefunds': { section: 'Ecommerce', can_filter: true, can_breakout: false },
-  'ga:transactionsPerUser': { section: 'Ecommerce', can_filter: true, can_breakout: false } };
+  "ga:hits": { section: "Session", can_filter: true, can_breakout: false },
+  "ga:internalPromotionCTR": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:internalPromotionClicks": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:internalPromotionViews": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:localProductRefundAmount": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:localRefundAmount": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productAddsToCart": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productCheckouts": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productDetailViews": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productListCTR": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productListClicks": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productListViews": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productRefundAmount": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productRefunds": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productRemovesFromCart": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:productRevenuePerPurchase": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:quantityAddedToCart": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:quantityCheckedOut": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:quantityRefunded": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:quantityRemovedFromCart": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:queryProductQuantity": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:refundAmount": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:relatedProductQuantity": {
+    section: "Related Products",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:revenuePerUser": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:sessionsPerUser": {
+    section: "User",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:totalRefunds": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+  "ga:transactionsPerUser": {
+    section: "Ecommerce",
+    can_filter: true,
+    can_breakout: false,
+  },
+};
 
-export const metrics = [ { id: 'ga:users', name: 'Users', description: 'The total number of users for the requested time period.', section: 'User', is_active: true },
-  { id: 'ga:newUsers', name: 'New Users', description: 'The number of users whose session was marked as a first-time session.', section: 'User', is_active: true },
-  { id: 'ga:percentNewSessions', name: '% New Sessions', description: 'The percentage of sessions by users who had never visited the property before.', section: 'User', is_active: true },
-  { id: 'ga:1dayUsers', name: '1 Day Active Users', description: 'Total number of 1-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 1-day period ending on the given date.', section: 'User', is_active: true },
-  { id: 'ga:7dayUsers', name: '7 Day Active Users', description: 'Total number of 7-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 7-day period ending on the given date.', section: 'User', is_active: true },
-  { id: 'ga:14dayUsers', name: '14 Day Active Users', description: 'Total number of 14-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 14-day period ending on the given date.', section: 'User', is_active: true },
-  { id: 'ga:30dayUsers', name: '30 Day Active Users', description: 'Total number of 30-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 30-day period ending on the given date.', section: 'User', is_active: true },
-  { id: 'ga:sessions', name: 'Sessions', description: 'The total number of sessions.', section: 'Session', is_active: true },
-  { id: 'ga:bounces', name: 'Bounces', description: 'The total number of single page (or single interaction hit) sessions for the property.', section: 'Session', is_active: true },
-  { id: 'ga:bounceRate', name: 'Bounce Rate', description: 'The percentage of single-page session (i.e., session in which the person left the property from the first page).', section: 'Session', is_active: true },
-  { id: 'ga:sessionDuration', name: 'Session Duration', description: 'Total duration (in seconds) of users\' sessions.', section: 'Session', is_active: true },
-  { id: 'ga:avgSessionDuration', name: 'Avg. Session Duration', description: 'The average duration (in seconds) of users\' sessions.', section: 'Session', is_active: true },
-  { id: 'ga:organicSearches', name: 'Organic Searches', description: 'The number of organic searches happened in a session. This metric is search engine agnostic.', section: 'Traffic Sources', is_active: true },
-  { id: 'ga:impressions', name: 'Impressions', description: 'Total number of campaign impressions.', section: 'Adwords', is_active: true },
-  { id: 'ga:adClicks', name: 'Clicks', description: 'Total number of times users have clicked on an ad to reach the property.', section: 'Adwords', is_active: true },
-  { id: 'ga:adCost', name: 'Cost', description: 'Derived cost for the advertising campaign. Its currency is the one you set in the AdWords account.', section: 'Adwords', is_active: true },
-  { id: 'ga:CPM', name: 'CPM', description: 'Cost per thousand impressions.', section: 'Adwords', is_active: true },
-  { id: 'ga:CPC', name: 'CPC', description: 'Cost to advertiser per click.', section: 'Adwords', is_active: true },
-  { id: 'ga:CTR', name: 'CTR', description: 'Click-through-rate for the ad. This is equal to the number of clicks divided by the number of impressions for the ad (e.g., how many times users clicked on one of the ads where that ad appeared).', section: 'Adwords', is_active: true },
-  { id: 'ga:costPerTransaction', name: 'Cost per Transaction', description: 'The cost per transaction for the property.', section: 'Adwords', is_active: true },
-  { id: 'ga:costPerGoalConversion', name: 'Cost per Goal Conversion', description: 'The cost per goal conversion for the property.', section: 'Adwords', is_active: true },
-  { id: 'ga:costPerConversion', name: 'Cost per Conversion', description: 'The cost per conversion (including ecommerce and goal conversions) for the property.', section: 'Adwords', is_active: true },
-  { id: 'ga:RPC', name: 'RPC', description: 'RPC or revenue-per-click, the average revenue (from ecommerce sales and/or goal value) you received for each click on one of the search ads.', section: 'Adwords', is_active: true },
-  { id: 'ga:ROAS', name: 'ROAS', description: 'Return On Ad Spend (ROAS) is the total transaction revenue and goal value divided by derived advertising cost.', section: 'Adwords', is_active: true },
-  { id: 'ga:goalXXStarts', name: 'Goal XX Starts', description: 'The total number of starts for the requested goal number.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalStartsAll', name: 'Goal Starts', description: 'Total number of starts for all goals defined in the profile.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalXXCompletions', name: 'Goal XX Completions', description: 'The total number of completions for the requested goal number.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalCompletionsAll', name: 'Goal Completions', description: 'Total number of completions for all goals defined in the profile.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalXXValue', name: 'Goal XX Value', description: 'The total numeric value for the requested goal number.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalValueAll', name: 'Goal Value', description: 'Total numeric value for all goals defined in the profile.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalValuePerSession', name: 'Per Session Goal Value', description: 'The average goal value of a session.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalXXConversionRate', name: 'Goal XX Conversion Rate', description: 'Percentage of sessions resulting in a conversion to the requested goal number.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalConversionRateAll', name: 'Goal Conversion Rate', description: 'The percentage of sessions which resulted in a conversion to at least one of the goals.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalXXAbandons', name: 'Goal XX Abandoned Funnels', description: 'The number of times users started conversion activity on the requested goal number without actually completing it.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalAbandonsAll', name: 'Abandoned Funnels', description: 'The overall number of times users started goals without actually completing them.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalXXAbandonRate', name: 'Goal XX Abandonment Rate', description: 'The rate at which the requested goal number was abandoned.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:goalAbandonRateAll', name: 'Total Abandonment Rate', description: 'Goal abandonment rate.', section: 'Goal Conversions', is_active: true },
-  { id: 'ga:pageValue', name: 'Page Value', description: 'The average value of this page or set of pages, which is equal to (ga:transactionRevenue + ga:goalValueAll) / ga:uniquePageviews.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:entrances', name: 'Entrances', description: 'The number of entrances to the property measured as the first pageview in a session, typically used with landingPagePath.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:entranceRate', name: 'Entrances / Pageviews', description: 'The percentage of pageviews in which this page was the entrance.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:pageviews', name: 'Pageviews', description: 'The total number of pageviews for the property.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:pageviewsPerSession', name: 'Pages / Session', description: 'The average number of pages viewed during a session, including repeated views of a single page.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:contentGroupUniqueViewsXX', name: 'Unique Views XX', description: 'The number of unique content group views. Content group views in different sessions are counted as unique content group views. Both the pagePath and pageTitle are used to determine content group view uniqueness.', section: 'Content Grouping', is_active: true },
-  { id: 'ga:uniquePageviews', name: 'Unique Pageviews', description: 'Unique Pageviews is the number of sessions during which the specified page was viewed at least once. A unique pageview is counted for each page URL + page title combination.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:timeOnPage', name: 'Time on Page', description: 'Time (in seconds) users spent on a particular page, calculated by subtracting the initial view time for a particular page from the initial view time for a subsequent page. This metric does not apply to exit pages of the property.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:avgTimeOnPage', name: 'Avg. Time on Page', description: 'The average time users spent viewing this page or a set of pages.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:exits', name: 'Exits', description: 'The number of exits from the property.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:exitRate', name: '% Exit', description: 'The percentage of exits from the property that occurred out of the total pageviews.', section: 'Page Tracking', is_active: true },
-  { id: 'ga:searchResultViews', name: 'Results Pageviews', description: 'The number of times a search result page was viewed.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchUniques', name: 'Total Unique Searches', description: 'Total number of unique keywords from internal searches within a session. For example, if "shoes" was searched for 3 times in a session, it would be counted only once.', section: 'Internal Search', is_active: true },
-  { id: 'ga:avgSearchResultViews', name: 'Results Pageviews / Search', description: 'The average number of times people viewed a page as a result of a search.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchSessions', name: 'Sessions with Search', description: 'The total number of sessions that included an internal search.', section: 'Internal Search', is_active: true },
-  { id: 'ga:percentSessionsWithSearch', name: '% Sessions with Search', description: 'The percentage of sessions with search.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchDepth', name: 'Search Depth', description: 'The total number of subsequent page views made after a use of the site\'s internal search feature.', section: 'Internal Search', is_active: true },
-  { id: 'ga:avgSearchDepth', name: 'Average Search Depth', description: 'The average number of pages people viewed after performing a search.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchRefinements', name: 'Search Refinements', description: 'The total number of times a refinement (transition) occurs between internal keywords search within a session. For example, if the sequence of keywords is "shoes", "shoes", "pants", "pants", this metric will be one because the transition between "shoes" and "pants" is different.', section: 'Internal Search', is_active: true },
-  { id: 'ga:percentSearchRefinements', name: '% Search Refinements', description: 'The percentage of the number of times a refinement (i.e., transition) occurs between internal keywords search within a session.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchDuration', name: 'Time after Search', description: 'The session duration when the site\'s internal search feature is used.', section: 'Internal Search', is_active: true },
-  { id: 'ga:avgSearchDuration', name: 'Time after Search', description: 'The average time (in seconds) users, after searching, spent on the property.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchExits', name: 'Search Exits', description: 'The number of exits on the site that occurred following a search result from the site\'s internal search feature.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchExitRate', name: '% Search Exits', description: 'The percentage of searches that resulted in an immediate exit from the property.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchGoalXXConversionRate', name: 'Site Search Goal XX Conversion Rate', description: 'The percentage of search sessions (i.e., sessions that included at least one search) which resulted in a conversion to the requested goal number.', section: 'Internal Search', is_active: true },
-  { id: 'ga:searchGoalConversionRateAll', name: 'Site Search Goal Conversion Rate', description: 'The percentage of search sessions (i.e., sessions that included at least one search) which resulted in a conversion to at least one of the goals.', section: 'Internal Search', is_active: true },
-  { id: 'ga:goalValueAllPerSearch', name: 'Per Search Goal Value', description: 'The average goal value of a search.', section: 'Internal Search', is_active: true },
-  { id: 'ga:pageLoadTime', name: 'Page Load Time (ms)', description: 'Total time (in milliseconds), from pageview initiation (e.g., a click on a page link) to page load completion in the browser, the pages in the sample set take to load.', section: 'Site Speed', is_active: true },
-  { id: 'ga:pageLoadSample', name: 'Page Load Sample', description: 'The sample set (or count) of pageviews used to calculate the average page load time.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgPageLoadTime', name: 'Avg. Page Load Time (sec)', description: 'The average time (in seconds) pages from the sample set take to load, from initiation of the pageview (e.g., a click on a page link) to load completion in the browser.', section: 'Site Speed', is_active: true },
-  { id: 'ga:domainLookupTime', name: 'Domain Lookup Time (ms)', description: 'The total time (in milliseconds) all samples spent in DNS lookup for this page.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgDomainLookupTime', name: 'Avg. Domain Lookup Time (sec)', description: 'The average time (in seconds) spent in DNS lookup for this page.', section: 'Site Speed', is_active: true },
-  { id: 'ga:pageDownloadTime', name: 'Page Download Time (ms)', description: 'The total time (in milliseconds) to download this page among all samples.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgPageDownloadTime', name: 'Avg. Page Download Time (sec)', description: 'The average time (in seconds) to download this page.', section: 'Site Speed', is_active: true },
-  { id: 'ga:redirectionTime', name: 'Redirection Time (ms)', description: 'The total time (in milliseconds) all samples spent in redirects before fetching this page. If there are no redirects, this is 0.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgRedirectionTime', name: 'Avg. Redirection Time (sec)', description: 'The average time (in seconds) spent in redirects before fetching this page. If there are no redirects, this is 0.', section: 'Site Speed', is_active: true },
-  { id: 'ga:serverConnectionTime', name: 'Server Connection Time (ms)', description: 'Total time (in milliseconds) all samples spent in establishing a TCP connection to this page.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgServerConnectionTime', name: 'Avg. Server Connection Time (sec)', description: 'The average time (in seconds) spent in establishing a TCP connection to this page.', section: 'Site Speed', is_active: true },
-  { id: 'ga:serverResponseTime', name: 'Server Response Time (ms)', description: 'The total time (in milliseconds) the site\'s server takes to respond to users\' requests among all samples; this includes the network time from users\' locations to the server.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgServerResponseTime', name: 'Avg. Server Response Time (sec)', description: 'The average time (in seconds) the site\'s server takes to respond to users\' requests; this includes the network time from users\' locations to the server.', section: 'Site Speed', is_active: true },
-  { id: 'ga:speedMetricsSample', name: 'Speed Metrics Sample', description: 'The sample set (or count) of pageviews used to calculate the averages of site speed metrics. This metric is used in all site speed average calculations, including avgDomainLookupTime, avgPageDownloadTime, avgRedirectionTime, avgServerConnectionTime, and avgServerResponseTime.', section: 'Site Speed', is_active: true },
-  { id: 'ga:domInteractiveTime', name: 'Document Interactive Time (ms)', description: 'The time (in milliseconds), including the network time from users\' locations to the site\'s server, the browser takes to parse the document (DOMInteractive). At this time, users can interact with the Document Object Model even though it is not fully loaded.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgDomInteractiveTime', name: 'Avg. Document Interactive Time (sec)', description: 'The average time (in seconds), including the network time from users\' locations to the site\'s server, the browser takes to parse the document and execute deferred and parser-inserted scripts.', section: 'Site Speed', is_active: true },
-  { id: 'ga:domContentLoadedTime', name: 'Document Content Loaded Time (ms)', description: 'The time (in milliseconds), including the network time from users\' locations to the site\'s server, the browser takes to parse the document and execute deferred and parser-inserted scripts (DOMContentLoaded). When parsing of the document is finished, the Document Object Model (DOM) is ready, but the referenced style sheets, images, and subframes may not be finished loading. This is often the starting point of Javascript framework execution, e.g., JQuery\'s onready() callback.', section: 'Site Speed', is_active: true },
-  { id: 'ga:avgDomContentLoadedTime', name: 'Avg. Document Content Loaded Time (sec)', description: 'The average time (in seconds) the browser takes to parse the document.', section: 'Site Speed', is_active: true },
-  { id: 'ga:domLatencyMetricsSample', name: 'DOM Latency Metrics Sample', description: 'Sample set (or count) of pageviews used to calculate the averages for site speed DOM metrics. This metric is used to calculate ga:avgDomContentLoadedTime and ga:avgDomInteractiveTime.', section: 'Site Speed', is_active: true },
-  { id: 'ga:screenviews', name: 'Screen Views', description: 'The total number of screenviews.', section: 'App Tracking', is_active: true },
-  { id: 'ga:uniqueScreenviews', name: 'Unique Screen Views', description: 'The number of unique screen views. Screen views in different sessions are counted as separate screen views.', section: 'App Tracking', is_active: true },
-  { id: 'ga:screenviewsPerSession', name: 'Screens / Session', description: 'The average number of screenviews per session.', section: 'App Tracking', is_active: true },
-  { id: 'ga:timeOnScreen', name: 'Time on Screen', description: 'The time spent viewing the current screen.', section: 'App Tracking', is_active: true },
-  { id: 'ga:avgScreenviewDuration', name: 'Avg. Time on Screen', description: 'Average time (in seconds) users spent on a screen.', section: 'App Tracking', is_active: true },
-  { id: 'ga:totalEvents', name: 'Total Events', description: 'The total number of events for the profile, across all categories.', section: 'Event Tracking', is_active: true },
-  { id: 'ga:uniqueDimensionCombinations', name: 'Unique Dimension Combinations', description: 'Unique Dimension Combinations counts the number of unique dimension-value combinations for each dimension in a report. This lets you build combined (concatenated) dimensions post-processing, which allows for more flexible reporting without having to update your tracking implementation or use additional custom-dimension slots. For more information, see https://support.google.com/analytics/answer/7084499.', section: 'Session', is_active: true },
-  { id: 'ga:eventValue', name: 'Event Value', description: 'Total value of events for the profile.', section: 'Event Tracking', is_active: true },
-  { id: 'ga:avgEventValue', name: 'Avg. Value', description: 'The average value of an event.', section: 'Event Tracking', is_active: true },
-  { id: 'ga:sessionsWithEvent', name: 'Sessions with Event', description: 'The total number of sessions with events.', section: 'Event Tracking', is_active: true },
-  { id: 'ga:eventsPerSessionWithEvent', name: 'Events / Session with Event', description: 'The average number of events per session with event.', section: 'Event Tracking', is_active: true },
-  { id: 'ga:transactions', name: 'Transactions', description: 'The total number of transactions.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:transactionsPerSession', name: 'Ecommerce Conversion Rate', description: 'The average number of transactions in a session.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:transactionRevenue', name: 'Revenue', description: 'The total sale revenue (excluding shipping and tax) of the transaction.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:revenuePerTransaction', name: 'Average Order Value', description: 'The average revenue of an ecommerce transaction.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:transactionRevenuePerSession', name: 'Per Session Value', description: 'Average transaction revenue for a session.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:transactionShipping', name: 'Shipping', description: 'The total cost of shipping.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:transactionTax', name: 'Tax', description: 'Total tax for the transaction.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:totalValue', name: 'Total Value', description: 'Total value for the property (including total revenue and total goal value).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:itemQuantity', name: 'Quantity', description: 'Total number of items purchased. For example, if users purchase 2 frisbees and 5 tennis balls, this will be 7.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:uniquePurchases', name: 'Unique Purchases', description: 'The number of product sets purchased. For example, if users purchase 2 frisbees and 5 tennis balls from the site, this will be 2.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:revenuePerItem', name: 'Average Price', description: 'The average revenue per item.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:itemRevenue', name: 'Product Revenue', description: 'The total revenue from purchased product items.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:itemsPerPurchase', name: 'Average QTY', description: 'The average quantity of this item (or group of items) sold per purchase.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:localTransactionRevenue', name: 'Local Revenue', description: 'Transaction revenue in local currency.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:localTransactionShipping', name: 'Local Shipping', description: 'Transaction shipping cost in local currency.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:localTransactionTax', name: 'Local Tax', description: 'Transaction tax in local currency.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:localItemRevenue', name: 'Local Product Revenue', description: 'Product revenue in local currency.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:socialInteractions', name: 'Social Actions', description: 'The total number of social interactions.', section: 'Social Interactions', is_active: true },
-  { id: 'ga:uniqueSocialInteractions', name: 'Unique Social Actions', description: 'The number of sessions during which the specified social action(s) occurred at least once. This is based on the the unique combination of socialInteractionNetwork, socialInteractionAction, and socialInteractionTarget.', section: 'Social Interactions', is_active: true },
-  { id: 'ga:socialInteractionsPerSession', name: 'Actions Per Social Session', description: 'The number of social interactions per session.', section: 'Social Interactions', is_active: true },
-  { id: 'ga:userTimingValue', name: 'User Timing (ms)', description: 'Total number of milliseconds for user timing.', section: 'User Timings', is_active: true },
-  { id: 'ga:userTimingSample', name: 'User Timing Sample', description: 'The number of hits sent for a particular userTimingCategory, userTimingLabel, or userTimingVariable.', section: 'User Timings', is_active: true },
-  { id: 'ga:avgUserTimingValue', name: 'Avg. User Timing (sec)', description: 'The average elapsed time.', section: 'User Timings', is_active: true },
-  { id: 'ga:exceptions', name: 'Exceptions', description: 'The number of exceptions sent to Google Analytics.', section: 'Exceptions', is_active: true },
-  { id: 'ga:exceptionsPerScreenview', name: 'Exceptions / Screen', description: 'The number of exceptions thrown divided by the number of screenviews.', section: 'Exceptions', is_active: true },
-  { id: 'ga:fatalExceptions', name: 'Crashes', description: 'The number of exceptions where isFatal is set to true.', section: 'Exceptions', is_active: true },
-  { id: 'ga:fatalExceptionsPerScreenview', name: 'Crashes / Screen', description: 'The number of fatal exceptions thrown divided by the number of screenviews.', section: 'Exceptions', is_active: true },
-  { id: 'ga:metricXX', name: 'Custom Metric XX Value', description: 'The value of the requested custom metric, where XX refers to the number or index of the custom metric. Note that the data type of ga:metricXX can be INTEGER, CURRENCY, or TIME.', section: 'Custom Variables or Columns', is_active: true },
+export const metrics = [
+  {
+    id: "ga:users",
+    name: "Users",
+    description: "The total number of users for the requested time period.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:newUsers",
+    name: "New Users",
+    description:
+      "The number of users whose session was marked as a first-time session.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:percentNewSessions",
+    name: "% New Sessions",
+    description:
+      "The percentage of sessions by users who had never visited the property before.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:1dayUsers",
+    name: "1 Day Active Users",
+    description:
+      "Total number of 1-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 1-day period ending on the given date.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:7dayUsers",
+    name: "7 Day Active Users",
+    description:
+      "Total number of 7-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 7-day period ending on the given date.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:14dayUsers",
+    name: "14 Day Active Users",
+    description:
+      "Total number of 14-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 14-day period ending on the given date.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:30dayUsers",
+    name: "30 Day Active Users",
+    description:
+      "Total number of 30-day active users for each day in the requested time period. At least one of ga:nthDay, ga:date, or ga:day must be specified as a dimension to query this metric. For a given date, the returned value will be the total number of unique users for the 30-day period ending on the given date.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:sessions",
+    name: "Sessions",
+    description: "The total number of sessions.",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:bounces",
+    name: "Bounces",
+    description:
+      "The total number of single page (or single interaction hit) sessions for the property.",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:bounceRate",
+    name: "Bounce Rate",
+    description:
+      "The percentage of single-page session (i.e., session in which the person left the property from the first page).",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:sessionDuration",
+    name: "Session Duration",
+    description: "Total duration (in seconds) of users' sessions.",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:avgSessionDuration",
+    name: "Avg. Session Duration",
+    description: "The average duration (in seconds) of users' sessions.",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:organicSearches",
+    name: "Organic Searches",
+    description:
+      "The number of organic searches happened in a session. This metric is search engine agnostic.",
+    section: "Traffic Sources",
+    is_active: true,
+  },
+  {
+    id: "ga:impressions",
+    name: "Impressions",
+    description: "Total number of campaign impressions.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:adClicks",
+    name: "Clicks",
+    description:
+      "Total number of times users have clicked on an ad to reach the property.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:adCost",
+    name: "Cost",
+    description:
+      "Derived cost for the advertising campaign. Its currency is the one you set in the AdWords account.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:CPM",
+    name: "CPM",
+    description: "Cost per thousand impressions.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:CPC",
+    name: "CPC",
+    description: "Cost to advertiser per click.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:CTR",
+    name: "CTR",
+    description:
+      "Click-through-rate for the ad. This is equal to the number of clicks divided by the number of impressions for the ad (e.g., how many times users clicked on one of the ads where that ad appeared).",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:costPerTransaction",
+    name: "Cost per Transaction",
+    description: "The cost per transaction for the property.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:costPerGoalConversion",
+    name: "Cost per Goal Conversion",
+    description: "The cost per goal conversion for the property.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:costPerConversion",
+    name: "Cost per Conversion",
+    description:
+      "The cost per conversion (including ecommerce and goal conversions) for the property.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:RPC",
+    name: "RPC",
+    description:
+      "RPC or revenue-per-click, the average revenue (from ecommerce sales and/or goal value) you received for each click on one of the search ads.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:ROAS",
+    name: "ROAS",
+    description:
+      "Return On Ad Spend (ROAS) is the total transaction revenue and goal value divided by derived advertising cost.",
+    section: "Adwords",
+    is_active: true,
+  },
+  {
+    id: "ga:goalXXStarts",
+    name: "Goal XX Starts",
+    description: "The total number of starts for the requested goal number.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalStartsAll",
+    name: "Goal Starts",
+    description: "Total number of starts for all goals defined in the profile.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalXXCompletions",
+    name: "Goal XX Completions",
+    description:
+      "The total number of completions for the requested goal number.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalCompletionsAll",
+    name: "Goal Completions",
+    description:
+      "Total number of completions for all goals defined in the profile.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalXXValue",
+    name: "Goal XX Value",
+    description: "The total numeric value for the requested goal number.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalValueAll",
+    name: "Goal Value",
+    description: "Total numeric value for all goals defined in the profile.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalValuePerSession",
+    name: "Per Session Goal Value",
+    description: "The average goal value of a session.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalXXConversionRate",
+    name: "Goal XX Conversion Rate",
+    description:
+      "Percentage of sessions resulting in a conversion to the requested goal number.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalConversionRateAll",
+    name: "Goal Conversion Rate",
+    description:
+      "The percentage of sessions which resulted in a conversion to at least one of the goals.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalXXAbandons",
+    name: "Goal XX Abandoned Funnels",
+    description:
+      "The number of times users started conversion activity on the requested goal number without actually completing it.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalAbandonsAll",
+    name: "Abandoned Funnels",
+    description:
+      "The overall number of times users started goals without actually completing them.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalXXAbandonRate",
+    name: "Goal XX Abandonment Rate",
+    description: "The rate at which the requested goal number was abandoned.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:goalAbandonRateAll",
+    name: "Total Abandonment Rate",
+    description: "Goal abandonment rate.",
+    section: "Goal Conversions",
+    is_active: true,
+  },
+  {
+    id: "ga:pageValue",
+    name: "Page Value",
+    description:
+      "The average value of this page or set of pages, which is equal to (ga:transactionRevenue + ga:goalValueAll) / ga:uniquePageviews.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:entrances",
+    name: "Entrances",
+    description:
+      "The number of entrances to the property measured as the first pageview in a session, typically used with landingPagePath.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:entranceRate",
+    name: "Entrances / Pageviews",
+    description:
+      "The percentage of pageviews in which this page was the entrance.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:pageviews",
+    name: "Pageviews",
+    description: "The total number of pageviews for the property.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:pageviewsPerSession",
+    name: "Pages / Session",
+    description:
+      "The average number of pages viewed during a session, including repeated views of a single page.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:contentGroupUniqueViewsXX",
+    name: "Unique Views XX",
+    description:
+      "The number of unique content group views. Content group views in different sessions are counted as unique content group views. Both the pagePath and pageTitle are used to determine content group view uniqueness.",
+    section: "Content Grouping",
+    is_active: true,
+  },
+  {
+    id: "ga:uniquePageviews",
+    name: "Unique Pageviews",
+    description:
+      "Unique Pageviews is the number of sessions during which the specified page was viewed at least once. A unique pageview is counted for each page URL + page title combination.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:timeOnPage",
+    name: "Time on Page",
+    description:
+      "Time (in seconds) users spent on a particular page, calculated by subtracting the initial view time for a particular page from the initial view time for a subsequent page. This metric does not apply to exit pages of the property.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:avgTimeOnPage",
+    name: "Avg. Time on Page",
+    description:
+      "The average time users spent viewing this page or a set of pages.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:exits",
+    name: "Exits",
+    description: "The number of exits from the property.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:exitRate",
+    name: "% Exit",
+    description:
+      "The percentage of exits from the property that occurred out of the total pageviews.",
+    section: "Page Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:searchResultViews",
+    name: "Results Pageviews",
+    description: "The number of times a search result page was viewed.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchUniques",
+    name: "Total Unique Searches",
+    description:
+      'Total number of unique keywords from internal searches within a session. For example, if "shoes" was searched for 3 times in a session, it would be counted only once.',
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:avgSearchResultViews",
+    name: "Results Pageviews / Search",
+    description:
+      "The average number of times people viewed a page as a result of a search.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchSessions",
+    name: "Sessions with Search",
+    description:
+      "The total number of sessions that included an internal search.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:percentSessionsWithSearch",
+    name: "% Sessions with Search",
+    description: "The percentage of sessions with search.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchDepth",
+    name: "Search Depth",
+    description:
+      "The total number of subsequent page views made after a use of the site's internal search feature.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:avgSearchDepth",
+    name: "Average Search Depth",
+    description:
+      "The average number of pages people viewed after performing a search.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchRefinements",
+    name: "Search Refinements",
+    description:
+      'The total number of times a refinement (transition) occurs between internal keywords search within a session. For example, if the sequence of keywords is "shoes", "shoes", "pants", "pants", this metric will be one because the transition between "shoes" and "pants" is different.',
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:percentSearchRefinements",
+    name: "% Search Refinements",
+    description:
+      "The percentage of the number of times a refinement (i.e., transition) occurs between internal keywords search within a session.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchDuration",
+    name: "Time after Search",
+    description:
+      "The session duration when the site's internal search feature is used.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:avgSearchDuration",
+    name: "Time after Search",
+    description:
+      "The average time (in seconds) users, after searching, spent on the property.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchExits",
+    name: "Search Exits",
+    description:
+      "The number of exits on the site that occurred following a search result from the site's internal search feature.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchExitRate",
+    name: "% Search Exits",
+    description:
+      "The percentage of searches that resulted in an immediate exit from the property.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchGoalXXConversionRate",
+    name: "Site Search Goal XX Conversion Rate",
+    description:
+      "The percentage of search sessions (i.e., sessions that included at least one search) which resulted in a conversion to the requested goal number.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:searchGoalConversionRateAll",
+    name: "Site Search Goal Conversion Rate",
+    description:
+      "The percentage of search sessions (i.e., sessions that included at least one search) which resulted in a conversion to at least one of the goals.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:goalValueAllPerSearch",
+    name: "Per Search Goal Value",
+    description: "The average goal value of a search.",
+    section: "Internal Search",
+    is_active: true,
+  },
+  {
+    id: "ga:pageLoadTime",
+    name: "Page Load Time (ms)",
+    description:
+      "Total time (in milliseconds), from pageview initiation (e.g., a click on a page link) to page load completion in the browser, the pages in the sample set take to load.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:pageLoadSample",
+    name: "Page Load Sample",
+    description:
+      "The sample set (or count) of pageviews used to calculate the average page load time.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgPageLoadTime",
+    name: "Avg. Page Load Time (sec)",
+    description:
+      "The average time (in seconds) pages from the sample set take to load, from initiation of the pageview (e.g., a click on a page link) to load completion in the browser.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:domainLookupTime",
+    name: "Domain Lookup Time (ms)",
+    description:
+      "The total time (in milliseconds) all samples spent in DNS lookup for this page.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgDomainLookupTime",
+    name: "Avg. Domain Lookup Time (sec)",
+    description:
+      "The average time (in seconds) spent in DNS lookup for this page.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:pageDownloadTime",
+    name: "Page Download Time (ms)",
+    description:
+      "The total time (in milliseconds) to download this page among all samples.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgPageDownloadTime",
+    name: "Avg. Page Download Time (sec)",
+    description: "The average time (in seconds) to download this page.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:redirectionTime",
+    name: "Redirection Time (ms)",
+    description:
+      "The total time (in milliseconds) all samples spent in redirects before fetching this page. If there are no redirects, this is 0.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgRedirectionTime",
+    name: "Avg. Redirection Time (sec)",
+    description:
+      "The average time (in seconds) spent in redirects before fetching this page. If there are no redirects, this is 0.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:serverConnectionTime",
+    name: "Server Connection Time (ms)",
+    description:
+      "Total time (in milliseconds) all samples spent in establishing a TCP connection to this page.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgServerConnectionTime",
+    name: "Avg. Server Connection Time (sec)",
+    description:
+      "The average time (in seconds) spent in establishing a TCP connection to this page.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:serverResponseTime",
+    name: "Server Response Time (ms)",
+    description:
+      "The total time (in milliseconds) the site's server takes to respond to users' requests among all samples; this includes the network time from users' locations to the server.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgServerResponseTime",
+    name: "Avg. Server Response Time (sec)",
+    description:
+      "The average time (in seconds) the site's server takes to respond to users' requests; this includes the network time from users' locations to the server.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:speedMetricsSample",
+    name: "Speed Metrics Sample",
+    description:
+      "The sample set (or count) of pageviews used to calculate the averages of site speed metrics. This metric is used in all site speed average calculations, including avgDomainLookupTime, avgPageDownloadTime, avgRedirectionTime, avgServerConnectionTime, and avgServerResponseTime.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:domInteractiveTime",
+    name: "Document Interactive Time (ms)",
+    description:
+      "The time (in milliseconds), including the network time from users' locations to the site's server, the browser takes to parse the document (DOMInteractive). At this time, users can interact with the Document Object Model even though it is not fully loaded.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgDomInteractiveTime",
+    name: "Avg. Document Interactive Time (sec)",
+    description:
+      "The average time (in seconds), including the network time from users' locations to the site's server, the browser takes to parse the document and execute deferred and parser-inserted scripts.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:domContentLoadedTime",
+    name: "Document Content Loaded Time (ms)",
+    description:
+      "The time (in milliseconds), including the network time from users' locations to the site's server, the browser takes to parse the document and execute deferred and parser-inserted scripts (DOMContentLoaded). When parsing of the document is finished, the Document Object Model (DOM) is ready, but the referenced style sheets, images, and subframes may not be finished loading. This is often the starting point of Javascript framework execution, e.g., JQuery's onready() callback.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:avgDomContentLoadedTime",
+    name: "Avg. Document Content Loaded Time (sec)",
+    description:
+      "The average time (in seconds) the browser takes to parse the document.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:domLatencyMetricsSample",
+    name: "DOM Latency Metrics Sample",
+    description:
+      "Sample set (or count) of pageviews used to calculate the averages for site speed DOM metrics. This metric is used to calculate ga:avgDomContentLoadedTime and ga:avgDomInteractiveTime.",
+    section: "Site Speed",
+    is_active: true,
+  },
+  {
+    id: "ga:screenviews",
+    name: "Screen Views",
+    description: "The total number of screenviews.",
+    section: "App Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:uniqueScreenviews",
+    name: "Unique Screen Views",
+    description:
+      "The number of unique screen views. Screen views in different sessions are counted as separate screen views.",
+    section: "App Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:screenviewsPerSession",
+    name: "Screens / Session",
+    description: "The average number of screenviews per session.",
+    section: "App Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:timeOnScreen",
+    name: "Time on Screen",
+    description: "The time spent viewing the current screen.",
+    section: "App Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:avgScreenviewDuration",
+    name: "Avg. Time on Screen",
+    description: "Average time (in seconds) users spent on a screen.",
+    section: "App Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:totalEvents",
+    name: "Total Events",
+    description:
+      "The total number of events for the profile, across all categories.",
+    section: "Event Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:uniqueDimensionCombinations",
+    name: "Unique Dimension Combinations",
+    description:
+      "Unique Dimension Combinations counts the number of unique dimension-value combinations for each dimension in a report. This lets you build combined (concatenated) dimensions post-processing, which allows for more flexible reporting without having to update your tracking implementation or use additional custom-dimension slots. For more information, see https://support.google.com/analytics/answer/7084499.",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:eventValue",
+    name: "Event Value",
+    description: "Total value of events for the profile.",
+    section: "Event Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:avgEventValue",
+    name: "Avg. Value",
+    description: "The average value of an event.",
+    section: "Event Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:sessionsWithEvent",
+    name: "Sessions with Event",
+    description: "The total number of sessions with events.",
+    section: "Event Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:eventsPerSessionWithEvent",
+    name: "Events / Session with Event",
+    description: "The average number of events per session with event.",
+    section: "Event Tracking",
+    is_active: true,
+  },
+  {
+    id: "ga:transactions",
+    name: "Transactions",
+    description: "The total number of transactions.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:transactionsPerSession",
+    name: "Ecommerce Conversion Rate",
+    description: "The average number of transactions in a session.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:transactionRevenue",
+    name: "Revenue",
+    description:
+      "The total sale revenue (excluding shipping and tax) of the transaction.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:revenuePerTransaction",
+    name: "Average Order Value",
+    description: "The average revenue of an ecommerce transaction.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:transactionRevenuePerSession",
+    name: "Per Session Value",
+    description: "Average transaction revenue for a session.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:transactionShipping",
+    name: "Shipping",
+    description: "The total cost of shipping.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:transactionTax",
+    name: "Tax",
+    description: "Total tax for the transaction.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:totalValue",
+    name: "Total Value",
+    description:
+      "Total value for the property (including total revenue and total goal value).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:itemQuantity",
+    name: "Quantity",
+    description:
+      "Total number of items purchased. For example, if users purchase 2 frisbees and 5 tennis balls, this will be 7.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:uniquePurchases",
+    name: "Unique Purchases",
+    description:
+      "The number of product sets purchased. For example, if users purchase 2 frisbees and 5 tennis balls from the site, this will be 2.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:revenuePerItem",
+    name: "Average Price",
+    description: "The average revenue per item.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:itemRevenue",
+    name: "Product Revenue",
+    description: "The total revenue from purchased product items.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:itemsPerPurchase",
+    name: "Average QTY",
+    description:
+      "The average quantity of this item (or group of items) sold per purchase.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:localTransactionRevenue",
+    name: "Local Revenue",
+    description: "Transaction revenue in local currency.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:localTransactionShipping",
+    name: "Local Shipping",
+    description: "Transaction shipping cost in local currency.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:localTransactionTax",
+    name: "Local Tax",
+    description: "Transaction tax in local currency.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:localItemRevenue",
+    name: "Local Product Revenue",
+    description: "Product revenue in local currency.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:socialInteractions",
+    name: "Social Actions",
+    description: "The total number of social interactions.",
+    section: "Social Interactions",
+    is_active: true,
+  },
+  {
+    id: "ga:uniqueSocialInteractions",
+    name: "Unique Social Actions",
+    description:
+      "The number of sessions during which the specified social action(s) occurred at least once. This is based on the the unique combination of socialInteractionNetwork, socialInteractionAction, and socialInteractionTarget.",
+    section: "Social Interactions",
+    is_active: true,
+  },
+  {
+    id: "ga:socialInteractionsPerSession",
+    name: "Actions Per Social Session",
+    description: "The number of social interactions per session.",
+    section: "Social Interactions",
+    is_active: true,
+  },
+  {
+    id: "ga:userTimingValue",
+    name: "User Timing (ms)",
+    description: "Total number of milliseconds for user timing.",
+    section: "User Timings",
+    is_active: true,
+  },
+  {
+    id: "ga:userTimingSample",
+    name: "User Timing Sample",
+    description:
+      "The number of hits sent for a particular userTimingCategory, userTimingLabel, or userTimingVariable.",
+    section: "User Timings",
+    is_active: true,
+  },
+  {
+    id: "ga:avgUserTimingValue",
+    name: "Avg. User Timing (sec)",
+    description: "The average elapsed time.",
+    section: "User Timings",
+    is_active: true,
+  },
+  {
+    id: "ga:exceptions",
+    name: "Exceptions",
+    description: "The number of exceptions sent to Google Analytics.",
+    section: "Exceptions",
+    is_active: true,
+  },
+  {
+    id: "ga:exceptionsPerScreenview",
+    name: "Exceptions / Screen",
+    description:
+      "The number of exceptions thrown divided by the number of screenviews.",
+    section: "Exceptions",
+    is_active: true,
+  },
+  {
+    id: "ga:fatalExceptions",
+    name: "Crashes",
+    description: "The number of exceptions where isFatal is set to true.",
+    section: "Exceptions",
+    is_active: true,
+  },
+  {
+    id: "ga:fatalExceptionsPerScreenview",
+    name: "Crashes / Screen",
+    description:
+      "The number of fatal exceptions thrown divided by the number of screenviews.",
+    section: "Exceptions",
+    is_active: true,
+  },
+  {
+    id: "ga:metricXX",
+    name: "Custom Metric XX Value",
+    description:
+      "The value of the requested custom metric, where XX refers to the number or index of the custom metric. Note that the data type of ga:metricXX can be INTEGER, CURRENCY, or TIME.",
+    section: "Custom Variables or Columns",
+    is_active: true,
+  },
   // { id: 'ga:dcmFloodlightQuantity', name: 'DFA Conversions', description: 'The number of DCM Floodlight conversions (Analytics 360 only).', section: 'DoubleClick Campaign Manager', is_active: true },
   // { id: 'ga:dcmFloodlightRevenue', name: 'DFA Revenue', description: 'DCM Floodlight revenue (Analytics 360 only).', section: 'DoubleClick Campaign Manager', is_active: true },
-  { id: 'ga:adsenseRevenue', name: 'AdSense Revenue', description: 'The total revenue from AdSense ads.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseAdUnitsViewed', name: 'AdSense Ad Units Viewed', description: 'The number of AdSense ad units viewed. An ad unit is a set of ads displayed as a result of one piece of the AdSense ad code. For details, see https://support.google.com/adsense/answer/32715?hl=en.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseAdsViewed', name: 'AdSense Impressions', description: 'The number of AdSense ads viewed. Multiple ads can be displayed within an ad Unit.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseAdsClicks', name: 'AdSense Ads Clicked', description: 'The number of times AdSense ads on the site were clicked.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsensePageImpressions', name: 'AdSense Page Impressions', description: 'The number of pageviews during which an AdSense ad was displayed. A page impression can have multiple ad Units.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseCTR', name: 'AdSense CTR', description: 'The percentage of page impressions resulted in a click on an AdSense ad.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseECPM', name: 'AdSense eCPM', description: 'The estimated cost per thousand page impressions. It is the AdSense Revenue per 1,000 page impressions.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseExits', name: 'AdSense Exits', description: 'The number of sessions ended due to a user clicking on an AdSense ad.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseViewableImpressionPercent', name: 'AdSense Viewable Impression %', description: 'The percentage of viewable impressions.', section: 'Adsense', is_active: true },
-  { id: 'ga:adsenseCoverage', name: 'AdSense Coverage', description: 'The percentage of ad requests that returned at least one ad.', section: 'Adsense', is_active: true },
+  {
+    id: "ga:adsenseRevenue",
+    name: "AdSense Revenue",
+    description: "The total revenue from AdSense ads.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseAdUnitsViewed",
+    name: "AdSense Ad Units Viewed",
+    description:
+      "The number of AdSense ad units viewed. An ad unit is a set of ads displayed as a result of one piece of the AdSense ad code. For details, see https://support.google.com/adsense/answer/32715?hl=en.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseAdsViewed",
+    name: "AdSense Impressions",
+    description:
+      "The number of AdSense ads viewed. Multiple ads can be displayed within an ad Unit.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseAdsClicks",
+    name: "AdSense Ads Clicked",
+    description: "The number of times AdSense ads on the site were clicked.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsensePageImpressions",
+    name: "AdSense Page Impressions",
+    description:
+      "The number of pageviews during which an AdSense ad was displayed. A page impression can have multiple ad Units.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseCTR",
+    name: "AdSense CTR",
+    description:
+      "The percentage of page impressions resulted in a click on an AdSense ad.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseECPM",
+    name: "AdSense eCPM",
+    description:
+      "The estimated cost per thousand page impressions. It is the AdSense Revenue per 1,000 page impressions.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseExits",
+    name: "AdSense Exits",
+    description:
+      "The number of sessions ended due to a user clicking on an AdSense ad.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseViewableImpressionPercent",
+    name: "AdSense Viewable Impression %",
+    description: "The percentage of viewable impressions.",
+    section: "Adsense",
+    is_active: true,
+  },
+  {
+    id: "ga:adsenseCoverage",
+    name: "AdSense Coverage",
+    description: "The percentage of ad requests that returned at least one ad.",
+    section: "Adsense",
+    is_active: true,
+  },
   // { id: 'ga:adxImpressions', name: 'AdX Impressions', description: 'An Ad Exchange ad impression is reported whenever an individual ad is displayed on the website. For example, if a page with two ad units is viewed once, we\'ll display two impressions.', section: 'Ad Exchange', is_active: true },
   // { id: 'ga:adxCoverage', name: 'AdX Coverage', description: 'Coverage is the percentage of ad requests that returned at least one ad. Generally, coverage can help identify sites where the Ad Exchange account isn\'t able to provide targeted ads. (Ad Impressions / Total Ad Requests) * 100', section: 'Ad Exchange', is_active: true },
   // { id: 'ga:adxMonetizedPageviews', name: 'AdX Monetized Pageviews', description: 'This measures the total number of pageviews on the property that were shown with an ad from the linked Ad Exchange account. Note that a single page can have multiple ad units.', section: 'Ad Exchange', is_active: true },
@@ -665,26 +2816,157 @@ export const metrics = [ { id: 'ga:users', name: 'Users', description: 'The tota
   // { id: 'ga:backfillRevenue', name: 'DFP Backfill Revenue', description: 'The total estimated revenue from backfill ads. If AdSense and Ad Exchange are both providing backfill ads, this metric is the sum of the two backfill accounts (DFP linking enabled and Hide DFP Revenue not enabled).', section: 'DoubleClick for Publishers Backfill', is_active: true },
   // { id: 'ga:backfillRevenuePer1000Sessions', name: 'DFP Backfill Revenue / 1000 Sessions', description: 'The total estimated revenue from backfill ads per 1,000 Analytics sessions. Note that this metric is based on sessions to the site and not ad impressions. If both AdSense and Ad Exchange are providing backfill ads, this metric is the sum of the two backfill accounts (DFP linking enabled and Hide DFP Revenue not enabled).', section: 'DoubleClick for Publishers Backfill', is_active: true },
   // { id: 'ga:backfillECPM', name: 'DFP Backfill eCPM', description: 'The effective cost per thousand pageviews. It is the DFP Backfill Revenue per 1,000 pageviews. If both AdSense and Ad Exchange are providing backfill ads, this metric is the sum of the two backfill accounts (DFP linking enabled and Hide DFP Revenue not enabled).', section: 'DoubleClick for Publishers Backfill', is_active: true },
-  { id: 'ga:buyToDetailRate', name: 'Buy-to-Detail Rate', description: 'Unique purchases divided by views of product detail pages (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:calcMetric_<NAME>', name: 'Calculated Metric', description: 'The value of the requested calculated metric, where <NAME> refers to the user-defined name of the calculated metric. Note that the data type of ga:calcMetric_<NAME> can be FLOAT, INTEGER, CURRENCY, TIME, or PERCENT. For details, see https://support.google.com/analytics/answer/6121409.', section: 'Custom Variables or Columns', is_active: true },
-  { id: 'ga:cartToDetailRate', name: 'Cart-to-Detail Rate', description: 'Product adds divided by views of product details (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:cohortActiveUsers', name: 'Users', description: 'This metric is relevant in the context of ga:cohortNthDay/ga:cohortNthWeek/ga:cohortNthMonth. It indicates the number of users in the cohort who are active in the time window corresponding to the cohort nth day/week/month. For example, for ga:cohortNthWeek = 1, number of users (in the cohort) who are active in week 1. If a request doesn\'t have any of ga:cohortNthDay/ga:cohortNthWeek/ga:cohortNthMonth, this metric will have the same value as ga:cohortTotalUsers.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortAppviewsPerUser', name: 'Appviews per User', description: 'App views per user for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortAppviewsPerUserWithLifetimeCriteria', name: 'Appviews Per User (LTV)', description: 'App views per user for the acquisition dimension for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortGoalCompletionsPerUser', name: 'Goal Completions per User', description: 'Goal completions per user for the acquisition dimension for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortGoalCompletionsPerUserWithLifetimeCriteria', name: 'Goal Completions Per User (LTV)', description: 'Goal completions per user for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortPageviewsPerUser', name: 'Pageviews per User', description: 'Pageviews per user for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortPageviewsPerUserWithLifetimeCriteria', name: 'Pageviews Per User (LTV)', description: 'Pageviews per user for the acquisition dimension for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortRetentionRate', name: 'User Retention', description: 'Cohort retention rate.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortRevenuePerUser', name: 'Revenue per User', description: 'Revenue per user for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortRevenuePerUserWithLifetimeCriteria', name: 'Revenue Per User (LTV)', description: 'Revenue per user for the acquisition dimension for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortSessionDurationPerUser', name: 'Session Duration per User', description: 'Session duration per user for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortSessionDurationPerUserWithLifetimeCriteria', name: 'Session Duration Per User (LTV)', description: 'Session duration per user for the acquisition dimension for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortSessionsPerUser', name: 'Sessions per User', description: 'Sessions per user for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortSessionsPerUserWithLifetimeCriteria', name: 'Sessions Per User (LTV)', description: 'Sessions per user for the acquisition dimension for a cohort.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortTotalUsers', name: 'Total Users', description: 'Number of users belonging to the cohort, also known as cohort size.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:cohortTotalUsersWithLifetimeCriteria', name: 'Users', description: 'This is relevant in the context of a request which has the dimensions ga:acquisitionTrafficChannel/ga:acquisitionSource/ga:acquisitionMedium/ga:acquisitionCampaign. It represents the number of users in the cohorts who are acquired through the current channel/source/medium/campaign. For example, for ga:acquisitionTrafficChannel=Direct, it represents the number users in the cohort, who were acquired directly. If none of these mentioned dimensions are present, then its value is equal to ga:cohortTotalUsers.', section: 'Lifetime Value and Cohorts', is_active: true },
-  { id: 'ga:correlationScore', name: 'Correlation Score', description: 'Correlation Score for related products.', section: 'Related Products', is_active: true },
+  {
+    id: "ga:buyToDetailRate",
+    name: "Buy-to-Detail Rate",
+    description:
+      "Unique purchases divided by views of product detail pages (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:calcMetric_<NAME>",
+    name: "Calculated Metric",
+    description:
+      "The value of the requested calculated metric, where <NAME> refers to the user-defined name of the calculated metric. Note that the data type of ga:calcMetric_<NAME> can be FLOAT, INTEGER, CURRENCY, TIME, or PERCENT. For details, see https://support.google.com/analytics/answer/6121409.",
+    section: "Custom Variables or Columns",
+    is_active: true,
+  },
+  {
+    id: "ga:cartToDetailRate",
+    name: "Cart-to-Detail Rate",
+    description:
+      "Product adds divided by views of product details (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortActiveUsers",
+    name: "Users",
+    description:
+      "This metric is relevant in the context of ga:cohortNthDay/ga:cohortNthWeek/ga:cohortNthMonth. It indicates the number of users in the cohort who are active in the time window corresponding to the cohort nth day/week/month. For example, for ga:cohortNthWeek = 1, number of users (in the cohort) who are active in week 1. If a request doesn't have any of ga:cohortNthDay/ga:cohortNthWeek/ga:cohortNthMonth, this metric will have the same value as ga:cohortTotalUsers.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortAppviewsPerUser",
+    name: "Appviews per User",
+    description: "App views per user for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortAppviewsPerUserWithLifetimeCriteria",
+    name: "Appviews Per User (LTV)",
+    description:
+      "App views per user for the acquisition dimension for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortGoalCompletionsPerUser",
+    name: "Goal Completions per User",
+    description:
+      "Goal completions per user for the acquisition dimension for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortGoalCompletionsPerUserWithLifetimeCriteria",
+    name: "Goal Completions Per User (LTV)",
+    description: "Goal completions per user for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortPageviewsPerUser",
+    name: "Pageviews per User",
+    description: "Pageviews per user for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortPageviewsPerUserWithLifetimeCriteria",
+    name: "Pageviews Per User (LTV)",
+    description:
+      "Pageviews per user for the acquisition dimension for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortRetentionRate",
+    name: "User Retention",
+    description: "Cohort retention rate.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortRevenuePerUser",
+    name: "Revenue per User",
+    description: "Revenue per user for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortRevenuePerUserWithLifetimeCriteria",
+    name: "Revenue Per User (LTV)",
+    description: "Revenue per user for the acquisition dimension for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortSessionDurationPerUser",
+    name: "Session Duration per User",
+    description: "Session duration per user for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortSessionDurationPerUserWithLifetimeCriteria",
+    name: "Session Duration Per User (LTV)",
+    description:
+      "Session duration per user for the acquisition dimension for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortSessionsPerUser",
+    name: "Sessions per User",
+    description: "Sessions per user for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortSessionsPerUserWithLifetimeCriteria",
+    name: "Sessions Per User (LTV)",
+    description:
+      "Sessions per user for the acquisition dimension for a cohort.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortTotalUsers",
+    name: "Total Users",
+    description:
+      "Number of users belonging to the cohort, also known as cohort size.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:cohortTotalUsersWithLifetimeCriteria",
+    name: "Users",
+    description:
+      "This is relevant in the context of a request which has the dimensions ga:acquisitionTrafficChannel/ga:acquisitionSource/ga:acquisitionMedium/ga:acquisitionCampaign. It represents the number of users in the cohorts who are acquired through the current channel/source/medium/campaign. For example, for ga:acquisitionTrafficChannel=Direct, it represents the number users in the cohort, who were acquired directly. If none of these mentioned dimensions are present, then its value is equal to ga:cohortTotalUsers.",
+    section: "Lifetime Value and Cohorts",
+    is_active: true,
+  },
+  {
+    id: "ga:correlationScore",
+    name: "Correlation Score",
+    description: "Correlation Score for related products.",
+    section: "Related Products",
+    is_active: true,
+  },
   // { id: 'ga:dbmCPA', name: 'DBM eCPA', description: 'DBM Revenue eCPA (Analytics 360 only, requires integration with DBM).', section: 'DoubleClick Bid Manager', is_active: true },
   // { id: 'ga:dbmCPC', name: 'DBM eCPC', description: 'DBM Revenue eCPC (Analytics 360 only, requires integration with DBM).', section: 'DoubleClick Bid Manager', is_active: true },
   // { id: 'ga:dbmCPM', name: 'DBM eCPM', description: 'DBM Revenue eCPM (Analytics 360 only, requires integration with DBM).', section: 'DoubleClick Bid Manager', is_active: true },
@@ -709,68 +2991,383 @@ export const metrics = [ { id: 'ga:users', name: 'Users', description: 'The tota
   // { id: 'ga:dsProfit', name: 'DS Profit', description: 'DS Profie (Analytics 360 only, requires integration with DS).', section: 'DoubleClick Search', is_active: true },
   // { id: 'ga:dsReturnOnAdSpend', name: 'DS ROAS', description: 'DS Return On Ad Spend (Analytics 360 only, requires integration with DS).', section: 'DoubleClick Search', is_active: true },
   // { id: 'ga:dsRevenuePerClick', name: 'DS RPC', description: 'DS Revenue Per Click (Analytics 360 only, requires integration with DS).', section: 'DoubleClick Search', is_active: true },
-  { id: 'ga:hits', name: 'Hits', description: 'Total number of hits for the view (profile). This metric sums all hit types, including pageview, custom event, ecommerce, and other types. Because this metric is based on the view (profile), not on the property, it is not the same as the property\'s hit volume.', section: 'Session', is_active: true },
-  { id: 'ga:internalPromotionCTR', name: 'Internal Promotion CTR', description: 'The rate at which users clicked through to view the internal promotion (ga:internalPromotionClicks / ga:internalPromotionViews) - (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:internalPromotionClicks', name: 'Internal Promotion Clicks', description: 'The number of clicks on an internal promotion (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:internalPromotionViews', name: 'Internal Promotion Views', description: 'The number of views of an internal promotion (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:localProductRefundAmount', name: 'Local Product Refund Amount', description: 'Refund amount in local currency for a given product (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:localRefundAmount', name: 'Local Refund Amount', description: 'Total refund amount in local currency for the transaction (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productAddsToCart', name: 'Product Adds To Cart', description: 'Number of times the product was added to the shopping cart (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productCheckouts', name: 'Product Checkouts', description: 'Number of times the product was included in the check-out process (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productDetailViews', name: 'Product Detail Views', description: 'Number of times users viewed the product-detail page (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productListCTR', name: 'Product List CTR', description: 'The rate at which users clicked through on the product in a product list (ga:productListClicks / ga:productListViews) - (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productListClicks', name: 'Product List Clicks', description: 'Number of times users clicked the product when it appeared in the product list (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productListViews', name: 'Product List Views', description: 'Number of times the product appeared in a product list (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productRefundAmount', name: 'Product Refund Amount', description: 'Total refund amount associated with the product (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productRefunds', name: 'Product Refunds', description: 'Number of times a refund was issued for the product (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productRemovesFromCart', name: 'Product Removes From Cart', description: 'Number of times the product was removed from the shopping cart (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:productRevenuePerPurchase', name: 'Product Revenue per Purchase', description: 'Average product revenue per purchase (commonly used with Product Coupon Code) (ga:itemRevenue / ga:uniquePurchases) - (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:quantityAddedToCart', name: 'Quantity Added To Cart', description: 'Number of product units added to the shopping cart (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:quantityCheckedOut', name: 'Quantity Checked Out', description: 'Number of product units included in check out (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:quantityRefunded', name: 'Quantity Refunded', description: 'Number of product units refunded (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:quantityRemovedFromCart', name: 'Quantity Removed From Cart', description: 'Number of product units removed from a shopping cart (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:queryProductQuantity', name: 'Queried Product Quantity', description: 'Quantity of the product being queried.', section: 'Related Products', is_active: true },
-  { id: 'ga:refundAmount', name: 'Refund Amount', description: 'Currency amount refunded for a transaction (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:relatedProductQuantity', name: 'Related Product Quantity', description: 'Quantity of the related product.', section: 'Related Products', is_active: true },
-  { id: 'ga:revenuePerUser', name: 'Revenue per User', description: 'The total sale revenue (excluding shipping and tax) of the transaction divided by the total number of users.', section: 'Ecommerce', is_active: true },
-  { id: 'ga:sessionsPerUser', name: 'Number of Sessions per User', description: 'The total number of sessions divided by the total number of users.', section: 'User', is_active: true },
-  { id: 'ga:totalRefunds', name: 'Refunds', description: 'Number of refunds that have been issued (Enhanced Ecommerce).', section: 'Ecommerce', is_active: true },
-  { id: 'ga:transactionsPerUser', name: 'Transactions per User', description: 'Total number of transactions divided by total number of users.', section: 'Ecommerce', is_active: true } ];
+  {
+    id: "ga:hits",
+    name: "Hits",
+    description:
+      "Total number of hits for the view (profile). This metric sums all hit types, including pageview, custom event, ecommerce, and other types. Because this metric is based on the view (profile), not on the property, it is not the same as the property's hit volume.",
+    section: "Session",
+    is_active: true,
+  },
+  {
+    id: "ga:internalPromotionCTR",
+    name: "Internal Promotion CTR",
+    description:
+      "The rate at which users clicked through to view the internal promotion (ga:internalPromotionClicks / ga:internalPromotionViews) - (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:internalPromotionClicks",
+    name: "Internal Promotion Clicks",
+    description:
+      "The number of clicks on an internal promotion (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:internalPromotionViews",
+    name: "Internal Promotion Views",
+    description:
+      "The number of views of an internal promotion (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:localProductRefundAmount",
+    name: "Local Product Refund Amount",
+    description:
+      "Refund amount in local currency for a given product (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:localRefundAmount",
+    name: "Local Refund Amount",
+    description:
+      "Total refund amount in local currency for the transaction (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productAddsToCart",
+    name: "Product Adds To Cart",
+    description:
+      "Number of times the product was added to the shopping cart (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productCheckouts",
+    name: "Product Checkouts",
+    description:
+      "Number of times the product was included in the check-out process (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productDetailViews",
+    name: "Product Detail Views",
+    description:
+      "Number of times users viewed the product-detail page (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productListCTR",
+    name: "Product List CTR",
+    description:
+      "The rate at which users clicked through on the product in a product list (ga:productListClicks / ga:productListViews) - (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productListClicks",
+    name: "Product List Clicks",
+    description:
+      "Number of times users clicked the product when it appeared in the product list (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productListViews",
+    name: "Product List Views",
+    description:
+      "Number of times the product appeared in a product list (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productRefundAmount",
+    name: "Product Refund Amount",
+    description:
+      "Total refund amount associated with the product (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productRefunds",
+    name: "Product Refunds",
+    description:
+      "Number of times a refund was issued for the product (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productRemovesFromCart",
+    name: "Product Removes From Cart",
+    description:
+      "Number of times the product was removed from the shopping cart (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:productRevenuePerPurchase",
+    name: "Product Revenue per Purchase",
+    description:
+      "Average product revenue per purchase (commonly used with Product Coupon Code) (ga:itemRevenue / ga:uniquePurchases) - (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:quantityAddedToCart",
+    name: "Quantity Added To Cart",
+    description:
+      "Number of product units added to the shopping cart (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:quantityCheckedOut",
+    name: "Quantity Checked Out",
+    description:
+      "Number of product units included in check out (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:quantityRefunded",
+    name: "Quantity Refunded",
+    description: "Number of product units refunded (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:quantityRemovedFromCart",
+    name: "Quantity Removed From Cart",
+    description:
+      "Number of product units removed from a shopping cart (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:queryProductQuantity",
+    name: "Queried Product Quantity",
+    description: "Quantity of the product being queried.",
+    section: "Related Products",
+    is_active: true,
+  },
+  {
+    id: "ga:refundAmount",
+    name: "Refund Amount",
+    description:
+      "Currency amount refunded for a transaction (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:relatedProductQuantity",
+    name: "Related Product Quantity",
+    description: "Quantity of the related product.",
+    section: "Related Products",
+    is_active: true,
+  },
+  {
+    id: "ga:revenuePerUser",
+    name: "Revenue per User",
+    description:
+      "The total sale revenue (excluding shipping and tax) of the transaction divided by the total number of users.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:sessionsPerUser",
+    name: "Number of Sessions per User",
+    description:
+      "The total number of sessions divided by the total number of users.",
+    section: "User",
+    is_active: true,
+  },
+  {
+    id: "ga:totalRefunds",
+    name: "Refunds",
+    description:
+      "Number of refunds that have been issued (Enhanced Ecommerce).",
+    section: "Ecommerce",
+    is_active: true,
+  },
+  {
+    id: "ga:transactionsPerUser",
+    name: "Transactions per User",
+    description:
+      "Total number of transactions divided by total number of users.",
+    section: "Ecommerce",
+    is_active: true,
+  },
+];
 
-export const segments = [ { id: 'gaid::-1', name: 'All Users', description: '', is_active: true },
-  { id: 'gaid::-2', name: 'New Users', description: 'sessions::condition::ga:userType==New Visitor', is_active: true },
-  { id: 'gaid::-3', name: 'Returning Users', description: 'sessions::condition::ga:userType==Returning Visitor', is_active: true },
-  { id: 'gaid::-4', name: 'Paid Traffic', description: 'sessions::condition::ga:medium=~^(cpc|ppc|cpa|cpm|cpv|cpp)$', is_active: true },
-  { id: 'gaid::-5', name: 'Organic Traffic', description: 'sessions::condition::ga:medium==organic', is_active: true },
-  { id: 'gaid::-6', name: 'Search Traffic', description: 'sessions::condition::ga:medium=~^(cpc|ppc|cpa|cpm|cpv|cpp|organic)$', is_active: true },
-  { id: 'gaid::-7', name: 'Direct Traffic', description: 'sessions::condition::ga:medium==(none)', is_active: true },
-  { id: 'gaid::-8', name: 'Referral Traffic', description: 'sessions::condition::ga:medium==referral', is_active: true },
-  { id: 'gaid::-9', name: 'Sessions with Conversions', description: 'sessions::condition::ga:goalCompletionsAll>0', is_active: true },
-  { id: 'gaid::-10', name: 'Sessions with Transactions', description: 'sessions::condition::ga:transactions>0', is_active: true },
-  { id: 'gaid::-11', name: 'Mobile and Tablet Traffic', description: 'sessions::condition::ga:deviceCategory==mobile,ga:deviceCategory==tablet', is_active: true },
-  { id: 'gaid::-12', name: 'Non-bounce Sessions', description: 'sessions::condition::ga:bounces==0', is_active: true },
-  { id: 'gaid::-13', name: 'Tablet Traffic', description: 'sessions::condition::ga:deviceCategory==tablet', is_active: true },
-  { id: 'gaid::-14', name: 'Mobile Traffic', description: 'sessions::condition::ga:deviceCategory==mobile', is_active: true },
-  { id: 'gaid::-15', name: 'Tablet and Desktop Traffic', description: 'sessions::condition::ga:deviceCategory==tablet,ga:deviceCategory==desktop', is_active: true },
-  { id: 'gaid::-16', name: 'Android Traffic', description: 'sessions::condition::ga:operatingSystem==Android', is_active: true },
-  { id: 'gaid::-17', name: 'iOS Traffic', description: 'sessions::condition::ga:operatingSystem=~^(iOS|iPad|iPhone|iPod)$', is_active: true },
-  { id: 'gaid::-18', name: 'Other Traffic (Neither iOS nor Android)', description: 'sessions::condition::ga:operatingSystem!~^(Android|iOS|iPad|iPhone|iPod)$', is_active: true },
-  { id: 'gaid::-19', name: 'Bounced Sessions', description: 'sessions::condition::ga:bounces>0', is_active: true },
-  { id: 'gaid::-100', name: 'Single Session Users', description: 'users::condition::ga:sessions==1', is_active: true },
-  { id: 'gaid::-101', name: 'Multi-session Users', description: 'users::condition::ga:sessions>1', is_active: true },
-  { id: 'gaid::-102', name: 'Converters', description: 'users::condition::ga:goalCompletionsAll>0,ga:transactions>0', is_active: true },
-  { id: 'gaid::-103', name: 'Non-Converters', description: 'users::condition::ga:goalCompletionsAll==0;ga:transactions==0', is_active: true },
-  { id: 'gaid::-104', name: 'Made a Purchase', description: 'users::condition::ga:transactions>0', is_active: true },
-  { id: 'gaid::-105', name: 'Performed Site Search', description: 'users::sequence::ga:searchKeyword!~^$|^\\(not set\\)$', is_active: true } ];
+export const segments = [
+  { id: "gaid::-1", name: "All Users", description: "", is_active: true },
+  {
+    id: "gaid::-2",
+    name: "New Users",
+    description: "sessions::condition::ga:userType==New Visitor",
+    is_active: true,
+  },
+  {
+    id: "gaid::-3",
+    name: "Returning Users",
+    description: "sessions::condition::ga:userType==Returning Visitor",
+    is_active: true,
+  },
+  {
+    id: "gaid::-4",
+    name: "Paid Traffic",
+    description: "sessions::condition::ga:medium=~^(cpc|ppc|cpa|cpm|cpv|cpp)$",
+    is_active: true,
+  },
+  {
+    id: "gaid::-5",
+    name: "Organic Traffic",
+    description: "sessions::condition::ga:medium==organic",
+    is_active: true,
+  },
+  {
+    id: "gaid::-6",
+    name: "Search Traffic",
+    description:
+      "sessions::condition::ga:medium=~^(cpc|ppc|cpa|cpm|cpv|cpp|organic)$",
+    is_active: true,
+  },
+  {
+    id: "gaid::-7",
+    name: "Direct Traffic",
+    description: "sessions::condition::ga:medium==(none)",
+    is_active: true,
+  },
+  {
+    id: "gaid::-8",
+    name: "Referral Traffic",
+    description: "sessions::condition::ga:medium==referral",
+    is_active: true,
+  },
+  {
+    id: "gaid::-9",
+    name: "Sessions with Conversions",
+    description: "sessions::condition::ga:goalCompletionsAll>0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-10",
+    name: "Sessions with Transactions",
+    description: "sessions::condition::ga:transactions>0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-11",
+    name: "Mobile and Tablet Traffic",
+    description:
+      "sessions::condition::ga:deviceCategory==mobile,ga:deviceCategory==tablet",
+    is_active: true,
+  },
+  {
+    id: "gaid::-12",
+    name: "Non-bounce Sessions",
+    description: "sessions::condition::ga:bounces==0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-13",
+    name: "Tablet Traffic",
+    description: "sessions::condition::ga:deviceCategory==tablet",
+    is_active: true,
+  },
+  {
+    id: "gaid::-14",
+    name: "Mobile Traffic",
+    description: "sessions::condition::ga:deviceCategory==mobile",
+    is_active: true,
+  },
+  {
+    id: "gaid::-15",
+    name: "Tablet and Desktop Traffic",
+    description:
+      "sessions::condition::ga:deviceCategory==tablet,ga:deviceCategory==desktop",
+    is_active: true,
+  },
+  {
+    id: "gaid::-16",
+    name: "Android Traffic",
+    description: "sessions::condition::ga:operatingSystem==Android",
+    is_active: true,
+  },
+  {
+    id: "gaid::-17",
+    name: "iOS Traffic",
+    description:
+      "sessions::condition::ga:operatingSystem=~^(iOS|iPad|iPhone|iPod)$",
+    is_active: true,
+  },
+  {
+    id: "gaid::-18",
+    name: "Other Traffic (Neither iOS nor Android)",
+    description:
+      "sessions::condition::ga:operatingSystem!~^(Android|iOS|iPad|iPhone|iPod)$",
+    is_active: true,
+  },
+  {
+    id: "gaid::-19",
+    name: "Bounced Sessions",
+    description: "sessions::condition::ga:bounces>0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-100",
+    name: "Single Session Users",
+    description: "users::condition::ga:sessions==1",
+    is_active: true,
+  },
+  {
+    id: "gaid::-101",
+    name: "Multi-session Users",
+    description: "users::condition::ga:sessions>1",
+    is_active: true,
+  },
+  {
+    id: "gaid::-102",
+    name: "Converters",
+    description: "users::condition::ga:goalCompletionsAll>0,ga:transactions>0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-103",
+    name: "Non-Converters",
+    description:
+      "users::condition::ga:goalCompletionsAll==0;ga:transactions==0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-104",
+    name: "Made a Purchase",
+    description: "users::condition::ga:transactions>0",
+    is_active: true,
+  },
+  {
+    id: "gaid::-105",
+    name: "Performed Site Search",
+    description: "users::sequence::ga:searchKeyword!~^$|^\\(not set\\)$",
+    is_active: true,
+  },
+];
 
 fields["ga:date"].grouping_options = [
-    "hour",
-    "day",
-    "week",
-    "month",
-    "year",
-    "hour-of-day",
-    "day-of-week",
-    "week-of-year",
-    "month-of-year"
+  "hour",
+  "day",
+  "week",
+  "month",
+  "year",
+  "hour-of-day",
+  "day-of-week",
+  "week-of-year",
+  "month-of-year",
 ];
diff --git a/frontend/src/metabase/lib/greeting.js b/frontend/src/metabase/lib/greeting.js
index 44f79ebfdecfd682fe0958d330ec8b1b43e0360d..e280444fffc3205a0d73505469306f1aed69de97 100644
--- a/frontend/src/metabase/lib/greeting.js
+++ b/frontend/src/metabase/lib/greeting.js
@@ -1,46 +1,49 @@
-import { t } from 'c-3po'
+import { t } from "c-3po";
 
 const greetingPrefixes = [
-    t`Hey there`,
-    t`How's it going`,
-    t`Howdy`,
-    t`Greetings`,
-    t`Good to see you`
+  t`Hey there`,
+  t`How's it going`,
+  t`Howdy`,
+  t`Greetings`,
+  t`Good to see you`,
 ];
 
 const subheadPrefixes = [
-    t`What do you want to know?`,
-    t`What's on your mind?`,
-    t`What do you want to find out?`
+  t`What do you want to know?`,
+  t`What's on your mind?`,
+  t`What do you want to find out?`,
 ];
 
 var Greeting = {
-    simpleGreeting: function() {
-        // TODO - this can result in an undefined thing
-        const randomIndex = Math.floor(Math.random() * (greetingPrefixes.length - 1));
-        return greetingPrefixes[randomIndex];
-    },
-
-	sayHello: function(personalization) {
-        if(personalization) {
-            var g = Greeting.simpleGreeting();
-            if (g === t`How's it going`){
-                return g + ', ' + personalization + '?';
-            } else {
-                return g + ', ' + personalization;
-            }
-
-        } else {
-        	return Greeting.simpleGreeting();
-        }
-    },
+  simpleGreeting: function() {
+    // TODO - this can result in an undefined thing
+    const randomIndex = Math.floor(
+      Math.random() * (greetingPrefixes.length - 1),
+    );
+    return greetingPrefixes[randomIndex];
+  },
+
+  sayHello: function(personalization) {
+    if (personalization) {
+      var g = Greeting.simpleGreeting();
+      if (g === t`How's it going`) {
+        return g + ", " + personalization + "?";
+      } else {
+        return g + ", " + personalization;
+      }
+    } else {
+      return Greeting.simpleGreeting();
+    }
+  },
 
-    encourageCuriosity: function() {
-        // TODO - this can result in an undefined thing
-        const randomIndex = Math.floor(Math.random() * (subheadPrefixes.length - 1));
+  encourageCuriosity: function() {
+    // TODO - this can result in an undefined thing
+    const randomIndex = Math.floor(
+      Math.random() * (subheadPrefixes.length - 1),
+    );
 
-        return subheadPrefixes[randomIndex];
-    }
+    return subheadPrefixes[randomIndex];
+  },
 };
 
 export default Greeting;
diff --git a/frontend/src/metabase/lib/groups.js b/frontend/src/metabase/lib/groups.js
index fffa8c193ac2a574616b5c30c102b57b76b265e1..1daa3403101358a4037d6e0ebbaa6b4eee42aa35 100644
--- a/frontend/src/metabase/lib/groups.js
+++ b/frontend/src/metabase/lib/groups.js
@@ -1,28 +1,25 @@
-
 export function isDefaultGroup(group) {
-    return group.name === "All Users";
+  return group.name === "All Users";
 }
 
 export function isAdminGroup(group) {
-    return group.name === "Administrators";
+  return group.name === "Administrators";
 }
 
 export function isMetaBotGroup(group) {
-    return group.name === "MetaBot";
+  return group.name === "MetaBot";
 }
 
 export function canEditPermissions(group) {
-    return !isAdminGroup(group);
+  return !isAdminGroup(group);
 }
 
 export function canEditMembership(group) {
-    return !isDefaultGroup(group);
+  return !isDefaultGroup(group);
 }
 
 export function getGroupColor(group) {
-    return (
-        isAdminGroup(group) ? "text-purple" :
-        isDefaultGroup(group) ? "text-grey-4" :
-        "text-brand"
-    );
+  return isAdminGroup(group)
+    ? "text-purple"
+    : isDefaultGroup(group) ? "text-grey-4" : "text-brand";
 }
diff --git a/frontend/src/metabase/lib/i18n-debug.js b/frontend/src/metabase/lib/i18n-debug.js
index 6a82a4b020af13f4eb1b11b8f6b3ed94579c19a2..9e724aa9cf649496fef542f8f9e9af25c3f28813 100644
--- a/frontend/src/metabase/lib/i18n-debug.js
+++ b/frontend/src/metabase/lib/i18n-debug.js
@@ -22,8 +22,8 @@ const SPECIAL_STRINGS = new Set([
   "StandardDeviation",
   "Average",
   "Min",
-  "Max"
-])
+  "Max",
+]);
 
 export function enableTranslatedStringReplacement() {
   const c3po = require("c-3po");
@@ -37,16 +37,12 @@ export function enableTranslatedStringReplacement() {
       // divide by 2 because Unicode `FULL BLOCK` is quite wide
       return new Array(Math.ceil(string.length / 2) + 1).join("â–ˆ");
     }
-  }
+  };
   // eslint-disable-next-line react/display-name
   c3po.jt = (...args) => {
     const elements = _jt(...args);
-    return (
-      <span style={{ backgroundColor: "currentcolor" }}>
-        {elements}
-      </span>
-    );
-  }
+    return <span style={{ backgroundColor: "currentcolor" }}>{elements}</span>;
+  };
 }
 
 if (window.localStorage && window.localStorage["metabase-i18n-debug"]) {
diff --git a/frontend/src/metabase/lib/i18n.js b/frontend/src/metabase/lib/i18n.js
index 6356a5cfe5afba236fa0266fff7154ac5debd272..f6c3abf944b94e151bf96e688f33d4d5382ef9e9 100644
--- a/frontend/src/metabase/lib/i18n.js
+++ b/frontend/src/metabase/lib/i18n.js
@@ -1,17 +1,16 @@
-
 import { addLocale, useLocale } from "c-3po";
 import { I18NApi } from "metabase/services";
 
 export async function loadLocalization(locale) {
-    // load and parse the locale
-    const translationsObject = await I18NApi.locale({ locale });
-    setLocalization(translationsObject);
+  // load and parse the locale
+  const translationsObject = await I18NApi.locale({ locale });
+  setLocalization(translationsObject);
 }
 
 export function setLocalization(translationsObject) {
-    const locale = translationsObject.headers.language;
+  const locale = translationsObject.headers.language;
 
-    // add and set locale with C-3PO
-    addLocale(locale, translationsObject);
-    useLocale(locale);
+  // add and set locale with C-3PO
+  addLocale(locale, translationsObject);
+  useLocale(locale);
 }
diff --git a/frontend/src/metabase/lib/keyboard.js b/frontend/src/metabase/lib/keyboard.js
index 8e03f21b0bf10beae1c4c4439b416406fdf969c1..73fb425046039294090095c40bf91e18fd3dc499 100644
--- a/frontend/src/metabase/lib/keyboard.js
+++ b/frontend/src/metabase/lib/keyboard.js
@@ -1,13 +1,12 @@
+export const KEYCODE_BACKSPACE = 8;
+export const KEYCODE_TAB = 9;
+export const KEYCODE_ENTER = 13;
+export const KEYCODE_ESCAPE = 27;
 
-export const KEYCODE_BACKSPACE     = 8;
-export const KEYCODE_TAB           = 9;
-export const KEYCODE_ENTER         = 13;
-export const KEYCODE_ESCAPE        = 27;
+export const KEYCODE_LEFT = 37;
+export const KEYCODE_UP = 38;
+export const KEYCODE_RIGHT = 39;
+export const KEYCODE_DOWN = 40;
 
-export const KEYCODE_LEFT          = 37;
-export const KEYCODE_UP            = 38;
-export const KEYCODE_RIGHT         = 39;
-export const KEYCODE_DOWN          = 40;
-
-export const KEYCODE_COMMA         = 188;
+export const KEYCODE_COMMA = 188;
 export const KEYCODE_FORWARD_SLASH = 191;
diff --git a/frontend/src/metabase/lib/permissions.js b/frontend/src/metabase/lib/permissions.js
index d339737f0bdaa90d892a098b48cf88a7b55bfaf0..55d2cf4e46264ab6df21d0f5ad7436be8224b868 100644
--- a/frontend/src/metabase/lib/permissions.js
+++ b/frontend/src/metabase/lib/permissions.js
@@ -1,4 +1,3 @@
-
 import { getIn, setIn } from "icepick";
 import _ from "underscore";
 
@@ -9,285 +8,501 @@ import Metadata from "metabase-lib/lib/metadata/Metadata";
 import Database from "metabase-lib/lib/metadata/Database";
 import Table from "metabase-lib/lib/metadata/Table";
 
-import type { Group, GroupId, GroupsPermissions } from "metabase/meta/types/Permissions";
-
-type TableEntityId = { databaseId: DatabaseId, schemaName: SchemaName, tableId: TableId };
+import type {
+  Group,
+  GroupId,
+  GroupsPermissions,
+} from "metabase/meta/types/Permissions";
+
+type TableEntityId = {
+  databaseId: DatabaseId,
+  schemaName: SchemaName,
+  tableId: TableId,
+};
 type SchemaEntityId = { databaseId: DatabaseId, schemaName: SchemaName };
 type DatabaseEntityId = { databaseId: DatabaseId };
 type EntityId = TableEntityId | SchemaEntityId | DatabaseEntityId;
 
 export function getPermission(
-    permissions: GroupsPermissions,
-    groupId: GroupId,
-    path: Array<string|number>,
-    isControlledType: bool = false
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  path: Array<string | number>,
+  isControlledType: boolean = false,
 ): string {
-    let value = getIn(permissions, [groupId].concat(path));
-    if (isControlledType) {
-        if (!value) {
-            return "none";
-        } else if (typeof value === "object") {
-            return "controlled";
-        } else {
-            return value;
-        }
-    } else if (value) {
-        return value;
+  let value = getIn(permissions, [groupId].concat(path));
+  if (isControlledType) {
+    if (!value) {
+      return "none";
+    } else if (typeof value === "object") {
+      return "controlled";
     } else {
-        return "none"
+      return value;
     }
+  } else if (value) {
+    return value;
+  } else {
+    return "none";
+  }
 }
 
 export function updatePermission(
-    permissions: GroupsPermissions,
-    groupId: GroupId,
-    path: Array<string|number>,
-    value: string,
-    entityIds: ?(Array<string>|Array<number>)
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  path: Array<string | number>,
+  value: string,
+  entityIds: ?(Array<string> | Array<number>),
 ): GroupsPermissions {
-    const fullPath = [groupId].concat(path);
-    let current = getIn(permissions, fullPath);
-    if (current === value || (current && typeof current === "object" && value === "controlled")) {
-        return permissions;
-    }
-    let newValue;
-    if (value === "controlled") {
-        newValue = {};
-        if (entityIds) {
-            for (let entityId of entityIds) {
-                newValue[entityId] = current
-            }
-        }
-    } else {
-        newValue = value;
-    }
-    for (var i = 0; i < fullPath.length; i++) {
-        if (typeof getIn(permissions, fullPath.slice(0, i)) === "string") {
-            permissions = setIn(permissions, fullPath.slice(0, i), {});
-        }
-    }
-    return setIn(permissions, fullPath, newValue);
-}
-
-export const getSchemasPermission = (permissions: GroupsPermissions, groupId: GroupId, { databaseId }: DatabaseEntityId): string => {
-    return getPermission(permissions, groupId, [databaseId, "schemas"], true);
-}
-
-export const getNativePermission = (permissions: GroupsPermissions, groupId: GroupId, { databaseId }: DatabaseEntityId): string => {
-    return getPermission(permissions, groupId, [databaseId, "native"]);
-}
-
-export const getTablesPermission = (permissions: GroupsPermissions, groupId: GroupId, { databaseId, schemaName }: SchemaEntityId): string => {
-    let schemas = getSchemasPermission(permissions, groupId, { databaseId });
-    if (schemas === "controlled") {
-        return getPermission(permissions, groupId, [databaseId, "schemas", schemaName], true);
-    } else {
-        return schemas;
+  const fullPath = [groupId].concat(path);
+  let current = getIn(permissions, fullPath);
+  if (
+    current === value ||
+    (current && typeof current === "object" && value === "controlled")
+  ) {
+    return permissions;
+  }
+  let newValue;
+  if (value === "controlled") {
+    newValue = {};
+    if (entityIds) {
+      for (let entityId of entityIds) {
+        newValue[entityId] = current;
+      }
     }
-}
-
-export const getFieldsPermission = (permissions: GroupsPermissions, groupId: GroupId, { databaseId, schemaName, tableId }: TableEntityId): string => {
-    let tables = getTablesPermission(permissions, groupId, { databaseId, schemaName });
-    if (tables === "controlled") {
-        return getPermission(permissions, groupId, [databaseId, "schemas", schemaName, tableId], true);
-    } else {
-        return tables;
+  } else {
+    newValue = value;
+  }
+  for (var i = 0; i < fullPath.length; i++) {
+    if (typeof getIn(permissions, fullPath.slice(0, i)) === "string") {
+      permissions = setIn(permissions, fullPath.slice(0, i), {});
     }
+  }
+  return setIn(permissions, fullPath, newValue);
 }
 
-export function downgradeNativePermissionsIfNeeded(permissions: GroupsPermissions, groupId: GroupId, { databaseId }: DatabaseEntityId, value: string, metadata: Metadata): GroupsPermissions {
-    let currentSchemas = getSchemasPermission(permissions, groupId, { databaseId });
-    let currentNative = getNativePermission(permissions, groupId, { databaseId });
-
-    if (value === "none") {
-        // if changing schemas to none, downgrade native to none
-        return updateNativePermission(permissions, groupId, { databaseId }, "none", metadata);
-    } else if (value === "controlled" && currentSchemas === "all" && currentNative === "write") {
-        // if changing schemas to controlled, downgrade native to read
-        return updateNativePermission(permissions, groupId, { databaseId }, "read", metadata);
-    } else {
-        return permissions;
-    }
+export const getSchemasPermission = (
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId }: DatabaseEntityId,
+): string => {
+  return getPermission(permissions, groupId, [databaseId, "schemas"], true);
+};
+
+export const getNativePermission = (
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId }: DatabaseEntityId,
+): string => {
+  return getPermission(permissions, groupId, [databaseId, "native"]);
+};
+
+export const getTablesPermission = (
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId, schemaName }: SchemaEntityId,
+): string => {
+  let schemas = getSchemasPermission(permissions, groupId, { databaseId });
+  if (schemas === "controlled") {
+    return getPermission(
+      permissions,
+      groupId,
+      [databaseId, "schemas", schemaName],
+      true,
+    );
+  } else {
+    return schemas;
+  }
+};
+
+export const getFieldsPermission = (
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId, schemaName, tableId }: TableEntityId,
+): string => {
+  let tables = getTablesPermission(permissions, groupId, {
+    databaseId,
+    schemaName,
+  });
+  if (tables === "controlled") {
+    return getPermission(
+      permissions,
+      groupId,
+      [databaseId, "schemas", schemaName, tableId],
+      true,
+    );
+  } else {
+    return tables;
+  }
+};
+
+export function downgradeNativePermissionsIfNeeded(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId }: DatabaseEntityId,
+  value: string,
+  metadata: Metadata,
+): GroupsPermissions {
+  let currentSchemas = getSchemasPermission(permissions, groupId, {
+    databaseId,
+  });
+  let currentNative = getNativePermission(permissions, groupId, { databaseId });
+
+  if (value === "none") {
+    // if changing schemas to none, downgrade native to none
+    return updateNativePermission(
+      permissions,
+      groupId,
+      { databaseId },
+      "none",
+      metadata,
+    );
+  } else if (
+    value === "controlled" &&
+    currentSchemas === "all" &&
+    currentNative === "write"
+  ) {
+    // if changing schemas to controlled, downgrade native to read
+    return updateNativePermission(
+      permissions,
+      groupId,
+      { databaseId },
+      "read",
+      metadata,
+    );
+  } else {
+    return permissions;
+  }
 }
 
-const metadataTableToTableEntityId = (table: Table): TableEntityId => ({ databaseId: table.db_id, schemaName: table.schema || "", tableId: table.id });
+const metadataTableToTableEntityId = (table: Table): TableEntityId => ({
+  databaseId: table.db_id,
+  schemaName: table.schema || "",
+  tableId: table.id,
+});
 
 // TODO Atte Keinänen 6/24/17 See if this method could be simplified
 const entityIdToMetadataTableFields = (entityId: EntityId) => ({
-    ...(entityId.databaseId ? {db_id: entityId.databaseId} : {}),
-    // $FlowFixMe Because schema name can be an empty string, which means an empty schema, this check becomes a little nasty
-    ...(entityId.schemaName !== undefined ? {schema: entityId.schemaName !== "" ? entityId.schemaName : null} : {}),
-    ...(entityId.tableId ? {id: entityId.tableId} : {})
+  ...(entityId.databaseId ? { db_id: entityId.databaseId } : {}),
+  // $FlowFixMe Because schema name can be an empty string, which means an empty schema, this check becomes a little nasty
+  ...(entityId.schemaName !== undefined
+    ? { schema: entityId.schemaName !== "" ? entityId.schemaName : null }
+    : {}),
+  ...(entityId.tableId ? { id: entityId.tableId } : {}),
 });
 
-function inferEntityPermissionValueFromChildTables(permissions: GroupsPermissions, groupId: GroupId, entityId: DatabaseEntityId|SchemaEntityId, metadata: Metadata) {
-    const { databaseId } = entityId;
-    const database = metadata && metadata.databases[databaseId];
-
-    const entityIdsForDescendantTables: TableEntityId[] = _.chain(database.tables)
-        .filter((t) => _.isMatch(t, entityIdToMetadataTableFields(entityId)))
-        .map(metadataTableToTableEntityId)
-        .value();
-
-    const entityIdsByPermValue = _.chain(entityIdsForDescendantTables)
-        .map((id) => getFieldsPermission(permissions, groupId, id))
-        .groupBy(_.identity)
-        .value();
-
-    const keys = Object.keys(entityIdsByPermValue);
-    const allTablesHaveSamePermissions = keys.length === 1;
-
-    if (allTablesHaveSamePermissions) {
-        // either "all" or "none"
-        return keys[0];
-    } else {
-        return "controlled";
-    }
+function inferEntityPermissionValueFromChildTables(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  entityId: DatabaseEntityId | SchemaEntityId,
+  metadata: Metadata,
+) {
+  const { databaseId } = entityId;
+  const database = metadata && metadata.databases[databaseId];
+
+  const entityIdsForDescendantTables: TableEntityId[] = _.chain(database.tables)
+    .filter(t => _.isMatch(t, entityIdToMetadataTableFields(entityId)))
+    .map(metadataTableToTableEntityId)
+    .value();
+
+  const entityIdsByPermValue = _.chain(entityIdsForDescendantTables)
+    .map(id => getFieldsPermission(permissions, groupId, id))
+    .groupBy(_.identity)
+    .value();
+
+  const keys = Object.keys(entityIdsByPermValue);
+  const allTablesHaveSamePermissions = keys.length === 1;
+
+  if (allTablesHaveSamePermissions) {
+    // either "all" or "none"
+    return keys[0];
+  } else {
+    return "controlled";
+  }
 }
 
 // Checks the child tables of a given entityId and updates the shared table and/or schema permission values according to table permissions
 // This method was added for keeping the UI in sync when modifying child permissions
-export function inferAndUpdateEntityPermissions(permissions: GroupsPermissions, groupId: GroupId, entityId: DatabaseEntityId|SchemaEntityId, metadata: Metadata) {
+export function inferAndUpdateEntityPermissions(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  entityId: DatabaseEntityId | SchemaEntityId,
+  metadata: Metadata,
+) {
+  // $FlowFixMe
+  const { databaseId, schemaName } = entityId;
+
+  if (schemaName) {
+    // Check all tables for current schema if their shared schema-level permission value should be updated
     // $FlowFixMe
-    const { databaseId, schemaName } = entityId;
-
-    if (schemaName) {
-        // Check all tables for current schema if their shared schema-level permission value should be updated
-        // $FlowFixMe
-        const tablesPermissionValue = inferEntityPermissionValueFromChildTables(permissions, groupId, { databaseId, schemaName }, metadata);
-        permissions = updateTablesPermission(permissions, groupId, { databaseId, schemaName }, tablesPermissionValue, metadata);
-    }
-
-    if (databaseId) {
-        // Check all tables for current database if schemas' shared database-level permission value should be updated
-        const schemasPermissionValue = inferEntityPermissionValueFromChildTables(permissions, groupId, { databaseId }, metadata);
-        permissions = updateSchemasPermission(permissions, groupId, { databaseId }, schemasPermissionValue, metadata);
-        permissions = downgradeNativePermissionsIfNeeded(permissions, groupId, { databaseId }, schemasPermissionValue, metadata);
-    }
-
-    return permissions;
+    const tablesPermissionValue = inferEntityPermissionValueFromChildTables(
+      permissions,
+      groupId,
+      { databaseId, schemaName },
+      metadata,
+    );
+    permissions = updateTablesPermission(
+      permissions,
+      groupId,
+      { databaseId, schemaName },
+      tablesPermissionValue,
+      metadata,
+    );
+  }
+
+  if (databaseId) {
+    // Check all tables for current database if schemas' shared database-level permission value should be updated
+    const schemasPermissionValue = inferEntityPermissionValueFromChildTables(
+      permissions,
+      groupId,
+      { databaseId },
+      metadata,
+    );
+    permissions = updateSchemasPermission(
+      permissions,
+      groupId,
+      { databaseId },
+      schemasPermissionValue,
+      metadata,
+    );
+    permissions = downgradeNativePermissionsIfNeeded(
+      permissions,
+      groupId,
+      { databaseId },
+      schemasPermissionValue,
+      metadata,
+    );
+  }
+
+  return permissions;
 }
 
-export function updateFieldsPermission(permissions: GroupsPermissions, groupId: GroupId, entityId: TableEntityId, value: string, metadata: Metadata): GroupsPermissions {
-    const { databaseId, schemaName, tableId } = entityId;
-
-    permissions = updateTablesPermission(permissions, groupId, { databaseId, schemaName }, "controlled", metadata);
-    permissions = updatePermission(permissions, groupId, [databaseId, "schemas", schemaName, tableId], value /* TODO: field ids, when enabled "controlled" fields */);
-
-    return permissions;
+export function updateFieldsPermission(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  entityId: TableEntityId,
+  value: string,
+  metadata: Metadata,
+): GroupsPermissions {
+  const { databaseId, schemaName, tableId } = entityId;
+
+  permissions = updateTablesPermission(
+    permissions,
+    groupId,
+    { databaseId, schemaName },
+    "controlled",
+    metadata,
+  );
+  permissions = updatePermission(
+    permissions,
+    groupId,
+    [databaseId, "schemas", schemaName, tableId],
+    value /* TODO: field ids, when enabled "controlled" fields */,
+  );
+
+  return permissions;
 }
 
-export function updateTablesPermission(permissions: GroupsPermissions, groupId: GroupId, { databaseId, schemaName }: SchemaEntityId, value: string, metadata: Metadata): GroupsPermissions {
-    const database = metadata && metadata.databases[databaseId];
-    const tableIds: ?number[] = database && database.tables.filter(t => (t.schema || "") === schemaName).map(t => t.id);
-
-    permissions = updateSchemasPermission(permissions, groupId, { databaseId }, "controlled", metadata);
-    permissions = updatePermission(permissions, groupId, [databaseId, "schemas", schemaName], value, tableIds);
-
-    return permissions;
+export function updateTablesPermission(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId, schemaName }: SchemaEntityId,
+  value: string,
+  metadata: Metadata,
+): GroupsPermissions {
+  const database = metadata && metadata.databases[databaseId];
+  const tableIds: ?(number[]) =
+    database &&
+    database.tables.filter(t => (t.schema || "") === schemaName).map(t => t.id);
+
+  permissions = updateSchemasPermission(
+    permissions,
+    groupId,
+    { databaseId },
+    "controlled",
+    metadata,
+  );
+  permissions = updatePermission(
+    permissions,
+    groupId,
+    [databaseId, "schemas", schemaName],
+    value,
+    tableIds,
+  );
+
+  return permissions;
 }
 
-export function updateSchemasPermission(permissions: GroupsPermissions, groupId: GroupId, { databaseId }: DatabaseEntityId, value: string, metadata: Metadata): GroupsPermissions {
-    const database = metadata.databases[databaseId];
-    const schemaNames = database && database.schemaNames();
-    const schemaNamesOrNoSchema = (schemaNames && schemaNames.length > 0) ? schemaNames : [""];
-
-    permissions = downgradeNativePermissionsIfNeeded(permissions, groupId, { databaseId }, value, metadata);
-    return updatePermission(permissions, groupId, [databaseId, "schemas"], value, schemaNamesOrNoSchema);
+export function updateSchemasPermission(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId }: DatabaseEntityId,
+  value: string,
+  metadata: Metadata,
+): GroupsPermissions {
+  const database = metadata.databases[databaseId];
+  const schemaNames = database && database.schemaNames();
+  const schemaNamesOrNoSchema =
+    schemaNames && schemaNames.length > 0 ? schemaNames : [""];
+
+  permissions = downgradeNativePermissionsIfNeeded(
+    permissions,
+    groupId,
+    { databaseId },
+    value,
+    metadata,
+  );
+  return updatePermission(
+    permissions,
+    groupId,
+    [databaseId, "schemas"],
+    value,
+    schemaNamesOrNoSchema,
+  );
 }
 
-export function updateNativePermission(permissions: GroupsPermissions, groupId: GroupId, { databaseId }: DatabaseEntityId, value: string, metadata: Metadata): GroupsPermissions {
-    // if enabling native query write access, give access to all schemas since they are equivalent
-    if (value === "write") {
-        permissions = updateSchemasPermission(permissions, groupId, { databaseId }, "all", metadata);
-    }
-    return updatePermission(permissions, groupId, [databaseId, "native"], value);
+export function updateNativePermission(
+  permissions: GroupsPermissions,
+  groupId: GroupId,
+  { databaseId }: DatabaseEntityId,
+  value: string,
+  metadata: Metadata,
+): GroupsPermissions {
+  // if enabling native query write access, give access to all schemas since they are equivalent
+  if (value === "write") {
+    permissions = updateSchemasPermission(
+      permissions,
+      groupId,
+      { databaseId },
+      "all",
+      metadata,
+    );
+  }
+  return updatePermission(permissions, groupId, [databaseId, "native"], value);
 }
 
 type PermissionsDiff = {
-    groups: {
-        [key: GroupId]: GroupPermissionsDiff
-    }
-}
+  groups: {
+    [key: GroupId]: GroupPermissionsDiff,
+  },
+};
 
 type GroupPermissionsDiff = {
-    name?: string,
-    databases: {
-        [key: DatabaseId]: DatabasePermissionsDiff
-    }
-}
+  name?: string,
+  databases: {
+    [key: DatabaseId]: DatabasePermissionsDiff,
+  },
+};
 
 type DatabasePermissionsDiff = {
-    name?: string,
-    native?: string,
-    revokedTables: {
-        [key: TableId]: TablePermissionsDiff
-    },
-    grantedTables: {
-        [key: TableId]: TablePermissionsDiff
-    },
-}
+  name?: string,
+  native?: string,
+  revokedTables: {
+    [key: TableId]: TablePermissionsDiff,
+  },
+  grantedTables: {
+    [key: TableId]: TablePermissionsDiff,
+  },
+};
 
 type TablePermissionsDiff = {
-    name?: string,
-}
+  name?: string,
+};
 
 function deleteIfEmpty(object: { [key: any]: any }, key: any) {
-    if (Object.keys(object[key]).length === 0) {
-        delete object[key];
-    }
+  if (Object.keys(object[key]).length === 0) {
+    delete object[key];
+  }
 }
 
-function diffDatabasePermissions(newPerms: GroupsPermissions, oldPerms: GroupsPermissions, groupId: GroupId, database: Database): DatabasePermissionsDiff {
-    const databaseDiff: DatabasePermissionsDiff = { grantedTables: {}, revokedTables: {} };
-    // get the native permisisons for this db
-    const oldNativePerm = getNativePermission(oldPerms, groupId, { databaseId: database.id });
-    const newNativePerm = getNativePermission(newPerms, groupId, { databaseId: database.id });
-    if (oldNativePerm !== newNativePerm) {
-        databaseDiff.native = newNativePerm;
-    }
-    // check each table in this db
-    for (const table of database.tables) {
-        const oldFieldsPerm = getFieldsPermission(oldPerms, groupId, { databaseId: database.id, schemaName: table.schema || "", tableId: table.id });
-        const newFieldsPerm = getFieldsPermission(newPerms, groupId, { databaseId: database.id, schemaName: table.schema || "", tableId: table.id });
-        if (oldFieldsPerm !== newFieldsPerm) {
-            if (newFieldsPerm === "none") {
-                databaseDiff.revokedTables[table.id] = { name: table.display_name };
-            } else {
-                databaseDiff.grantedTables[table.id] = { name: table.display_name };
-            }
-        }
-    }
-    // remove types that have no tables
-    for (let type of ["grantedTables", "revokedTables"]) {
-        deleteIfEmpty(databaseDiff, type);
+function diffDatabasePermissions(
+  newPerms: GroupsPermissions,
+  oldPerms: GroupsPermissions,
+  groupId: GroupId,
+  database: Database,
+): DatabasePermissionsDiff {
+  const databaseDiff: DatabasePermissionsDiff = {
+    grantedTables: {},
+    revokedTables: {},
+  };
+  // get the native permisisons for this db
+  const oldNativePerm = getNativePermission(oldPerms, groupId, {
+    databaseId: database.id,
+  });
+  const newNativePerm = getNativePermission(newPerms, groupId, {
+    databaseId: database.id,
+  });
+  if (oldNativePerm !== newNativePerm) {
+    databaseDiff.native = newNativePerm;
+  }
+  // check each table in this db
+  for (const table of database.tables) {
+    const oldFieldsPerm = getFieldsPermission(oldPerms, groupId, {
+      databaseId: database.id,
+      schemaName: table.schema || "",
+      tableId: table.id,
+    });
+    const newFieldsPerm = getFieldsPermission(newPerms, groupId, {
+      databaseId: database.id,
+      schemaName: table.schema || "",
+      tableId: table.id,
+    });
+    if (oldFieldsPerm !== newFieldsPerm) {
+      if (newFieldsPerm === "none") {
+        databaseDiff.revokedTables[table.id] = { name: table.display_name };
+      } else {
+        databaseDiff.grantedTables[table.id] = { name: table.display_name };
+      }
     }
-    return databaseDiff;
+  }
+  // remove types that have no tables
+  for (let type of ["grantedTables", "revokedTables"]) {
+    deleteIfEmpty(databaseDiff, type);
+  }
+  return databaseDiff;
 }
 
-function diffGroupPermissions(newPerms: GroupsPermissions, oldPerms: GroupsPermissions, groupId: GroupId, metadata: Metadata): GroupPermissionsDiff {
-    let groupDiff: GroupPermissionsDiff = { databases: {} };
-    for (const database of metadata.databasesList()) {
-        groupDiff.databases[database.id] = diffDatabasePermissions(newPerms, oldPerms, groupId, database);
-        deleteIfEmpty(groupDiff.databases, database.id);
-        if (groupDiff.databases[database.id]) {
-            groupDiff.databases[database.id].name = database.name;
-        }
+function diffGroupPermissions(
+  newPerms: GroupsPermissions,
+  oldPerms: GroupsPermissions,
+  groupId: GroupId,
+  metadata: Metadata,
+): GroupPermissionsDiff {
+  let groupDiff: GroupPermissionsDiff = { databases: {} };
+  for (const database of metadata.databasesList()) {
+    groupDiff.databases[database.id] = diffDatabasePermissions(
+      newPerms,
+      oldPerms,
+      groupId,
+      database,
+    );
+    deleteIfEmpty(groupDiff.databases, database.id);
+    if (groupDiff.databases[database.id]) {
+      groupDiff.databases[database.id].name = database.name;
     }
-    deleteIfEmpty(groupDiff, "databases");
-    return groupDiff;
+  }
+  deleteIfEmpty(groupDiff, "databases");
+  return groupDiff;
 }
 
-export function diffPermissions(newPerms: GroupsPermissions, oldPerms: GroupsPermissions, groups: Array<Group>, metadata: Metadata): PermissionsDiff {
-    let permissionsDiff: PermissionsDiff = { groups: {} };
-    if (newPerms && oldPerms && metadata) {
-        for (let group of groups) {
-            permissionsDiff.groups[group.id] = diffGroupPermissions(newPerms, oldPerms, group.id, metadata);
-            deleteIfEmpty(permissionsDiff.groups, group.id);
-            if (permissionsDiff.groups[group.id]) {
-                permissionsDiff.groups[group.id].name = group.name;
-            }
-        }
+export function diffPermissions(
+  newPerms: GroupsPermissions,
+  oldPerms: GroupsPermissions,
+  groups: Array<Group>,
+  metadata: Metadata,
+): PermissionsDiff {
+  let permissionsDiff: PermissionsDiff = { groups: {} };
+  if (newPerms && oldPerms && metadata) {
+    for (let group of groups) {
+      permissionsDiff.groups[group.id] = diffGroupPermissions(
+        newPerms,
+        oldPerms,
+        group.id,
+        metadata,
+      );
+      deleteIfEmpty(permissionsDiff.groups, group.id);
+      if (permissionsDiff.groups[group.id]) {
+        permissionsDiff.groups[group.id].name = group.name;
+      }
     }
-    return permissionsDiff;
+  }
+  return permissionsDiff;
 }
diff --git a/frontend/src/metabase/lib/promise.js b/frontend/src/metabase/lib/promise.js
index 0ac06ae6d1d35860e79b4ae496ab6082273a07f3..df4435d96277b365992a0e7544b46a0eac21081d 100644
--- a/frontend/src/metabase/lib/promise.js
+++ b/frontend/src/metabase/lib/promise.js
@@ -1,39 +1,42 @@
 // return a promise wrapping the provided one but with a "cancel" method
 export function cancelable(promise) {
-    let canceled = false;
+  let canceled = false;
 
-    const wrappedPromise = new Promise((resolve, reject) => {
-        promise.then(
-            (value) => canceled ? reject({ isCanceled: true }) : resolve(value),
-            (error) => canceled ? reject({ isCanceled: true }) : reject(error)
-        );
-    });
+  const wrappedPromise = new Promise((resolve, reject) => {
+    promise.then(
+      value => (canceled ? reject({ isCanceled: true }) : resolve(value)),
+      error => (canceled ? reject({ isCanceled: true }) : reject(error)),
+    );
+  });
 
-    wrappedPromise.cancel = function() {
-        canceled = true;
-    };
+  wrappedPromise.cancel = function() {
+    canceled = true;
+  };
 
-    return wrappedPromise;
+  return wrappedPromise;
 }
 
 // if a promise doesn't resolve/reject within a given duration it will reject
 export function timeout(promise, duration, error) {
-    return new Promise((resolve, reject) => {
-        promise.then(resolve, reject);
-        setTimeout(() => reject(error || new Error("Operation timed out")), duration);
-    });
+  return new Promise((resolve, reject) => {
+    promise.then(resolve, reject);
+    setTimeout(
+      () => reject(error || new Error("Operation timed out")),
+      duration,
+    );
+  });
 }
 
 // returns a promise that resolves after a given duration
 export function delay(duration) {
-    return new Promise((resolve, reject) => setTimeout(resolve, duration));
+  return new Promise((resolve, reject) => setTimeout(resolve, duration));
 }
 
 export function defer() {
-    let deferrred = {}
-    deferrred.promise = new Promise((resolve, reject) => {
-        deferrred.resolve = resolve;
-        deferrred.reject = reject;
-    });
-    return deferrred;
+  let deferrred = {};
+  deferrred.promise = new Promise((resolve, reject) => {
+    deferrred.resolve = resolve;
+    deferrred.reject = reject;
+  });
+  return deferrred;
 }
diff --git a/frontend/src/metabase/lib/pulse.js b/frontend/src/metabase/lib/pulse.js
index 9b18a7ca2ea188164dbbd13c5328fcb224c40d0b..175253f5c69e15a3f2be4d1278c0b44e0a2d6a3a 100644
--- a/frontend/src/metabase/lib/pulse.js
+++ b/frontend/src/metabase/lib/pulse.js
@@ -1,49 +1,71 @@
-
 export function channelIsValid(channel, channelSpec) {
-    if (!channelSpec) {
+  if (!channelSpec) {
+    return false;
+  }
+  switch (channel.schedule_type) {
+    case "monthly":
+      if (channel.schedule_frame != null && channel.schedule_hour != null) {
+        return true;
+      }
+    // these cases intentionally fall though
+    case "weekly":
+      if (channel.schedule_day == null) {
         return false;
+      }
+    case "daily":
+      if (channel.schedule_hour == null) {
+        return false;
+      }
+    case "hourly":
+      break;
+    default:
+      return false;
+  }
+  if (channelSpec.recipients) {
+    if (!channel.recipients /* || channel.recipients.length === 0*/) {
+      return false;
     }
-    switch (channel.schedule_type) {
-        case "monthly": if (channel.schedule_frame != null &&
-                            channel.schedule_hour != null) { return true }
-        // these cases intentionally fall though
-        case "weekly": if (channel.schedule_day == null) { return false }
-        case "daily":  if (channel.schedule_hour == null) { return false }
-        case "hourly": break;
-        default:       return false;
-    }
-    if (channelSpec.recipients) {
-        if (!channel.recipients/* || channel.recipients.length === 0*/) {
-            return false;
-        }
-    }
-    if (channelSpec.fields) {
-        for (let field of channelSpec.fields) {
-            if (field.required && channel.details && (channel.details[field.name] == null || channel.details[field.name] == "")) {
-                return false;
-            }
-        }
+  }
+  if (channelSpec.fields) {
+    for (let field of channelSpec.fields) {
+      if (
+        field.required &&
+        channel.details &&
+        (channel.details[field.name] == null ||
+          channel.details[field.name] == "")
+      ) {
+        return false;
+      }
     }
-    return true;
+  }
+  return true;
 }
 
 export function pulseIsValid(pulse, channelSpecs) {
-    return (
-        pulse.name &&
-        pulse.cards.length > 0 &&
-        pulse.channels.filter((c) => channelIsValid(c, channelSpecs && channelSpecs[c.channel_type])).length > 0
-    ) || false;
+  return (
+    (pulse.name &&
+      pulse.cards.length > 0 &&
+      pulse.channels.filter(c =>
+        channelIsValid(c, channelSpecs && channelSpecs[c.channel_type]),
+      ).length > 0) ||
+    false
+  );
 }
 
 export function emailIsEnabled(pulse) {
-    return pulse.channels.filter(c => c.channel_type === "email" && c.enabled).length > 0;
+  return (
+    pulse.channels.filter(c => c.channel_type === "email" && c.enabled).length >
+    0
+  );
 }
 
 export function cleanPulse(pulse, channelSpecs) {
-    return {
-        ...pulse,
-        channels: pulse.channels.filter((c) => channelIsValid(c, channelSpecs && channelSpecs[c.channel_type]))
-    };
+  return {
+    ...pulse,
+    channels: pulse.channels.filter(c =>
+      channelIsValid(c, channelSpecs && channelSpecs[c.channel_type]),
+    ),
+  };
 }
 
 export function getDefaultChannel(channelSpecs) {
@@ -60,16 +82,16 @@ export function getDefaultChannel(channelSpecs) {
 }
 
 export function createChannel(channelSpec) {
-    const details = {};
+  const details = {};
 
-    return {
-        channel_type: channelSpec.type,
-        enabled: true,
-        recipients: [],
-        details: details,
-        schedule_type: channelSpec.schedules[0],
-        schedule_day: "mon",
-        schedule_hour: 8,
-        schedule_frame: "first"
-    };
+  return {
+    channel_type: channelSpec.type,
+    enabled: true,
+    recipients: [],
+    details: details,
+    schedule_type: channelSpec.schedules[0],
+    schedule_day: "mon",
+    schedule_hour: 8,
+    schedule_frame: "first",
+  };
 }
diff --git a/frontend/src/metabase/lib/query.js b/frontend/src/metabase/lib/query.js
index e17d7a371d1dbd2f9ee42e1bd360bf2fd02e1193..ae9ae5f3e1d7fd73659a0ec23a2474e056d7da0f 100644
--- a/frontend/src/metabase/lib/query.js
+++ b/frontend/src/metabase/lib/query.js
@@ -2,7 +2,7 @@ import React from "react";
 
 import inflection from "inflection";
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Utils from "metabase/lib/utils";
 import { getOperators } from "metabase/lib/schema_metadata";
 import { createLookupByProperty } from "metabase/lib/table";
@@ -17,765 +17,960 @@ import * as F from "./query/field";
 import { mbql, mbqlEq } from "./query/util";
 
 export const NEW_QUERY_TEMPLATES = {
+  query: {
+    database: null,
+    type: "query",
     query: {
-        database: null,
-        type: "query",
-        query: {
-            source_table: null
-        }
+      source_table: null,
     },
+  },
+  native: {
+    database: null,
+    type: "native",
     native: {
-        database: null,
-        type: "native",
-        native: {
-            query: ""
-        }
-    }
+      query: "",
+    },
+  },
 };
 
 export function createQuery(type = "query", databaseId, tableId) {
-    let dataset_query = Utils.copy(NEW_QUERY_TEMPLATES[type]);
+  let dataset_query = Utils.copy(NEW_QUERY_TEMPLATES[type]);
 
-    if (databaseId) {
-        dataset_query.database = databaseId;
-    }
+  if (databaseId) {
+    dataset_query.database = databaseId;
+  }
 
-    if (type === "query" && databaseId && tableId) {
-        dataset_query.query.source_table = tableId;
-    }
+  if (type === "query" && databaseId && tableId) {
+    dataset_query.query.source_table = tableId;
+  }
 
-    return dataset_query;
+  return dataset_query;
 }
 
-
 const METRIC_NAME_BY_AGGREGATION = {
-    "count": "count",
-    "cum_count": "count",
-    "sum": "sum",
-    "cum_sum": "sum",
-    "distinct": "count",
-    "avg": "avg",
-    "min": "min",
-    "max": "max",
-}
+  count: "count",
+  cum_count: "count",
+  sum: "sum",
+  cum_sum: "sum",
+  distinct: "count",
+  avg: "avg",
+  min: "min",
+  max: "max",
+};
 
 const METRIC_TYPE_BY_AGGREGATION = {
-    "count": TYPE.Integer,
-    "cum_count": TYPE.Integer,
-    "sum": TYPE.Float,
-    "cum_sum": TYPE.Float,
-    "distinct": TYPE.Integer,
-    "avg": TYPE.Float,
-    "min": TYPE.Float,
-    "max": TYPE.Float,
-}
-
-const SORTABLE_AGGREGATION_TYPES = new Set(["avg", "count", "distinct", "stddev", "sum", "min", "max"]);
+  count: TYPE.Integer,
+  cum_count: TYPE.Integer,
+  sum: TYPE.Float,
+  cum_sum: TYPE.Float,
+  distinct: TYPE.Integer,
+  avg: TYPE.Float,
+  min: TYPE.Float,
+  max: TYPE.Float,
+};
 
+const SORTABLE_AGGREGATION_TYPES = new Set([
+  "avg",
+  "count",
+  "distinct",
+  "stddev",
+  "sum",
+  "min",
+  "max",
+]);
 
 var Query = {
+  isStructured(dataset_query) {
+    return dataset_query && dataset_query.type === "query";
+  },
+
+  isNative(dataset_query) {
+    return dataset_query && dataset_query.type === "native";
+  },
+
+  canRun(query, tableMetadata) {
+    if (
+      !query ||
+      query.source_table == null ||
+      !Query.hasValidAggregation(query)
+    ) {
+      return false;
+    }
+    // check that the table supports this aggregation, if we have tableMetadata
+    if (tableMetadata) {
+      let aggs = Query.getAggregations(query);
+      if (aggs.length === 0) {
+        if (
+          !_.findWhere(tableMetadata.aggregation_options, { short: "rows" })
+        ) {
+          return false;
+        }
+      } else {
+        for (const [agg] of aggs) {
+          if (
+            !mbqlEq(agg, "metric") &&
+            !_.findWhere(tableMetadata.aggregation_options, { short: agg })
+          ) {
+            // return false;
+          }
+        }
+      }
+    }
+    return true;
+  },
 
-    isStructured(dataset_query) {
-        return dataset_query && dataset_query.type === "query";
-    },
-
-    isNative(dataset_query) {
-        return dataset_query && dataset_query.type === "native";
-    },
+  cleanQuery(query) {
+    if (!query) {
+      return query;
+    }
 
-    canRun(query, tableMetadata) {
-        if (!query || query.source_table == null || !Query.hasValidAggregation(query)) {
-            return false;
-        }
-        // check that the table supports this aggregation, if we have tableMetadata
-        if (tableMetadata) {
-            let aggs = Query.getAggregations(query);
-            if (aggs.length === 0) {
-                if (!_.findWhere(tableMetadata.aggregation_options, { short: "rows" })) {
-                    return false;
-                }
-            } else {
-                for (const [agg] of aggs) {
-                    if (!mbqlEq(agg, "metric") && !_.findWhere(tableMetadata.aggregation_options, { short: agg })) {
-                        // return false;
-                    }
-                }
-            }
-        }
-        return true;
-    },
+    // it's possible the user left some half-done parts of the query on screen when they hit the run button, so find those
+    // things now and clear them out so that we have a nice clean set of valid clauses in our query
 
-    cleanQuery(query) {
-        if (!query) {
-            return query;
-        }
+    // aggregations
+    query.aggregation = Query.getAggregations(query);
+    if (query.aggregation.length === 0) {
+      delete query.aggregation;
+    }
 
-        // it's possible the user left some half-done parts of the query on screen when they hit the run button, so find those
-        // things now and clear them out so that we have a nice clean set of valid clauses in our query
+    // breakouts
+    query.breakout = Query.getBreakouts(query);
+    if (query.breakout.length === 0) {
+      delete query.breakout;
+    }
 
-        // aggregations
-        query.aggregation = Query.getAggregations(query);
-        if (query.aggregation.length === 0) {
-            delete query.aggregation;
-        }
+    // filters
+    const filters = Query.getFilters(query).filter(filter =>
+      _.all(filter, a => a != null),
+    );
+    if (filters.length > 0) {
+      query.filter = ["AND", ...filters];
+    } else {
+      delete query.filter;
+    }
 
-        // breakouts
-        query.breakout = Query.getBreakouts(query);
-        if (query.breakout.length === 0) {
-            delete query.breakout;
-        }
+    if (query.order_by) {
+      query.order_by = query.order_by
+        .map(s => {
+          let field = s[0];
 
-        // filters
-        const filters = Query.getFilters(query).filter(filter =>
-            _.all(filter, a => a != null)
-        );
-        if (filters.length > 0) {
-            query.filter = ["AND", ...filters];
-        } else {
-            delete query.filter;
-        }
+          // remove incomplete sorts
+          if (!Query.isValidField(field) || s[1] == null) {
+            return null;
+          }
 
-        if (query.order_by) {
-            query.order_by = query.order_by.map((s) => {
-                let field = s[0];
-
-                // remove incomplete sorts
-                if (!Query.isValidField(field) || s[1] == null) {
-                    return null;
-                }
-
-                if (Query.isAggregateField(field)) {
-                    // remove aggregation sort if we can't sort by this aggregation
-                    if (Query.canSortByAggregateField(query, field[1])) {
-                        return s;
-                    }
-                } else if (Query.hasValidBreakout(query)) {
-                    let exactMatches = query.breakout.filter(b => Query.isSameField(b, field, true));
-                    if (exactMatches.length > 0) {
-                        return s;
-                    }
-                    let targetMatches = query.breakout.filter(b => Query.isSameField(b, field, false));
-                    if (targetMatches.length > 0) {
-                        // query processor expect the order_by clause to match the breakout's datetime-field unit or fk-> target,
-                        // so just replace it with the one that matches the target field
-                        // NOTE: if we have more than one breakout for the same target field this could match the wrong one
-                        if (targetMatches.length > 1) {
-                            console.warn("Sort clause matches more than one breakout field", s[0], targetMatches);
-                        }
-                        return [targetMatches[0], s[1]];
-                    }
-                } else if (Query.isBareRows(query)) {
-                    return s;
-                }
-
-                // otherwise remove sort if it doesn't have a breakout but isn't a bare rows aggregation
-                return null;
-            }).filter(s => s != null);
-
-            if (query.order_by.length === 0) {
-                delete query.order_by;
+          if (Query.isAggregateField(field)) {
+            // remove aggregation sort if we can't sort by this aggregation
+            if (Query.canSortByAggregateField(query, field[1])) {
+              return s;
             }
-        }
-
-        if (typeof query.limit !== "number") {
-            delete query.limit;
-        }
-
-        if (query.expressions) delete query.expressions['']; // delete any empty expressions
-
-        return query;
-    },
-
-    canAddDimensions(query) {
-        var MAX_DIMENSIONS = 2;
-        return query && query.breakout && (query.breakout.length < MAX_DIMENSIONS);
-    },
-
-    numDimensions(query) {
-        if (query && query.breakout) {
-            return query.breakout.filter(function(b) {
-                return b !== null;
-            }).length;
-        }
-
-        return 0;
-    },
-
-    hasValidBreakout(query) {
-        return (query && query.breakout &&
-                    query.breakout.length > 0 &&
-                    query.breakout[0] !== null);
-    },
-
-    canSortByAggregateField(query, index) {
-        if (!Query.hasValidBreakout(query)) {
-            return false;
-        }
-        const aggregations = Query.getAggregations(query);
-        return (
-            aggregations[index] && aggregations[index][0] &&
-            SORTABLE_AGGREGATION_TYPES.has(mbql(aggregations[index][0]))
-        );
-    },
-
-    isSegmentFilter(filter) {
-        return Array.isArray(filter) && filter[0] === "SEGMENT";
-    },
-
-    canAddLimitAndSort(query) {
-        if (Query.isBareRows(query)) {
-            return true;
-        } else if (Query.hasValidBreakout(query)) {
-            return true;
-        } else {
-            return false;
-        }
-    },
-
-    getSortableFields(query, fields) {
-        // in bare rows all fields are sortable, otherwise we only sort by our breakout columns
-        if (Query.isBareRows(query)) {
-            return fields;
-        } else if (Query.hasValidBreakout(query)) {
-            // further filter field list down to only fields in our breakout clause
-            var breakoutFieldList = [];
-
-            const breakouts = Query.getBreakouts(query);
-            breakouts.map(function (breakoutField) {
-                const fieldId = Query.getFieldTargetId(breakoutField);
-                const field = _.findWhere(fields, { id: fieldId });
-                if (field) {
-                    breakoutFieldList.push(field);
-                }
-            });
-
-            const aggregations = Query.getAggregations(query);
-            for (const [index, aggregation] of aggregations.entries()) {
-                if (Query.canSortByAggregateField(query, index)) {
-                    breakoutFieldList.push({
-                        id: ["aggregation",  index],
-                        name: aggregation[0], // e.g. "sum"
-                        display_name: aggregation[0]
-                    });
-                }
+          } else if (Query.hasValidBreakout(query)) {
+            let exactMatches = query.breakout.filter(b =>
+              Query.isSameField(b, field, true),
+            );
+            if (exactMatches.length > 0) {
+              return s;
             }
+            let targetMatches = query.breakout.filter(b =>
+              Query.isSameField(b, field, false),
+            );
+            if (targetMatches.length > 0) {
+              // query processor expect the order_by clause to match the breakout's datetime-field unit or fk-> target,
+              // so just replace it with the one that matches the target field
+              // NOTE: if we have more than one breakout for the same target field this could match the wrong one
+              if (targetMatches.length > 1) {
+                console.warn(
+                  "Sort clause matches more than one breakout field",
+                  s[0],
+                  targetMatches,
+                );
+              }
+              return [targetMatches[0], s[1]];
+            }
+          } else if (Query.isBareRows(query)) {
+            return s;
+          }
+
+          // otherwise remove sort if it doesn't have a breakout but isn't a bare rows aggregation
+          return null;
+        })
+        .filter(s => s != null);
+
+      if (query.order_by.length === 0) {
+        delete query.order_by;
+      }
+    }
 
-            return breakoutFieldList;
-        } else {
-            return [];
-        }
-    },
-
-    canAddSort(query) {
-        // TODO: allow for multiple sorting choices
-        return false;
-    },
-
-    getExpressions(query) {
-        return query.expressions || {};
-    },
-
-    setExpression(query, name, expression) {
-        if (name && expression) {
-            let expressions = query.expressions || {};
-            expressions[name] = expression;
-            query.expressions = expressions;
-        }
-
-        return query;
-    },
-
-    // remove an expression with NAME. Returns scrubbed QUERY with all references to expression removed.
-    removeExpression(query, name) {
-        if (!query.expressions) return query;
-
-        delete query.expressions[name];
-
-        if (_.isEmpty(query.expressions)) delete query.expressions;
-
-        // ok, now "scrub" the query to remove any references to the expression
-        function isExpressionReference(obj) {
-            return obj && obj.constructor === Array && obj.length === 2 && obj[0] === 'expression' && obj[1] === name;
-        }
-
-        function removeExpressionReferences(obj) {
-            return isExpressionReference(obj) ? null                                         :
-                   obj.constructor === Array  ? _.map(obj, removeExpressionReferences)       :
-                   typeof obj === 'object'    ? _.mapObject(obj, removeExpressionReferences) :
-                                                obj;
-        }
-
-        return this.cleanQuery(removeExpressionReferences(query));
-    },
-
-    isRegularField(field) {
-        return typeof field === "number";
-    },
-
-    isLocalField(field) {
-        return Array.isArray(field) && mbqlEq(field[0], "field-id");
-    },
-
-    isForeignKeyField(field) {
-        return Array.isArray(field) && mbqlEq(field[0], "fk->");
-    },
-
-    isDatetimeField(field) {
-        return Array.isArray(field) && mbqlEq(field[0], "datetime-field");
-    },
-
-    isBinningStrategy: F.isBinningStrategy,
-
-    isExpressionField(field) {
-        return Array.isArray(field) && field.length === 2 && mbqlEq(field[0], "expression");
-    },
-
-    isAggregateField(field) {
-        return Array.isArray(field) && mbqlEq(field[0], "aggregation");
-    },
-
-    // field literal has the formal ["field-literal", <field-name>, <field-base-type>]
-    isFieldLiteral(field) {
-        return Array.isArray(field) && field.length === 3 && mbqlEq(field[0], "field-literal") && _.isString(field[1]) && _.isString(field[2]);
-    },
-
-    isValidField(field) {
-        return (
-            (Query.isRegularField(field)) ||
-            (Query.isLocalField(field)) ||
-            (Query.isForeignKeyField(field) && Query.isRegularField(field[1]) && Query.isRegularField(field[2])) ||
-            // datetime field can  be either 4-item (deprecated): ["datetime-field", <field>, "as", <unit>]
-            // or 3 item (preferred style): ["datetime-field", <field>, <unit>]
-            (Query.isDatetimeField(field)   && Query.isValidField(field[1]) &&
-                (field.length === 4 ?
-                    (field[2] === "as" && typeof field[3] === "string") : // deprecated
-                    typeof field[2] === "string")) ||
-            (Query.isExpressionField(field) && _.isString(field[1])) ||
-            (Query.isAggregateField(field)  && typeof field[1] === "number") ||
-            Query.isFieldLiteral(field)
-        );
-    },
-
-    isSameField: function(fieldA, fieldB, exact = false) {
-        if (exact) {
-            return _.isEqual(fieldA, fieldB);
-        } else {
-            return Query.getFieldTargetId(fieldA) === Query.getFieldTargetId(fieldB);
-        }
-    },
-
-    // gets the target field ID (recursively) from any type of field, including raw field ID, fk->, and datetime-field cast.
-    getFieldTargetId: function(field) {
-        if (Query.isRegularField(field)) {
-            return field;
-        } else if (Query.isLocalField(field)) {
-            return field[1];
-        } else if (Query.isForeignKeyField(field)) {
-            return Query.getFieldTargetId(field[2]);
-        } else if (Query.isDatetimeField(field)) {
-            return Query.getFieldTargetId(field[1]);
-        } else if (Query.isBinningStrategy(field)) {
-            return Query.getFieldTargetId(field[1]);
-        } else if (Query.isFieldLiteral(field)) {
-            return field;
-        }
-        console.warn("Unknown field type: ", field);
-    },
-
-    // gets the table and field definitions from from a raw, fk->, or datetime-field field
-    getFieldTarget: function(field, tableDef, path = []) {
-        if (Query.isRegularField(field)) {
-            return { table: tableDef, field: Table.getField(tableDef, field), path };
-        } else if (Query.isLocalField(field)) {
-            return Query.getFieldTarget(field[1], tableDef, path);
-        } else if (Query.isForeignKeyField(field)) {
-            let fkFieldDef = Table.getField(tableDef, field[1]);
-            let targetTableDef = fkFieldDef && fkFieldDef.target.table;
-            return Query.getFieldTarget(field[2], targetTableDef, path.concat(fkFieldDef));
-        } else if (Query.isDatetimeField(field)) {
-            return {
-                ...Query.getFieldTarget(field[1], tableDef, path),
-                unit: Query.getDatetimeUnit(field)
-            };
-        } else if (Query.isBinningStrategy(field)) {
-            return Query.getFieldTarget(field[1], tableDef, path);
-        } else if (Query.isExpressionField(field)) {
-            // hmmm, since this is a dynamic field we'll need to build this here
-            let fieldDef = {
-                display_name: field[1],
-                name: field[1],
-                // TODO: we need to do something better here because filtering depends on knowing a sensible type for the field
-                base_type: TYPE.Integer,
-                operators_lookup: {},
-                operators: [],
-                active: true,
-                fk_target_field_id: null,
-                parent_id: null,
-                preview_display: true,
-                special_type: null,
-                target: null,
-                visibility_type: "normal"
-            };
-            fieldDef.operators = getOperators(fieldDef, tableDef);
-            fieldDef.operators_lookup = createLookupByProperty(fieldDef.operators, "name");
-
-            return {
-                table: tableDef,
-                field: fieldDef,
-                path: path
-            };
-        } else if (Query.isFieldLiteral(field)) {
-            return { table: tableDef, field: Table.getField(tableDef, field), path }; // just pretend it's a normal field
-        }
+    if (typeof query.limit !== "number") {
+      delete query.limit;
+    }
 
-        console.warn("Unknown field type: ", field);
-    },
+    if (query.expressions) delete query.expressions[""]; // delete any empty expressions
 
-    getFieldPath(fieldId, tableDef) {
-        let path = [];
-        while (fieldId != null) {
-            let field = Table.getField(tableDef, fieldId);
-            path.unshift(field);
-            fieldId = field && field.parent_id;
-        }
-        return path;
-    },
-
-    getFieldPathName(fieldId, tableDef) {
-        return Query.getFieldPath(fieldId, tableDef).map(formatField).join(": ")
-    },
+    return query;
+  },
 
-    getDatetimeUnit(field) {
-        if (field.length === 4) {
-            return field[3]; // deprecated
-        } else {
-            return field[2];
-        }
-    },
+  canAddDimensions(query) {
+    var MAX_DIMENSIONS = 2;
+    return query && query.breakout && query.breakout.length < MAX_DIMENSIONS;
+  },
 
-    getFieldOptions(fields, includeJoins = false, filterFn = _.identity, usedFields = {}) {
-        var results = {
-            count: 0,
-            fields: null,
-            fks: []
-        };
-        // filter based on filterFn, then remove fks if they'll be duplicated in the joins fields
-        results.fields = filterFn(fields).filter((f) => !usedFields[f.id] && (!isFK(f.special_type) || !includeJoins));
-        results.count += results.fields.length;
-        if (includeJoins) {
-            results.fks = fields.filter((f) => isFK(f.special_type) && f.target).map((joinField) => {
-                var targetFields = filterFn(joinField.target.table.fields).filter(f => (!Array.isArray(f.id) || f.id[0] !== "aggregation") && !usedFields[f.id]);
-                results.count += targetFields.length;
-                return {
-                    field: joinField,
-                    fields: targetFields
-                };
-            }).filter((r) => r.fields.length > 0);
-        }
+  numDimensions(query) {
+    if (query && query.breakout) {
+      return query.breakout.filter(function(b) {
+        return b !== null;
+      }).length;
+    }
 
-        return results;
-    },
+    return 0;
+  },
+
+  hasValidBreakout(query) {
+    return (
+      query &&
+      query.breakout &&
+      query.breakout.length > 0 &&
+      query.breakout[0] !== null
+    );
+  },
+
+  canSortByAggregateField(query, index) {
+    if (!Query.hasValidBreakout(query)) {
+      return false;
+    }
+    const aggregations = Query.getAggregations(query);
+    return (
+      aggregations[index] &&
+      aggregations[index][0] &&
+      SORTABLE_AGGREGATION_TYPES.has(mbql(aggregations[index][0]))
+    );
+  },
+
+  isSegmentFilter(filter) {
+    return Array.isArray(filter) && filter[0] === "SEGMENT";
+  },
+
+  canAddLimitAndSort(query) {
+    if (Query.isBareRows(query)) {
+      return true;
+    } else if (Query.hasValidBreakout(query)) {
+      return true;
+    } else {
+      return false;
+    }
+  },
+
+  getSortableFields(query, fields) {
+    // in bare rows all fields are sortable, otherwise we only sort by our breakout columns
+    if (Query.isBareRows(query)) {
+      return fields;
+    } else if (Query.hasValidBreakout(query)) {
+      // further filter field list down to only fields in our breakout clause
+      var breakoutFieldList = [];
+
+      const breakouts = Query.getBreakouts(query);
+      breakouts.map(function(breakoutField) {
+        const fieldId = Query.getFieldTargetId(breakoutField);
+        const field = _.findWhere(fields, { id: fieldId });
+        if (field) {
+          breakoutFieldList.push(field);
+        }
+      });
+
+      const aggregations = Query.getAggregations(query);
+      for (const [index, aggregation] of aggregations.entries()) {
+        if (Query.canSortByAggregateField(query, index)) {
+          breakoutFieldList.push({
+            id: ["aggregation", index],
+            name: aggregation[0], // e.g. "sum"
+            display_name: aggregation[0],
+          });
+        }
+      }
+
+      return breakoutFieldList;
+    } else {
+      return [];
+    }
+  },
+
+  canAddSort(query) {
+    // TODO: allow for multiple sorting choices
+    return false;
+  },
+
+  getExpressions(query) {
+    return query.expressions || {};
+  },
+
+  setExpression(query, name, expression) {
+    if (name && expression) {
+      let expressions = query.expressions || {};
+      expressions[name] = expression;
+      query.expressions = expressions;
+    }
 
-    formatField(fieldDef, options = {}) {
-        let name = stripId(fieldDef && (fieldDef.display_name || fieldDef.name));
-        return name;
-    },
+    return query;
+  },
 
-    getFieldName(tableMetadata, field, options) {
-        try {
-            let target = Query.getFieldTarget(field, tableMetadata);
-            let components = [];
-            if (target.path) {
-                for (const fieldDef of target.path) {
-                    components.push(Query.formatField(fieldDef, options), " → ");
-                }
-            }
-            components.push(Query.formatField(target.field, options));
-            if (target.unit) {
-                components.push(` (${target.unit})`)
-            }
-            return components;
-        } catch (e) {
-            console.warn("Couldn't format field name for field", field, "in table", tableMetadata);
-        }
-        return "[Unknown Field]";
-    },
+  // remove an expression with NAME. Returns scrubbed QUERY with all references to expression removed.
+  removeExpression(query, name) {
+    if (!query.expressions) return query;
 
-    getTableDescription(tableMetadata) {
-        return [inflection.pluralize(tableMetadata.display_name)];
-    },
+    delete query.expressions[name];
 
-    getAggregationDescription(tableMetadata, query, options) {
-        return conjunctList(Query.getAggregations(query).map(aggregation => {
-            if (NamedClause.isNamed(aggregation)) {
-                return [NamedClause.getName(aggregation)];
-            }
-            if (AggregationClause.isMetric(aggregation)) {
-                let metric = _.findWhere(tableMetadata.metrics, { id: AggregationClause.getMetric(aggregation) });
-                let name = metric ? metric.name : "[Unknown Metric]";
-                return [options.jsx ? <span className="text-green text-bold">{name}</span> : name];
-            }
-            switch (aggregation[0]) {
-                case "rows":      return           [t`Raw data`];
-                case "count":     return              [t`Count`];
-                case "cum_count": return   [t`Cumulative count`];
-                case "avg":       return            [t`Average of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                case "distinct":  return    [t`Distinct values of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                case "stddev":    return [t`Standard deviation of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                case "sum":       return                [t`Sum of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                case "cum_sum":   return     [t`Cumulative sum of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                case "max":       return            [t`Maximum of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                case "min":       return            [t`Minimum of `, Query.getFieldName(tableMetadata, aggregation[1], options)];
-                default:          return [formatExpression(aggregation, { tableMetadata })]
-            }
-        }), "and");
-    },
+    if (_.isEmpty(query.expressions)) delete query.expressions;
 
-    getBreakoutDescription(tableMetadata, { breakout }, options) {
-        if (breakout && breakout.length > 0) {
-            return [t`Grouped by `, joinList(breakout.map((b) => Query.getFieldName(tableMetadata, b, options)), " and ")];
-        }
-    },
-
-    getFilterDescription(tableMetadata, query, options) {
-        // getFilters returns list of filters without the implied "AND"
-        let filters = ["AND"].concat(Query.getFilters(query));
-        if (filters && filters.length > 1) {
-            return [t`Filtered by `, Query.getFilterClauseDescription(tableMetadata, filters, options)];
-        }
-    },
+    // ok, now "scrub" the query to remove any references to the expression
+    function isExpressionReference(obj) {
+      return (
+        obj &&
+        obj.constructor === Array &&
+        obj.length === 2 &&
+        obj[0] === "expression" &&
+        obj[1] === name
+      );
+    }
 
-    getFilterClauseDescription(tableMetadata, filter, options) {
-        if (filter[0] === "AND" || filter[0] === "OR") {
-            let clauses = filter.slice(1).map((f) => Query.getFilterClauseDescription(tableMetadata, f, options));
-            return conjunctList(clauses, filter[0].toLowerCase());
-        } else if (filter[0] === "SEGMENT") {
-            let segment = _.findWhere(tableMetadata.segments, { id: filter[1] });
-            let name = segment ? segment.name : "[Unknown Segment]";
-            return options.jsx ? <span className="text-purple text-bold">{name}</span> : name;
-        } else {
-            return Query.getFieldName(tableMetadata, filter[1], options);
-        }
-    },
+    function removeExpressionReferences(obj) {
+      return isExpressionReference(obj)
+        ? null
+        : obj.constructor === Array
+          ? _.map(obj, removeExpressionReferences)
+          : typeof obj === "object"
+            ? _.mapObject(obj, removeExpressionReferences)
+            : obj;
+    }
 
-    getOrderByDescription(tableMetadata, { order_by }, options) {
-        if (order_by && order_by.length > 0) {
-            return [t`Sorted by `, joinList(order_by.map(o => Query.getFieldName(tableMetadata, o[0], options) + " " + o[1]), " and ")];
-        }
-    },
+    return this.cleanQuery(removeExpressionReferences(query));
+  },
+
+  isRegularField(field) {
+    return typeof field === "number";
+  },
+
+  isLocalField(field) {
+    return Array.isArray(field) && mbqlEq(field[0], "field-id");
+  },
+
+  isForeignKeyField(field) {
+    return Array.isArray(field) && mbqlEq(field[0], "fk->");
+  },
+
+  isDatetimeField(field) {
+    return Array.isArray(field) && mbqlEq(field[0], "datetime-field");
+  },
+
+  isBinningStrategy: F.isBinningStrategy,
+
+  isExpressionField(field) {
+    return (
+      Array.isArray(field) &&
+      field.length === 2 &&
+      mbqlEq(field[0], "expression")
+    );
+  },
+
+  isAggregateField(field) {
+    return Array.isArray(field) && mbqlEq(field[0], "aggregation");
+  },
+
+  // field literal has the formal ["field-literal", <field-name>, <field-base-type>]
+  isFieldLiteral(field) {
+    return (
+      Array.isArray(field) &&
+      field.length === 3 &&
+      mbqlEq(field[0], "field-literal") &&
+      _.isString(field[1]) &&
+      _.isString(field[2])
+    );
+  },
+
+  isValidField(field) {
+    return (
+      Query.isRegularField(field) ||
+      Query.isLocalField(field) ||
+      (Query.isForeignKeyField(field) &&
+        Query.isRegularField(field[1]) &&
+        Query.isRegularField(field[2])) ||
+      // datetime field can  be either 4-item (deprecated): ["datetime-field", <field>, "as", <unit>]
+      // or 3 item (preferred style): ["datetime-field", <field>, <unit>]
+      (Query.isDatetimeField(field) &&
+        Query.isValidField(field[1]) &&
+        (field.length === 4
+          ? field[2] === "as" && typeof field[3] === "string" // deprecated
+          : typeof field[2] === "string")) ||
+      (Query.isExpressionField(field) && _.isString(field[1])) ||
+      (Query.isAggregateField(field) && typeof field[1] === "number") ||
+      Query.isFieldLiteral(field)
+    );
+  },
+
+  isSameField: function(fieldA, fieldB, exact = false) {
+    if (exact) {
+      return _.isEqual(fieldA, fieldB);
+    } else {
+      return Query.getFieldTargetId(fieldA) === Query.getFieldTargetId(fieldB);
+    }
+  },
+
+  // gets the target field ID (recursively) from any type of field, including raw field ID, fk->, and datetime-field cast.
+  getFieldTargetId: function(field) {
+    if (Query.isRegularField(field)) {
+      return field;
+    } else if (Query.isLocalField(field)) {
+      return field[1];
+    } else if (Query.isForeignKeyField(field)) {
+      return Query.getFieldTargetId(field[2]);
+    } else if (Query.isDatetimeField(field)) {
+      return Query.getFieldTargetId(field[1]);
+    } else if (Query.isBinningStrategy(field)) {
+      return Query.getFieldTargetId(field[1]);
+    } else if (Query.isFieldLiteral(field)) {
+      return field;
+    }
+    console.warn("Unknown field type: ", field);
+  },
+
+  // gets the table and field definitions from from a raw, fk->, or datetime-field field
+  getFieldTarget: function(field, tableDef, path = []) {
+    if (Query.isRegularField(field)) {
+      return { table: tableDef, field: Table.getField(tableDef, field), path };
+    } else if (Query.isLocalField(field)) {
+      return Query.getFieldTarget(field[1], tableDef, path);
+    } else if (Query.isForeignKeyField(field)) {
+      let fkFieldDef = Table.getField(tableDef, field[1]);
+      let targetTableDef = fkFieldDef && fkFieldDef.target.table;
+      return Query.getFieldTarget(
+        field[2],
+        targetTableDef,
+        path.concat(fkFieldDef),
+      );
+    } else if (Query.isDatetimeField(field)) {
+      return {
+        ...Query.getFieldTarget(field[1], tableDef, path),
+        unit: Query.getDatetimeUnit(field),
+      };
+    } else if (Query.isBinningStrategy(field)) {
+      return Query.getFieldTarget(field[1], tableDef, path);
+    } else if (Query.isExpressionField(field)) {
+      // hmmm, since this is a dynamic field we'll need to build this here
+      let fieldDef = {
+        display_name: field[1],
+        name: field[1],
+        // TODO: we need to do something better here because filtering depends on knowing a sensible type for the field
+        base_type: TYPE.Integer,
+        operators_lookup: {},
+        operators: [],
+        active: true,
+        fk_target_field_id: null,
+        parent_id: null,
+        preview_display: true,
+        special_type: null,
+        target: null,
+        visibility_type: "normal",
+      };
+      fieldDef.operators = getOperators(fieldDef, tableDef);
+      fieldDef.operators_lookup = createLookupByProperty(
+        fieldDef.operators,
+        "name",
+      );
+
+      return {
+        table: tableDef,
+        field: fieldDef,
+        path: path,
+      };
+    } else if (Query.isFieldLiteral(field)) {
+      return { table: tableDef, field: Table.getField(tableDef, field), path }; // just pretend it's a normal field
+    }
 
-    getLimitDescription(tableMetadata, { limit }) {
-        if (limit != null) {
-            return [limit, " ", inflection.inflect("row", limit)];
-        }
-    },
+    console.warn("Unknown field type: ", field);
+  },
 
-    generateQueryDescription(tableMetadata, query, options = {}) {
-        if (!tableMetadata) {
-            return "";
-        }
+  getFieldPath(fieldId, tableDef) {
+    let path = [];
+    while (fieldId != null) {
+      let field = Table.getField(tableDef, fieldId);
+      path.unshift(field);
+      fieldId = field && field.parent_id;
+    }
+    return path;
+  },
+
+  getFieldPathName(fieldId, tableDef) {
+    return Query.getFieldPath(fieldId, tableDef)
+      .map(formatField)
+      .join(": ");
+  },
+
+  getDatetimeUnit(field) {
+    if (field.length === 4) {
+      return field[3]; // deprecated
+    } else {
+      return field[2];
+    }
+  },
+
+  getFieldOptions(
+    fields,
+    includeJoins = false,
+    filterFn = _.identity,
+    usedFields = {},
+  ) {
+    var results = {
+      count: 0,
+      fields: null,
+      fks: [],
+    };
+    // filter based on filterFn, then remove fks if they'll be duplicated in the joins fields
+    results.fields = filterFn(fields).filter(
+      f => !usedFields[f.id] && (!isFK(f.special_type) || !includeJoins),
+    );
+    results.count += results.fields.length;
+    if (includeJoins) {
+      results.fks = fields
+        .filter(f => isFK(f.special_type) && f.target)
+        .map(joinField => {
+          var targetFields = filterFn(joinField.target.table.fields).filter(
+            f =>
+              (!Array.isArray(f.id) || f.id[0] !== "aggregation") &&
+              !usedFields[f.id],
+          );
+          results.count += targetFields.length;
+          return {
+            field: joinField,
+            fields: targetFields,
+          };
+        })
+        .filter(r => r.fields.length > 0);
+    }
 
-        options = {
-            jsx: false,
-            sections: ["table", "aggregation", "breakout", "filter", "order_by", "limit"],
-            ...options
-        };
-
-        const sectionFns = {
-            table:       Query.getTableDescription,
-            aggregation: Query.getAggregationDescription,
-            breakout:    Query.getBreakoutDescription,
-            filter:      Query.getFilterDescription,
-            order_by:    Query.getOrderByDescription,
-            limit:       Query.getLimitDescription
-        }
+    return results;
+  },
+
+  formatField(fieldDef, options = {}) {
+    let name = stripId(fieldDef && (fieldDef.display_name || fieldDef.name));
+    return name;
+  },
+
+  getFieldName(tableMetadata, field, options) {
+    try {
+      let target = Query.getFieldTarget(field, tableMetadata);
+      let components = [];
+      if (target.path) {
+        for (const fieldDef of target.path) {
+          components.push(Query.formatField(fieldDef, options), " → ");
+        }
+      }
+      components.push(Query.formatField(target.field, options));
+      if (target.unit) {
+        components.push(` (${target.unit})`);
+      }
+      return components;
+    } catch (e) {
+      console.warn(
+        "Couldn't format field name for field",
+        field,
+        "in table",
+        tableMetadata,
+      );
+    }
+    return "[Unknown Field]";
+  },
+
+  getTableDescription(tableMetadata) {
+    return [inflection.pluralize(tableMetadata.display_name)];
+  },
+
+  getAggregationDescription(tableMetadata, query, options) {
+    return conjunctList(
+      Query.getAggregations(query).map(aggregation => {
+        if (NamedClause.isNamed(aggregation)) {
+          return [NamedClause.getName(aggregation)];
+        }
+        if (AggregationClause.isMetric(aggregation)) {
+          let metric = _.findWhere(tableMetadata.metrics, {
+            id: AggregationClause.getMetric(aggregation),
+          });
+          let name = metric ? metric.name : "[Unknown Metric]";
+          return [
+            options.jsx ? (
+              <span className="text-green text-bold">{name}</span>
+            ) : (
+              name
+            ),
+          ];
+        }
+        switch (aggregation[0]) {
+          case "rows":
+            return [t`Raw data`];
+          case "count":
+            return [t`Count`];
+          case "cum_count":
+            return [t`Cumulative count`];
+          case "avg":
+            return [
+              t`Average of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          case "distinct":
+            return [
+              t`Distinct values of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          case "stddev":
+            return [
+              t`Standard deviation of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          case "sum":
+            return [
+              t`Sum of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          case "cum_sum":
+            return [
+              t`Cumulative sum of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          case "max":
+            return [
+              t`Maximum of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          case "min":
+            return [
+              t`Minimum of `,
+              Query.getFieldName(tableMetadata, aggregation[1], options),
+            ];
+          default:
+            return [formatExpression(aggregation, { tableMetadata })];
+        }
+      }),
+      "and",
+    );
+  },
+
+  getBreakoutDescription(tableMetadata, { breakout }, options) {
+    if (breakout && breakout.length > 0) {
+      return [
+        t`Grouped by `,
+        joinList(
+          breakout.map(b => Query.getFieldName(tableMetadata, b, options)),
+          " and ",
+        ),
+      ];
+    }
+  },
+
+  getFilterDescription(tableMetadata, query, options) {
+    // getFilters returns list of filters without the implied "AND"
+    let filters = ["AND"].concat(Query.getFilters(query));
+    if (filters && filters.length > 1) {
+      return [
+        t`Filtered by `,
+        Query.getFilterClauseDescription(tableMetadata, filters, options),
+      ];
+    }
+  },
+
+  getFilterClauseDescription(tableMetadata, filter, options) {
+    if (filter[0] === "AND" || filter[0] === "OR") {
+      let clauses = filter
+        .slice(1)
+        .map(f => Query.getFilterClauseDescription(tableMetadata, f, options));
+      return conjunctList(clauses, filter[0].toLowerCase());
+    } else if (filter[0] === "SEGMENT") {
+      let segment = _.findWhere(tableMetadata.segments, { id: filter[1] });
+      let name = segment ? segment.name : "[Unknown Segment]";
+      return options.jsx ? (
+        <span className="text-purple text-bold">{name}</span>
+      ) : (
+        name
+      );
+    } else {
+      return Query.getFieldName(tableMetadata, filter[1], options);
+    }
+  },
+
+  getOrderByDescription(tableMetadata, { order_by }, options) {
+    if (order_by && order_by.length > 0) {
+      return [
+        t`Sorted by `,
+        joinList(
+          order_by.map(
+            o => Query.getFieldName(tableMetadata, o[0], options) + " " + o[1],
+          ),
+          " and ",
+        ),
+      ];
+    }
+  },
 
-        // these array gymnastics are needed to support JSX formatting
-        let sections = options.sections
-            .map((section) => _.flatten(sectionFns[section](tableMetadata, query, options)).filter(s => !!s))
-            .filter(s => s && s.length > 0);
+  getLimitDescription(tableMetadata, { limit }) {
+    if (limit != null) {
+      return [limit, " ", inflection.inflect("row", limit)];
+    }
+  },
 
-        let description = _.flatten(joinList(sections, ", "));
-        if (options.jsx) {
-            return <span>{description}</span>;
-        } else {
-            return description.join("");
-        }
-    },
+  generateQueryDescription(tableMetadata, query, options = {}) {
+    if (!tableMetadata) {
+      return "";
+    }
 
-    getDatetimeFieldUnit(field) {
-        return field && field[3];
-    },
+    options = {
+      jsx: false,
+      sections: [
+        "table",
+        "aggregation",
+        "breakout",
+        "filter",
+        "order_by",
+        "limit",
+      ],
+      ...options,
+    };
+
+    const sectionFns = {
+      table: Query.getTableDescription,
+      aggregation: Query.getAggregationDescription,
+      breakout: Query.getBreakoutDescription,
+      filter: Query.getFilterDescription,
+      order_by: Query.getOrderByDescription,
+      limit: Query.getLimitDescription,
+    };
+
+    // these array gymnastics are needed to support JSX formatting
+    let sections = options.sections
+      .map(section =>
+        _.flatten(sectionFns[section](tableMetadata, query, options)).filter(
+          s => !!s,
+        ),
+      )
+      .filter(s => s && s.length > 0);
+
+    let description = _.flatten(joinList(sections, ", "));
+    if (options.jsx) {
+      return <span>{description}</span>;
+    } else {
+      return description.join("");
+    }
+  },
 
-    getAggregationType(aggregation) {
-        return aggregation && aggregation[0];
-    },
+  getDatetimeFieldUnit(field) {
+    return field && field[3];
+  },
 
-    getAggregationField(aggregation) {
-        return aggregation && aggregation[1];
-    },
+  getAggregationType(aggregation) {
+    return aggregation && aggregation[0];
+  },
 
-    getQueryColumn(tableMetadata, field) {
-        let target = Query.getFieldTarget(field, tableMetadata);
-        let column = { ...target.field };
-        if (Query.isDatetimeField(field)) {
-            column.unit = Query.getDatetimeFieldUnit(field);
-        }
-        return column;
-    },
+  getAggregationField(aggregation) {
+    return aggregation && aggregation[1];
+  },
 
-    getQueryColumns(tableMetadata, query) {
-        let columns = Query.getBreakouts(query).map(b => Query.getQueryColumn(tableMetadata, b));
-        if (Query.isBareRows(query)) {
-            if (columns.length === 0) {
-                return null;
-            }
-        } else {
-            for (const aggregation of Query.getAggregations(query)) {
-                const type = Query.getAggregationType(aggregation)
-                columns.push({
-                    name: METRIC_NAME_BY_AGGREGATION[type],
-                    base_type: METRIC_TYPE_BY_AGGREGATION[type],
-                    special_type: TYPE.Number
-                });
-            }
-        }
-        return columns;
+  getQueryColumn(tableMetadata, field) {
+    let target = Query.getFieldTarget(field, tableMetadata);
+    let column = { ...target.field };
+    if (Query.isDatetimeField(field)) {
+      column.unit = Query.getDatetimeFieldUnit(field);
     }
-}
+    return column;
+  },
+
+  getQueryColumns(tableMetadata, query) {
+    let columns = Query.getBreakouts(query).map(b =>
+      Query.getQueryColumn(tableMetadata, b),
+    );
+    if (Query.isBareRows(query)) {
+      if (columns.length === 0) {
+        return null;
+      }
+    } else {
+      for (const aggregation of Query.getAggregations(query)) {
+        const type = Query.getAggregationType(aggregation);
+        columns.push({
+          name: METRIC_NAME_BY_AGGREGATION[type],
+          base_type: METRIC_TYPE_BY_AGGREGATION[type],
+          special_type: TYPE.Number,
+        });
+      }
+    }
+    return columns;
+  },
+};
 
 for (const prop in Q) {
-    // eslint-disable-next-line import/namespace
-    Query[prop] = Q[prop];
+  // eslint-disable-next-line import/namespace
+  Query[prop] = Q[prop];
 }
 
 import { isMath } from "metabase/lib/expressions";
 
 export const NamedClause = {
-    isNamed(clause) {
-        return Array.isArray(clause) && mbqlEq(clause[0], "named");
-    },
-    getName(clause) {
-        return NamedClause.isNamed(clause) ? clause[2] : null;
-    },
-    getContent(clause) {
-        return NamedClause.isNamed(clause) ? clause[1] : clause;
-    },
-    setName(clause, name) {
-        return ["named", NamedClause.getContent(clause), name];
-    },
-    setContent(clause, content) {
-        return NamedClause.isNamed(clause) ?
-            ["named", content, NamedClause.getName(clause)] :
-            content;
-    }
-}
+  isNamed(clause) {
+    return Array.isArray(clause) && mbqlEq(clause[0], "named");
+  },
+  getName(clause) {
+    return NamedClause.isNamed(clause) ? clause[2] : null;
+  },
+  getContent(clause) {
+    return NamedClause.isNamed(clause) ? clause[1] : clause;
+  },
+  setName(clause, name) {
+    return ["named", NamedClause.getContent(clause), name];
+  },
+  setContent(clause, content) {
+    return NamedClause.isNamed(clause)
+      ? ["named", content, NamedClause.getName(clause)]
+      : content;
+  },
+};
 
 export const AggregationClause = {
-
-    // predicate function to test if a given aggregation clause is fully formed
-    isValid(aggregation) {
-        if (aggregation && _.isArray(aggregation) &&
-                ((aggregation.length === 1 && aggregation[0] !== null) ||
-                 (aggregation.length === 2 && aggregation[0] !== null && aggregation[1] !== null))) {
-            return true;
-        }
-        return false;
-    },
-
-    // predicate function to test if the given aggregation clause represents a Bare Rows aggregation
-    isBareRows(aggregation) {
-        return AggregationClause.isValid(aggregation) && mbqlEq(aggregation[0], "rows");
-    },
-
-    // predicate function to test if a given aggregation clause represents a standard aggregation
-    isStandard(aggregation) {
-        return AggregationClause.isValid(aggregation) && !mbqlEq(aggregation[0], "metric");
-    },
-
-    getAggregation(aggregation) {
-        return aggregation && mbql(aggregation[0]);
-    },
-
-    // predicate function to test if a given aggregation clause represents a metric
-    isMetric(aggregation) {
-        return AggregationClause.isValid(aggregation) && mbqlEq(aggregation[0], "metric");
-    },
-
-    // get the metricId from a metric aggregation clause
-    getMetric(aggregation) {
-        if (aggregation && AggregationClause.isMetric(aggregation)) {
-            return aggregation[1];
-        } else {
-            return null;
-        }
-    },
-
-    isCustom(aggregation) {
-        // for now treal all named clauses as custom
-        return aggregation && NamedClause.isNamed(aggregation) || isMath(aggregation) || (
-            AggregationClause.isStandard(aggregation) && _.any(aggregation.slice(1), (arg) => isMath(arg))
-        );
-    },
-
-    // get the operator from a standard aggregation clause
-    getOperator(aggregation) {
-        if (aggregation && aggregation.length > 0 && !mbqlEq(aggregation[0], "metric")) {
-            return aggregation[0];
-        } else {
-            return null;
-        }
-    },
-
-    // get the fieldId from a standard aggregation clause
-    getField(aggregation) {
-        if (aggregation && aggregation.length > 1 && !mbqlEq(aggregation[0], "metric")) {
-            return aggregation[1];
-        } else {
-            return null;
-        }
-    },
-
-    // set the fieldId on a standard aggregation clause
-    setField(aggregation, fieldId) {
-        if (aggregation && aggregation.length > 0 && aggregation[0] && aggregation[0] !== "METRIC") {
-            return [aggregation[0], fieldId];
-        } else {
-            // TODO: is there a better failure response than just returning the aggregation unmodified??
-            return aggregation;
-        }
+  // predicate function to test if a given aggregation clause is fully formed
+  isValid(aggregation) {
+    if (
+      aggregation &&
+      _.isArray(aggregation) &&
+      ((aggregation.length === 1 && aggregation[0] !== null) ||
+        (aggregation.length === 2 &&
+          aggregation[0] !== null &&
+          aggregation[1] !== null))
+    ) {
+      return true;
     }
-}
+    return false;
+  },
+
+  // predicate function to test if the given aggregation clause represents a Bare Rows aggregation
+  isBareRows(aggregation) {
+    return (
+      AggregationClause.isValid(aggregation) && mbqlEq(aggregation[0], "rows")
+    );
+  },
+
+  // predicate function to test if a given aggregation clause represents a standard aggregation
+  isStandard(aggregation) {
+    return (
+      AggregationClause.isValid(aggregation) &&
+      !mbqlEq(aggregation[0], "metric")
+    );
+  },
+
+  getAggregation(aggregation) {
+    return aggregation && mbql(aggregation[0]);
+  },
+
+  // predicate function to test if a given aggregation clause represents a metric
+  isMetric(aggregation) {
+    return (
+      AggregationClause.isValid(aggregation) && mbqlEq(aggregation[0], "metric")
+    );
+  },
+
+  // get the metricId from a metric aggregation clause
+  getMetric(aggregation) {
+    if (aggregation && AggregationClause.isMetric(aggregation)) {
+      return aggregation[1];
+    } else {
+      return null;
+    }
+  },
+
+  isCustom(aggregation) {
+    // for now treal all named clauses as custom
+    return (
+      (aggregation && NamedClause.isNamed(aggregation)) ||
+      isMath(aggregation) ||
+      (AggregationClause.isStandard(aggregation) &&
+        _.any(aggregation.slice(1), arg => isMath(arg)))
+    );
+  },
+
+  // get the operator from a standard aggregation clause
+  getOperator(aggregation) {
+    if (
+      aggregation &&
+      aggregation.length > 0 &&
+      !mbqlEq(aggregation[0], "metric")
+    ) {
+      return aggregation[0];
+    } else {
+      return null;
+    }
+  },
+
+  // get the fieldId from a standard aggregation clause
+  getField(aggregation) {
+    if (
+      aggregation &&
+      aggregation.length > 1 &&
+      !mbqlEq(aggregation[0], "metric")
+    ) {
+      return aggregation[1];
+    } else {
+      return null;
+    }
+  },
+
+  // set the fieldId on a standard aggregation clause
+  setField(aggregation, fieldId) {
+    if (
+      aggregation &&
+      aggregation.length > 0 &&
+      aggregation[0] &&
+      aggregation[0] !== "METRIC"
+    ) {
+      return [aggregation[0], fieldId];
+    } else {
+      // TODO: is there a better failure response than just returning the aggregation unmodified??
+      return aggregation;
+    }
+  },
+};
 
 export const BreakoutClause = {
-
-    setBreakout(breakout, index, value) {
-        if (!breakout) {
-            breakout = [];
-        }
-        return [...breakout.slice(0,index), value, ...breakout.slice(index + 1)];
-    },
-
-    removeBreakout(breakout, index) {
-        if (!breakout) {
-            breakout = [];
-        }
-        return [...breakout.slice(0,index), ...breakout.slice(index + 1)];
+  setBreakout(breakout, index, value) {
+    if (!breakout) {
+      breakout = [];
     }
-}
+    return [...breakout.slice(0, index), value, ...breakout.slice(index + 1)];
+  },
 
+  removeBreakout(breakout, index) {
+    if (!breakout) {
+      breakout = [];
+    }
+    return [...breakout.slice(0, index), ...breakout.slice(index + 1)];
+  },
+};
 
 function joinList(list, joiner) {
-    return _.flatten(list.map((l, i) => i === list.length - 1 ? [l] : [l, joiner]), true);
+  return _.flatten(
+    list.map((l, i) => (i === list.length - 1 ? [l] : [l, joiner])),
+    true,
+  );
 }
 
 function conjunctList(list, conjunction) {
-    switch (list.length) {
-        case 0: return null;
-        case 1: return list[0];
-        case 2: return [list[0], " ", conjunction, " ", list[1]];
-        default: return [list.slice(0, -1).join(", "), ", ", conjunction, " ", list[list.length - 1]];
-    }
+  switch (list.length) {
+    case 0:
+      return null;
+    case 1:
+      return list[0];
+    case 2:
+      return [list[0], " ", conjunction, " ", list[1]];
+    default:
+      return [
+        list.slice(0, -1).join(", "),
+        ", ",
+        conjunction,
+        " ",
+        list[list.length - 1],
+      ];
+  }
 }
 
 export default Query;
diff --git a/frontend/src/metabase/lib/query/aggregation.js b/frontend/src/metabase/lib/query/aggregation.js
index 136c64853f3a2989922fad1510f91cc644153dbe..a8df392c13cf99856e138ef6f09c76583cfdb8ef 100644
--- a/frontend/src/metabase/lib/query/aggregation.js
+++ b/frontend/src/metabase/lib/query/aggregation.js
@@ -6,52 +6,68 @@ import _ from "underscore";
 import type { AggregationClause, Aggregation } from "metabase/meta/types/Query";
 
 // returns canonical list of Aggregations, i.e. with deprecated "rows" removed
-export function getAggregations(aggregation: ?AggregationClause): Aggregation[] {
-    let aggregations: Aggregation[];
-    if (Array.isArray(aggregation) && Array.isArray(aggregation[0])) {
-        aggregations = (aggregation: any);
-    } else if (Array.isArray(aggregation) && typeof aggregation[0] === "string") {
-        // legacy
-        aggregations = [(aggregation: any)];
-    } else {
-        aggregations = [];
-    }
-    return aggregations.filter(agg => agg && agg[0] && !mbqlEq(agg[0], "rows"));
+export function getAggregations(
+  aggregation: ?AggregationClause,
+): Aggregation[] {
+  let aggregations: Aggregation[];
+  if (Array.isArray(aggregation) && Array.isArray(aggregation[0])) {
+    aggregations = (aggregation: any);
+  } else if (Array.isArray(aggregation) && typeof aggregation[0] === "string") {
+    // legacy
+    aggregations = [(aggregation: any)];
+  } else {
+    aggregations = [];
+  }
+  return aggregations.filter(agg => agg && agg[0] && !mbqlEq(agg[0], "rows"));
 }
 
 // turns a list of Aggregations into the canonical AggregationClause
 function getAggregationClause(aggregations: Aggregation[]): ?AggregationClause {
-    aggregations = getAggregations(aggregations);
-    if (aggregations.length === 0) {
-        return undefined;
-    } else {
-        return aggregations;
-    }
+  aggregations = getAggregations(aggregations);
+  if (aggregations.length === 0) {
+    return undefined;
+  } else {
+    return aggregations;
+  }
 }
 
-export function addAggregation(aggregation: ?AggregationClause, newAggregation: Aggregation): ?AggregationClause {
-    return getAggregationClause(add(getAggregations(aggregation), newAggregation));
+export function addAggregation(
+  aggregation: ?AggregationClause,
+  newAggregation: Aggregation,
+): ?AggregationClause {
+  return getAggregationClause(
+    add(getAggregations(aggregation), newAggregation),
+  );
 }
-export function updateAggregation(aggregation: ?AggregationClause, index: number, updatedAggregation: Aggregation): ?AggregationClause {
-    return getAggregationClause(update(getAggregations(aggregation), index, updatedAggregation));
+export function updateAggregation(
+  aggregation: ?AggregationClause,
+  index: number,
+  updatedAggregation: Aggregation,
+): ?AggregationClause {
+  return getAggregationClause(
+    update(getAggregations(aggregation), index, updatedAggregation),
+  );
 }
-export function removeAggregation(aggregation: ?AggregationClause, index: number): ?AggregationClause {
-    return getAggregationClause(remove(getAggregations(aggregation), index));
+export function removeAggregation(
+  aggregation: ?AggregationClause,
+  index: number,
+): ?AggregationClause {
+  return getAggregationClause(remove(getAggregations(aggregation), index));
 }
 export function clearAggregations(ac: ?AggregationClause): ?AggregationClause {
-    return getAggregationClause(clear());
+  return getAggregationClause(clear());
 }
 
 // MISC
 
 export function isBareRows(ac: ?AggregationClause) {
-    return getAggregations(ac).length === 0;
+  return getAggregations(ac).length === 0;
 }
 
 export function hasEmptyAggregation(ac: ?AggregationClause): boolean {
-    return _.any(getAggregations(ac), (aggregation) => !noNullValues(aggregation));
+  return _.any(getAggregations(ac), aggregation => !noNullValues(aggregation));
 }
 
 export function hasValidAggregation(ac: ?AggregationClause): boolean {
-    return _.all(getAggregations(ac), (aggregation) => noNullValues(aggregation));
+  return _.all(getAggregations(ac), aggregation => noNullValues(aggregation));
 }
diff --git a/frontend/src/metabase/lib/query/breakout.js b/frontend/src/metabase/lib/query/breakout.js
index 5e1f0f9b6961fa8cf43ea1f5266572217868e8d2..76cd8b0257e4526dfdb1672afb368171699834f5 100644
--- a/frontend/src/metabase/lib/query/breakout.js
+++ b/frontend/src/metabase/lib/query/breakout.js
@@ -10,32 +10,49 @@ import { add, update, remove, clear } from "./util";
 
 // returns canonical list of Breakouts, with nulls removed
 export function getBreakouts(breakouts: ?BreakoutClause): Breakout[] {
-    return (breakouts || []).filter(b => b != null);
+  return (breakouts || []).filter(b => b != null);
 }
 
-export function getBreakoutFields(breakouts: ?BreakoutClause, tableMetadata: TableMetadata): Field[] {
-    return getBreakouts(breakouts).map(breakout => (Q.getFieldTarget(breakout, tableMetadata) || {}).field);
+export function getBreakoutFields(
+  breakouts: ?BreakoutClause,
+  tableMetadata: TableMetadata,
+): Field[] {
+  return getBreakouts(breakouts).map(
+    breakout => (Q.getFieldTarget(breakout, tableMetadata) || {}).field,
+  );
 }
 
 // turns a list of Breakouts into the canonical BreakoutClause
 export function getBreakoutClause(breakouts: Breakout[]): ?BreakoutClause {
-    breakouts = getBreakouts(breakouts);
-    if (breakouts.length === 0) {
-        return undefined;
-    } else {
-        return breakouts;
-    }
+  breakouts = getBreakouts(breakouts);
+  if (breakouts.length === 0) {
+    return undefined;
+  } else {
+    return breakouts;
+  }
 }
 
-export function addBreakout(breakout: ?BreakoutClause, newBreakout: Breakout): ?BreakoutClause {
-    return getBreakoutClause(add(getBreakouts(breakout), newBreakout));
+export function addBreakout(
+  breakout: ?BreakoutClause,
+  newBreakout: Breakout,
+): ?BreakoutClause {
+  return getBreakoutClause(add(getBreakouts(breakout), newBreakout));
 }
-export function updateBreakout(breakout: ?BreakoutClause, index: number, updatedBreakout: Breakout): ?BreakoutClause {
-    return getBreakoutClause(update(getBreakouts(breakout), index, updatedBreakout));
+export function updateBreakout(
+  breakout: ?BreakoutClause,
+  index: number,
+  updatedBreakout: Breakout,
+): ?BreakoutClause {
+  return getBreakoutClause(
+    update(getBreakouts(breakout), index, updatedBreakout),
+  );
 }
-export function removeBreakout(breakout: ?BreakoutClause, index: number): ?BreakoutClause {
-    return getBreakoutClause(remove(getBreakouts(breakout), index));
+export function removeBreakout(
+  breakout: ?BreakoutClause,
+  index: number,
+): ?BreakoutClause {
+  return getBreakoutClause(remove(getBreakouts(breakout), index));
 }
 export function clearBreakouts(breakout: ?BreakoutClause): ?BreakoutClause {
-    return getBreakoutClause(clear());
+  return getBreakoutClause(clear());
 }
diff --git a/frontend/src/metabase/lib/query/expression.js b/frontend/src/metabase/lib/query/expression.js
index e8612329c618ed9d05e84ee003dfa949edbed196..65be82c61492276ca6f50ec825c0b589493f0739 100644
--- a/frontend/src/metabase/lib/query/expression.js
+++ b/frontend/src/metabase/lib/query/expression.js
@@ -1,27 +1,52 @@
 import _ from "underscore";
 
-import type { ExpressionName, ExpressionClause, Expression } from "metabase/meta/types/Query";
+import type {
+  ExpressionName,
+  ExpressionClause,
+  Expression,
+} from "metabase/meta/types/Query";
 
-export function getExpressions(expressions: ?ExpressionClause = {}): ExpressionClause {
-    return expressions;
+export function getExpressions(
+  expressions: ?ExpressionClause = {},
+): ExpressionClause {
+  return expressions;
 }
 
-export function getExpressionsList(expressions: ?ExpressionClause = {}): Array<{ name: ExpressionName, expression: Expression }> {
-    return Object.entries(expressions).map(([name, expression]) => ({ name, expression }));
+export function getExpressionsList(
+  expressions: ?ExpressionClause = {},
+): Array<{ name: ExpressionName, expression: Expression }> {
+  return Object.entries(expressions).map(([name, expression]) => ({
+    name,
+    expression,
+  }));
 }
 
-export function addExpression(expressions: ?ExpressionClause = {}, name: ExpressionName, expression: Expression): ?ExpressionClause {
-    return { ...expressions, [name]: expression };
+export function addExpression(
+  expressions: ?ExpressionClause = {},
+  name: ExpressionName,
+  expression: Expression,
+): ?ExpressionClause {
+  return { ...expressions, [name]: expression };
 }
-export function updateExpression(expressions: ?ExpressionClause = {}, name: ExpressionName, expression: Expression, oldName?: ExpressionName): ?ExpressionClause {
-    if (oldName != null) {
-        expressions = removeExpression(expressions, oldName);
-    }
-    return addExpression(expressions, name, expression);
+export function updateExpression(
+  expressions: ?ExpressionClause = {},
+  name: ExpressionName,
+  expression: Expression,
+  oldName?: ExpressionName,
+): ?ExpressionClause {
+  if (oldName != null) {
+    expressions = removeExpression(expressions, oldName);
+  }
+  return addExpression(expressions, name, expression);
 }
-export function removeExpression(expressions: ?ExpressionClause = {}, name: ExpressionName): ?ExpressionClause {
-    return _.omit(expressions, name)
+export function removeExpression(
+  expressions: ?ExpressionClause = {},
+  name: ExpressionName,
+): ?ExpressionClause {
+  return _.omit(expressions, name);
 }
-export function clearExpressions(expressions: ?ExpressionClause): ?ExpressionClause {
-    return {};
+export function clearExpressions(
+  expressions: ?ExpressionClause,
+): ?ExpressionClause {
+  return {};
 }
diff --git a/frontend/src/metabase/lib/query/field.js b/frontend/src/metabase/lib/query/field.js
index 8fa7e367af1870553b1e689cadf05a94947a0a0d..93b04a266cd02675f11f8ecaf732f915b24cc7f1 100644
--- a/frontend/src/metabase/lib/query/field.js
+++ b/frontend/src/metabase/lib/query/field.js
@@ -1,4 +1,3 @@
-
 import { mbqlEq } from "./util";
 
 import type { Field as FieldReference } from "metabase/meta/types/Query";
@@ -7,57 +6,63 @@ import type { Value } from "metabase/meta/types/Dataset";
 
 // gets the target field ID (recursively) from any type of field, including raw field ID, fk->, and datetime-field cast.
 export function getFieldTargetId(field: FieldReference): ?FieldId {
-    if (isRegularField(field)) {
-        // $FlowFixMe
-        return field;
-    } else if (isLocalField(field)) {
-        // $FlowFixMe
-        return field[1];
-    } else if (isForeignKeyField(field)) {
-        // $FlowFixMe
-        return getFieldTargetId(field[2]);
-    } else if (isDatetimeField(field)) {
-        // $FlowFixMe
-        return getFieldTargetId(field[1]);
-    } else if (isBinningStrategy(field)) {
-        // $FlowFixMe
-        return getFieldTargetId(field[1]);
-    } else if (isFieldLiteral(field)) {
-        return field;
-    }
-    console.warn("Unknown field type: ", field);
+  if (isRegularField(field)) {
+    // $FlowFixMe
+    return field;
+  } else if (isLocalField(field)) {
+    // $FlowFixMe
+    return field[1];
+  } else if (isForeignKeyField(field)) {
+    // $FlowFixMe
+    return getFieldTargetId(field[2]);
+  } else if (isDatetimeField(field)) {
+    // $FlowFixMe
+    return getFieldTargetId(field[1]);
+  } else if (isBinningStrategy(field)) {
+    // $FlowFixMe
+    return getFieldTargetId(field[1]);
+  } else if (isFieldLiteral(field)) {
+    return field;
+  }
+  console.warn("Unknown field type: ", field);
 }
 
 export function isRegularField(field: FieldReference): boolean {
-    return typeof field === "number";
+  return typeof field === "number";
 }
 
 export function isLocalField(field: FieldReference): boolean {
-    return Array.isArray(field) && mbqlEq(field[0], "field-id");
+  return Array.isArray(field) && mbqlEq(field[0], "field-id");
 }
 
 export function isForeignKeyField(field: FieldReference): boolean {
-    return Array.isArray(field) && mbqlEq(field[0], "fk->");
+  return Array.isArray(field) && mbqlEq(field[0], "fk->");
 }
 
 export function isDatetimeField(field: FieldReference): boolean {
-    return Array.isArray(field) && mbqlEq(field[0], "datetime-field");
+  return Array.isArray(field) && mbqlEq(field[0], "datetime-field");
 }
 
 export function isBinningStrategy(field: FieldReference): boolean {
-    return Array.isArray(field) && mbqlEq(field[0], "binning-strategy");
+  return Array.isArray(field) && mbqlEq(field[0], "binning-strategy");
 }
 
 export function isFieldLiteral(field: FieldReference): boolean {
-    return Array.isArray(field) && field.length === 3 && mbqlEq(field[0], "field-literal");
+  return (
+    Array.isArray(field) &&
+    field.length === 3 &&
+    mbqlEq(field[0], "field-literal")
+  );
 }
 
 export function isExpressionField(field: FieldReference): boolean {
-    return Array.isArray(field) && field.length === 2 && mbqlEq(field[0], "expression");
+  return (
+    Array.isArray(field) && field.length === 2 && mbqlEq(field[0], "expression")
+  );
 }
 
 export function isAggregateField(field: FieldReference): boolean {
-    return Array.isArray(field) && mbqlEq(field[0], "aggregation");
+  return Array.isArray(field) && mbqlEq(field[0], "aggregation");
 }
 
 import _ from "underscore";
@@ -65,33 +70,43 @@ import _ from "underscore";
 // Metadata field "values" type is inconsistent
 // https://github.com/metabase/metabase/issues/3417
 export function getFieldValues(field: ?Field): FieldValues {
-    const values = field && field.values;
-    if (Array.isArray(values)) {
-        if (values.length === 0 || Array.isArray(values[0])) {
-            return values;
-        } else {
-            // console.warn("deprecated field values array!", values);
-            return values.map(value => [value]);
-        }
-    } else if (values && Array.isArray(values.values)) {
-        // console.warn("deprecated field values object!", values);
+  const values = field && field.values;
+  if (Array.isArray(values)) {
+    if (values.length === 0 || Array.isArray(values[0])) {
+      return values;
+    } else {
+      // console.warn("deprecated field values array!", values);
+      return values.map(value => [value]);
+    }
+  } else if (values && Array.isArray(values.values)) {
+    // console.warn("deprecated field values object!", values);
 
-        if (Array.isArray(values.human_readable_values)) {
-            return _.zip(values.values, values.human_readable_values || {});
-        } else if (Array.isArray(values.values)) {
-            // TODO Atte Keinänen 7/12/17: I don't honestly know why we can have a field in `values` property.
-            return getFieldValues(values);
-        } else {
-            // console.warn("missing field values", field);
-            return [];
-        }
+    if (Array.isArray(values.human_readable_values)) {
+      return _.zip(values.values, values.human_readable_values || {});
+    } else if (Array.isArray(values.values)) {
+      // TODO Atte Keinänen 7/12/17: I don't honestly know why we can have a field in `values` property.
+      return getFieldValues(values);
     } else {
-        // console.warn("missing field values", field);
-        return [];
+      // console.warn("missing field values", field);
+      return [];
     }
+  } else {
+    // console.warn("missing field values", field);
+    return [];
+  }
+}
+
+// merge field values and remappings
+export function getRemappings(field: ?Field) {
+  const remappings = (field && field.remappings) || [];
+  const fieldValues = getFieldValues(field);
+  return [...fieldValues, ...remappings];
 }
 
-export function getHumanReadableValue(value: Value, fieldValues?: FieldValues = []) {
-    const fieldValue = _.findWhere(fieldValues, { [0]: value });
-    return fieldValue && fieldValue.length === 2 ? fieldValue[1] : String(value);
+export function getHumanReadableValue(
+  value: Value,
+  fieldValues?: FieldValues = [],
+) {
+  const fieldValue = _.findWhere(fieldValues, { [0]: value });
+  return fieldValue && fieldValue.length === 2 ? fieldValue[1] : String(value);
 }
diff --git a/frontend/src/metabase/lib/query/filter.js b/frontend/src/metabase/lib/query/filter.js
index 8f3851141ecba3fae9eb97e66791a5ccc4e39c48..81abd119012eb47355ffee529f62d294a14ae516 100644
--- a/frontend/src/metabase/lib/query/filter.js
+++ b/frontend/src/metabase/lib/query/filter.js
@@ -1,62 +1,121 @@
 /* @flow */
 
-import { mbqlEq, op, args, noNullValues, add, update, remove, clear } from "./util";
+import {
+  mbqlEq,
+  op,
+  args,
+  noNullValues,
+  add,
+  update,
+  remove,
+  clear,
+} from "./util";
 
-import type { FilterClause, Filter } from "metabase/meta/types/Query";
+import type {
+  FilterClause,
+  Filter,
+  FilterOptions,
+} from "metabase/meta/types/Query";
 
 // returns canonical list of Filters
 export function getFilters(filter: ?FilterClause): Filter[] {
-    if (!filter || Array.isArray(filter) && filter.length === 0) {
-        return [];
-    } else if (mbqlEq(op(filter), "and")) {
-        return args(filter);
-    } else {
-        return [filter];
-    }
+  if (!filter || (Array.isArray(filter) && filter.length === 0)) {
+    return [];
+  } else if (mbqlEq(op(filter), "and")) {
+    return args(filter);
+  } else {
+    return [filter];
+  }
 }
 
 // turns a list of Filters into the canonical FilterClause, either `undefined`, `filter`, or `["and", filter...]`
 function getFilterClause(filters: Filter[]): ?FilterClause {
-    if (filters.length === 0) {
-        return undefined;
-    } else if (filters.length === 1) {
-        return filters[0];
-    } else {
-        return (["and", ...filters]: any);
-    }
+  if (filters.length === 0) {
+    return undefined;
+  } else if (filters.length === 1) {
+    return filters[0];
+  } else {
+    return (["and", ...filters]: any);
+  }
 }
 
-export function addFilter(filter: ?FilterClause, newFilter: FilterClause): ?FilterClause {
-    return getFilterClause(add(getFilters(filter), newFilter));
+export function addFilter(
+  filter: ?FilterClause,
+  newFilter: FilterClause,
+): ?FilterClause {
+  return getFilterClause(add(getFilters(filter), newFilter));
 }
-export function updateFilter(filter: ?FilterClause, index: number, updatedFilter: FilterClause): ?FilterClause {
-    return getFilterClause(update(getFilters(filter), index, updatedFilter));
+export function updateFilter(
+  filter: ?FilterClause,
+  index: number,
+  updatedFilter: FilterClause,
+): ?FilterClause {
+  return getFilterClause(update(getFilters(filter), index, updatedFilter));
 }
-export function removeFilter(filter: ?FilterClause, index: number): ?FilterClause {
-    return getFilterClause(remove(getFilters(filter), index));
+export function removeFilter(
+  filter: ?FilterClause,
+  index: number,
+): ?FilterClause {
+  return getFilterClause(remove(getFilters(filter), index));
 }
 export function clearFilters(filter: ?FilterClause): ?FilterClause {
-    return getFilterClause(clear());
+  return getFilterClause(clear());
 }
 
 // MISC
 
 export function canAddFilter(filter: ?FilterClause): boolean {
-    const filters = getFilters(filter);
-    if (filters.length > 0) {
-        return noNullValues(filters[filters.length - 1]);
-    }
-    return true;
+  const filters = getFilters(filter);
+  if (filters.length > 0) {
+    return noNullValues(filters[filters.length - 1]);
+  }
+  return true;
 }
 
 export function isSegmentFilter(filter: FilterClause): boolean {
-    return Array.isArray(filter) && mbqlEq(filter[0], "segment");
+  return Array.isArray(filter) && mbqlEq(filter[0], "segment");
 }
 
 export function isCompoundFilter(filter: FilterClause): boolean {
-    return Array.isArray(filter) && (mbqlEq(filter[0], "and") || mbqlEq(filter[0], "or"));
+  return (
+    Array.isArray(filter) &&
+    (mbqlEq(filter[0], "and") || mbqlEq(filter[0], "or"))
+  );
 }
 
 export function isFieldFilter(filter: FilterClause): boolean {
-    return !isSegmentFilter(filter) && !isCompoundFilter(filter);
+  return !isSegmentFilter(filter) && !isCompoundFilter(filter);
+}
+
+// TODO: is it safe to assume if the last item is an object then it's options?
+export function hasFilterOptions(filter: Filter): boolean {
+  const o = filter[filter.length - 1];
+  return !!o && typeof o == "object" && o.constructor == Object;
+}
+
+export function getFilterOptions(filter: Filter): FilterOptions {
+  // NOTE: just make a new "any" variable since getting flow to type checking this is a nightmare
+  let _filter: any = filter;
+  if (hasFilterOptions(filter)) {
+    return _filter[_filter.length - 1];
+  } else {
+    return {};
+  }
+}
+
+export function setFilterOptions<T: Filter>(
+  filter: T,
+  options: FilterOptions,
+): T {
+  // NOTE: just make a new "any" variable since getting flow to type checking this is a nightmare
+  let _filter: any = filter;
+  // if we have option, strip it off for now
+  if (hasFilterOptions(filter)) {
+    _filter = _filter.slice(0, -1);
+  }
+  // if options isn't emtpy, append it
+  if (Object.keys(options).length > 0) {
+    _filter = [..._filter, options];
+  }
+  return _filter;
 }
diff --git a/frontend/src/metabase/lib/query/limit.js b/frontend/src/metabase/lib/query/limit.js
index 06f6fefafd7365877ee5eae47726c74ccb29cfce..8a8548dc3a0c148328882ad9d7d3b48aa9dbcb1e 100644
--- a/frontend/src/metabase/lib/query/limit.js
+++ b/frontend/src/metabase/lib/query/limit.js
@@ -3,13 +3,13 @@
 import type { LimitClause } from "metabase/meta/types/Query";
 
 export function getLimit(lc: ?LimitClause): ?number {
-    return lc;
+  return lc;
 }
 
 export function updateLimit(lc: ?LimitClause, limit: ?number): ?LimitClause {
-    return limit;
+  return limit;
 }
 
 export function clearLimit(lc: ?LimitClause): ?LimitClause {
-    return undefined;
+  return undefined;
 }
diff --git a/frontend/src/metabase/lib/query/order_by.js b/frontend/src/metabase/lib/query/order_by.js
index 78e5b64e74ca55c6e4c8cfffb73423e8e5737677..7b6a628cf35e4664ad54ebb9cb8b57c6f8256a95 100644
--- a/frontend/src/metabase/lib/query/order_by.js
+++ b/frontend/src/metabase/lib/query/order_by.js
@@ -6,28 +6,38 @@ import { add, update, remove, clear } from "./util";
 
 // returns canonical list of OrderBys, with nulls removed
 export function getOrderBys(breakout: ?OrderByClause): OrderBy[] {
-    return (breakout || []).filter(b => b != null);
+  return (breakout || []).filter(b => b != null);
 }
 
 // turns a list of OrderBys into the canonical OrderByClause
 export function getOrderByClause(breakouts: OrderBy[]): ?OrderByClause {
-    breakouts = getOrderBys(breakouts);
-    if (breakouts.length === 0) {
-        return undefined;
-    } else {
-        return breakouts;
-    }
+  breakouts = getOrderBys(breakouts);
+  if (breakouts.length === 0) {
+    return undefined;
+  } else {
+    return breakouts;
+  }
 }
 
-export function addOrderBy(breakout: ?OrderByClause, newOrderBy: OrderBy): ?OrderByClause {
-    return getOrderByClause(add(getOrderBys(breakout), newOrderBy));
+export function addOrderBy(
+  breakout: ?OrderByClause,
+  newOrderBy: OrderBy,
+): ?OrderByClause {
+  return getOrderByClause(add(getOrderBys(breakout), newOrderBy));
 }
-export function updateOrderBy(breakout: ?OrderByClause, index: number, updatedOrderBy: OrderBy): ?OrderByClause {
-    return getOrderByClause(update(getOrderBys(breakout), index, updatedOrderBy));
+export function updateOrderBy(
+  breakout: ?OrderByClause,
+  index: number,
+  updatedOrderBy: OrderBy,
+): ?OrderByClause {
+  return getOrderByClause(update(getOrderBys(breakout), index, updatedOrderBy));
 }
-export function removeOrderBy(breakout: ?OrderByClause, index: number): ?OrderByClause {
-    return getOrderByClause(remove(getOrderBys(breakout), index));
+export function removeOrderBy(
+  breakout: ?OrderByClause,
+  index: number,
+): ?OrderByClause {
+  return getOrderByClause(remove(getOrderBys(breakout), index));
 }
 export function clearOrderBy(breakout: ?OrderByClause): ?OrderByClause {
-    return getOrderByClause(clear());
+  return getOrderByClause(clear());
 }
diff --git a/frontend/src/metabase/lib/query/query.js b/frontend/src/metabase/lib/query/query.js
index 4f2aa00f1387373c0547faf43af601ded006451c..ee356c62bb8fd641292e4ed17bf0015aef6f0d9e 100644
--- a/frontend/src/metabase/lib/query/query.js
+++ b/frontend/src/metabase/lib/query/query.js
@@ -1,13 +1,19 @@
 /* @flow */
 
 import type {
-    StructuredQuery as SQ,
-    Aggregation, AggregationClause,
-    Breakout, BreakoutClause,
-    Filter, FilterClause,
-    LimitClause,
-    OrderBy, OrderByClause,
-    ExpressionClause, ExpressionName, Expression
+  StructuredQuery as SQ,
+  Aggregation,
+  AggregationClause,
+  Breakout,
+  BreakoutClause,
+  Filter,
+  FilterClause,
+  LimitClause,
+  OrderBy,
+  OrderByClause,
+  ExpressionClause,
+  ExpressionName,
+  Expression,
 } from "metabase/meta/types/Query";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
 
@@ -23,111 +29,169 @@ import _ from "underscore";
 
 // AGGREGATION
 
-export const getAggregations     = (query: SQ)                                          => A.getAggregations(query.aggregation);
-export const addAggregation      = (query: SQ, aggregation: Aggregation)                => setAggregationClause(query, A.addAggregation(query.aggregation, aggregation));
-export const updateAggregation   = (query: SQ, index: number, aggregation: Aggregation) => setAggregationClause(query, A.updateAggregation(query.aggregation, index, aggregation));
-export const removeAggregation   = (query: SQ, index: number)                           => setAggregationClause(query, A.removeAggregation(query.aggregation, index));
-export const clearAggregations   = (query: SQ)                                          => setAggregationClause(query, A.clearAggregations(query.aggregation));
-
-export const isBareRows          = (query: SQ) => A.isBareRows(query.aggregation);
-export const hasEmptyAggregation = (query: SQ) => A.hasEmptyAggregation(query.aggregation);
-export const hasValidAggregation = (query: SQ) => A.hasValidAggregation(query.aggregation);
+export const getAggregations = (query: SQ) =>
+  A.getAggregations(query.aggregation);
+export const addAggregation = (query: SQ, aggregation: Aggregation) =>
+  setAggregationClause(query, A.addAggregation(query.aggregation, aggregation));
+export const updateAggregation = (
+  query: SQ,
+  index: number,
+  aggregation: Aggregation,
+) =>
+  setAggregationClause(
+    query,
+    A.updateAggregation(query.aggregation, index, aggregation),
+  );
+export const removeAggregation = (query: SQ, index: number) =>
+  setAggregationClause(query, A.removeAggregation(query.aggregation, index));
+export const clearAggregations = (query: SQ) =>
+  setAggregationClause(query, A.clearAggregations(query.aggregation));
+
+export const isBareRows = (query: SQ) => A.isBareRows(query.aggregation);
+export const hasEmptyAggregation = (query: SQ) =>
+  A.hasEmptyAggregation(query.aggregation);
+export const hasValidAggregation = (query: SQ) =>
+  A.hasValidAggregation(query.aggregation);
 
 // BREAKOUT
 
-export const getBreakouts   = (query: SQ)                                    => B.getBreakouts(query.breakout);
-export const addBreakout    = (query: SQ, breakout: Breakout)                => setBreakoutClause(query, B.addBreakout(query.breakout, breakout));
-export const updateBreakout = (query: SQ, index: number, breakout: Breakout) => setBreakoutClause(query, B.updateBreakout(query.breakout, index, breakout));
-export const removeBreakout = (query: SQ, index: number)                     => setBreakoutClause(query, B.removeBreakout(query.breakout, index));
-export const clearBreakouts = (query: SQ)                                    => setBreakoutClause(query, B.clearBreakouts(query.breakout));
+export const getBreakouts = (query: SQ) => B.getBreakouts(query.breakout);
+export const addBreakout = (query: SQ, breakout: Breakout) =>
+  setBreakoutClause(query, B.addBreakout(query.breakout, breakout));
+export const updateBreakout = (query: SQ, index: number, breakout: Breakout) =>
+  setBreakoutClause(query, B.updateBreakout(query.breakout, index, breakout));
+export const removeBreakout = (query: SQ, index: number) =>
+  setBreakoutClause(query, B.removeBreakout(query.breakout, index));
+export const clearBreakouts = (query: SQ) =>
+  setBreakoutClause(query, B.clearBreakouts(query.breakout));
 
-export const getBreakoutFields = (query: SQ, tableMetadata: TableMetadata) => B.getBreakoutFields(query.breakout, tableMetadata);
+export const getBreakoutFields = (query: SQ, tableMetadata: TableMetadata) =>
+  B.getBreakoutFields(query.breakout, tableMetadata);
 
 // FILTER
 
-export const getFilters   = (query: SQ)                                 => F.getFilters(query.filter);
-export const addFilter    = (query: SQ, filter: Filter)                 => setFilterClause(query, F.addFilter(query.filter, filter));
-export const updateFilter = (query: SQ, index: number, filter: Filter)  => setFilterClause(query, F.updateFilter(query.filter, index, filter));
-export const removeFilter = (query: SQ, index: number)                  => setFilterClause(query, F.removeFilter(query.filter, index));
-export const clearFilters = (query: SQ)                                 => setFilterClause(query, F.clearFilters(query.filter));
+export const getFilters = (query: SQ) => F.getFilters(query.filter);
+export const addFilter = (query: SQ, filter: Filter) =>
+  setFilterClause(query, F.addFilter(query.filter, filter));
+export const updateFilter = (query: SQ, index: number, filter: Filter) =>
+  setFilterClause(query, F.updateFilter(query.filter, index, filter));
+export const removeFilter = (query: SQ, index: number) =>
+  setFilterClause(query, F.removeFilter(query.filter, index));
+export const clearFilters = (query: SQ) =>
+  setFilterClause(query, F.clearFilters(query.filter));
 
 export const canAddFilter = (query: SQ) => F.canAddFilter(query.filter);
 
 // ORDER_BY
 
-export const getOrderBys   = (query: SQ)                                   => O.getOrderBys(query.order_by);
-export const addOrderBy    = (query: SQ, order_by: OrderBy)                => setOrderByClause(query, O.addOrderBy(query.order_by, order_by));
-export const updateOrderBy = (query: SQ, index: number, order_by: OrderBy) => setOrderByClause(query, O.updateOrderBy(query.order_by, index, order_by));
-export const removeOrderBy = (query: SQ, index: number)                    => setOrderByClause(query, O.removeOrderBy(query.order_by, index));
-export const clearOrderBy  = (query: SQ)                                   => setOrderByClause(query, O.clearOrderBy(query.order_by));
+export const getOrderBys = (query: SQ) => O.getOrderBys(query.order_by);
+export const addOrderBy = (query: SQ, order_by: OrderBy) =>
+  setOrderByClause(query, O.addOrderBy(query.order_by, order_by));
+export const updateOrderBy = (query: SQ, index: number, order_by: OrderBy) =>
+  setOrderByClause(query, O.updateOrderBy(query.order_by, index, order_by));
+export const removeOrderBy = (query: SQ, index: number) =>
+  setOrderByClause(query, O.removeOrderBy(query.order_by, index));
+export const clearOrderBy = (query: SQ) =>
+  setOrderByClause(query, O.clearOrderBy(query.order_by));
 
 // LIMIT
 
-export const getLimit    = (query: SQ)                     => L.getLimit(query.limit);
-export const updateLimit = (query: SQ, limit: LimitClause) => setLimitClause(query, L.updateLimit(query.limit, limit));
-export const clearLimit  = (query: SQ)                     => setLimitClause(query, L.clearLimit(query.limit));
+export const getLimit = (query: SQ) => L.getLimit(query.limit);
+export const updateLimit = (query: SQ, limit: LimitClause) =>
+  setLimitClause(query, L.updateLimit(query.limit, limit));
+export const clearLimit = (query: SQ) =>
+  setLimitClause(query, L.clearLimit(query.limit));
 
 // EXPRESSIONS
 
-export const getExpressions     = (query: SQ) => E.getExpressions(query.expressions);
-export const getExpressionsList = (query: SQ) => E.getExpressionsList(query.expressions);
-export const addExpression    = (query: SQ, name: ExpressionName, expression: Expression) =>
-    setExpressionClause(query, E.addExpression(query.expressions, name, expression));
-export const updateExpression = (query: SQ, name: ExpressionName, expression: Expression, oldName: ExpressionName) =>
-    setExpressionClause(query, E.updateExpression(query.expressions, name, expression, oldName));
+export const getExpressions = (query: SQ) =>
+  E.getExpressions(query.expressions);
+export const getExpressionsList = (query: SQ) =>
+  E.getExpressionsList(query.expressions);
+export const addExpression = (
+  query: SQ,
+  name: ExpressionName,
+  expression: Expression,
+) =>
+  setExpressionClause(
+    query,
+    E.addExpression(query.expressions, name, expression),
+  );
+export const updateExpression = (
+  query: SQ,
+  name: ExpressionName,
+  expression: Expression,
+  oldName: ExpressionName,
+) =>
+  setExpressionClause(
+    query,
+    E.updateExpression(query.expressions, name, expression, oldName),
+  );
 export const removeExpression = (query: SQ, name: ExpressionName) =>
-    setExpressionClause(query, E.removeExpression(query.expressions, name));
-export const clearExpression  = (query: SQ) =>
-    setExpressionClause(query, E.clearExpressions(query.expressions));
+  setExpressionClause(query, E.removeExpression(query.expressions, name));
+export const clearExpression = (query: SQ) =>
+  setExpressionClause(query, E.clearExpressions(query.expressions));
 
 // we can enforce various constraints in these functions:
 
-function setAggregationClause(query: SQ, aggregationClause: ?AggregationClause): SQ {
-    let wasBareRows = A.isBareRows(query.aggregation);
-    let isBareRows = A.isBareRows(aggregationClause);
-    // when switching to or from bare rows clear out any sorting clauses
-    if (isBareRows !== wasBareRows) {
-        clearOrderBy(query);
-    }
-    // for bare rows we always clear out any dimensions because they don't make sense
-    if (isBareRows) {
-        clearBreakouts(query);
-    }
-    return setClause("aggregation", query, aggregationClause);
+function setAggregationClause(
+  query: SQ,
+  aggregationClause: ?AggregationClause,
+): SQ {
+  let wasBareRows = A.isBareRows(query.aggregation);
+  let isBareRows = A.isBareRows(aggregationClause);
+  // when switching to or from bare rows clear out any sorting clauses
+  if (isBareRows !== wasBareRows) {
+    clearOrderBy(query);
+  }
+  // for bare rows we always clear out any dimensions because they don't make sense
+  if (isBareRows) {
+    clearBreakouts(query);
+  }
+  return setClause("aggregation", query, aggregationClause);
 }
 function setBreakoutClause(query: SQ, breakoutClause: ?BreakoutClause): SQ {
-    let breakoutIds = B.getBreakouts(breakoutClause).filter(id => id != null);
-    for (const [index, sort] of getOrderBys(query).entries()) {
-        let sortId = Query.getFieldTargetId(sort[0]);
-        if (sortId != null && !_.contains(breakoutIds, sortId)) {
-            query = removeOrderBy(query, index);
-        }
+  let breakoutIds = B.getBreakouts(breakoutClause).filter(id => id != null);
+  for (const [index, sort] of getOrderBys(query).entries()) {
+    let sortId = Query.getFieldTargetId(sort[0]);
+    if (sortId != null && !_.contains(breakoutIds, sortId)) {
+      query = removeOrderBy(query, index);
     }
-    return setClause("breakout", query, breakoutClause);
+  }
+  return setClause("breakout", query, breakoutClause);
 }
 function setFilterClause(query: SQ, filterClause: ?FilterClause): SQ {
-    return setClause("filter", query, filterClause);
+  return setClause("filter", query, filterClause);
 }
 function setOrderByClause(query: SQ, orderByClause: ?OrderByClause): SQ {
-    return setClause("order_by", query, orderByClause);
+  return setClause("order_by", query, orderByClause);
 }
 function setLimitClause(query: SQ, limitClause: ?LimitClause): SQ {
-    return setClause("limit", query, limitClause);
+  return setClause("limit", query, limitClause);
 }
-function setExpressionClause(query: SQ, expressionClause: ?ExpressionClause): SQ {
-    if (expressionClause && Object.keys(expressionClause).length === 0) {
-        expressionClause = null;
-    }
-    return setClause("expressions", query, expressionClause);
+function setExpressionClause(
+  query: SQ,
+  expressionClause: ?ExpressionClause,
+): SQ {
+  if (expressionClause && Object.keys(expressionClause).length === 0) {
+    expressionClause = null;
+  }
+  return setClause("expressions", query, expressionClause);
 }
 
-type FilterClauseName = "filter"|"aggregation"|"breakout"|"order_by"|"limit"|"expressions";
+type FilterClauseName =
+  | "filter"
+  | "aggregation"
+  | "breakout"
+  | "order_by"
+  | "limit"
+  | "expressions";
 function setClause(clauseName: FilterClauseName, query: SQ, clause: ?any): SQ {
-    query = { ...query };
-    if (clause == null) {
-        delete query[clauseName];
-    } else {
-        query[clauseName] = clause
-    }
-    return query;
+  query = { ...query };
+  if (clause == null) {
+    delete query[clauseName];
+  } else {
+    query[clauseName] = clause;
+  }
+  return query;
 }
diff --git a/frontend/src/metabase/lib/query/table.js b/frontend/src/metabase/lib/query/table.js
index 96d9786e175aca8c45a0c442a5a0f2de71b2d73b..57ca9a6ce1b873b9228e1ad4b6ec395caa284aea 100644
--- a/frontend/src/metabase/lib/query/table.js
+++ b/frontend/src/metabase/lib/query/table.js
@@ -3,12 +3,12 @@
 import _ from "underscore";
 
 export function getField(table, fieldId) {
-    if (table) {
-        // sometimes we populate fields_lookup, sometimes we don't :(
-        if (table.fields_lookup) {
-            return table.fields_lookup[fieldId];
-        } else {
-            return _.findWhere(table.fields, { id: fieldId });
-        }
+  if (table) {
+    // sometimes we populate fields_lookup, sometimes we don't :(
+    if (table.fields_lookup) {
+      return table.fields_lookup[fieldId];
+    } else {
+      return _.findWhere(table.fields, { id: fieldId });
     }
+  }
 }
diff --git a/frontend/src/metabase/lib/query/util.js b/frontend/src/metabase/lib/query/util.js
index 806b6d3423fd27397de91a5925516b7e4522e524..f858f63b47ae6e3471da76880fd33aae8be10c6a 100644
--- a/frontend/src/metabase/lib/query/util.js
+++ b/frontend/src/metabase/lib/query/util.js
@@ -2,47 +2,52 @@
 
 import _ from "underscore";
 
-export const mbql = (a: string):string =>
-    typeof a === "string" ? a.toLowerCase().replace(/_/g, "-") : a;
+export const mbql = (a: string): string =>
+  typeof a === "string" ? a.toLowerCase().replace(/_/g, "-") : a;
 
-export const mbqlEq = (a: string, b: string): boolean =>
-    mbql(a) === mbql(b);
+export const mbqlEq = (a: string, b: string): boolean => mbql(a) === mbql(b);
 
 // determines whether 2 field IDs are equal. This is needed rather than
 // doing a simple comparison because field IDs are not guaranteed to be numeric:
 // the might be FieldLiterals, e.g. [field-literal <name> <unit>], instead.
 export const fieldIdsEq = (a: any, b: any): boolean => {
-    if (typeof a !== typeof b) return false;
+  if (typeof a !== typeof b) return false;
 
-    if (typeof a === "number") return a === b;
+  if (typeof a === "number") return a === b;
 
-    if (a == null && b == null) return true;
+  if (a == null && b == null) return true;
 
-    // field literals
-    if (Array.isArray(a) && Array.isArray(b) &&
-        a.length === 3 && b.length === 3 &&
-        a[0] === "field-literal" && b[0] === "field-literal") {
-        return a[1] === b[1];
-    }
+  // field literals
+  if (
+    Array.isArray(a) &&
+    Array.isArray(b) &&
+    a.length === 3 &&
+    b.length === 3 &&
+    a[0] === "field-literal" &&
+    b[0] === "field-literal"
+  ) {
+    return a[1] === b[1];
+  }
 
-    console.warn("Don't know how to compare these IDs:", a, b);
-    return false;
-}
+  console.warn("Don't know how to compare these IDs:", a, b);
+  return false;
+};
 
 export const noNullValues = (clause: any[]): boolean =>
-    _.all(clause, c => c != null);
+  _.all(clause, c => c != null);
 
 // these are mostly to circumvent Flow type checking :-/
-export const op = (clause: any): string =>
-    clause[0];
-export const args = (clause: any[]): any[] =>
-    clause.slice(1);
-
-export const add = <T>(items: T[], item: T): T[] =>
-    [...items, item];
-export const update = <T>(items: T[], index: number, newItem: T): T[] =>
-    [...items.slice(0, index), newItem, ...items.slice(index + 1)];
-export const remove = <T>(items: T[], index: number): T[] =>
-    [...items.slice(0, index), ...items.slice(index + 1)];
-export const clear = <T>(): T[] =>
-    [];
+export const op = (clause: any): string => clause[0];
+export const args = (clause: any[]): any[] => clause.slice(1);
+
+export const add = <T>(items: T[], item: T): T[] => [...items, item];
+export const update = <T>(items: T[], index: number, newItem: T): T[] => [
+  ...items.slice(0, index),
+  newItem,
+  ...items.slice(index + 1),
+];
+export const remove = <T>(items: T[], index: number): T[] => [
+  ...items.slice(0, index),
+  ...items.slice(index + 1),
+];
+export const clear = <T>(): T[] => [];
diff --git a/frontend/src/metabase/lib/query_time.js b/frontend/src/metabase/lib/query_time.js
index 335d9ab79fe9a2f96045fc18598de373e44fe9ba..3f009e4ba9f2f6828c989aefacfe8b23b94ed269 100644
--- a/frontend/src/metabase/lib/query_time.js
+++ b/frontend/src/metabase/lib/query_time.js
@@ -5,229 +5,244 @@ import { mbqlEq } from "metabase/lib/query/util";
 import { formatTimeWithUnit } from "metabase/lib/formatting";
 
 export const DATETIME_UNITS = [
-    // "default",
-    "minute",
-    "hour",
-    "day",
-    "week",
-    "month",
-    "quarter",
-    "year",
-    // "minute-of-hour",
-    "hour-of-day",
-    "day-of-week",
-    "day-of-month",
-    // "day-of-year",
-    "week-of-year",
-    "month-of-year",
-    "quarter-of-year",
-]
-
+  // "default",
+  "minute",
+  "hour",
+  "day",
+  "week",
+  "month",
+  "quarter",
+  "year",
+  // "minute-of-hour",
+  "hour-of-day",
+  "day-of-week",
+  "day-of-month",
+  // "day-of-year",
+  "week-of-year",
+  "month-of-year",
+  "quarter-of-year",
+];
 
 export function computeFilterTimeRange(filter) {
-    let expandedFilter;
-    if (mbqlEq(filter[0], "time-interval")) {
-        expandedFilter = expandTimeIntervalFilter(filter);
-    } else {
-        expandedFilter = filter;
-    }
-
-    let [operator, field, ...values] = expandedFilter;
-    let bucketing = parseFieldBucketing(field, "day");
-
-    let start, end;
-    if (operator === "=" && values[0]) {
-        let point = absolute(values[0]);
-        start = point.clone().startOf(bucketing);
-        end = point.clone().endOf(bucketing);
-    } else if (operator === ">" && values[0]) {
-        start = absolute(values[0]).endOf(bucketing);
-        end = max();
-    } else if (operator === "<" && values[0]) {
-        start = min();
-        end = absolute(values[0]).startOf(bucketing);
-    } else if (operator === "BETWEEN" && values[0] && values[1]) {
-        start = absolute(values[0]).startOf(bucketing);
-        end = absolute(values[1]).endOf(bucketing);
-    }
-
-    return [start, end];
+  let expandedFilter;
+  if (mbqlEq(filter[0], "time-interval")) {
+    expandedFilter = expandTimeIntervalFilter(filter);
+  } else {
+    expandedFilter = filter;
+  }
+
+  let [operator, field, ...values] = expandedFilter;
+  let bucketing = parseFieldBucketing(field, "day");
+
+  let start, end;
+  if (operator === "=" && values[0]) {
+    let point = absolute(values[0]);
+    start = point.clone().startOf(bucketing);
+    end = point.clone().endOf(bucketing);
+  } else if (operator === ">" && values[0]) {
+    start = absolute(values[0]).endOf(bucketing);
+    end = max();
+  } else if (operator === "<" && values[0]) {
+    start = min();
+    end = absolute(values[0]).startOf(bucketing);
+  } else if (operator === "BETWEEN" && values[0] && values[1]) {
+    start = absolute(values[0]).startOf(bucketing);
+    end = absolute(values[1]).endOf(bucketing);
+  }
+
+  return [start, end];
 }
 
 export function expandTimeIntervalFilter(filter) {
-    let [operator, field, n, unit] = filter;
-
-    if (!mbqlEq(operator, "time-interval")) {
-        throw new Error("translateTimeInterval expects operator 'time-interval'");
-    }
-
-    if (n === "current") {
-        n = 0;
-    } else if (n === "last") {
-        n = -1;
-    } else if (n === "next") {
-        n = 1;
-    }
-
-    field = ["datetime-field", field, "as", unit];
-
-    if (n < -1) {
-        return ["BETWEEN", field, ["relative-datetime", n-1, unit], ["relative-datetime", -1, unit]];
-    } else if (n > 1) {
-        return ["BETWEEN", field, ["relative-datetime", 1, unit], ["relative-datetime", n, unit]];
-    } else if (n === 0) {
-        return ["=", field, ["relative-datetime", "current"]];
-    } else {
-        return ["=", field, ["relative-datetime", n, unit]];
-    }
+  let [operator, field, n, unit] = filter;
+
+  if (!mbqlEq(operator, "time-interval")) {
+    throw new Error("translateTimeInterval expects operator 'time-interval'");
+  }
+
+  if (n === "current") {
+    n = 0;
+  } else if (n === "last") {
+    n = -1;
+  } else if (n === "next") {
+    n = 1;
+  }
+
+  field = ["datetime-field", field, "as", unit];
+
+  if (n < -1) {
+    return [
+      "BETWEEN",
+      field,
+      ["relative-datetime", n - 1, unit],
+      ["relative-datetime", -1, unit],
+    ];
+  } else if (n > 1) {
+    return [
+      "BETWEEN",
+      field,
+      ["relative-datetime", 1, unit],
+      ["relative-datetime", n, unit],
+    ];
+  } else if (n === 0) {
+    return ["=", field, ["relative-datetime", "current"]];
+  } else {
+    return ["=", field, ["relative-datetime", n, unit]];
+  }
 }
 
 export function generateTimeFilterValuesDescriptions(filter) {
-    let [operator, field, ...values] = filter;
-    let bucketing = parseFieldBucketing(field);
-
-    if (mbqlEq(operator, "time-interval")) {
-        let [n, unit] = values;
-        return generateTimeIntervalDescription(n, unit);
-    } else {
-        return values.map(value => generateTimeValueDescription(value, bucketing));
-    }
+  let [operator, field, ...values] = filter;
+  let bucketing = parseFieldBucketing(field);
+
+  if (mbqlEq(operator, "time-interval")) {
+    let [n, unit] = values;
+    return generateTimeIntervalDescription(n, unit);
+  } else {
+    return values.map(value => generateTimeValueDescription(value, bucketing));
+  }
 }
 
 export function generateTimeIntervalDescription(n, unit) {
-    if (unit === "day") {
-        switch (n) {
-            case "current":
-            case 0:
-                return ["Today"];
-            case "next":
-            case 1:
-                return ["Tomorrow"];
-            case "last":
-            case -1:
-                return ["Yesterday"];
-        }
+  if (unit === "day") {
+    switch (n) {
+      case "current":
+      case 0:
+        return ["Today"];
+      case "next":
+      case 1:
+        return ["Tomorrow"];
+      case "last":
+      case -1:
+        return ["Yesterday"];
     }
+  }
 
-    if (!unit && n === 0) return "Today"; // ['relative-datetime', 'current'] is a legal MBQL form but has no unit
+  if (!unit && n === 0) return "Today"; // ['relative-datetime', 'current'] is a legal MBQL form but has no unit
 
-    unit = inflection.capitalize(unit);
-    if (typeof n === "string") {
-        if (n === "current") {
-            n = "this";
-        }
-        return [inflection.capitalize(n) + " " + unit];
+  unit = inflection.capitalize(unit);
+  if (typeof n === "string") {
+    if (n === "current") {
+      n = "this";
+    }
+    return [inflection.capitalize(n) + " " + unit];
+  } else {
+    if (n < 0) {
+      return ["Past " + -n + " " + inflection.inflect(unit, -n)];
+    } else if (n > 0) {
+      return ["Next " + n + " " + inflection.inflect(unit, n)];
     } else {
-        if (n < 0) {
-            return ["Past " + (-n) + " " + inflection.inflect(unit, -n)];
-        } else if (n > 0) {
-            return ["Next " + (n) + " " + inflection.inflect(unit, n)];
-        } else {
-            return ["This " + unit];
-        }
+      return ["This " + unit];
     }
+  }
 }
 
 export function generateTimeValueDescription(value, bucketing) {
-    if (typeof value === "string") {
-        let m = moment(value);
-        if (bucketing) {
-            return formatTimeWithUnit(value, bucketing);
-        } else if (m.hours() || m.minutes()) {
-            return m.format("MMMM D, YYYY hh:mm a");
-        } else {
-            return m.format("MMMM D, YYYY");
-        }
-    } else if (Array.isArray(value) && mbqlEq(value[0], "relative-datetime")) {
-        let n = value[1];
-        let unit = value[2];
-
-        if (n === "current") {
-            n = 0;
-            unit = bucketing;
-        }
-
-        if (bucketing === unit) {
-            return generateTimeIntervalDescription(n, unit);
-        } else {
-            // FIXME: what to do if the bucketing and unit don't match?
-            if (n === 0) {
-                return "Now";
-            } else {
-                return Math.abs(n) + " " + inflection.inflect(unit, Math.abs(n)) + (n < 0 ? " ago" : " from now");
-            }
-        }
+  if (typeof value === "string") {
+    let m = moment(value);
+    if (bucketing) {
+      return formatTimeWithUnit(value, bucketing);
+    } else if (m.hours() || m.minutes()) {
+      return m.format("MMMM D, YYYY hh:mm a");
+    } else {
+      return m.format("MMMM D, YYYY");
+    }
+  } else if (Array.isArray(value) && mbqlEq(value[0], "relative-datetime")) {
+    let n = value[1];
+    let unit = value[2];
+
+    if (n === "current") {
+      n = 0;
+      unit = bucketing;
+    }
+
+    if (bucketing === unit) {
+      return generateTimeIntervalDescription(n, unit);
     } else {
-        console.warn("Unknown datetime format", value);
-        return "[Unknown]";
+      // FIXME: what to do if the bucketing and unit don't match?
+      if (n === 0) {
+        return "Now";
+      } else {
+        return (
+          Math.abs(n) +
+          " " +
+          inflection.inflect(unit, Math.abs(n)) +
+          (n < 0 ? " ago" : " from now")
+        );
+      }
     }
+  } else {
+    console.warn("Unknown datetime format", value);
+    return "[Unknown]";
+  }
 }
 
 export function formatBucketing(bucketing = "") {
-    let words = bucketing.split("-");
-    words[0] = inflection.capitalize(words[0]);
-    return words.join(" ");
+  let words = bucketing.split("-");
+  words[0] = inflection.capitalize(words[0]);
+  return words.join(" ");
 }
 
 export function absolute(date) {
-    if (typeof date === "string") {
-        return moment(date);
-    } else if (Array.isArray(date) && mbqlEq(date[0], "relative-datetime")) {
-        return moment().add(date[1], date[2]);
-    } else {
-        console.warn("Unknown datetime format", date);
-    }
+  if (typeof date === "string") {
+    return moment(date);
+  } else if (Array.isArray(date) && mbqlEq(date[0], "relative-datetime")) {
+    return moment().add(date[1], date[2]);
+  } else {
+    console.warn("Unknown datetime format", date);
+  }
 }
 
 export function parseFieldBucketing(field, defaultUnit = null) {
-    if (Array.isArray(field)) {
-        if (mbqlEq(field[0], "datetime-field")) {
-            if (field.length === 4) {
-                // Deprecated legacy format [datetime-field field "as" unit], see DatetimeFieldDimension for more info
-                return field[3];
-            } else {
-                // Current format [datetime-field field unit]
-                return field[2]
-            }
-        } if (mbqlEq(field[0], "fk->") || mbqlEq(field[0], "field-id")) {
-            return defaultUnit;
-        } if (mbqlEq(field[0], "field-literal")) {
-            return defaultUnit;
-        }
-        else {
-            console.warn("Unknown field format", field);
-        }
+  if (Array.isArray(field)) {
+    if (mbqlEq(field[0], "datetime-field")) {
+      if (field.length === 4) {
+        // Deprecated legacy format [datetime-field field "as" unit], see DatetimeFieldDimension for more info
+        return field[3];
+      } else {
+        // Current format [datetime-field field unit]
+        return field[2];
+      }
     }
-    return defaultUnit;
+    if (mbqlEq(field[0], "fk->") || mbqlEq(field[0], "field-id")) {
+      return defaultUnit;
+    }
+    if (mbqlEq(field[0], "field-literal")) {
+      return defaultUnit;
+    } else {
+      console.warn("Unknown field format", field);
+    }
+  }
+  return defaultUnit;
 }
 
 // returns field with "datetime-field" removed
 export function parseFieldTarget(field) {
-    if (mbqlEq(field[0], "datetime-field")) {
-        return field[1];
-    } else {
-        return field;
-    }
+  if (mbqlEq(field[0], "datetime-field")) {
+    return field[1];
+  } else {
+    return field;
+  }
 }
 
 export function parseFieldTargetId(field) {
-    if (Number.isInteger(field)) return field;
+  if (Number.isInteger(field)) return field;
 
-    if (Array.isArray(field)) {
-        if (mbqlEq(field[0], "field-id"))       return field[1];
-        if (mbqlEq(field[0], "fk->"))           return field[1];
-        if (mbqlEq(field[0], "datetime-field")) return parseFieldTargetId(field[1]);
-        if (mbqlEq(field[0], "field-literal"))  return field;
-    }
+  if (Array.isArray(field)) {
+    if (mbqlEq(field[0], "field-id")) return field[1];
+    if (mbqlEq(field[0], "fk->")) return field[1];
+    if (mbqlEq(field[0], "datetime-field")) return parseFieldTargetId(field[1]);
+    if (mbqlEq(field[0], "field-literal")) return field;
+  }
 
-    console.warn("Unknown field format", field);
-    return field;
+  console.warn("Unknown field format", field);
+  return field;
 }
 
 // 271821 BC and 275760 AD and should be far enough in the past/future
 function max() {
-    return moment(new Date(864000000000000));
+  return moment(new Date(864000000000000));
 }
 function min() {
-    return moment(new Date(-864000000000000));
+  return moment(new Date(-864000000000000));
 }
diff --git a/frontend/src/metabase/lib/redux.js b/frontend/src/metabase/lib/redux.js
index 18ed1a8b088419dc7ec2cd511b5f869127761921..325f34ce6f49f283d9cb5db27dbe97dff427f3c9 100644
--- a/frontend/src/metabase/lib/redux.js
+++ b/frontend/src/metabase/lib/redux.js
@@ -11,155 +11,166 @@ export { handleActions, createAction } from "redux-actions";
 // similar to createAction but accepts a (redux-thunk style) thunk and dispatches based on whether
 // the promise returned from the thunk resolves or rejects, similar to redux-promise
 export function createThunkAction(actionType, actionThunkCreator) {
-    function fn(...actionArgs) {
-        var thunk = actionThunkCreator(...actionArgs);
-        return async function(dispatch, getState) {
-            try {
-
-                let payload = await thunk(dispatch, getState);
-                let dispatchValue = { type: actionType, payload };
-                dispatch(dispatchValue);
-
-                return dispatchValue;
-            } catch (error) {
-                dispatch({ type: actionType, payload: error, error: true });
-                throw error;
-            }
-        }
-    }
-    fn.toString = () => actionType;
-    return fn;
+  function fn(...actionArgs) {
+    var thunk = actionThunkCreator(...actionArgs);
+    return async function(dispatch, getState) {
+      try {
+        let payload = await thunk(dispatch, getState);
+        let dispatchValue = { type: actionType, payload };
+        dispatch(dispatchValue);
+
+        return dispatchValue;
+      } catch (error) {
+        dispatch({ type: actionType, payload: error, error: true });
+        throw error;
+      }
+    };
+  }
+  fn.toString = () => actionType;
+  return fn;
 }
 
 // turns string timestamps into moment objects
-export function momentifyTimestamps(object, keys = ["created_at", "updated_at"]) {
-    object = { ...object };
-    for (let timestamp of keys) {
-        if (timestamp in object) {
-            object[timestamp] = moment(object[timestamp]);
-        }
+export function momentifyTimestamps(
+  object,
+  keys = ["created_at", "updated_at"],
+) {
+  object = { ...object };
+  for (let timestamp of keys) {
+    if (timestamp in object) {
+      object[timestamp] = moment(object[timestamp]);
     }
-    return object;
+  }
+  return object;
 }
 
 export function momentifyObjectsTimestamps(objects, keys) {
-    return _.mapObject(objects, o => momentifyTimestamps(o, keys));
+  return _.mapObject(objects, o => momentifyTimestamps(o, keys));
 }
 
 export function momentifyArraysTimestamps(array, keys) {
-    return _.map(array, o => momentifyTimestamps(o, keys));
+  return _.map(array, o => momentifyTimestamps(o, keys));
 }
 
 // turns into id indexed map
-export const resourceListToMap = (resources) =>
-    resources.reduce((map, resource) => ({ ...map, [resource.id]: resource }), {});
+export const resourceListToMap = resources =>
+  resources.reduce(
+    (map, resource) => ({ ...map, [resource.id]: resource }),
+    {},
+  );
 
 export const fetchData = async ({
-    dispatch,
-    getState,
-    requestStatePath,
-    existingStatePath,
-    getData,
-    reload
+  dispatch,
+  getState,
+  requestStatePath,
+  existingStatePath,
+  getData,
+  reload,
 }) => {
-    const existingData = getIn(getState(), existingStatePath);
-    const statePath = requestStatePath.concat(['fetch']);
-    try {
-        const requestState = getIn(getState(), ["requests", "states", ...statePath]);
-        if (!requestState || requestState.error || reload) {
-            dispatch(setRequestState({ statePath, state: "LOADING" }));
-            const data = await getData();
-
-            // NOTE Atte Keinänen 8/23/17:
-            // Dispatch `setRequestState` after clearing the call stack because we want to the actual data to be updated
-            // before we notify components via `state.requests.fetches` that fetching the data is completed
-            setTimeout(() => dispatch(setRequestState({ statePath, state: "LOADED" })), 0);
-
-            return data;
-        }
-
-        return existingData;
-    }
-    catch(error) {
-        dispatch(setRequestState({ statePath, error }));
-        console.error(error);
-        return existingData;
+  const existingData = getIn(getState(), existingStatePath);
+  const statePath = requestStatePath.concat(["fetch"]);
+  try {
+    const requestState = getIn(getState(), [
+      "requests",
+      "states",
+      ...statePath,
+    ]);
+    if (!requestState || requestState.error || reload) {
+      dispatch(setRequestState({ statePath, state: "LOADING" }));
+      const data = await getData();
+
+      // NOTE Atte Keinänen 8/23/17:
+      // Dispatch `setRequestState` after clearing the call stack because we want to the actual data to be updated
+      // before we notify components via `state.requests.fetches` that fetching the data is completed
+      setTimeout(
+        () => dispatch(setRequestState({ statePath, state: "LOADED" })),
+        0,
+      );
+
+      return data;
     }
-}
+
+    return existingData;
+  } catch (error) {
+    dispatch(setRequestState({ statePath, error }));
+    console.error(error);
+    return existingData;
+  }
+};
 
 export const updateData = async ({
-    dispatch,
-    getState,
-    requestStatePath,
-    existingStatePath,
-    // specify any request paths that need to be invalidated after this update
-    dependentRequestStatePaths,
-    putData
+  dispatch,
+  getState,
+  requestStatePath,
+  existingStatePath,
+  // specify any request paths that need to be invalidated after this update
+  dependentRequestStatePaths,
+  putData,
 }) => {
-    const existingData = getIn(getState(), existingStatePath);
-    const statePath = requestStatePath.concat(['update']);
-    try {
-        dispatch(setRequestState({ statePath, state: "LOADING" }));
-        const data = await putData();
-        dispatch(setRequestState({ statePath, state: "LOADED" }));
-
-        (dependentRequestStatePaths || [])
-            .forEach(statePath => dispatch(clearRequestState({ statePath })));
-
-        return data;
-    }
-    catch(error) {
-        dispatch(setRequestState({ statePath, error }));
-        console.error(error);
-        return existingData;
-    }
-}
+  const existingData = getIn(getState(), existingStatePath);
+  const statePath = requestStatePath.concat(["update"]);
+  try {
+    dispatch(setRequestState({ statePath, state: "LOADING" }));
+    const data = await putData();
+    dispatch(setRequestState({ statePath, state: "LOADED" }));
+
+    (dependentRequestStatePaths || []).forEach(statePath =>
+      dispatch(clearRequestState({ statePath })),
+    );
+
+    return data;
+  } catch (error) {
+    dispatch(setRequestState({ statePath, error }));
+    console.error(error);
+    return existingData;
+  }
+};
 
 // helper for working with normalizr
 // merge each entity from newEntities with existing entity, if any
 // this ensures partial entities don't overwrite existing entities with more properties
 export function mergeEntities(entities, newEntities) {
-    entities = { ...entities };
-    for (const id in newEntities) {
-        if (id in entities) {
-            entities[id] = { ...entities[id], ...newEntities[id] };
-        } else {
-            entities[id] = newEntities[id];
-        }
+  entities = { ...entities };
+  for (const id in newEntities) {
+    if (id in entities) {
+      entities[id] = { ...entities[id], ...newEntities[id] };
+    } else {
+      entities[id] = newEntities[id];
     }
-    return entities;
+  }
+  return entities;
 }
 
 // helper for working with normalizr
 // reducer that merges payload.entities
 export function handleEntities(actionPattern, entityType, reducer) {
-    return (state, action) => {
-        if (state === undefined) {
-            state = {};
-        }
-        let entities = getIn(action, ["payload", "entities", entityType]);
-        if (actionPattern.test(action.type) && entities) {
-            state = mergeEntities(state, entities);
-        }
-        return reducer(state, action);
+  return (state, action) => {
+    if (state === undefined) {
+      state = {};
+    }
+    let entities = getIn(action, ["payload", "entities", entityType]);
+    if (actionPattern.test(action.type) && entities) {
+      state = mergeEntities(state, entities);
     }
+    return reducer(state, action);
+  };
 }
 
 // for filtering non-DOM props from redux-form field objects
 // https://github.com/erikras/redux-form/issues/1441
 export const formDomOnlyProps = ({
-    initialValue,
-    autofill,
-    onUpdate,
-    valid,
-    invalid,
-    dirty,
-    pristine,
-    active,
-    touched,
-    visited,
-    autofilled,
-    error,
-    defaultValue,
-    ...domProps
-}) => domProps
+  initialValue,
+  autofill,
+  onUpdate,
+  valid,
+  invalid,
+  dirty,
+  pristine,
+  active,
+  touched,
+  visited,
+  autofilled,
+  error,
+  defaultValue,
+  ...domProps
+}) => domProps;
diff --git a/frontend/src/metabase/lib/request.js b/frontend/src/metabase/lib/request.js
index 50ebfca385f3b664c2d96d37cf4a0cfd3570557e..d4fcc808d6e4b7f69ac6f5ab48778054801fb2e3 100644
--- a/frontend/src/metabase/lib/request.js
+++ b/frontend/src/metabase/lib/request.js
@@ -1,201 +1,218 @@
 import { AsyncApi } from "metabase/services";
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 export class RestfulRequest {
-    // API endpoint that is used for the request
-    endpoint = null
-
-    // Prefix for request Redux actions
-    actionPrefix = null
-
-    // Name of the request result property
-    // In general, using the default value `result` is good for consistency
-    // but using an existing prop name (like `xray` or `dashboard`) temporarily
-    // can make the migration process from old implementation to this request API a lot easier
-    resultPropName = 'result'
-
-    // If `true`, then the result (either an object an array) will be converted to a dictionary
-    // where the dictionary key is the `id` field of the result.
-    // This dictionary is merged to the possibly pre-existing dictionary.
-    storeAsDictionary = false
-
-    constructor({ endpoint, actionPrefix, resultPropName, storeAsDictionary } = {}) {
-        this.endpoint = endpoint
-        this.actionPrefix = actionPrefix
-        this.resultPropName = resultPropName || this.resultPropName
-        this.storeAsDictionary = storeAsDictionary
-
-        this.actions = {
-            requestStarted: `${this.actionPrefix}/REQUEST_STARTED`,
-            requestSuccessful: `${this.actionPrefix}/REQUEST_SUCCESSFUL`,
-            requestFailed: `${this.actionPrefix}/REQUEST_FAILED`,
-            resetRequest: `${this.actionPrefix}/REQUEST_RESET`
-        }
-    }
-
-    // Triggers the request; modelled as a Redux thunk action so wrap this to `dispatch()` call
-    trigger = (params) =>
-        async (dispatch) => {
-            dispatch.action(this.actions.requestStarted)
-            try {
-                const result = await this.endpoint(params)
-                dispatch.action(this.actions.requestSuccessful, { result })
-            } catch(error) {
-                dispatch.action(this.actions.requestFailed, { error })
-                throw error;
-            }
-
-        }
-
-    reset = () => (dispatch) => dispatch(this.actions.reset)
-
-    mergeToDictionary = (dict, result) => {
-        dict = dict || {}
-        result = _.isArray(result)
-            ? _.indexBy(result, "id")
-            : { [result.id]: result }
-
-        return { ...dict, ...result }
+  // API endpoint that is used for the request
+  endpoint = null;
+
+  // Prefix for request Redux actions
+  actionPrefix = null;
+
+  // Name of the request result property
+  // In general, using the default value `result` is good for consistency
+  // but using an existing prop name (like `xray` or `dashboard`) temporarily
+  // can make the migration process from old implementation to this request API a lot easier
+  resultPropName = "result";
+
+  // If `true`, then the result (either an object an array) will be converted to a dictionary
+  // where the dictionary key is the `id` field of the result.
+  // This dictionary is merged to the possibly pre-existing dictionary.
+  storeAsDictionary = false;
+
+  constructor({
+    endpoint,
+    actionPrefix,
+    resultPropName,
+    storeAsDictionary,
+  } = {}) {
+    this.endpoint = endpoint;
+    this.actionPrefix = actionPrefix;
+    this.resultPropName = resultPropName || this.resultPropName;
+    this.storeAsDictionary = storeAsDictionary;
+
+    this.actions = {
+      requestStarted: `${this.actionPrefix}/REQUEST_STARTED`,
+      requestSuccessful: `${this.actionPrefix}/REQUEST_SUCCESSFUL`,
+      requestFailed: `${this.actionPrefix}/REQUEST_FAILED`,
+      resetRequest: `${this.actionPrefix}/REQUEST_RESET`,
+    };
+  }
+
+  // Triggers the request; modelled as a Redux thunk action so wrap this to `dispatch()` call
+  trigger = params => async dispatch => {
+    dispatch.action(this.actions.requestStarted);
+    try {
+      const result = await this.endpoint(params);
+      dispatch.action(this.actions.requestSuccessful, { result });
+    } catch (error) {
+      dispatch.action(this.actions.requestFailed, { error });
+      throw error;
     }
-
-    getReducers = () => ({
-        [this.actions.requestStarted]: (state) => ({ ...state, loading: true, error: null }),
-        [this.actions.requestSuccessful]: (state, { payload: { result }}) => ({
-            ...state,
-            [this.resultPropName]: this.storeAsDictionary
-                ? this.mergeToDictionary(state[this.resultPropName], result)
-                : result,
-            loading: false,
-            fetched: true,
-            error: null
-        }),
-        [this.actions.requestFailed]: (state, { payload: { error } }) => ({
-            ...state,
-            loading: false,
-            error: error
-        }),
-        [this.actions.resetRequest]: (state) => ({ ...state, ...this.getDefaultState() })
-    })
-
-    getDefaultState = () => ({
-        [this.resultPropName]: null,
-        loading: false,
-        fetched: false,
-        error: null
-    })
+  };
+
+  reset = () => dispatch => dispatch(this.actions.reset);
+
+  mergeToDictionary = (dict, result) => {
+    dict = dict || {};
+    result = _.isArray(result)
+      ? _.indexBy(result, "id")
+      : { [result.id]: result };
+
+    return { ...dict, ...result };
+  };
+
+  getReducers = () => ({
+    [this.actions.requestStarted]: state => ({
+      ...state,
+      loading: true,
+      error: null,
+    }),
+    [this.actions.requestSuccessful]: (state, { payload: { result } }) => ({
+      ...state,
+      [this.resultPropName]: this.storeAsDictionary
+        ? this.mergeToDictionary(state[this.resultPropName], result)
+        : result,
+      loading: false,
+      fetched: true,
+      error: null,
+    }),
+    [this.actions.requestFailed]: (state, { payload: { error } }) => ({
+      ...state,
+      loading: false,
+      error: error,
+    }),
+    [this.actions.resetRequest]: state => ({
+      ...state,
+      ...this.getDefaultState(),
+    }),
+  });
+
+  getDefaultState = () => ({
+    [this.resultPropName]: null,
+    loading: false,
+    fetched: false,
+    error: null,
+  });
 }
 
-const POLLING_INTERVAL = 100
+const POLLING_INTERVAL = 100;
 
 export class BackgroundJobRequest {
-    // API endpoint that creates a new background job
-    creationEndpoint = null
-
-    // Prefix for request Redux actions
-    actionPrefix = null
-
-    // Name of the request result property
-    // In general, using the default value `result` is good for consistency
-    // but using an existing prop name (like `xray` or `dashboard`) temporarily
-    // can make the migration process from old implementation to this request API a lot easier
-    resultPropName = 'result'
-
-    pollingTimeoutId = null
-
-    constructor({ creationEndpoint, actionPrefix, resultPropName } = {}) {
-        this.creationEndpoint = creationEndpoint
-        this.actionPrefix = actionPrefix
-        this.resultPropName = resultPropName || this.resultPropName
-
-        this.actions = {
-            requestStarted: `${this.actionPrefix}/REQUEST_STARTED`,
-            requestSuccessful: `${this.actionPrefix}/REQUEST_SUCCESSFUL`,
-            requestFailed: `${this.actionPrefix}/REQUEST_FAILED`,
-            resetRequest: `${this.actionPrefix}/REQUEST_RESET`
-        }
+  // API endpoint that creates a new background job
+  creationEndpoint = null;
+
+  // Prefix for request Redux actions
+  actionPrefix = null;
+
+  // Name of the request result property
+  // In general, using the default value `result` is good for consistency
+  // but using an existing prop name (like `xray` or `dashboard`) temporarily
+  // can make the migration process from old implementation to this request API a lot easier
+  resultPropName = "result";
+
+  pollingTimeoutId = null;
+
+  constructor({ creationEndpoint, actionPrefix, resultPropName } = {}) {
+    this.creationEndpoint = creationEndpoint;
+    this.actionPrefix = actionPrefix;
+    this.resultPropName = resultPropName || this.resultPropName;
+
+    this.actions = {
+      requestStarted: `${this.actionPrefix}/REQUEST_STARTED`,
+      requestSuccessful: `${this.actionPrefix}/REQUEST_SUCCESSFUL`,
+      requestFailed: `${this.actionPrefix}/REQUEST_FAILED`,
+      resetRequest: `${this.actionPrefix}/REQUEST_RESET`,
+    };
+  }
+
+  // Triggers the request; modelled as a Redux thunk action so wrap this to `dispatch()` call
+  trigger = params => {
+    return async dispatch => {
+      dispatch.action(this.actions.requestStarted);
+
+      try {
+        const newJobId = await this._createNewJob(params);
+        const result = await this._pollForResult(newJobId);
+        dispatch.action(this.actions.requestSuccessful, { result });
+      } catch (error) {
+        dispatch.action(this.actions.requestFailed, { error });
+        throw error;
+      }
+    };
+  };
+
+  _createNewJob = async requestParams => {
+    return (await this.creationEndpoint(requestParams))["job-id"];
+  };
+
+  _pollForResult = jobId => {
+    if (this.pollingTimeoutId) {
+      clearTimeout(this.pollingTimeoutId);
     }
 
-    // Triggers the request; modelled as a Redux thunk action so wrap this to `dispatch()` call
-    trigger = (params) => {
-        return async (dispatch) => {
-            dispatch.action(this.actions.requestStarted)
-
-            try {
-                const newJobId = await this._createNewJob(params)
-                const result = await this._pollForResult(newJobId)
-                dispatch.action(this.actions.requestSuccessful, { result })
-            } catch(error) {
-                dispatch.action(this.actions.requestFailed, { error })
-                throw error;
-            }
+    return new Promise((resolve, reject) => {
+      const poll = async () => {
+        try {
+          const response = await AsyncApi.status({ jobId });
+
+          if (response.status === "done") {
+            resolve(response.result);
+          } else if (response.status === "error") {
+            throw new Error(response.result.cause);
+          } else if (response.status === "result-not-available") {
+            // The job result has been deleted; this is an unexpected state as we just
+            // created the job so simply throw a descriptive error
+            reject(new ResultNoAvailableError());
+          } else {
+            this.pollingTimeoutId = setTimeout(poll, POLLING_INTERVAL);
+          }
+        } catch (error) {
+          this.pollingTimeoutId = null;
+          reject(error);
         }
-    }
-
-    _createNewJob = async (requestParams) => {
-        return (await this.creationEndpoint(requestParams))["job-id"]
-    }
-
-    _pollForResult = (jobId) => {
-        if (this.pollingTimeoutId) {
-            clearTimeout(this.pollingTimeoutId);
-        }
-
-        return new Promise((resolve, reject) => {
-            const poll = async () => {
-                try {
-                    const response = await AsyncApi.status({ jobId })
-
-                    if (response.status === 'done') {
-                        resolve(response.result)
-                    } else if (response.status === 'error') {
-                       throw new Error(response.result.cause)
-                    } else if (response.status === 'result-not-available') {
-                        // The job result has been deleted; this is an unexpected state as we just
-                        // created the job so simply throw a descriptive error
-                        reject(new ResultNoAvailableError())
-                    } else {
-                        this.pollingTimeoutId = setTimeout(poll, POLLING_INTERVAL)
-                    }
-                } catch (error) {
-                    this.pollingTimeoutId = null
-                    reject(error)
-                }
-            }
-
-            poll()
-        })
-    }
-
-    reset = () => (dispatch) => dispatch(this.actions.reset)
-
-    getReducers = () => ({
-        [this.actions.requestStarted]: (state) => ({...state, loading: true, error: null }),
-        [this.actions.requestSuccessful]: (state, { payload: { result }}) => ({
-            ...state,
-            [this.resultPropName]: result,
-            loading: false,
-            fetched: true,
-            error: null
-        }),
-        [this.actions.requestFailed]: (state, { payload: { error } }) => ({
-            ...state,
-            loading: false,
-            error: error
-        }),
-        [this.actions.resetRequest]: (state) => ({ ...state, ...this.getDefaultState() })
-    })
-
-    getDefaultState = () => ({
-        [this.resultPropName]: null,
-        loading: false,
-        fetched: false,
-        error: null
-    })
+      };
+
+      poll();
+    });
+  };
+
+  reset = () => dispatch => dispatch(this.actions.reset);
+
+  getReducers = () => ({
+    [this.actions.requestStarted]: state => ({
+      ...state,
+      loading: true,
+      error: null,
+    }),
+    [this.actions.requestSuccessful]: (state, { payload: { result } }) => ({
+      ...state,
+      [this.resultPropName]: result,
+      loading: false,
+      fetched: true,
+      error: null,
+    }),
+    [this.actions.requestFailed]: (state, { payload: { error } }) => ({
+      ...state,
+      loading: false,
+      error: error,
+    }),
+    [this.actions.resetRequest]: state => ({
+      ...state,
+      ...this.getDefaultState(),
+    }),
+  });
+
+  getDefaultState = () => ({
+    [this.resultPropName]: null,
+    loading: false,
+    fetched: false,
+    error: null,
+  });
 }
 
 class ResultNoAvailableError extends Error {
-    constructor() {
-        super()
-        this.message = t`Background job result isn't available for an unknown reason`
-    }
+  constructor() {
+    super();
+    this.message = t`Background job result isn't available for an unknown reason`;
+  }
 }
diff --git a/frontend/src/metabase/lib/schema_metadata.js b/frontend/src/metabase/lib/schema_metadata.js
index 4d25fe7aff3f2378a5d00efc3246f0ada1ea8804..af7c5612690526313f9825db85b6ad0938c36bd5 100644
--- a/frontend/src/metabase/lib/schema_metadata.js
+++ b/frontend/src/metabase/lib/schema_metadata.js
@@ -1,7 +1,11 @@
 import _ from "underscore";
-import { t } from 'c-3po';
-import { isa, isFK as isTypeFK, isPK as isTypePK, TYPE } from "metabase/lib/types";
-import { getFieldValues } from "metabase/lib/query/field";
+import { t } from "c-3po";
+import {
+  isa,
+  isFK as isTypeFK,
+  isPK as isTypePK,
+  TYPE,
+} from "metabase/lib/types";
 
 // primary field types used for picking operators, etc
 export const NUMBER = "NUMBER";
@@ -11,6 +15,7 @@ export const BOOLEAN = "BOOLEAN";
 export const DATE_TIME = "DATE_TIME";
 export const LOCATION = "LOCATION";
 export const COORDINATE = "COORDINATE";
+export const FOREIGN_KEY = "FOREIGN_KEY";
 
 // other types used for various purporses
 export const ENTITY = "ENTITY";
@@ -23,80 +28,92 @@ export const UNKNOWN = "UNKNOWN";
 // define various type hierarchies
 // NOTE: be sure not to create cycles using the "other" types
 const TYPES = {
-    [DATE_TIME]: {
-        base: [TYPE.DateTime],
-        special: [TYPE.DateTime]
-    },
-    [NUMBER]: {
-        base: [TYPE.Number],
-        special: [TYPE.Number]
-    },
-    [STRING]: {
-        base: [TYPE.Text],
-        special: [TYPE.Text]
-    },
-    [STRING_LIKE]: {
-        base: [TYPE.TextLike]
-    },
-    [BOOLEAN]: {
-        base: [TYPE.Boolean]
-    },
-    [COORDINATE]: {
-        special: [TYPE.Coordinate]
-    },
-    [LOCATION]: {
-        special: [TYPE.Address]
-    },
-    [ENTITY]: {
-        special: [TYPE.FK, TYPE.PK, TYPE.Name]
-    },
-    [SUMMABLE]: {
-        include: [NUMBER],
-        exclude: [ENTITY, LOCATION, DATE_TIME]
-    },
-    [CATEGORY]: {
-        base: [TYPE.Boolean],
-        special: [TYPE.Category],
-        include: [LOCATION]
-    },
-    // NOTE: this is defunct right now.  see definition of isDimension below.
-    [DIMENSION]: {
-        include: [DATE_TIME, CATEGORY, ENTITY]
-    }
+  [DATE_TIME]: {
+    base: [TYPE.DateTime],
+    special: [TYPE.DateTime],
+  },
+  [NUMBER]: {
+    base: [TYPE.Number],
+    special: [TYPE.Number],
+  },
+  [STRING]: {
+    base: [TYPE.Text],
+    special: [TYPE.Text],
+  },
+  [STRING_LIKE]: {
+    base: [TYPE.TextLike],
+  },
+  [BOOLEAN]: {
+    base: [TYPE.Boolean],
+  },
+  [COORDINATE]: {
+    special: [TYPE.Coordinate],
+  },
+  [LOCATION]: {
+    special: [TYPE.Address],
+  },
+  [ENTITY]: {
+    special: [TYPE.FK, TYPE.PK, TYPE.Name],
+  },
+  [FOREIGN_KEY]: {
+    special: [TYPE.FK],
+  },
+  [SUMMABLE]: {
+    include: [NUMBER],
+    exclude: [ENTITY, LOCATION, DATE_TIME],
+  },
+  [CATEGORY]: {
+    base: [TYPE.Boolean],
+    special: [TYPE.Category],
+    include: [LOCATION],
+  },
+  // NOTE: this is defunct right now.  see definition of isDimension below.
+  [DIMENSION]: {
+    include: [DATE_TIME, CATEGORY, ENTITY],
+  },
 };
 
 export function isFieldType(type, field) {
-    if (!field) return false;
-
-    const typeDefinition = TYPES[type];
-    // check to see if it belongs to any of the field types:
-    for (const prop of ["base", "special"]) {
-        const allowedTypes = typeDefinition[prop];
-        if (!allowedTypes) continue;
-
-        const fieldType = field[prop + "_type"];
-        for (const allowedType of allowedTypes) {
-            if (isa(fieldType, allowedType)) return true;
-        }
-    }
+  if (!field) return false;
 
-    // recursively check to see if it's NOT another field type:
-    for (const excludedType of (typeDefinition.exclude || [])) {
-        if (isFieldType(excludedType, field)) return false;
-    }
+  const typeDefinition = TYPES[type];
+  // check to see if it belongs to any of the field types:
+  for (const prop of ["base", "special"]) {
+    const allowedTypes = typeDefinition[prop];
+    if (!allowedTypes) continue;
 
-    // recursively check to see if it's another field type:
-    for (const includedType of (typeDefinition.include || [])) {
-        if (isFieldType(includedType, field)) return true;
+    const fieldType = field[prop + "_type"];
+    for (const allowedType of allowedTypes) {
+      if (isa(fieldType, allowedType)) return true;
     }
-    return false;
+  }
+
+  // recursively check to see if it's NOT another field type:
+  for (const excludedType of typeDefinition.exclude || []) {
+    if (isFieldType(excludedType, field)) return false;
+  }
+
+  // recursively check to see if it's another field type:
+  for (const includedType of typeDefinition.include || []) {
+    if (isFieldType(includedType, field)) return true;
+  }
+  return false;
 }
 
 export function getFieldType(field) {
-    // try more specific types first, then more generic types
-    for (const type of [DATE_TIME, LOCATION, COORDINATE, NUMBER, STRING, STRING_LIKE, BOOLEAN]) {
-        if (isFieldType(type, field)) return type;
-    }
+  // try more specific types first, then more generic types
+  for (const type of [
+    DATE_TIME,
+    LOCATION,
+    COORDINATE,
+    FOREIGN_KEY,
+    NUMBER,
+    STRING,
+    STRING_LIKE,
+    BOOLEAN,
+  ]) {
+    if (isFieldType(type, field)) return type;
+  }
 }
 
 export const isDate = isFieldType.bind(null, DATE_TIME);
@@ -106,466 +123,509 @@ export const isString = isFieldType.bind(null, STRING);
 export const isSummable = isFieldType.bind(null, SUMMABLE);
 export const isCategory = isFieldType.bind(null, CATEGORY);
 
-export const isDimension = (col) => (col && col.source !== "aggregation");
-export const isMetric    = (col) => (col && col.source !== "breakout") && isSummable(col);
+export const isDimension = col => col && col.source !== "aggregation";
+export const isMetric = col =>
+  col && col.source !== "breakout" && isSummable(col);
 
-export const isFK = (field) => field && isTypeFK(field.special_type);
-export const isPK = (field) => field && isTypePK(field.special_type);
+export const isFK = field => field && isTypeFK(field.special_type);
+export const isPK = field => field && isTypePK(field.special_type);
+export const isEntityName = field =>
+  isa(field && field.special_type, TYPE.Name);
 
-export const isAny = (col) => true;
+export const isAny = col => true;
 
-export const isNumericBaseType = (field) => isa(field && field.base_type, TYPE.Number);
+export const isNumericBaseType = field =>
+  isa(field && field.base_type, TYPE.Number);
 
 // ZipCode, ID, etc derive from Number but should not be formatted as numbers
-export const isNumber = (field) => field && isNumericBaseType(field) && (field.special_type == null || field.special_type === TYPE.Number);
-
-export const isTime         = (field) => isa(field && field.base_type, TYPE.Time);
-
-export const isAddress      = (field) => isa(field && field.special_type, TYPE.Address);
-export const isState        = (field) => isa(field && field.special_type, TYPE.State);
-export const isCountry      = (field) => isa(field && field.special_type, TYPE.Country);
-export const isCoordinate   = (field) => isa(field && field.special_type, TYPE.Coordinate);
-export const isLatitude     = (field) => isa(field && field.special_type, TYPE.Latitude);
-export const isLongitude    = (field) => isa(field && field.special_type, TYPE.Longitude);
-
-export const isID           = (field) => isFK(field) || isPK(field);
+export const isNumber = field =>
+  field &&
+  isNumericBaseType(field) &&
+  (field.special_type == null || field.special_type === TYPE.Number);
+
+export const isTime = field => isa(field && field.base_type, TYPE.Time);
+
+export const isAddress = field =>
+  isa(field && field.special_type, TYPE.Address);
+export const isState = field => isa(field && field.special_type, TYPE.State);
+export const isCountry = field =>
+  isa(field && field.special_type, TYPE.Country);
+export const isCoordinate = field =>
+  isa(field && field.special_type, TYPE.Coordinate);
+export const isLatitude = field =>
+  isa(field && field.special_type, TYPE.Latitude);
+export const isLongitude = field =>
+  isa(field && field.special_type, TYPE.Longitude);
+
+export const isID = field => isFK(field) || isPK(field);
 
 // operator argument constructors:
 
 function freeformArgument(field, table) {
-    return {
-        type: "text"
-    };
+  return {
+    type: "text",
+  };
 }
 
 function numberArgument(field, table) {
-    return {
-        type: "number"
-    };
+  return {
+    type: "number",
+  };
 }
 
-
 function comparableArgument(field, table) {
-    if (isDate(field)) {
-        return {
-            type: "date"
-        };
-    }
-
-    if (isNumeric(field)) {
-        return {
-            type: "number"
-        };
-    }
+  if (isDate(field)) {
+    return {
+      type: "date",
+    };
+  }
 
+  if (isNumeric(field)) {
     return {
-        type: "text"
+      type: "number",
     };
-}
+  }
 
+  return {
+    type: "text",
+  };
+}
 
 function equivalentArgument(field, table) {
-    if (isBoolean(field)) {
-        return {
-            type: "select",
-            values: [
-                { key: true, name: t`True` },
-                { key: false, name: t`False` }
-            ]
-        };
-    }
-
-    if (isCategory(field)) {
-        const values = getFieldValues(field)
-        if (values && values.length > 0) {
-            return {
-                type: "select",
-                values: values
-                    .filter(([value, displayValue]) => value != null)
-                    .map(([value, displayValue]) => ({
-                        key: value,
-                        // NOTE Atte Keinänen 8/7/17: Similar logic as in getHumanReadableValue of lib/query/field
-                        name: displayValue ? displayValue : String(value)
-                    }))
-                    .sort((a, b) => a.key === b.key ? 0 : (a.key < b.key ? -1 : 1))
-            };
-        }
-    }
-
-    if (isDate(field)) {
-        return {
-            type: "date"
-        };
-    }
+  if (isBoolean(field)) {
+    return {
+      type: "select",
+      values: [{ key: true, name: t`True` }, { key: false, name: t`False` }],
+    };
+  }
 
-    if (isNumeric(field)) {
-        return {
-            type: "number"
-        };
-    }
+  if (isDate(field)) {
+    return {
+      type: "date",
+    };
+  }
 
+  if (isNumeric(field)) {
     return {
-        type: "text"
+      type: "number",
     };
+  }
+
+  return {
+    type: "text",
+  };
 }
 
 function longitudeFieldSelectArgument(field, table) {
-    return {
-        type: "select",
-        values: table.fields
-            .filter(field => isa(field.special_type, TYPE.Longitude))
-            .map(field => ({
-                key: field.id,
-                name: field.display_name
-            }))
-    };
+  return {
+    type: "select",
+    values: table.fields
+      .filter(field => isa(field.special_type, TYPE.Longitude))
+      .map(field => ({
+        key: field.id,
+        name: field.display_name,
+      })),
+  };
 }
 
+const CASE_SENSITIVE_OPTION = {
+  "case-sensitive": {
+    defaultValue: true,
+  },
+};
+
 const OPERATORS = {
-    "=": {
-        validArgumentsFilters: [equivalentArgument],
-        multi: true
-    },
-    "!=": {
-        validArgumentsFilters: [equivalentArgument],
-        multi: true
-    },
-    "IS_NULL": {
-        validArgumentsFilters: []
-    },
-    "NOT_NULL": {
-        validArgumentsFilters: []
-    },
-    "<": {
-        validArgumentsFilters: [comparableArgument]
-    },
-    "<=": {
-        validArgumentsFilters: [comparableArgument]
-    },
-    ">": {
-        validArgumentsFilters: [comparableArgument]
-    },
-    ">=": {
-        validArgumentsFilters: [comparableArgument]
-    },
-    "INSIDE": {
-        validArgumentsFilters: [longitudeFieldSelectArgument, numberArgument, numberArgument, numberArgument, numberArgument],
-        placeholders: [t`Select longitude field`, t`Enter upper latitude`, t`Enter left longitude`, t`Enter lower latitude`, t`Enter right longitude`]
-    },
-    "BETWEEN": {
-        validArgumentsFilters: [comparableArgument, comparableArgument]
-    },
-    "STARTS_WITH": {
-        validArgumentsFilters: [freeformArgument]
-    },
-    "ENDS_WITH": {
-        validArgumentsFilters: [freeformArgument]
-    },
-    "CONTAINS": {
-        validArgumentsFilters: [freeformArgument]
-    },
-    "DOES_NOT_CONTAIN": {
-        validArgumentsFilters: [freeformArgument]
-    }
+  "=": {
+    validArgumentsFilters: [equivalentArgument],
+    multi: true,
+  },
+  "!=": {
+    validArgumentsFilters: [equivalentArgument],
+    multi: true,
+  },
+  IS_NULL: {
+    validArgumentsFilters: [],
+  },
+  NOT_NULL: {
+    validArgumentsFilters: [],
+  },
+  "<": {
+    validArgumentsFilters: [comparableArgument],
+  },
+  "<=": {
+    validArgumentsFilters: [comparableArgument],
+  },
+  ">": {
+    validArgumentsFilters: [comparableArgument],
+  },
+  ">=": {
+    validArgumentsFilters: [comparableArgument],
+  },
+  INSIDE: {
+    validArgumentsFilters: [
+      longitudeFieldSelectArgument,
+      numberArgument,
+      numberArgument,
+      numberArgument,
+      numberArgument,
+    ],
+    placeholders: [
+      t`Select longitude field`,
+      t`Enter upper latitude`,
+      t`Enter left longitude`,
+      t`Enter lower latitude`,
+      t`Enter right longitude`,
+    ],
+  },
+  BETWEEN: {
+    validArgumentsFilters: [comparableArgument, comparableArgument],
+  },
+  STARTS_WITH: {
+    validArgumentsFilters: [freeformArgument],
+    options: CASE_SENSITIVE_OPTION,
+    optionsDefaults: { "case-sensitive": false },
+  },
+  ENDS_WITH: {
+    validArgumentsFilters: [freeformArgument],
+    options: CASE_SENSITIVE_OPTION,
+    optionsDefaults: { "case-sensitive": false },
+  },
+  CONTAINS: {
+    validArgumentsFilters: [freeformArgument],
+    options: CASE_SENSITIVE_OPTION,
+    optionsDefaults: { "case-sensitive": false },
+  },
+  DOES_NOT_CONTAIN: {
+    validArgumentsFilters: [freeformArgument],
+    options: CASE_SENSITIVE_OPTION,
+    optionsDefaults: { "case-sensitive": false },
+  },
 };
 
+const DEFAULT_OPERATORS = [
+  { name: "=", verboseName: t`Is` },
+  { name: "!=", verboseName: t`Is not` },
+  { name: "IS_NULL", verboseName: t`Is empty` },
+  { name: "NOT_NULL", verboseName: t`Not empty` },
+];
+
 // ordered list of operators and metadata per type
 const OPERATORS_BY_TYPE_ORDERED = {
-    [NUMBER]: [
-        { name: "=",                verboseName: t`Equal` },
-        { name: "!=",               verboseName: t`Not equal` },
-        { name: ">",                verboseName: t`Greater than` },
-        { name: "<",                verboseName: t`Less than` },
-        { name: "BETWEEN",          verboseName: t`Between` },
-        { name: ">=",               verboseName: t`Greater than or equal to`, advanced: true },
-        { name: "<=",               verboseName: t`Less than or equal to`, advanced: true },
-        { name: "IS_NULL",          verboseName: t`Is empty`, advanced: true },
-        { name: "NOT_NULL",         verboseName: t`Not empty`, advanced: true }
-    ],
-    [STRING]: [
-        { name: "=",                verboseName: t`Is` },
-        { name: "!=",               verboseName: t`Is not` },
-        { name: "CONTAINS",         verboseName: t`Contains`},
-        { name: "DOES_NOT_CONTAIN", verboseName: t`Does not contain`},
-        { name: "IS_NULL",          verboseName: t`Is empty`, advanced: true },
-        { name: "NOT_NULL",         verboseName: t`Not empty`, advanced: true },
-        { name: "STARTS_WITH",      verboseName: t`Starts with`, advanced: true},
-        { name: "ENDS_WITH",        verboseName: t`Ends with`, advanced: true}
-    ],
-    [STRING_LIKE]: [
-        { name: "=",                verboseName: t`Is` },
-        { name: "!=",               verboseName: t`Is not` },
-        { name: "IS_NULL",          verboseName: t`Is empty`, advanced: true },
-        { name: "NOT_NULL",         verboseName: t`Not empty`, advanced: true }
-    ],
-    [DATE_TIME]: [
-        { name: "=",                verboseName: t`Is` },
-        { name: "<",                verboseName: t`Before` },
-        { name: ">",                verboseName: t`After` },
-        { name: "BETWEEN",          verboseName: t`Between` },
-        { name: "IS_NULL",          verboseName: t`Is empty`, advanced: true },
-        { name: "NOT_NULL",         verboseName: t`Not empty`, advanced: true }
-    ],
-    [LOCATION]: [
-        { name: "=",                verboseName: t`Is` },
-        { name: "!=",               verboseName: t`Is not` },
-        { name: "IS_NULL",          verboseName: t`Is empty`, advanced: true },
-        { name: "NOT_NULL",         verboseName: t`Not empty`, advanced: true }
-    ],
-    [COORDINATE]: [
-        { name: "=",                verboseName: t`Is` },
-        { name: "!=",               verboseName: t`Is no` },
-        { name: "INSIDE",           verboseName: t`Inside` }
-    ],
-    [BOOLEAN]: [
-        { name: "=",                verboseName: t`Is`, multi: false, defaults: [true] },
-        { name: "IS_NULL",          verboseName: t`Is empty` },
-        { name: "NOT_NULL",         verboseName: t`Not empty` }
-    ],
-    [UNKNOWN]: [
-        { name: "=",                verboseName: t`Is` },
-        { name: "!=",               verboseName: t`Is not` },
-        { name: "IS_NULL",          verboseName: t`Is empty`, advanced: true },
-        { name: "NOT_NULL",         verboseName: t`Not empty`, advanced: true }
-    ]
+  [NUMBER]: [
+    { name: "=", verboseName: t`Equal` },
+    { name: "!=", verboseName: t`Not equal` },
+    { name: ">", verboseName: t`Greater than` },
+    { name: "<", verboseName: t`Less than` },
+    { name: "BETWEEN", verboseName: t`Between` },
+    { name: ">=", verboseName: t`Greater than or equal to` },
+    { name: "<=", verboseName: t`Less than or equal to` },
+    { name: "IS_NULL", verboseName: t`Is empty` },
+    { name: "NOT_NULL", verboseName: t`Not empty` },
+  ],
+  [STRING]: [
+    { name: "=", verboseName: t`Is` },
+    { name: "!=", verboseName: t`Is not` },
+    { name: "CONTAINS", verboseName: t`Contains` },
+    { name: "DOES_NOT_CONTAIN", verboseName: t`Does not contain` },
+    { name: "IS_NULL", verboseName: t`Is empty` },
+    { name: "NOT_NULL", verboseName: t`Not empty` },
+    { name: "STARTS_WITH", verboseName: t`Starts with` },
+    { name: "ENDS_WITH", verboseName: t`Ends with` },
+  ],
+  [STRING_LIKE]: [
+    { name: "=", verboseName: t`Is` },
+    { name: "!=", verboseName: t`Is not` },
+    { name: "IS_NULL", verboseName: t`Is empty` },
+    { name: "NOT_NULL", verboseName: t`Not empty` },
+  ],
+  [DATE_TIME]: [
+    { name: "=", verboseName: t`Is` },
+    { name: "<", verboseName: t`Before` },
+    { name: ">", verboseName: t`After` },
+    { name: "BETWEEN", verboseName: t`Between` },
+    { name: "IS_NULL", verboseName: t`Is empty` },
+    { name: "NOT_NULL", verboseName: t`Not empty` },
+  ],
+  [LOCATION]: [
+    { name: "=", verboseName: t`Is` },
+    { name: "!=", verboseName: t`Is not` },
+    { name: "IS_NULL", verboseName: t`Is empty` },
+    { name: "NOT_NULL", verboseName: t`Not empty` },
+  ],
+  [COORDINATE]: [
+    { name: "=", verboseName: t`Is` },
+    { name: "!=", verboseName: t`Is not` },
+    { name: "INSIDE", verboseName: t`Inside` },
+  ],
+  [BOOLEAN]: [
+    { name: "=", verboseName: t`Is`, multi: false, defaults: [true] },
+    { name: "IS_NULL", verboseName: t`Is empty` },
+    { name: "NOT_NULL", verboseName: t`Not empty` },
+  ],
+  [FOREIGN_KEY]: DEFAULT_OPERATORS,
+  [UNKNOWN]: DEFAULT_OPERATORS,
 };
 
 const MORE_VERBOSE_NAMES = {
-    "equal": "is equal to",
-    "not equal": "is not equal to",
-    "before": "is before",
-    "after": "is after",
-    "not empty": "is not empty",
-    "less than": "is less than",
-    "greater than": "is greater than",
-    "less than or equal to": "is less than or equal to",
-    "greater than or equal to": "is greater than or equal to"
-}
+  equal: "is equal to",
+  "not equal": "is not equal to",
+  before: "is before",
+  after: "is after",
+  "not empty": "is not empty",
+  "less than": "is less than",
+  "greater than": "is greater than",
+  "less than or equal to": "is less than or equal to",
+  "greater than or equal to": "is greater than or equal to",
+};
 
 export function getOperators(field, table) {
-    const type = getFieldType(field) || UNKNOWN;
-    return OPERATORS_BY_TYPE_ORDERED[type].map(operatorForType => {
-        const operator = OPERATORS[operatorForType.name];
-        const verboseNameLower = operatorForType.verboseName.toLowerCase();
-        return {
-            ...operator,
-            ...operatorForType,
-            moreVerboseName: MORE_VERBOSE_NAMES[verboseNameLower] || verboseNameLower,
-            fields: operator.validArgumentsFilters.map(validArgumentsFilter => validArgumentsFilter(field, table))
-        };
-    });
+  const type = getFieldType(field) || UNKNOWN;
+  return OPERATORS_BY_TYPE_ORDERED[type].map(operatorForType => {
+    const operator = OPERATORS[operatorForType.name];
+    const verboseNameLower = operatorForType.verboseName.toLowerCase();
+    return {
+      ...operator,
+      ...operatorForType,
+      moreVerboseName: MORE_VERBOSE_NAMES[verboseNameLower] || verboseNameLower,
+      fields: operator.validArgumentsFilters.map(validArgumentsFilter =>
+        validArgumentsFilter(field, table),
+      ),
+    };
+  });
 }
 
 // Breakouts and Aggregation options
 function allFields(fields) {
-    return fields;
+  return fields;
 }
 
 function summableFields(fields) {
-    return _.filter(fields, isSummable);
+  return _.filter(fields, isSummable);
 }
 
 function dimensionFields(fields) {
-    return _.filter(fields, isDimension);
+  return _.filter(fields, isDimension);
 }
 
-var Aggregators = [{
+var Aggregators = [
+  {
     name: t`Raw data`,
     short: "rows",
     description: t`Just a table with the rows in the answer, no additional operations.`,
     validFieldsFilters: [],
     requiresField: false,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Count of rows`,
     short: "count",
     description: t`Total number of rows in the answer.`,
     validFieldsFilters: [],
     requiresField: false,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Sum of ...`,
     short: "sum",
     description: t`Sum of all the values of a column.`,
     validFieldsFilters: [summableFields],
     requiresField: true,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Average of ...`,
     short: "avg",
     description: t`Average of all the values of a column`,
     validFieldsFilters: [summableFields],
     requiresField: true,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Number of distinct values of ...`,
     short: "distinct",
-    description:  t`Number of unique values of a column among all the rows in the answer.`,
+    description: t`Number of unique values of a column among all the rows in the answer.`,
     validFieldsFilters: [allFields],
     requiresField: true,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Cumulative sum of ...`,
     short: "cum_sum",
     description: t`Additive sum of all the values of a column.\ne.x. total revenue over time.`,
     validFieldsFilters: [summableFields],
     requiresField: true,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Cumulative count of rows`,
     short: "cum_count",
     description: t`Additive count of the number of rows.\ne.x. total number of sales over time.`,
     validFieldsFilters: [],
     requiresField: false,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Standard deviation of ...`,
     short: "stddev",
     description: t`Number which expresses how much the values of a column vary among all rows in the answer.`,
     validFieldsFilters: [summableFields],
     requiresField: true,
-    requiredDriverFeature: "standard-deviation-aggregations"
-}, {
+    requiredDriverFeature: "standard-deviation-aggregations",
+  },
+  {
     name: t`Minimum of ...`,
     short: "min",
     description: t`Minimum value of a column`,
     validFieldsFilters: [summableFields],
     requiresField: true,
-    requiredDriverFeature: "basic-aggregations"
-}, {
+    requiredDriverFeature: "basic-aggregations",
+  },
+  {
     name: t`Maximum of ...`,
     short: "max",
     description: t`Maximum value of a column`,
     validFieldsFilters: [summableFields],
     requiresField: true,
-    requiredDriverFeature: "basic-aggregations"
-}];
+    requiredDriverFeature: "basic-aggregations",
+  },
+];
 
 var BreakoutAggregator = {
-    name: t`Break out by dimension`,
-    short: "breakout",
-    validFieldsFilters: [dimensionFields]
+  name: t`Break out by dimension`,
+  short: "breakout",
+  validFieldsFilters: [dimensionFields],
 };
 
 function populateFields(aggregator, fields) {
-    return {
-        name: aggregator.name,
-        short: aggregator.short,
-        description: aggregator.description || '',
-        advanced: aggregator.advanced || false,
-        fields: _.map(aggregator.validFieldsFilters, function(validFieldsFilterFn) {
-            return validFieldsFilterFn(fields);
-        }),
-        validFieldsFilters: aggregator.validFieldsFilters,
-        requiresField: aggregator.requiresField,
-        requiredDriverFeature: aggregator.requiredDriverFeature
-    };
+  return {
+    name: aggregator.name,
+    short: aggregator.short,
+    description: aggregator.description || "",
+    advanced: aggregator.advanced || false,
+    fields: _.map(aggregator.validFieldsFilters, function(validFieldsFilterFn) {
+      return validFieldsFilterFn(fields);
+    }),
+    validFieldsFilters: aggregator.validFieldsFilters,
+    requiresField: aggregator.requiresField,
+    requiredDriverFeature: aggregator.requiredDriverFeature,
+  };
 }
 
 // TODO: unit test
 export function getAggregators(table) {
-    const supportedAggregations = Aggregators.filter(function (agg) {
-        return !(agg.requiredDriverFeature && table.db && !_.contains(table.db.features, agg.requiredDriverFeature));
-    });
-    return _.map(supportedAggregations, function(aggregator) {
-        return populateFields(aggregator, table.fields);
-    });
+  const supportedAggregations = Aggregators.filter(function(agg) {
+    return !(
+      agg.requiredDriverFeature &&
+      table.db &&
+      !_.contains(table.db.features, agg.requiredDriverFeature)
+    );
+  });
+  return _.map(supportedAggregations, function(aggregator) {
+    return populateFields(aggregator, table.fields);
+  });
 }
 
 export function getAggregatorsWithFields(table) {
-    return getAggregators(table).filter(aggregation =>
-        !aggregation.requiresField || aggregation.fields.reduce((ok, fields) => ok && fields.length > 0, true)
-    );
+  return getAggregators(table).filter(
+    aggregation =>
+      !aggregation.requiresField ||
+      aggregation.fields.reduce((ok, fields) => ok && fields.length > 0, true),
+  );
 }
 
 // TODO: unit test
 export function getAggregator(short) {
-    return _.findWhere(Aggregators, { short: short });
+  return _.findWhere(Aggregators, { short: short });
 }
 
 export const isCompatibleAggregatorForField = (aggregator, field) =>
-    aggregator.validFieldsFilters.every(filter => filter([field]).length === 1)
+  aggregator.validFieldsFilters.every(filter => filter([field]).length === 1);
 
 export function getBreakouts(fields) {
-    var result = populateFields(BreakoutAggregator, fields);
-    result.fields = result.fields[0];
-    result.validFieldsFilter = result.validFieldsFilters[0];
-    return result;
+  var result = populateFields(BreakoutAggregator, fields);
+  result.fields = result.fields[0];
+  result.validFieldsFilter = result.validFieldsFilters[0];
+  return result;
 }
 
 export function addValidOperatorsToFields(table) {
-    for (let field of table.fields) {
-        field.operators = getOperators(field, table);
-    }
-    table.aggregation_options = getAggregatorsWithFields(table);
-    table.breakout_options = getBreakouts(table.fields);
-    return table;
+  for (let field of table.fields) {
+    field.operators = getOperators(field, table);
+  }
+  table.aggregation_options = getAggregatorsWithFields(table);
+  table.breakout_options = getBreakouts(table.fields);
+  return table;
 }
 
 export function hasLatitudeAndLongitudeColumns(columnDefs) {
-    let hasLatitude = false;
-    let hasLongitude = false;
-    for (const col of columnDefs) {
-        if (isa(col.special_type, TYPE.Latitude)) {
-            hasLatitude = true;
-        }
-        if (isa(col.special_type, TYPE.Longitude)) {
-            hasLongitude = true;
-        }
+  let hasLatitude = false;
+  let hasLongitude = false;
+  for (const col of columnDefs) {
+    if (isa(col.special_type, TYPE.Latitude)) {
+      hasLatitude = true;
+    }
+    if (isa(col.special_type, TYPE.Longitude)) {
+      hasLongitude = true;
     }
-    return hasLatitude && hasLongitude;
+  }
+  return hasLatitude && hasLongitude;
 }
 
 export function foreignKeyCountsByOriginTable(fks) {
-    if (fks === null || !Array.isArray(fks)) {
-        return null;
-    }
-
-    return fks.map(function(fk) {
-        return ('origin' in fk) ? fk.origin.table.id : null;
-    }).reduce(function(prev, curr, idx, array) {
-        if (curr in prev) {
-            prev[curr]++;
-        } else {
-            prev[curr] = 1;
-        }
-
-        return prev;
+  if (fks === null || !Array.isArray(fks)) {
+    return null;
+  }
+
+  return fks
+    .map(function(fk) {
+      return "origin" in fk ? fk.origin.table.id : null;
+    })
+    .reduce(function(prev, curr, idx, array) {
+      if (curr in prev) {
+        prev[curr]++;
+      } else {
+        prev[curr] = 1;
+      }
+
+      return prev;
     }, {});
 }
 
 export const ICON_MAPPING = {
-    [DATE_TIME]:  'calendar',
-    [LOCATION]: 'location',
-    [COORDINATE]: 'location',
-    [STRING]: 'string',
-    [STRING_LIKE]: 'string',
-    [NUMBER]: 'int',
-    [BOOLEAN]: 'io'
+  [DATE_TIME]: "calendar",
+  [LOCATION]: "location",
+  [COORDINATE]: "location",
+  [STRING]: "string",
+  [STRING_LIKE]: "string",
+  [NUMBER]: "int",
+  [BOOLEAN]: "io",
+  [FOREIGN_KEY]: "connections",
 };
 
 export function getIconForField(field) {
-    return ICON_MAPPING[getFieldType(field)];
+  return ICON_MAPPING[getFieldType(field)] || "unknown";
 }
 
 export function computeMetadataStrength(table) {
-    var total = 0;
-    var completed = 0;
-    function score(value) {
-        total++;
-        if (value) { completed++; }
-    }
-
-    score(table.description);
-    if (table.fields) {
-        table.fields.forEach(function(field) {
-            score(field.description);
-            score(field.special_type);
-            if (isFK(field)) {
-                score(field.target);
-            }
-        });
+  var total = 0;
+  var completed = 0;
+  function score(value) {
+    total++;
+    if (value) {
+      completed++;
     }
+  }
+
+  score(table.description);
+  if (table.fields) {
+    table.fields.forEach(function(field) {
+      score(field.description);
+      score(field.special_type);
+      if (isFK(field)) {
+        score(field.target);
+      }
+    });
+  }
 
-    return (completed / total);
+  return completed / total;
 }
diff --git a/frontend/src/metabase/lib/settings.js b/frontend/src/metabase/lib/settings.js
index 33be3d5e0e0235c35b33ecb1adc34d2088e54cef..32d974ecd84589cfda2d01e2056c72148680a2fc 100644
--- a/frontend/src/metabase/lib/settings.js
+++ b/frontend/src/metabase/lib/settings.js
@@ -1,6 +1,6 @@
 import _ from "underscore";
-import inflection from 'inflection';
-import { t } from 'c-3po';
+import inflection from "inflection";
+import { t } from "c-3po";
 import MetabaseUtils from "metabase/lib/utils";
 
 const mb_settings = _.clone(window.MetabaseBootstrap);
@@ -9,99 +9,120 @@ const settingListeners = {};
 
 // provides access to Metabase application settings
 const MetabaseSettings = {
-
-    get: function(propName, defaultValue = null) {
-        return mb_settings[propName] !== undefined ? mb_settings[propName] : defaultValue;
-    },
-
-    set: function(key, value) {
-        if (mb_settings[key] !== value) {
-            mb_settings[key] = value;
-            if (settingListeners[key]) {
-                for (const listener of settingListeners[key]) {
-                    setTimeout(() => listener(value));
-                }
-            }
-        }
-    },
-
-    setAll: function(settings) {
-        for (const key in settings) {
-            MetabaseSettings.set(key, settings[key]);
+  get: function(propName, defaultValue = null) {
+    return mb_settings[propName] !== undefined
+      ? mb_settings[propName]
+      : defaultValue;
+  },
+
+  set: function(key, value) {
+    if (mb_settings[key] !== value) {
+      mb_settings[key] = value;
+      if (settingListeners[key]) {
+        for (const listener of settingListeners[key]) {
+          setTimeout(() => listener(value));
         }
-    },
-
-    // these are all special accessors which provide a lookup of a property plus some additional help
-    adminEmail: function() {
-        return mb_settings.admin_email;
-    },
-
-    isEmailConfigured: function() {
-        return mb_settings.email_configured;
-    },
-
-    isTrackingEnabled: function() {
-        return mb_settings.anon_tracking_enabled || false;
-    },
-
-    hasSetupToken: function() {
-        return (mb_settings.setup_token !== undefined && mb_settings.setup_token !== null);
-    },
-
-    ssoEnabled: function() {
-        return mb_settings.google_auth_client_id != null;
-    },
-
-    ldapEnabled: function() {
-        return mb_settings.ldap_configured;
-    },
-
-    hideEmbedBranding: () => mb_settings.hide_embed_branding,
-
-    metastoreUrl: () => mb_settings.metastore_url,
-
-    newVersionAvailable: function(settings) {
-        let versionInfo = _.findWhere(settings, {key: "version-info"}),
-            currentVersion = MetabaseSettings.get("version").tag;
-
-        if (versionInfo) versionInfo = versionInfo.value;
-
-        return (versionInfo && MetabaseUtils.compareVersions(currentVersion, versionInfo.latest.version) < 0);
-    },
-
-    passwordComplexity: function(capitalize) {
-        const complexity = this.get('password_complexity');
-
-        const clauseDescription = function(clause) {
-            switch (clause) {
-                case "lower": return t`lower case letter`;
-                case "upper": return t`upper case letter`;
-                case "digit": return t`number`;
-                case "special": return t`special character`;
-            }
-        };
-
-        let description = (capitalize === false) ? t`must be` + " " + complexity.total + " " + t`characters long` : t`Must be` + " " + complexity.total + " " + t`characters long`,
-            clauses = [];
-
-        ["lower", "upper", "digit", "special"].forEach(function(clause) {
-            if (clause in complexity) {
-                let desc = (complexity[clause] > 1) ? inflection.pluralize(clauseDescription(clause)) : clauseDescription(clause);
-                clauses.push(MetabaseUtils.numberToWord(complexity[clause])+" "+desc);
-            }
-        });
-
-        if (clauses.length > 0) {
-            return description+" "+ t`and include` +" "+clauses.join(", ");
-        } else {
-            return description;
-        }
-    },
+      }
+    }
+  },
 
-    on: function(setting, callback) {
-        settingListeners[setting] = settingListeners[setting] || [];
-        settingListeners[setting].push(callback);
+  setAll: function(settings) {
+    for (const key in settings) {
+      MetabaseSettings.set(key, settings[key]);
     }
-}
+  },
+
+  // these are all special accessors which provide a lookup of a property plus some additional help
+  adminEmail: function() {
+    return mb_settings.admin_email;
+  },
+
+  isEmailConfigured: function() {
+    return mb_settings.email_configured;
+  },
+
+  isTrackingEnabled: function() {
+    return mb_settings.anon_tracking_enabled || false;
+  },
+
+  hasSetupToken: function() {
+    return (
+      mb_settings.setup_token !== undefined && mb_settings.setup_token !== null
+    );
+  },
+
+  ssoEnabled: function() {
+    return mb_settings.google_auth_client_id != null;
+  },
+
+  ldapEnabled: function() {
+    return mb_settings.ldap_configured;
+  },
+
+  hideEmbedBranding: () => mb_settings.hide_embed_branding,
+
+  metastoreUrl: () => mb_settings.metastore_url,
+
+  newVersionAvailable: function(settings) {
+    let versionInfo = _.findWhere(settings, { key: "version-info" }),
+      currentVersion = MetabaseSettings.get("version").tag;
+
+    if (versionInfo) versionInfo = versionInfo.value;
+
+    return (
+      versionInfo &&
+      MetabaseUtils.compareVersions(
+        currentVersion,
+        versionInfo.latest.version,
+      ) < 0
+    );
+  },
+
+  passwordComplexity: function(capitalize) {
+    const complexity = this.get("password_complexity");
+
+    const clauseDescription = function(clause) {
+      switch (clause) {
+        case "lower":
+          return t`lower case letter`;
+        case "upper":
+          return t`upper case letter`;
+        case "digit":
+          return t`number`;
+        case "special":
+          return t`special character`;
+      }
+    };
+
+    let description =
+        capitalize === false
+          ? t`must be` + " " + complexity.total + " " + t`characters long`
+          : t`Must be` + " " + complexity.total + " " + t`characters long`,
+      clauses = [];
+
+    ["lower", "upper", "digit", "special"].forEach(function(clause) {
+      if (clause in complexity) {
+        let desc =
+          complexity[clause] > 1
+            ? inflection.pluralize(clauseDescription(clause))
+            : clauseDescription(clause);
+        clauses.push(
+          MetabaseUtils.numberToWord(complexity[clause]) + " " + desc,
+        );
+      }
+    });
+
+    if (clauses.length > 0) {
+      return description + " " + t`and include` + " " + clauses.join(", ");
+    } else {
+      return description;
+    }
+  },
+
+  on: function(setting, callback) {
+    settingListeners[setting] = settingListeners[setting] || [];
+    settingListeners[setting].push(callback);
+  },
+};
 
 export default MetabaseSettings;
diff --git a/frontend/src/metabase/lib/string.js b/frontend/src/metabase/lib/string.js
index 5fc78bed7c0d03ef9b8ef507ffb250accfc4c7cc..8fb1e7a7af791f04675c0a026a5b0a78b015f3d8 100644
--- a/frontend/src/metabase/lib/string.js
+++ b/frontend/src/metabase/lib/string.js
@@ -2,17 +2,22 @@ import _ from "underscore";
 
 // Creates a regex that will find an order dependent, case insensitive substring. All whitespace will be rendered as ".*" in the regex, to create a fuzzy search.
 export function createMultiwordSearchRegex(input) {
-    if (input) {
-        return new RegExp(
-            _.map(input.split(/\s+/), (word) => {
-                return RegExp.escape(word);
-            }).join(".*"),
-            "i");
-    }
+  if (input) {
+    return new RegExp(
+      _.map(input.split(/\s+/), word => {
+        return RegExp.escape(word);
+      }).join(".*"),
+      "i",
+    );
+  }
 }
 
-export const countLines = (str) => str.split(/\n/g).length
+export const countLines = str => str.split(/\n/g).length;
 
 export function caseInsensitiveSearch(haystack, needle) {
-    return !needle || (haystack != null && haystack.toLowerCase().indexOf(needle.toLowerCase()) >= 0);
+  return (
+    !needle ||
+    (haystack != null &&
+      haystack.toLowerCase().indexOf(needle.toLowerCase()) >= 0)
+  );
 }
diff --git a/frontend/src/metabase/lib/table.js b/frontend/src/metabase/lib/table.js
index b88b680136c8061a0728d76b8f60c31ab530de33..cb6b69a2753983e326a82de5dbc3a4e272eb9ecf 100644
--- a/frontend/src/metabase/lib/table.js
+++ b/frontend/src/metabase/lib/table.js
@@ -5,76 +5,80 @@ import _ from "underscore";
 import { MetabaseApi } from "metabase/services";
 
 export function isQueryable(table) {
-    return table.visibility_type == null;
+  return table.visibility_type == null;
 }
 
 export async function loadTableAndForeignKeys(tableId) {
-    let [table, foreignKeys] = await Promise.all([
-        MetabaseApi.table_query_metadata({ tableId }),
-        MetabaseApi.table_fks({ tableId })
-    ]);
+  let [table, foreignKeys] = await Promise.all([
+    MetabaseApi.table_query_metadata({ tableId }),
+    MetabaseApi.table_fks({ tableId }),
+  ]);
 
-    await augmentTable(table);
+  await augmentTable(table);
 
-    return {
-        table,
-        foreignKeys
-    };
+  return {
+    table,
+    foreignKeys,
+  };
 }
 
 export async function augmentTable(table) {
-    table = populateQueryOptions(table);
-    table = await loadForeignKeyTables(table);
-    return table;
+  table = populateQueryOptions(table);
+  table = await loadForeignKeyTables(table);
+  return table;
 }
 
 export function augmentDatabase(database) {
-    database.tables_lookup = createLookupByProperty(database.tables, "id");
-    for (let table of database.tables) {
-        addValidOperatorsToFields(table);
-        table.fields_lookup = createLookupByProperty(table.fields, "id");
-        for (let field of table.fields) {
-            addFkTargets(field, database.tables_lookup);
-            field.operators_lookup = createLookupByProperty(field.operators, "name");
-        }
+  database.tables_lookup = createLookupByProperty(database.tables, "id");
+  for (let table of database.tables) {
+    addValidOperatorsToFields(table);
+    table.fields_lookup = createLookupByProperty(table.fields, "id");
+    for (let field of table.fields) {
+      addFkTargets(field, database.tables_lookup);
+      field.operators_lookup = createLookupByProperty(field.operators, "name");
     }
-    return database;
+  }
+  return database;
 }
 
 async function loadForeignKeyTables(table) {
-    // Load joinable tables
-    await Promise.all(table.fields.filter((f) => f.target != null).map(async (field) => {
-        let targetTable = await MetabaseApi.table_query_metadata({ tableId: field.target.table_id });
-        field.target.table = populateQueryOptions(targetTable);
-    }));
-    return table;
+  // Load joinable tables
+  await Promise.all(
+    table.fields.filter(f => f.target != null).map(async field => {
+      let targetTable = await MetabaseApi.table_query_metadata({
+        tableId: field.target.table_id,
+      });
+      field.target.table = populateQueryOptions(targetTable);
+    }),
+  );
+  return table;
 }
 
 function populateQueryOptions(table) {
-    table = addValidOperatorsToFields(table);
+  table = addValidOperatorsToFields(table);
 
-    table.fields_lookup = {};
+  table.fields_lookup = {};
 
-    _.each(table.fields, function(field) {
-        table.fields_lookup[field.id] = field;
-        field.operators_lookup = {};
-        _.each(field.operators, function(operator) {
-            field.operators_lookup[operator.name] = operator;
-        });
+  _.each(table.fields, function(field) {
+    table.fields_lookup[field.id] = field;
+    field.operators_lookup = {};
+    _.each(field.operators, function(operator) {
+      field.operators_lookup[operator.name] = operator;
     });
+  });
 
-    return table;
+  return table;
 }
 
 function addFkTargets(field, tables) {
-    if (field.target != null) {
-        field.target.table = tables[field.target.table_id];
-    }
+  if (field.target != null) {
+    field.target.table = tables[field.target.table_id];
+  }
 }
 
 export function createLookupByProperty(items, property) {
-    return items.reduce((lookup, item) => {
-        lookup[item[property]] = item;
-        return lookup;
-    }, {});
+  return items.reduce((lookup, item) => {
+    lookup[item[property]] = item;
+    return lookup;
+  }, {});
 }
diff --git a/frontend/src/metabase/lib/time.js b/frontend/src/metabase/lib/time.js
index aa17af36250e370b18ba0ce517a15560d73d884b..b46c97a56fd7a178aaffe72ca34a6037005720b3 100644
--- a/frontend/src/metabase/lib/time.js
+++ b/frontend/src/metabase/lib/time.js
@@ -3,24 +3,32 @@ import moment from "moment";
 // only attempt to parse the timezone if we're sure we have one (either Z or ±hh:mm or +-hhmm)
 // moment normally interprets the DD in YYYY-MM-DD as an offset :-/
 export function parseTimestamp(value, unit) {
-    if (moment.isMoment(value)) {
-        return value;
-    } else if (typeof value === "string" && /(Z|[+-]\d\d:?\d\d)$/.test(value)) {
-        return moment.parseZone(value);
-    } else if (unit === "year") {
-        // workaround for https://github.com/metabase/metabase/issues/1992
-        return moment().year(value).startOf("year");
-    } else {
-        return moment.utc(value);
-    }
+  if (moment.isMoment(value)) {
+    return value;
+  } else if (typeof value === "string" && /(Z|[+-]\d\d:?\d\d)$/.test(value)) {
+    return moment.parseZone(value);
+  } else if (unit === "year") {
+    // workaround for https://github.com/metabase/metabase/issues/1992
+    return moment()
+      .year(value)
+      .startOf("year");
+  } else {
+    return moment.utc(value);
+  }
 }
 
 export function parseTime(value) {
-    if (moment.isMoment(value)) {
-        return value;
-    } else if (typeof value === "string"){
-        return moment(value, ["HH:mm:SS.sssZZ", "HH:mm:SS.sss", "HH:mm:SS.sss", "HH:mm:SS", "HH:mm"])
-    } else {
-        return moment.utc(value);
-    }
+  if (moment.isMoment(value)) {
+    return value;
+  } else if (typeof value === "string") {
+    return moment(value, [
+      "HH:mm:SS.sssZZ",
+      "HH:mm:SS.sss",
+      "HH:mm:SS.sss",
+      "HH:mm:SS",
+      "HH:mm",
+    ]);
+  } else {
+    return moment.utc(value);
+  }
 }
diff --git a/frontend/src/metabase/lib/types.js b/frontend/src/metabase/lib/types.js
index 8e0a83bbe37ab231216c5c658a247608608c2ea6..bc4c134c60228337529091054f36539112ffa4d6 100644
--- a/frontend/src/metabase/lib/types.js
+++ b/frontend/src/metabase/lib/types.js
@@ -9,40 +9,39 @@ const PARENTS = MetabaseSettings.get("types");
 /// isa(TYPE.BigInteger, TYPE.Number) -> true
 /// isa(TYPE.Text, TYPE.Boolean) -> false
 export function isa(child, ancestor) {
-    if (!child || !ancestor) return false;
+  if (!child || !ancestor) return false;
 
-    if (child === ancestor) return true;
+  if (child === ancestor) return true;
 
-    const parents = PARENTS[child];
-    if (!parents) {
-        if (child !== "type/*") console.error("Invalid type:", child); // the base type is the only type with no parents, so anything else that gets here is invalid
-        return false;
-    }
+  const parents = PARENTS[child];
+  if (!parents) {
+    if (child !== "type/*") console.error("Invalid type:", child); // the base type is the only type with no parents, so anything else that gets here is invalid
+    return false;
+  }
 
-    for (const parent of parents) {
-        if (isa(parent, ancestor)) return true;
-    }
+  for (const parent of parents) {
+    if (isa(parent, ancestor)) return true;
+  }
 
-    return false;
+  return false;
 }
 
 // build a pretty sweet dictionary of top-level types, so people can do TYPE.Latitude instead of "type/Latitude" and get error messages / etc.
 // this should also make it easier to keep track of things when we tweak the type hierarchy
 export let TYPE = {};
 for (const type of _.keys(PARENTS)) {
-    const key = type.substring(5); // strip off "type/"
-    TYPE[key] = type;
+  const key = type.substring(5); // strip off "type/"
+  TYPE[key] = type;
 }
 
-
 // convenience functions since these operations are super-common
 // this will also make it easier to tweak how these checks work in the future,
 // e.g. when we add an `is_pk` column and eliminate the PK special type we can just look for places that use isPK
 
 export function isPK(type) {
-    return isa(type, TYPE.PK);
+  return isa(type, TYPE.PK);
 }
 
 export function isFK(type) {
-    return isa(type, TYPE.FK);
+  return isa(type, TYPE.FK);
 }
diff --git a/frontend/src/metabase/lib/urls.js b/frontend/src/metabase/lib/urls.js
index dac95ac097c8694da2a79f5779b71d9b2e9de906..a3d7095645467dbd621ec4aa8525414e6001f18f 100644
--- a/frontend/src/metabase/lib/urls.js
+++ b/frontend/src/metabase/lib/urls.js
@@ -1,95 +1,98 @@
 import { serializeCardForUrl } from "metabase/lib/card";
-import MetabaseSettings from "metabase/lib/settings"
+import MetabaseSettings from "metabase/lib/settings";
 import Question from "metabase-lib/lib/Question";
 
 // provides functions for building urls to things we care about
 
 export const newQuestion = () => "/question/new";
 export function question(cardId, hash = "", query = "") {
-    if (hash && typeof hash === "object") {
-        hash = serializeCardForUrl(hash);
-    }
-    if (query && typeof query === "object") {
-        query = Object.entries(query)
-            .map(kv => {
-                if (Array.isArray(kv[1])) {
-                    return kv[1].map(v => `${encodeURIComponent(kv[0])}=${encodeURIComponent(v)}`).join('&');
-                } else {
-                    return kv.map(encodeURIComponent).join("=");
-                }
-            }).join("&");
-    }
-    if (hash && hash.charAt(0) !== "#") {
-        hash = "#" + hash;
-    }
-    if (query && query.charAt(0) !== "?") {
-        query = "?" + query;
-    }
-    // NOTE that this is for an ephemeral card link, not an editable card
-    return cardId != null
-        ? `/question/${cardId}${query}${hash}`
-        : `/question${query}${hash}`;
+  if (hash && typeof hash === "object") {
+    hash = serializeCardForUrl(hash);
+  }
+  if (query && typeof query === "object") {
+    query = Object.entries(query)
+      .map(kv => {
+        if (Array.isArray(kv[1])) {
+          return kv[1]
+            .map(v => `${encodeURIComponent(kv[0])}=${encodeURIComponent(v)}`)
+            .join("&");
+        } else {
+          return kv.map(encodeURIComponent).join("=");
+        }
+      })
+      .join("&");
+  }
+  if (hash && hash.charAt(0) !== "#") {
+    hash = "#" + hash;
+  }
+  if (query && query.charAt(0) !== "?") {
+    query = "?" + query;
+  }
+  // NOTE that this is for an ephemeral card link, not an editable card
+  return cardId != null
+    ? `/question/${cardId}${query}${hash}`
+    : `/question${query}${hash}`;
 }
 
 export function plainQuestion() {
-    return Question.create({ metadata: null }).getUrl();
+  return Question.create({ metadata: null }).getUrl();
 }
 
-export function dashboard(dashboardId, {addCardWithId} = {}) {
-    return addCardWithId != null
-        ? `/dashboard/${dashboardId}#add=${addCardWithId}`
-        : `/dashboard/${dashboardId}`;
+export function dashboard(dashboardId, { addCardWithId } = {}) {
+  return addCardWithId != null
+    ? `/dashboard/${dashboardId}#add=${addCardWithId}`
+    : `/dashboard/${dashboardId}`;
 }
 
 export function modelToUrl(model, modelId) {
-    switch (model) {
-        case "card":
-            return question(modelId);
-        case "dashboard":
-            return dashboard(modelId);
-        case "pulse":
-            return pulse(modelId);
-        default:
-            return null;
-    }
+  switch (model) {
+    case "card":
+      return question(modelId);
+    case "dashboard":
+      return dashboard(modelId);
+    case "pulse":
+      return pulse(modelId);
+    default:
+      return null;
+  }
 }
 
 export function pulse(pulseId) {
-    return `/pulse/#${pulseId}`;
+  return `/pulse/#${pulseId}`;
 }
 
 export function tableRowsQuery(databaseId, tableId, metricId, segmentId) {
-    let query = `?db=${databaseId}&table=${tableId}`;
+  let query = `?db=${databaseId}&table=${tableId}`;
 
-    if (metricId) {
-        query += `&metric=${metricId}`;
-    }
+  if (metricId) {
+    query += `&metric=${metricId}`;
+  }
 
-    if (segmentId) {
-        query += `&segment=${segmentId}`;
-    }
+  if (segmentId) {
+    query += `&segment=${segmentId}`;
+  }
 
-    return question(null, query);
+  return question(null, query);
 }
 
 export function collection(collection) {
-    return `/questions/collections/${collection.slug}`;
+  return `/questions/collections/${collection.slug}`;
 }
 
 export function label(label) {
-    return `/questions/search?label=${encodeURIComponent(label.slug)}`;
+  return `/questions/search?label=${encodeURIComponent(label.slug)}`;
 }
 
 export function publicCard(uuid, type = null) {
-    const siteUrl = MetabaseSettings.get("site_url");
-    return `${siteUrl}/public/question/${uuid}` + (type ? `.${type}` : ``);
+  const siteUrl = MetabaseSettings.get("site_url");
+  return `${siteUrl}/public/question/${uuid}` + (type ? `.${type}` : ``);
 }
 
 export function publicDashboard(uuid) {
-    const siteUrl = MetabaseSettings.get("site_url");
-    return `${siteUrl}/public/dashboard/${uuid}`;
+  const siteUrl = MetabaseSettings.get("site_url");
+  return `${siteUrl}/public/dashboard/${uuid}`;
 }
 
 export function embedCard(token, type = null) {
-    return `/embed/question/${token}` + (type ? `.${type}` : ``);
+  return `/embed/question/${token}` + (type ? `.${type}` : ``);
 }
diff --git a/frontend/src/metabase/lib/utils.js b/frontend/src/metabase/lib/utils.js
index a1edc504e720997e335f1676099401e2b4b80216..c6ce64ed1459bc1c9c94a9ec5c8432486789fa04 100644
--- a/frontend/src/metabase/lib/utils.js
+++ b/frontend/src/metabase/lib/utils.js
@@ -1,124 +1,169 @@
 import generatePassword from "password-generator";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 function s4() {
-    return Math.floor((1 + Math.random()) * 0x10000)
-      .toString(16)
-      .substring(1);
+  return Math.floor((1 + Math.random()) * 0x10000)
+    .toString(16)
+    .substring(1);
 }
 
 // provides functions for building urls to things we care about
 var MetabaseUtils = {
-    generatePassword: function(length, complexity) {
-        const len = length || 14;
-
-        if (!complexity) {
-            return generatePassword(len, false);
-        } else {
-            let password = "";
-            let tries = 0;
-            while(!isStrongEnough(password) && tries < 100) {
-                password = generatePassword(len, false, /[\w\d\?\-]/);
-                tries++;
-            }
-            return password;
-        }
-
-        function isStrongEnough(password) {
-            var uc = password.match(/([A-Z])/g);
-            var lc = password.match(/([a-z])/g);
-            var di = password.match(/([\d])/g);
-            var sc = password.match(/([!@#\$%\^\&*\)\(+=._-{}])/g);
-
-            return (uc && uc.length >= (complexity.upper || 0) &&
-                    lc && lc.length >= (complexity.lower || 0) &&
-                    di && di.length >= (complexity.digit || 0) &&
-                    sc && sc.length >= (complexity.special || 0));
-        }
-    },
-
-    isEmpty: function(str) {
-        if (str != null) str = String(str); // make sure 'str' is actually a string
-        return (str == null || 0 === str.length || str.match(/^\s+$/) != null);
-    },
-
-    // pretty limited.  just does 0-9 for right now.
-    numberToWord: function(num) {
-        var names = [t`zero`,t`one`,t`two`,t`three`,t`four`,t`five`,t`six`,t`seven`,t`eight`,t`nine`];
-
-        if (num >= 0 && num <= 9) {
-            return names[num];
-        } else {
-            return ""+num;
-        }
-    },
-
-    uuid: function() {
-        return s4() + s4() + '-' + s4() + '-' + s4() + '-' + s4() + '-' + s4() + s4() + s4();
-    },
-
-    isUUID(uuid) {
-        return typeof uuid === "string" && /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(uuid);
-    },
-
-    isBase64(string) {
-        return typeof string === "string" && /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(string);
-    },
-
-    isJWT(string) {
-        return typeof string === "string" && /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(string);
-    },
-
-    validEmail: function(email) {
-        var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
-        return re.test(email);
-    },
-
-    equals: function(a, b) {
-        // FIXME: ugghhhhhhhhh
-        return JSON.stringify(a) === JSON.stringify(b);
-    },
-
-    copy: function(a) {
-        // FIXME: ugghhhhhhhhh
-        return JSON.parse(JSON.stringify(a));
-    },
-
-    // this should correctly compare all version formats Metabase uses, e.x.
-    // 0.0.9, 0.0.10-snapshot, 0.0.10-alpha1, 0.0.10-rc1, 0.0.10-rc2, 0.0.10-rc10
-    // 0.0.10, 0.1.0, 0.2.0, 0.10.0, 1.1.0
-    compareVersions: function(aVersion, bVersion) {
-        const SPECIAL_COMPONENTS = {
-            "snapshot": -4,
-            "alpha": -3,
-            "beta": -2,
-            "rc": -1,
-        };
-
-        const getComponents = (x) =>
-            // v1.2.3-BETA1
-            x.toLowerCase()
-            // v1.2.3-beta1
-            .replace(/^v/, "")
-            // 1.2.3-beta1
-            .split(/[.-]*([0-9]+)[.-]*/).filter(c => c)
-            // ["1", "2", "3", "beta", "1"]
-            .map(c => SPECIAL_COMPONENTS[c] || parseInt(c, 10));
-            // [1, 2, 3, -2, 1]
-
-        let aComponents = getComponents(aVersion);
-        let bComponents = getComponents(bVersion);
-        for (let i = 0; i < Math.max(aComponents.length, bComponents.length); i++) {
-            let a = aComponents[i];
-            let b = bComponents[i];
-            if (b == undefined || a < b) {
-                return -1;
-            } else if (a == undefined || b < a) {
-                return 1;
-            }
-        }
-        return 0;
+  generatePassword: function(length, complexity) {
+    const len = length || 14;
+
+    if (!complexity) {
+      return generatePassword(len, false);
+    } else {
+      let password = "";
+      let tries = 0;
+      while (!isStrongEnough(password) && tries < 100) {
+        password = generatePassword(len, false, /[\w\d\?\-]/);
+        tries++;
+      }
+      return password;
     }
-}
+
+    function isStrongEnough(password) {
+      var uc = password.match(/([A-Z])/g);
+      var lc = password.match(/([a-z])/g);
+      var di = password.match(/([\d])/g);
+      var sc = password.match(/([!@#\$%\^\&*\)\(+=._-{}])/g);
+
+      return (
+        uc &&
+        uc.length >= (complexity.upper || 0) &&
+        lc &&
+        lc.length >= (complexity.lower || 0) &&
+        di &&
+        di.length >= (complexity.digit || 0) &&
+        sc &&
+        sc.length >= (complexity.special || 0)
+      );
+    }
+  },
+
+  isEmpty: function(str) {
+    if (str != null) str = String(str); // make sure 'str' is actually a string
+    return str == null || 0 === str.length || str.match(/^\s+$/) != null;
+  },
+
+  // pretty limited.  just does 0-9 for right now.
+  numberToWord: function(num) {
+    var names = [
+      t`zero`,
+      t`one`,
+      t`two`,
+      t`three`,
+      t`four`,
+      t`five`,
+      t`six`,
+      t`seven`,
+      t`eight`,
+      t`nine`,
+    ];
+
+    if (num >= 0 && num <= 9) {
+      return names[num];
+    } else {
+      return "" + num;
+    }
+  },
+
+  uuid: function() {
+    return (
+      s4() +
+      s4() +
+      "-" +
+      s4() +
+      "-" +
+      s4() +
+      "-" +
+      s4() +
+      "-" +
+      s4() +
+      s4() +
+      s4()
+    );
+  },
+
+  isUUID(uuid) {
+    return (
+      typeof uuid === "string" &&
+      /^[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}$/.test(
+        uuid,
+      )
+    );
+  },
+
+  isBase64(string) {
+    return (
+      typeof string === "string" &&
+      /^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(
+        string,
+      )
+    );
+  },
+
+  isJWT(string) {
+    return (
+      typeof string === "string" &&
+      /^[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+\.[A-Za-z0-9_-]+$/.test(string)
+    );
+  },
+
+  validEmail: function(email) {
+    var re = /^(([^<>()[\]\\.,;:\s@\"]+(\.[^<>()[\]\\.,;:\s@\"]+)*)|(\".+\"))@((\[[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\])|(([a-zA-Z\-0-9]+\.)+[a-zA-Z]{2,}))$/;
+    return re.test(email);
+  },
+
+  equals: function(a, b) {
+    // FIXME: ugghhhhhhhhh
+    return JSON.stringify(a) === JSON.stringify(b);
+  },
+
+  copy: function(a) {
+    // FIXME: ugghhhhhhhhh
+    return JSON.parse(JSON.stringify(a));
+  },
+
+  // this should correctly compare all version formats Metabase uses, e.x.
+  // 0.0.9, 0.0.10-snapshot, 0.0.10-alpha1, 0.0.10-rc1, 0.0.10-rc2, 0.0.10-rc10
+  // 0.0.10, 0.1.0, 0.2.0, 0.10.0, 1.1.0
+  compareVersions: function(aVersion, bVersion) {
+    const SPECIAL_COMPONENTS = {
+      snapshot: -4,
+      alpha: -3,
+      beta: -2,
+      rc: -1,
+    };
+
+    const getComponents = x =>
+      // v1.2.3-BETA1
+      x
+        .toLowerCase()
+        // v1.2.3-beta1
+        .replace(/^v/, "")
+        // 1.2.3-beta1
+        .split(/[.-]*([0-9]+)[.-]*/)
+        .filter(c => c)
+        // ["1", "2", "3", "beta", "1"]
+        .map(c => SPECIAL_COMPONENTS[c] || parseInt(c, 10));
+    // [1, 2, 3, -2, 1]
+
+    let aComponents = getComponents(aVersion);
+    let bComponents = getComponents(bVersion);
+    for (let i = 0; i < Math.max(aComponents.length, bComponents.length); i++) {
+      let a = aComponents[i];
+      let b = bComponents[i];
+      if (b == undefined || a < b) {
+        return -1;
+      } else if (a == undefined || b < a) {
+        return 1;
+      }
+    }
+    return 0;
+  },
+};
 
 export default MetabaseUtils;
diff --git a/frontend/src/metabase/meta/Card.js b/frontend/src/metabase/meta/Card.js
index 3d73996f46bc47c99fce3d573156704a1e4d11d0..ed2c484b1a6717c033e73573faeed3140356c968 100644
--- a/frontend/src/metabase/meta/Card.js
+++ b/frontend/src/metabase/meta/Card.js
@@ -1,6 +1,10 @@
 /* @flow */
 
-import { getTemplateTagParameters, getParameterTargetFieldId, parameterToMBQLFilter } from "metabase/meta/Parameter";
+import {
+  getTemplateTagParameters,
+  getParameterTargetFieldId,
+  parameterToMBQLFilter,
+} from "metabase/meta/Parameter";
 
 import * as Query from "metabase/lib/query/query";
 import Q from "metabase/lib/query"; // legacy
@@ -10,203 +14,248 @@ import * as Urls from "metabase/lib/urls";
 import _ from "underscore";
 import { assoc, updateIn } from "icepick";
 
-import type { StructuredQuery, NativeQuery, TemplateTag } from "metabase/meta/types/Query";
-import type { Card, DatasetQuery, StructuredDatasetQuery, NativeDatasetQuery } from "metabase/meta/types/Card";
-import type { Parameter, ParameterMapping, ParameterValues } from "metabase/meta/types/Parameter";
+import type {
+  StructuredQuery,
+  NativeQuery,
+  TemplateTag,
+} from "metabase/meta/types/Query";
+import type {
+  Card,
+  DatasetQuery,
+  StructuredDatasetQuery,
+  NativeDatasetQuery,
+} from "metabase/meta/types/Card";
+import type {
+  Parameter,
+  ParameterMapping,
+  ParameterValues,
+} from "metabase/meta/types/Parameter";
 import type { Metadata, TableMetadata } from "metabase/meta/types/Metadata";
 
 declare class Object {
-    static values<T>(object: { [key:string]: T }): Array<T>;
+  static values<T>(object: { [key: string]: T }): Array<T>;
 }
 
 // TODO Atte Keinänen 6/5/17 Should these be moved to corresponding metabase-lib classes?
 // Is there any reason behind keeping them in a central place?
 
 export const STRUCTURED_QUERY_TEMPLATE: StructuredDatasetQuery = {
-    type: "query",
-    database: null,
-    query: {
-        source_table: null,
-        aggregation: undefined,
-        breakout: undefined,
-        filter: undefined
-    }
+  type: "query",
+  database: null,
+  query: {
+    source_table: null,
+    aggregation: undefined,
+    breakout: undefined,
+    filter: undefined,
+  },
 };
 
 export const NATIVE_QUERY_TEMPLATE: NativeDatasetQuery = {
-    type: "native",
-    database: null,
-    native: {
-        query: "",
-        template_tags: {}
-    }
+  type: "native",
+  database: null,
+  native: {
+    query: "",
+    template_tags: {},
+  },
 };
 
-export function isStructured(card: Card): bool {
-    return card.dataset_query.type === "query";
+export function isStructured(card: Card): boolean {
+  return card.dataset_query.type === "query";
 }
 
-export function isNative(card: Card): bool {
-    return card.dataset_query.type === "native";
+export function isNative(card: Card): boolean {
+  return card.dataset_query.type === "native";
 }
 
-export function canRun(card: Card): bool {
-    if (card.dataset_query.type === "query") {
-        const query = getQuery(card);
-        return query != null && query.source_table != undefined && Query.hasValidAggregation(query);
-    } else if (card.dataset_query.type === "native") {
-        const native : NativeQuery = card.dataset_query.native;
-        return native && card.dataset_query.database != undefined && native.query !== "";
-    } else {
-        return false;
-    }
+export function canRun(card: Card): boolean {
+  if (card.dataset_query.type === "query") {
+    const query = getQuery(card);
+    return (
+      query != null &&
+      query.source_table != undefined &&
+      Query.hasValidAggregation(query)
+    );
+  } else if (card.dataset_query.type === "native") {
+    const native: NativeQuery = card.dataset_query.native;
+    return (
+      native && card.dataset_query.database != undefined && native.query !== ""
+    );
+  } else {
+    return false;
+  }
 }
 
 export function cardIsEquivalent(cardA: Card, cardB: Card): boolean {
-    cardA = updateIn(cardA, ["dataset_query", "parameters"], parameters => parameters || []);
-    cardB = updateIn(cardB, ["dataset_query", "parameters"], parameters => parameters || []);
-    cardA = _.pick(cardA, "dataset_query", "display", "visualization_settings");
-    cardB = _.pick(cardB, "dataset_query", "display", "visualization_settings");
-    return _.isEqual(cardA, cardB);
+  cardA = updateIn(
+    cardA,
+    ["dataset_query", "parameters"],
+    parameters => parameters || [],
+  );
+  cardB = updateIn(
+    cardB,
+    ["dataset_query", "parameters"],
+    parameters => parameters || [],
+  );
+  cardA = _.pick(cardA, "dataset_query", "display", "visualization_settings");
+  cardB = _.pick(cardB, "dataset_query", "display", "visualization_settings");
+  return _.isEqual(cardA, cardB);
 }
 
 export function getQuery(card: Card): ?StructuredQuery {
-    if (card.dataset_query.type === "query") {
-        return card.dataset_query.query;
-    } else {
-        return null;
-    }
+  if (card.dataset_query.type === "query") {
+    return card.dataset_query.query;
+  } else {
+    return null;
+  }
 }
 
-export function getTableMetadata(card: Card, metadata: Metadata): ?TableMetadata {
-    const query = getQuery(card);
-    if (query && query.source_table != null) {
-        return metadata.tables[query.source_table] || null;
-    }
-    return null;
+export function getTableMetadata(
+  card: Card,
+  metadata: Metadata,
+): ?TableMetadata {
+  const query = getQuery(card);
+  if (query && query.source_table != null) {
+    return metadata.tables[query.source_table] || null;
+  }
+  return null;
 }
 
 export function getTemplateTags(card: ?Card): Array<TemplateTag> {
-    return card && card.dataset_query && card.dataset_query.type === "native" && card.dataset_query.native.template_tags ?
-        Object.values(card.dataset_query.native.template_tags) :
-        [];
+  return card &&
+    card.dataset_query &&
+    card.dataset_query.type === "native" &&
+    card.dataset_query.native.template_tags
+    ? Object.values(card.dataset_query.native.template_tags)
+    : [];
 }
 
 export function getParameters(card: ?Card): Parameter[] {
-    if (card && card.parameters) {
-        return card.parameters;
-    }
+  if (card && card.parameters) {
+    return card.parameters;
+  }
 
-    const tags: TemplateTag[] = getTemplateTags(card);
-    return getTemplateTagParameters(tags);
+  const tags: TemplateTag[] = getTemplateTags(card);
+  return getTemplateTagParameters(tags);
 }
 
-export function getParametersWithExtras(card: Card, parameterValues?: ParameterValues): Parameter[] {
-    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 getParametersWithExtras(
+  card: Card,
+  parameterValues?: ParameterValues,
+): Parameter[] {
+  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;
+  });
 }
 
 // NOTE Atte Keinänen 7/5/17: Still used in dashboards and public questions.
 // Query builder uses `Question.getResults` which contains similar logic.
 export function applyParameters(
-    card: Card,
-    parameters: Parameter[],
-    parameterValues: ParameterValues = {},
-    parameterMappings: ParameterMapping[] = []
+  card: Card,
+  parameters: Parameter[],
+  parameterValues: ParameterValues = {},
+  parameterMappings: ParameterMapping[] = [],
 ): DatasetQuery {
-    const datasetQuery = Utils.copy(card.dataset_query);
-    // clean the query
-    if (datasetQuery.type === "query") {
-        datasetQuery.query = Q.cleanQuery(datasetQuery.query);
+  const datasetQuery = Utils.copy(card.dataset_query);
+  // clean the query
+  if (datasetQuery.type === "query") {
+    datasetQuery.query = Q.cleanQuery(datasetQuery.query);
+  }
+  datasetQuery.parameters = [];
+  for (const parameter of parameters || []) {
+    let value = parameterValues[parameter.id];
+    if (value == null) {
+      continue;
     }
-    datasetQuery.parameters = [];
-    for (const parameter of parameters || []) {
-        let value = parameterValues[parameter.id];
-        if (value == null) {
-            continue;
-        }
-
-        const mapping = _.findWhere(parameterMappings, {
-            card_id: card.id || card.original_card_id,
-            parameter_id: parameter.id
-        });
-        if (mapping) {
-            // mapped target, e.x. on a dashboard
-            datasetQuery.parameters.push({
-                type: parameter.type,
-                target: mapping.target,
-                value: value
-            });
-        } else if (parameter.target) {
-            // inline target, e.x. on a card
-            datasetQuery.parameters.push({
-                type: parameter.type,
-                target: parameter.target,
-                value: value
-            });
-        }
+
+    const mapping = _.findWhere(parameterMappings, {
+      card_id: card.id || card.original_card_id,
+      parameter_id: parameter.id,
+    });
+    if (mapping) {
+      // mapped target, e.x. on a dashboard
+      datasetQuery.parameters.push({
+        type: parameter.type,
+        target: mapping.target,
+        value: value,
+      });
+    } else if (parameter.target) {
+      // inline target, e.x. on a card
+      datasetQuery.parameters.push({
+        type: parameter.type,
+        target: parameter.target,
+        value: value,
+      });
     }
+  }
 
-    return datasetQuery;
+  return datasetQuery;
 }
 
 /** returns a question URL with parameters added to query string or MBQL filters */
 export function questionUrlWithParameters(
-    card: Card,
-    metadata: Metadata,
-    parameters: Parameter[],
-    parameterValues: ParameterValues = {},
-    parameterMappings: ParameterMapping[] = [],
-    cardIsDirty: boolean = true
+  card: Card,
+  metadata: Metadata,
+  parameters: Parameter[],
+  parameterValues: ParameterValues = {},
+  parameterMappings: ParameterMapping[] = [],
+  cardIsDirty: boolean = true,
 ): DatasetQuery {
-    if (!card.dataset_query) {
-        return Urls.question(card.id);
-    }
+  if (!card.dataset_query) {
+    return Urls.question(card.id);
+  }
 
-    card = Utils.copy(card);
+  card = Utils.copy(card);
 
-    const cardParameters = getParameters(card);
-    const datasetQuery = applyParameters(
-        card,
-        parameters,
-        parameterValues,
-        parameterMappings
-    );
+  const cardParameters = getParameters(card);
+  const datasetQuery = applyParameters(
+    card,
+    parameters,
+    parameterValues,
+    parameterMappings,
+  );
 
-    // If we have a clean question without parameters applied, don't add the dataset query hash
-    if (!cardIsDirty && datasetQuery.parameters && datasetQuery.parameters.length === 0) {
-        return Urls.question(card.id);
-    }
+  // If we have a clean question without parameters applied, don't add the dataset query hash
+  if (
+    !cardIsDirty &&
+    datasetQuery.parameters &&
+    datasetQuery.parameters.length === 0
+  ) {
+    return Urls.question(card.id);
+  }
 
-    const query = {};
-    for (const datasetParameter of datasetQuery.parameters || []) {
-        const cardParameter = _.find(cardParameters, p =>
-            Utils.equals(p.target, datasetParameter.target));
-        if (cardParameter) {
-            // if the card has a real parameter we can use, use that
-            query[cardParameter.slug] = datasetParameter.value;
-        } else if (isStructured(card)) {
-            // if the card is structured, try converting the parameter to an MBQL filter clause
-            const filter = parameterToMBQLFilter(datasetParameter, metadata);
-            if (filter) {
-                card = updateIn(card, ["dataset_query", "query"], query =>
-                    Query.addFilter(query, filter));
-            } else {
-                console.warn("UNHANDLED PARAMETER", datasetParameter);
-            }
-        } else {
-            console.warn("UNHANDLED PARAMETER", datasetParameter);
-        }
+  const query = {};
+  for (const datasetParameter of datasetQuery.parameters || []) {
+    const cardParameter = _.find(cardParameters, p =>
+      Utils.equals(p.target, datasetParameter.target),
+    );
+    if (cardParameter) {
+      // if the card has a real parameter we can use, use that
+      query[cardParameter.slug] = datasetParameter.value;
+    } else if (isStructured(card)) {
+      // if the card is structured, try converting the parameter to an MBQL filter clause
+      const filter = parameterToMBQLFilter(datasetParameter, metadata);
+      if (filter) {
+        card = updateIn(card, ["dataset_query", "query"], query =>
+          Query.addFilter(query, filter),
+        );
+      } else {
+        console.warn("UNHANDLED PARAMETER", datasetParameter);
+      }
+    } else {
+      console.warn("UNHANDLED PARAMETER", datasetParameter);
     }
-    return Urls.question(null, card.dataset_query ? card : undefined, query);
+  }
+  return Urls.question(null, card.dataset_query ? card : undefined, query);
 }
diff --git a/frontend/src/metabase/meta/Dashboard.js b/frontend/src/metabase/meta/Dashboard.js
index 7ae10d3f73bbc9f439dd080138e90102318e90f7..fe301310e7b0e406e146fcd2563ccc717ab8c7f8 100644
--- a/frontend/src/metabase/meta/Dashboard.js
+++ b/frontend/src/metabase/meta/Dashboard.js
@@ -7,8 +7,15 @@ import Field from "metabase-lib/lib/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, DimensionTarget, VariableTarget } from "./types/Parameter";
-import { t } from 'c-3po';
+import type {
+  ParameterOption,
+  Parameter,
+  ParameterType,
+  ParameterMappingUIOption,
+  DimensionTarget,
+  VariableTarget,
+} from "./types/Parameter";
+import { t } from "c-3po";
 import { getTemplateTags } from "./Card";
 
 import { slugify, stripId } from "metabase/lib/formatting";
@@ -19,297 +26,377 @@ import { mbqlEq } from "metabase/lib/query/util";
 import _ from "underscore";
 
 export const PARAMETER_OPTIONS: Array<ParameterOption> = [
-    {
-        type: "date/month-year",
-        name: t`Month and Year`,
-        description: t`Like January, 2016`
-    },
-    {
-        type: "date/quarter-year",
-        name: t`Quarter and Year`,
-        description: t`Like Q1, 2016`
-    },
-    {
-        type: "date/single",
-        name: t`Single Date`,
-        description: t`Like January 31, 2016`
-    },
-    {
-        type: "date/range",
-        name: t`Date Range`,
-        description: t`Like December 25, 2015 - February 14, 2016`
-    },
-    {
-        type: "date/relative",
-        name: t`Relative Date`,
-        description: t`Like "the last 7 days" or "this month"`
-    },
-    {
-        type: "date/all-options",
-        name: t`Date Filter`,
-        menuName: t`All Options`,
-        description: t`Contains all of the above`
-    },
-    {
-        type: "location/city",
-        name: t`City`
-    },
-    {
-        type: "location/state",
-        name: t`State`
-    },
-    {
-        type: "location/zip_code",
-        name: t`ZIP or Postal Code`
-    },
-    {
-        type: "location/country",
-        name: t`Country`
-    },
-    {
-        type: "id",
-        name: t`ID`
-    },
-    {
-        type: "category",
-        name: t`Category`
-    },
+  {
+    type: "date/month-year",
+    name: t`Month and Year`,
+    description: t`Like January, 2016`,
+  },
+  {
+    type: "date/quarter-year",
+    name: t`Quarter and Year`,
+    description: t`Like Q1, 2016`,
+  },
+  {
+    type: "date/single",
+    name: t`Single Date`,
+    description: t`Like January 31, 2016`,
+  },
+  {
+    type: "date/range",
+    name: t`Date Range`,
+    description: t`Like December 25, 2015 - February 14, 2016`,
+  },
+  {
+    type: "date/relative",
+    name: t`Relative Date`,
+    description: t`Like "the last 7 days" or "this month"`,
+  },
+  {
+    type: "date/all-options",
+    name: t`Date Filter`,
+    menuName: t`All Options`,
+    description: t`Contains all of the above`,
+  },
+  {
+    type: "location/city",
+    name: t`City`,
+  },
+  {
+    type: "location/state",
+    name: t`State`,
+  },
+  {
+    type: "location/zip_code",
+    name: t`ZIP or Postal Code`,
+  },
+  {
+    type: "location/country",
+    name: t`Country`,
+  },
+  {
+    type: "id",
+    name: t`ID`,
+  },
+  {
+    type: "category",
+    name: t`Category`,
+  },
 ];
 
 export type ParameterSection = {
-    id: string,
-    name: string,
-    description: string,
-    options: Array<ParameterOption>
+  id: string,
+  name: string,
+  description: string,
+  options: Array<ParameterOption>,
 };
 
 export const PARAMETER_SECTIONS: Array<ParameterSection> = [
-    { id: "date",     name: t`Time`,             description: t`Date range, relative date, time of day, etc.`, options: [] },
-    { id: "location", name: t`Location`,         description: t`City, State, Country, ZIP code.`, options: [] },
-    { id: "id",       name: t`ID`,               description: t`User ID, product ID, event ID, etc.`, options: [] },
-    { id: "category", name: t`Other Categories`, description: t`Category, Type, Model, Rating, etc.`, options: [] },
+  {
+    id: "date",
+    name: t`Time`,
+    description: t`Date range, relative date, time of day, etc.`,
+    options: [],
+  },
+  {
+    id: "location",
+    name: t`Location`,
+    description: t`City, State, Country, ZIP code.`,
+    options: [],
+  },
+  {
+    id: "id",
+    name: t`ID`,
+    description: t`User ID, product ID, event ID, etc.`,
+    options: [],
+  },
+  {
+    id: "category",
+    name: t`Other Categories`,
+    description: t`Category, Type, Model, Rating, etc.`,
+    options: [],
+  },
 ];
 
 for (const option of PARAMETER_OPTIONS) {
-    let sectionId = option.type.split("/")[0];
-    let section = _.findWhere(PARAMETER_SECTIONS, { id: sectionId });
-    if (!section) {
-        section = _.findWhere(PARAMETER_SECTIONS, { id: "category" });
-    }
-    if (section) {
-        section.options = section.options || [];
-        section.options.push(option);
-    }
+  let sectionId = option.type.split("/")[0];
+  let section = _.findWhere(PARAMETER_SECTIONS, { id: sectionId });
+  if (!section) {
+    section = _.findWhere(PARAMETER_SECTIONS, { id: "category" });
+  }
+  if (section) {
+    section.options = section.options || [];
+    section.options.push(option);
+  }
 }
 
 type Dimension = {
-    name: string,
-    parentName: string,
-    target: DimensionTarget,
-    field_id: number,
-    depth: number
+  name: string,
+  parentName: string,
+  target: DimensionTarget,
+  field_id: number,
+  depth: number,
 };
 
 type Variable = {
-    name: string,
-    target: VariableTarget,
-    type: string
+  name: string,
+  target: VariableTarget,
+  type: string,
 };
 
 type FieldFilter = (field: Field) => boolean;
 type TemplateTagFilter = (tag: TemplateTag) => boolean;
 
 export function getFieldDimension(field: Field): Dimension {
-    return {
-        name: field.display_name,
-        field_id: field.id,
-        parentName: field.table.display_name,
-        target: ["field-id", field.id],
-        depth: 0
-    };
+  return {
+    name: field.display_name,
+    field_id: field.id,
+    parentName: field.table.display_name,
+    target: ["field-id", field.id],
+    depth: 0,
+  };
 }
 
-export function getTagDimension(tag: TemplateTag, dimension: Dimension): Dimension {
-    return {
-        name: dimension.name,
-        parentName: dimension.parentName,
-        target: ["template-tag", tag.name],
-        field_id: dimension.field_id,
-        depth: 0
-    }
+export function getTagDimension(
+  tag: TemplateTag,
+  dimension: Dimension,
+): Dimension {
+  return {
+    name: dimension.name,
+    parentName: dimension.parentName,
+    target: ["template-tag", tag.name],
+    field_id: dimension.field_id,
+    depth: 0,
+  };
 }
 
-export function getCardDimensions(metadata: Metadata, card: Card, filter: FieldFilter = () => true): Array<Dimension> {
-    if (card.dataset_query.type === "query") {
-        const table = card.dataset_query.query.source_table != null ? metadata.tables[card.dataset_query.query.source_table] : null;
-        if (table) {
-            return getTableDimensions(table, 1, filter);
-        }
-    } else if (card.dataset_query.type === "native") {
-        let dimensions = [];
-        for (const tag of getTemplateTags(card)) {
-            if (tag.type === "dimension" && Array.isArray(tag.dimension) && mbqlEq(tag.dimension[0], "field-id")) {
-                const field = metadata.fields[tag.dimension[1]];
-                if (field && filter(field)) {
-                    let fieldDimension = getFieldDimension(field);
-                    dimensions.push(getTagDimension(tag, fieldDimension));
-                }
-            }
+export function getCardDimensions(
+  metadata: Metadata,
+  card: Card,
+  filter: FieldFilter = () => true,
+): Array<Dimension> {
+  if (card.dataset_query.type === "query") {
+    const table =
+      card.dataset_query.query.source_table != null
+        ? metadata.tables[card.dataset_query.query.source_table]
+        : null;
+    if (table) {
+      return getTableDimensions(table, 1, filter);
+    }
+  } else if (card.dataset_query.type === "native") {
+    let dimensions = [];
+    for (const tag of getTemplateTags(card)) {
+      if (
+        tag.type === "dimension" &&
+        Array.isArray(tag.dimension) &&
+        mbqlEq(tag.dimension[0], "field-id")
+      ) {
+        const field = metadata.fields[tag.dimension[1]];
+        if (field && filter(field)) {
+          let fieldDimension = getFieldDimension(field);
+          dimensions.push(getTagDimension(tag, fieldDimension));
         }
-        return dimensions;
+      }
     }
-    return [];
+    return dimensions;
+  }
+  return [];
 }
 
 function getDimensionTargetFieldId(target: DimensionTarget): ?FieldId {
-    if (Array.isArray(target) && mbqlEq(target[0], "template-tag")) {
-        return null;
-    } else {
-        return Query.getFieldTargetId(target);
-    }
+  if (Array.isArray(target) && mbqlEq(target[0], "template-tag")) {
+    return null;
+  } else {
+    return Query.getFieldTargetId(target);
+  }
 }
 
-export function getTableDimensions(table: Table, depth: number, filter: FieldFilter = () => true): Array<Dimension> {
-    return _.chain(table.fields)
-        .map(field => {
-            let targetField = field.target;
-            if (targetField && depth > 0) {
-                let targetTable = targetField.table;
-                return getTableDimensions(targetTable, depth - 1, filter).map((dimension: Dimension) => ({
-                    ...dimension,
-                    parentName: stripId(field.display_name),
-                    target: ["fk->", field.id, getDimensionTargetFieldId(dimension.target)],
-                    depth: dimension.depth + 1
-                }));
-            } else if (filter(field)) {
-                return [getFieldDimension(field)];
-            }
-        })
-        .flatten()
-        .filter(dimension => dimension != null)
-        .value();
+export function getTableDimensions(
+  table: Table,
+  depth: number,
+  filter: FieldFilter = () => true,
+): Array<Dimension> {
+  return _.chain(table.fields)
+    .map(field => {
+      let targetField = field.target;
+      if (targetField && depth > 0) {
+        let targetTable = targetField.table;
+        return getTableDimensions(targetTable, depth - 1, filter).map(
+          (dimension: Dimension) => ({
+            ...dimension,
+            parentName: stripId(field.display_name),
+            target: [
+              "fk->",
+              field.id,
+              getDimensionTargetFieldId(dimension.target),
+            ],
+            depth: dimension.depth + 1,
+          }),
+        );
+      } else if (filter(field)) {
+        return [getFieldDimension(field)];
+      }
+    })
+    .flatten()
+    .filter(dimension => dimension != null)
+    .value();
 }
 
-export function getCardVariables(metadata: Metadata, card: Card, filter: TemplateTagFilter = () => true): Array<Variable> {
-    if (card.dataset_query.type === "native") {
-        let variables = [];
-        for (const tag of getTemplateTags(card)) {
-            if (!filter || filter(tag)) {
-                variables.push({
-                    name: tag.display_name || tag.name,
-                    type: tag.type,
-                    target: ["template-tag", tag.name]
-                });
-            }
-        }
-        return variables;
+export function getCardVariables(
+  metadata: Metadata,
+  card: Card,
+  filter: TemplateTagFilter = () => true,
+): Array<Variable> {
+  if (card.dataset_query.type === "native") {
+    let variables = [];
+    for (const tag of getTemplateTags(card)) {
+      if (!filter || filter(tag)) {
+        variables.push({
+          name: tag.display_name || tag.name,
+          type: tag.type,
+          target: ["template-tag", tag.name],
+        });
+      }
     }
-    return [];
+    return variables;
+  }
+  return [];
 }
 
 function fieldFilterForParameter(parameter: Parameter) {
-    return fieldFilterForParameterType(parameter.type);
+  return fieldFilterForParameterType(parameter.type);
 }
 
-export function fieldFilterForParameterType(parameterType: ParameterType): FieldFilter {
-    const [type] = parameterType.split("/");
-    switch (type) {
-        case "date":        return (field: Field) => field.isDate();
-        case "id":          return (field: Field) => field.isID();
-        case "category":    return (field: Field) => field.isCategory();
-    }
+export function fieldFilterForParameterType(
+  parameterType: ParameterType,
+): FieldFilter {
+  const [type] = parameterType.split("/");
+  switch (type) {
+    case "date":
+      return (field: Field) => field.isDate();
+    case "id":
+      return (field: Field) => field.isID();
+    case "category":
+      return (field: Field) => field.isCategory();
+  }
 
-    switch (parameterType) {
-        case "location/city":     return (field: Field) => isa(field.special_type, TYPE.City);
-        case "location/state":    return (field: Field) => isa(field.special_type, TYPE.State);
-        case "location/zip_code": return (field: Field) => isa(field.special_type, TYPE.ZipCode);
-        case "location/country":  return (field: Field) => isa(field.special_type, TYPE.Country);
-    }
-    return (field: Field) => false;
+  switch (parameterType) {
+    case "location/city":
+      return (field: Field) => isa(field.special_type, TYPE.City);
+    case "location/state":
+      return (field: Field) => isa(field.special_type, TYPE.State);
+    case "location/zip_code":
+      return (field: Field) => isa(field.special_type, TYPE.ZipCode);
+    case "location/country":
+      return (field: Field) => isa(field.special_type, TYPE.Country);
+  }
+  return (field: Field) => false;
 }
 
 export function parameterOptionsForField(field: Field): ParameterOption[] {
-    return PARAMETER_OPTIONS.filter(option => fieldFilterForParameterType(option.type)(field));
+  return PARAMETER_OPTIONS.filter(option =>
+    fieldFilterForParameterType(option.type)(field),
+  );
 }
 
 function tagFilterForParameter(parameter: Parameter): TemplateTagFilter {
-    const [type, subtype] = parameter.type.split("/");
-    switch (type) {
-        case "date":        return (tag: TemplateTag) => subtype === "single" && tag.type === "date";
-        case "location":    return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
-        case "id":          return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
-        case "category":    return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
-    }
-    return (tag: TemplateTag) => false;
+  const [type, subtype] = parameter.type.split("/");
+  switch (type) {
+    case "date":
+      return (tag: TemplateTag) => subtype === "single" && tag.type === "date";
+    case "location":
+      return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
+    case "id":
+      return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
+    case "category":
+      return (tag: TemplateTag) => tag.type === "number" || tag.type === "text";
+  }
+  return (tag: TemplateTag) => false;
 }
 
 const VARIABLE_ICONS = {
-    "text": "string",
-    "number": "int",
-    "date": "calendar"
+  text: "string",
+  number: "int",
+  date: "calendar",
 };
 
-export function getParameterMappingOptions(metadata: Metadata, parameter: Parameter, card: Card): Array<ParameterMappingUIOption> {
-    let options = [];
+export function getParameterMappingOptions(
+  metadata: Metadata,
+  parameter: Parameter,
+  card: Card,
+): Array<ParameterMappingUIOption> {
+  let options = [];
 
-    // dimensions
-    options.push(
-        ...getCardDimensions(metadata, card, fieldFilterForParameter(parameter))
-            .map((dimension: Dimension) => {
-                const field = metadata.fields[dimension.field_id];
-                return {
-                    name: dimension.name,
-                    target: ["dimension", dimension.target],
-                    icon: field && field.icon(),
-                    sectionName: dimension.parentName,
-                    isFk: dimension.depth > 0
-                };
-            })
-    );
+  // dimensions
+  options.push(
+    ...getCardDimensions(
+      metadata,
+      card,
+      fieldFilterForParameter(parameter),
+    ).map((dimension: Dimension) => {
+      const field = metadata.fields[dimension.field_id];
+      return {
+        name: dimension.name,
+        target: ["dimension", dimension.target],
+        icon: field && field.icon(),
+        sectionName: dimension.parentName,
+        isFk: dimension.depth > 0,
+      };
+    }),
+  );
 
-    // variables
-    options.push(
-        ...getCardVariables(metadata, card, tagFilterForParameter(parameter))
-            .map((variable: Variable) => ({
-                name: variable.name,
-                target: ["variable", variable.target],
-                icon: VARIABLE_ICONS[variable.type],
-                sectionName: "Variables",
-                isVariable: true
-            }))
-    );
+  // variables
+  options.push(
+    ...getCardVariables(metadata, card, tagFilterForParameter(parameter)).map(
+      (variable: Variable) => ({
+        name: variable.name,
+        target: ["variable", variable.target],
+        icon: VARIABLE_ICONS[variable.type],
+        sectionName: "Variables",
+        isVariable: true,
+      }),
+    ),
+  );
 
-    return options;
+  return options;
 }
 
-export function createParameter(option: ParameterOption, parameters: Array<ParameterOption> = []): Parameter {
-    let name = option.name;
-    let nameIndex = 0;
-    // get a unique name
-    while (_.any(parameters, (p) => p.name === name)) {
-        name = option.name + " " + (++nameIndex);
-    }
-    let parameter = {
-       name: "",
-       slug: "",
-       id: Math.floor(Math.random()*Math.pow(2,32)).toString(16),
-       type: option.type,
-    };
-    return setParameterName(parameter, name);
+export function createParameter(
+  option: ParameterOption,
+  parameters: Array<ParameterOption> = [],
+): Parameter {
+  let name = option.name;
+  let nameIndex = 0;
+  // get a unique name
+  while (_.any(parameters, p => p.name === name)) {
+    name = option.name + " " + ++nameIndex;
+  }
+  let parameter = {
+    name: "",
+    slug: "",
+    id: Math.floor(Math.random() * Math.pow(2, 32)).toString(16),
+    type: option.type,
+  };
+  return setParameterName(parameter, name);
 }
 
-export function setParameterName(parameter: Parameter, name: string): Parameter {
-    let slug = slugify(name);
-    return {
-        ...parameter,
-        name: name,
-        slug: slug
-    };
+export function setParameterName(
+  parameter: Parameter,
+  name: string,
+): Parameter {
+  let slug = slugify(name);
+  return {
+    ...parameter,
+    name: name,
+    slug: slug,
+  };
 }
 
-export function setParameterDefaultValue(parameter: Parameter, value: string): Parameter {
-    return {
-        ...parameter,
-        default: value
-    };
+export function setParameterDefaultValue(
+  parameter: Parameter,
+  value: string,
+): Parameter {
+  return {
+    ...parameter,
+    default: value,
+  };
 }
diff --git a/frontend/src/metabase/meta/Parameter.js b/frontend/src/metabase/meta/Parameter.js
index 8cb8f020877ebdba6b514279167368190d81313d..cd9a1cf954210381b7d8eb5749796b198153fb18 100644
--- a/frontend/src/metabase/meta/Parameter.js
+++ b/frontend/src/metabase/meta/Parameter.js
@@ -1,8 +1,20 @@
 /* @flow */
 
 import type { DatasetQuery } from "metabase/meta/types/Card";
-import type { TemplateTag, LocalFieldReference, ForeignFieldReference, FieldFilter } from "metabase/meta/types/Query";
-import type { Parameter, ParameterInstance, ParameterTarget, ParameterValue, ParameterValueOrArray, ParameterValues } from "metabase/meta/types/Parameter";
+import type {
+  TemplateTag,
+  LocalFieldReference,
+  ForeignFieldReference,
+  FieldFilter,
+} from "metabase/meta/types/Query";
+import type {
+  Parameter,
+  ParameterInstance,
+  ParameterTarget,
+  ParameterValue,
+  ParameterValueOrArray,
+  ParameterValues,
+} from "metabase/meta/types/Parameter";
 import type { FieldId } from "metabase/meta/types/Field";
 import type { Metadata } from "metabase/meta/types/Metadata";
 
@@ -12,132 +24,196 @@ import Q from "metabase/lib/query";
 import { mbqlEq } from "metabase/lib/query/util";
 import { isNumericBaseType } from "metabase/lib/schema_metadata";
 
-
 // NOTE: this should mirror `template-tag-parameters` in src/metabase/api/embed.clj
 export function getTemplateTagParameters(tags: TemplateTag[]): Parameter[] {
-    return tags.filter(tag => tag.type != null && (tag.widget_type || tag.type !== "dimension"))
-        .map(tag => ({
-            id: tag.id,
-            type: tag.widget_type || (tag.type === "date" ? "date/single" : "category"),
-            target: tag.type === "dimension" ?
-                ["dimension", ["template-tag", tag.name]]:
-                ["variable", ["template-tag", tag.name]],
-            name: tag.display_name,
-            slug: tag.name,
-            default: tag.default
-        }))
+  return tags
+    .filter(
+      tag => tag.type != null && (tag.widget_type || tag.type !== "dimension"),
+    )
+    .map(tag => ({
+      id: tag.id,
+      type:
+        tag.widget_type || (tag.type === "date" ? "date/single" : "category"),
+      target:
+        tag.type === "dimension"
+          ? ["dimension", ["template-tag", tag.name]]
+          : ["variable", ["template-tag", tag.name]],
+      name: tag.display_name,
+      slug: tag.name,
+      default: tag.default,
+    }));
 }
 
-export const getParametersBySlug = (parameters: Parameter[], parameterValues: ParameterValues): {[key:string]: string} => {
-    let result = {};
-    for (const parameter of parameters) {
-        if (parameterValues[parameter.id] != undefined) {
-            result[parameter.slug] = parameterValues[parameter.id];
-        }
+export const getParametersBySlug = (
+  parameters: Parameter[],
+  parameterValues: ParameterValues,
+): { [key: string]: string } => {
+  let result = {};
+  for (const parameter of parameters) {
+    if (parameterValues[parameter.id] != undefined) {
+      result[parameter.slug] = parameterValues[parameter.id];
     }
-    return result;
-}
+  }
+  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);
+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;
+  }
+  return null;
 }
 
-type Deserializer = { testRegex: RegExp, deserialize: DeserializeFn}
-type DeserializeFn = (match: any[], fieldRef: LocalFieldReference | ForeignFieldReference) => FieldFilter;
+type Deserializer = { testRegex: RegExp, deserialize: DeserializeFn };
+type DeserializeFn = (
+  match: any[],
+  fieldRef: LocalFieldReference | ForeignFieldReference,
+) => FieldFilter;
 
 const timeParameterValueDeserializers: Deserializer[] = [
-    {testRegex: /^past([0-9]+)([a-z]+)s(~)?$/, deserialize: (matches, fieldRef) =>
-        // $FlowFixMe: not matching TimeIntervalFilter for some reason
-        ["time-interval", fieldRef, -parseInt(matches[0]), matches[1]].concat(matches[2] ? [{ "include-current": true }] : [])
-    },
-    {testRegex: /^next([0-9]+)([a-z]+)s(~)?$/, deserialize: (matches, fieldRef) =>
-        // $FlowFixMe: not matching TimeIntervalFilter for some reason
-        ["time-interval", fieldRef, parseInt(matches[0]), matches[1]].concat(matches[2] ? [{ "include-current": true }] : [])
-    },
-    {testRegex: /^this([a-z]+)$/, deserialize: (matches, fieldRef) =>
-        ["time-interval", fieldRef, "current", matches[0]]
-    },
-    {testRegex: /^~([0-9-T:]+)$/, deserialize: (matches, fieldRef) =>
-        ["<", fieldRef, matches[0]]
-    },
-    {testRegex: /^([0-9-T:]+)~$/, deserialize: (matches, fieldRef) =>
-        [">", fieldRef, matches[0]]
-    },
-    {testRegex: /^(\d{4}-\d{2})$/, deserialize: (matches, fieldRef) =>
-        ["=", ["datetime-field", fieldRef, "month"], moment(matches[0], "YYYY-MM").format("YYYY-MM-DD")]
-    },
-    {testRegex: /^(Q\d-\d{4})$/, deserialize: (matches, fieldRef) =>
-        ["=", ["datetime-field", fieldRef, "quarter"], moment(matches[0], "[Q]Q-YYYY").format("YYYY-MM-DD")]
-    },
-    {testRegex: /^([0-9-T:]+)$/, deserialize: (matches, fieldRef) =>
-        ["=", fieldRef, matches[0]]
-    },
-    // TODO 3/27/17 Atte Keinänen
-    // Unify BETWEEN -> between, IS_NULL -> is-null, NOT_NULL -> not-null throughout the codebase
-    {testRegex: /^([0-9-T:]+)~([0-9-T:]+)$/, deserialize: (matches, fieldRef) =>
-        // $FlowFixMe
-        ["BETWEEN", fieldRef, matches[0], matches[1]]
-    },
+  {
+    testRegex: /^past([0-9]+)([a-z]+)s(~)?$/,
+    deserialize: (matches, fieldRef) =>
+      // $FlowFixMe: not matching TimeIntervalFilter for some reason
+      ["time-interval", fieldRef, -parseInt(matches[0]), matches[1]].concat(
+        matches[2] ? [{ "include-current": true }] : [],
+      ),
+  },
+  {
+    testRegex: /^next([0-9]+)([a-z]+)s(~)?$/,
+    deserialize: (matches, fieldRef) =>
+      // $FlowFixMe: not matching TimeIntervalFilter for some reason
+      ["time-interval", fieldRef, parseInt(matches[0]), matches[1]].concat(
+        matches[2] ? [{ "include-current": true }] : [],
+      ),
+  },
+  {
+    testRegex: /^this([a-z]+)$/,
+    deserialize: (matches, fieldRef) => [
+      "time-interval",
+      fieldRef,
+      "current",
+      matches[0],
+    ],
+  },
+  {
+    testRegex: /^~([0-9-T:]+)$/,
+    deserialize: (matches, fieldRef) => ["<", fieldRef, matches[0]],
+  },
+  {
+    testRegex: /^([0-9-T:]+)~$/,
+    deserialize: (matches, fieldRef) => [">", fieldRef, matches[0]],
+  },
+  {
+    testRegex: /^(\d{4}-\d{2})$/,
+    deserialize: (matches, fieldRef) => [
+      "=",
+      ["datetime-field", fieldRef, "month"],
+      moment(matches[0], "YYYY-MM").format("YYYY-MM-DD"),
+    ],
+  },
+  {
+    testRegex: /^(Q\d-\d{4})$/,
+    deserialize: (matches, fieldRef) => [
+      "=",
+      ["datetime-field", fieldRef, "quarter"],
+      moment(matches[0], "[Q]Q-YYYY").format("YYYY-MM-DD"),
+    ],
+  },
+  {
+    testRegex: /^([0-9-T:]+)$/,
+    deserialize: (matches, fieldRef) => ["=", fieldRef, matches[0]],
+  },
+  // TODO 3/27/17 Atte Keinänen
+  // Unify BETWEEN -> between, IS_NULL -> is-null, NOT_NULL -> not-null throughout the codebase
+  {
+    testRegex: /^([0-9-T:]+)~([0-9-T:]+)$/,
+    deserialize: (matches, fieldRef) =>
+      // $FlowFixMe
+      ["BETWEEN", fieldRef, matches[0], matches[1]],
+  },
 ];
 
-export function dateParameterValueToMBQL(parameterValue: ParameterValue, fieldRef: LocalFieldReference|ForeignFieldReference): ?FieldFilter {
-    const deserializer: ?Deserializer =
-        timeParameterValueDeserializers.find((des) => des.testRegex.test(parameterValue));
-
-    if (deserializer) {
-        const substringMatches = deserializer.testRegex.exec(parameterValue).splice(1);
-        return deserializer.deserialize(substringMatches, fieldRef);
-    } else {
-        return null;
-    }
+export function dateParameterValueToMBQL(
+  parameterValue: ParameterValue,
+  fieldRef: LocalFieldReference | ForeignFieldReference,
+): ?FieldFilter {
+  const deserializer: ?Deserializer = timeParameterValueDeserializers.find(
+    des => des.testRegex.test(parameterValue),
+  );
+
+  if (deserializer) {
+    const substringMatches = deserializer.testRegex
+      .exec(parameterValue)
+      .splice(1);
+    return deserializer.deserialize(substringMatches, fieldRef);
+  } else {
+    return null;
+  }
 }
 
-export function stringParameterValueToMBQL(parameterValue: ParameterValueOrArray, fieldRef: LocalFieldReference|ForeignFieldReference): ?FieldFilter {
-    if (Array.isArray(parameterValue)) {
-        // $FlowFixMe: thinks we're returning a nested array which concat does not do
-        return ["=", fieldRef].concat(parameterValue);
-    } else {
-        return ["=", fieldRef, parameterValue];
-    }
+export function stringParameterValueToMBQL(
+  parameterValue: ParameterValueOrArray,
+  fieldRef: LocalFieldReference | ForeignFieldReference,
+): ?FieldFilter {
+  if (Array.isArray(parameterValue)) {
+    // $FlowFixMe: thinks we're returning a nested array which concat does not do
+    return ["=", fieldRef].concat(parameterValue);
+  } else {
+    return ["=", fieldRef, parameterValue];
+  }
 }
 
-export function numberParameterValueToMBQL(parameterValue: ParameterValue, fieldRef: LocalFieldReference|ForeignFieldReference): ?FieldFilter {
-    return ["=", fieldRef, parseFloat(parameterValue)];
+export function numberParameterValueToMBQL(
+  parameterValue: ParameterValue,
+  fieldRef: LocalFieldReference | ForeignFieldReference,
+): ?FieldFilter {
+  return ["=", fieldRef, parseFloat(parameterValue)];
 }
 
 /** compiles a parameter with value to an MBQL clause */
-export function parameterToMBQLFilter(parameter: ParameterInstance, metadata: Metadata): ?FieldFilter {
-    if (!parameter.target || parameter.target[0] !== "dimension" || !Array.isArray(parameter.target[1]) || parameter.target[1][0] === "template-tag") {
-        return null;
-    }
-
-    // $FlowFixMe: doesn't understand parameter.target[1] is a field reference
-    const fieldRef: LocalFieldReference|ForeignFieldReference = parameter.target[1]
-
-    if (parameter.type.indexOf("date/") === 0) {
-        return dateParameterValueToMBQL(parameter.value, fieldRef);
+export function parameterToMBQLFilter(
+  parameter: ParameterInstance,
+  metadata: Metadata,
+): ?FieldFilter {
+  if (
+    !parameter.target ||
+    parameter.target[0] !== "dimension" ||
+    !Array.isArray(parameter.target[1]) ||
+    parameter.target[1][0] === "template-tag"
+  ) {
+    return null;
+  }
+
+  // $FlowFixMe: doesn't understand parameter.target[1] is a field reference
+  const fieldRef: LocalFieldReference | ForeignFieldReference =
+    parameter.target[1];
+
+  if (parameter.type.indexOf("date/") === 0) {
+    return dateParameterValueToMBQL(parameter.value, fieldRef);
+  } else {
+    const fieldId = Q.getFieldTargetId(fieldRef);
+    const field = metadata.fields[fieldId];
+    // if the field is numeric, parse the value as a number
+    if (isNumericBaseType(field)) {
+      return numberParameterValueToMBQL(parameter.value, fieldRef);
     } else {
-        const fieldId = Q.getFieldTargetId(fieldRef);
-        const field = metadata.fields[fieldId];
-        // if the field is numeric, parse the value as a number
-        if (isNumericBaseType(field)) {
-            return numberParameterValueToMBQL(parameter.value, fieldRef);
-        } else {
-            return stringParameterValueToMBQL(parameter.value, fieldRef);
-        }
+      return stringParameterValueToMBQL(parameter.value, fieldRef);
     }
+  }
 }
diff --git a/frontend/src/metabase/meta/types/Card.js b/frontend/src/metabase/meta/types/Card.js
index 97fef51e191b924f23575f58e5a1a8d002e770f5..63040064e8a6158993ab31d47d23c965d2a3756e 100644
--- a/frontend/src/metabase/meta/types/Card.js
+++ b/frontend/src/metabase/meta/types/Card.js
@@ -8,44 +8,44 @@ import type { Parameter, ParameterInstance } from "./Parameter";
 export type CardId = number;
 
 export type VisualizationSettings = {
-    [key: string]: any
-}
+  [key: string]: any,
+};
 
 export type UnsavedCard = {
-    dataset_query: DatasetQuery,
-    display: string,
-    visualization_settings: VisualizationSettings,
-    parameters?: Array<Parameter>,
-    original_card_id?: CardId
-}
+  dataset_query: DatasetQuery,
+  display: string,
+  visualization_settings: VisualizationSettings,
+  parameters?: Array<Parameter>,
+  original_card_id?: CardId,
+};
 
 export type Card = {
-    id: CardId,
-    name: ?string,
-    description: ?string,
-    dataset_query: DatasetQuery,
-    display: string,
-    visualization_settings: VisualizationSettings,
-    parameters?: Array<Parameter>,
-    can_write: boolean,
-    public_uuid: string,
-
-    // Not part of the card API contract, a field used by query builder for showing lineage
-    original_card_id?: CardId,
+  id: CardId,
+  name: ?string,
+  description: ?string,
+  dataset_query: DatasetQuery,
+  display: string,
+  visualization_settings: VisualizationSettings,
+  parameters?: Array<Parameter>,
+  can_write: boolean,
+  public_uuid: string,
+
+  // Not part of the card API contract, a field used by query builder for showing lineage
+  original_card_id?: CardId,
 };
 
 export type StructuredDatasetQuery = {
-    type: "query",
-    database: ?DatabaseId,
-    query: StructuredQuery,
-    parameters?: Array<ParameterInstance>
+  type: "query",
+  database: ?DatabaseId,
+  query: StructuredQuery,
+  parameters?: Array<ParameterInstance>,
 };
 
 export type NativeDatasetQuery = {
-    type: "native",
-    database: ?DatabaseId,
-    native: NativeQuery,
-    parameters?: Array<ParameterInstance>
+  type: "native",
+  database: ?DatabaseId,
+  native: NativeQuery,
+  parameters?: Array<ParameterInstance>,
 };
 
 /**
diff --git a/frontend/src/metabase/meta/types/Collection.js b/frontend/src/metabase/meta/types/Collection.js
index 13095f04db1666fee39fcd8e7761753e73884ddb..129a4c25737e26e21f90e7905fb5fbd0845bc9fa 100644
--- a/frontend/src/metabase/meta/types/Collection.js
+++ b/frontend/src/metabase/meta/types/Collection.js
@@ -3,7 +3,7 @@
 export type CollectionId = number;
 
 export type Collection = {
-    id: CollectionId,
-    name: string,
-    color: string,
-}
+  id: CollectionId,
+  name: string,
+  color: string,
+};
diff --git a/frontend/src/metabase/meta/types/Dashboard.js b/frontend/src/metabase/meta/types/Dashboard.js
index f9a6927988e89c36278f7465dfc0ef3f70120e21..e48cd9358eacbdb649651ce3fe96ac8b49a2e769 100644
--- a/frontend/src/metabase/meta/types/Dashboard.js
+++ b/frontend/src/metabase/meta/types/Dashboard.js
@@ -6,47 +6,47 @@ import type { Parameter, ParameterMapping } from "./Parameter";
 export type DashboardId = number;
 
 export type Dashboard = {
-    id: DashboardId,
-    name: string,
-    favorite: boolean,
-    archived: boolean,
-    created_at: ?string,
-    creator_id: number,
-    description: ?string,
-    caveats?: string,
-    points_of_interest?: string,
-    show_in_getting_started?: boolean,
-    // incomplete
-    parameters: Array<Parameter>
-}
+  id: DashboardId,
+  name: string,
+  favorite: boolean,
+  archived: boolean,
+  created_at: ?string,
+  creator_id: number,
+  description: ?string,
+  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>,
+  id: DashboardId,
+  name: string,
+  description: ?string,
+  ordered_cards: Array<DashCard>,
+  // incomplete
+  parameters: Array<Parameter>,
 };
 
 export type DashCardId = number;
 
 export type DashCard = {
-    id: DashCardId,
+  id: DashCardId,
 
-    card_id: CardId,
-    dashboard_id: DashboardId,
+  card_id: CardId,
+  dashboard_id: DashboardId,
 
-    card: Card,
-    series: Array<Card>,
+  card: Card,
+  series: Array<Card>,
 
-    // incomplete
-    parameter_mappings: Array<ParameterMapping>,
-    visualization_settings: VisualizationSettings,
+  // incomplete
+  parameter_mappings: Array<ParameterMapping>,
+  visualization_settings: VisualizationSettings,
 
-    col: number,
-    row: number,
-    sizeY: number,
-    sizeX: number
+  col: number,
+  row: number,
+  sizeY: number,
+  sizeX: number,
 };
diff --git a/frontend/src/metabase/meta/types/Database.js b/frontend/src/metabase/meta/types/Database.js
index 16e9b5324999398a2ebf67f2f891396da8c80e42..ffbe3b8c37e49dbe2d922aea3d4aabe3a36387fa 100644
--- a/frontend/src/metabase/meta/types/Database.js
+++ b/frontend/src/metabase/meta/types/Database.js
@@ -1,4 +1,3 @@
-
 import type { ISO8601Time } from ".";
 import type { Table } from "./Table";
 
@@ -7,38 +6,38 @@ export type DatabaseId = number;
 export type DatabaseType = string; // "h2" | "postgres" | etc
 
 export type DatabaseFeature =
-    "basic-aggregations" |
-    "standard-deviation-aggregations"|
-    "expression-aggregations" |
-    "foreign-keys" |
-    "native-parameters" |
-    "expressions"
+  | "basic-aggregations"
+  | "standard-deviation-aggregations"
+  | "expression-aggregations"
+  | "foreign-keys"
+  | "native-parameters"
+  | "expressions";
 
 export type DatabaseDetails = {
-    [key: string]: any
-}
+  [key: string]: any,
+};
 
 export type DatabaseEngine = string;
 
 export type DatabaseNativePermission = "write" | "read";
 
 export type Database = {
-    id:                 DatabaseId,
-    name:               string,
-    description:        ?string,
+  id: DatabaseId,
+  name: string,
+  description: ?string,
 
-    tables:             Table[],
+  tables: Table[],
 
-    details:            DatabaseDetails,
-    engine:             DatabaseType,
-    features:           DatabaseFeature[],
-    is_full_sync:       boolean,
-    is_sample:          boolean,
-    native_permissions: DatabaseNativePermission,
+  details: DatabaseDetails,
+  engine: DatabaseType,
+  features: DatabaseFeature[],
+  is_full_sync: boolean,
+  is_sample: boolean,
+  native_permissions: DatabaseNativePermission,
 
-    caveats:            ?string,
-    points_of_interest: ?string,
+  caveats: ?string,
+  points_of_interest: ?string,
 
-    created_at:         ISO8601Time,
-    updated_at:         ISO8601Time,
+  created_at: ISO8601Time,
+  updated_at: ISO8601Time,
 };
diff --git a/frontend/src/metabase/meta/types/Dataset.js b/frontend/src/metabase/meta/types/Dataset.js
index 71637bdf852c860a2108a53ca957927ca339d3a9..06cdd97c7747ea329ac548066967a71287b571d5 100644
--- a/frontend/src/metabase/meta/types/Dataset.js
+++ b/frontend/src/metabase/meta/types/Dataset.js
@@ -8,31 +8,31 @@ import type { DatetimeUnit } from "./Query";
 export type ColumnName = string;
 
 export type BinningInfo = {
-    bin_width: number
-}
+  bin_width: number,
+};
 
 // TODO: incomplete
 export type Column = {
-    id: ?FieldId,
-    name: ColumnName,
-    display_name: string,
-    base_type: string,
-    special_type: ?string,
-    source?: "fields"|"aggregation"|"breakout",
-    unit?: DatetimeUnit,
-    binning_info?: BinningInfo
+  id: ?FieldId,
+  name: ColumnName,
+  display_name: string,
+  base_type: string,
+  special_type: ?string,
+  source?: "fields" | "aggregation" | "breakout",
+  unit?: DatetimeUnit,
+  binning_info?: BinningInfo,
 };
 
-export type Value = string|number|ISO8601Time|boolean|null|{};
+export type Value = string | number | ISO8601Time | boolean | null | {};
 export type Row = Value[];
 
 export type DatasetData = {
-    cols: Column[],
-    columns: ColumnName[],
-    rows: Row[]
+  cols: Column[],
+  columns: ColumnName[],
+  rows: Row[],
 };
 
 export type Dataset = {
-    data: DatasetData,
-    json_query: DatasetQuery
+  data: DatasetData,
+  json_query: DatasetQuery,
 };
diff --git a/frontend/src/metabase/meta/types/Field.js b/frontend/src/metabase/meta/types/Field.js
index 53b48f640f716852d39e830818eefa9a5d166b19..48e48f5046575645b53fc3d206252d24a3da3906 100644
--- a/frontend/src/metabase/meta/types/Field.js
+++ b/frontend/src/metabase/meta/types/Field.js
@@ -9,48 +9,54 @@ export type FieldId = number;
 export type BaseType = string;
 export type SpecialType = string;
 
-export type FieldVisibilityType = "details-only" | "hidden" | "normal" | "retired";
+export type FieldVisibilityType =
+  | "details-only"
+  | "hidden"
+  | "normal"
+  | "retired";
 
 export type Field = {
-    id:                 FieldId,
+  id: FieldId,
 
-    name:               string,
-    display_name:       string,
-    description:        string,
-    base_type:          BaseType,
-    special_type:       SpecialType,
-    active:             boolean,
-    visibility_type:    FieldVisibilityType,
-    preview_display:    boolean,
-    position:           number,
-    parent_id:          ?FieldId,
+  name: string,
+  display_name: string,
+  description: string,
+  base_type: BaseType,
+  special_type: SpecialType,
+  active: boolean,
+  visibility_type: FieldVisibilityType,
+  preview_display: boolean,
+  position: number,
+  parent_id: ?FieldId,
 
-    // raw_column_id:   number // unused?
+  // raw_column_id:   number // unused?
 
-    table_id:           TableId,
+  table_id: TableId,
 
-    fk_target_field_id: ?FieldId,
+  fk_target_field_id: ?FieldId,
 
-    max_value:          ?number,
-    min_value:          ?number,
+  max_value: ?number,
+  min_value: ?number,
 
-    caveats:            ?string,
-    points_of_interest: ?string,
+  caveats: ?string,
+  points_of_interest: ?string,
 
-    last_analyzed:      ISO8601Time,
-    created_at:         ISO8601Time,
-    updated_at:         ISO8601Time,
+  last_analyzed: ISO8601Time,
+  created_at: ISO8601Time,
+  updated_at: ISO8601Time,
 
-    values?:            FieldValues,
-    dimensions?:        FieldDimension
+  values?: FieldValues,
+  dimensions?: FieldDimension,
 };
 
 export type RawFieldValue = Value;
 export type HumanReadableFieldValue = string;
 
-export type FieldValue = [RawFieldValue] | [RawFieldValue, HumanReadableFieldValue];
+export type FieldValue =
+  | [RawFieldValue]
+  | [RawFieldValue, HumanReadableFieldValue];
 export type FieldValues = FieldValue[];
 
 export type FieldDimension = {
-    name: string
-}
+  name: string,
+};
diff --git a/frontend/src/metabase/meta/types/Metadata.js b/frontend/src/metabase/meta/types/Metadata.js
index c7be4a60645d04141065733b356b8a9fe9670bad..0abefb4802f20f47d31d9559fa8753937d0ac35f 100644
--- a/frontend/src/metabase/meta/types/Metadata.js
+++ b/frontend/src/metabase/meta/types/Metadata.js
@@ -9,102 +9,101 @@ import type { Segment, SegmentId } from "metabase/meta/types/Segment";
 import type { Metric, MetricId } from "metabase/meta/types/Metric";
 
 export type Metadata = {
-    databases: { [id: DatabaseId]: DatabaseMetadata },
-    tables:    { [id: TableId]:    TableMetadata },
-    fields:    { [id: FieldId]:    FieldMetadata },
-    metrics:   { [id: MetricId]:   MetricMetadata },
-    segments:  { [id: SegmentId]:  SegmentMetadata },
-}
+  databases: { [id: DatabaseId]: DatabaseMetadata },
+  tables: { [id: TableId]: TableMetadata },
+  fields: { [id: FieldId]: FieldMetadata },
+  metrics: { [id: MetricId]: MetricMetadata },
+  segments: { [id: SegmentId]: SegmentMetadata },
+};
 
 export type DatabaseMetadata = Database & {
-    tables:              TableMetadata[],
-    tables_lookup:       { [id: TableId]: TableMetadata },
-}
+  tables: TableMetadata[],
+  tables_lookup: { [id: TableId]: TableMetadata },
+};
 
 export type TableMetadata = Table & {
-    db:                  DatabaseMetadata,
+  db: DatabaseMetadata,
 
-    fields:              FieldMetadata[],
-    fields_lookup:       { [id: FieldId]: FieldMetadata },
+  fields: FieldMetadata[],
+  fields_lookup: { [id: FieldId]: FieldMetadata },
 
-    segments:            SegmentMetadata[],
-    metrics:             MetricMetadata[],
+  segments: SegmentMetadata[],
+  metrics: MetricMetadata[],
 
-    aggregation_options: AggregationOption[],
-    breakout_options:    BreakoutOption,
-}
+  aggregation_options: AggregationOption[],
+  breakout_options: BreakoutOption,
+};
 
 export type FieldMetadata = Field & {
-    table:              TableMetadata,
-    target:             FieldMetadata,
+  table: TableMetadata,
+  target: FieldMetadata,
 
-    operators:    Operator[],
-    operators_lookup:   { [key: OperatorName]: Operator }
-}
+  operators: Operator[],
+  operators_lookup: { [key: OperatorName]: Operator },
+};
 
 export type SegmentMetadata = Segment & {
-    table:              TableMetadata,
-}
+  table: TableMetadata,
+};
 
 export type MetricMetadata = Metric & {
-    table:              TableMetadata,
-}
+  table: TableMetadata,
+};
 
 export type FieldValue = {
-    name: string,
-    key: string
-}
+  name: string,
+  key: string,
+};
 
 export type OperatorName = string;
 
 export type Operator = {
-    name: OperatorName,
-    verboseName: string,
-    moreVerboseName: string,
-    fields: OperatorField[],
-    multi: bool,
-    advanced: bool,
-    placeholders?: string[],
-    validArgumentsFilters: ValidArgumentsFilter[],
-}
+  name: OperatorName,
+  verboseName: string,
+  moreVerboseName: string,
+  fields: OperatorField[],
+  multi: boolean,
+  placeholders?: string[],
+  validArgumentsFilters: ValidArgumentsFilter[],
+};
 
 export type OperatorField = {
-    type: string,
-    values: FieldValue[]
-}
+  type: string,
+  values: FieldValue[],
+};
 
-export type ValidArgumentsFilter = (field: Field, table: Table) => bool;
+export type ValidArgumentsFilter = (field: Field, table: Table) => boolean;
 
 export type AggregationOption = {
-    name: string,
-    short: string,
-    fields: Field[],
-    validFieldsFilter: (fields: Field[]) => Field[]
-}
+  name: string,
+  short: string,
+  fields: Field[],
+  validFieldsFilter: (fields: Field[]) => Field[],
+};
 
 export type BreakoutOption = {
-    name: string,
-    short: string,
-    fields: Field[],
-    validFieldsFilter: (fields: Field[]) => Field[]
-}
+  name: string,
+  short: string,
+  fields: Field[],
+  validFieldsFilter: (fields: Field[]) => Field[],
+};
 
 export type FieldOptions = {
-    count: number,
+  count: number,
+  fields: Field[],
+  fks: {
+    field: Field,
     fields: Field[],
-    fks: {
-        field: Field,
-        fields: Field[]
-    }
+  },
 };
 
 import Dimension from "metabase-lib/lib/Dimension";
 
 export type DimensionOptions = {
-    count: 0,
+  count: 0,
+  dimensions: Dimension[],
+  fks: Array<{
+    field: FieldMetadata,
     dimensions: Dimension[],
-    fks: Array<{
-        field: FieldMetadata,
-        dimensions: Dimension[]
-    }>
+  }>,
 };
diff --git a/frontend/src/metabase/meta/types/Metric.js b/frontend/src/metabase/meta/types/Metric.js
index ee5146bed28a0147b9082d7da995915085cbdd2a..d2d42bc964b1c09f6f7b97467f333e0acfd3984a 100644
--- a/frontend/src/metabase/meta/types/Metric.js
+++ b/frontend/src/metabase/meta/types/Metric.js
@@ -6,8 +6,8 @@ export type MetricId = number;
 
 // TODO: incomplete
 export type Metric = {
-    name: string,
-    id: MetricId,
-    table_id: TableId,
-    is_active: bool
+  name: string,
+  id: MetricId,
+  table_id: TableId,
+  is_active: boolean,
 };
diff --git a/frontend/src/metabase/meta/types/Parameter.js b/frontend/src/metabase/meta/types/Parameter.js
index 886516df730bf6b47adf0aa99cd8bca3824033df..6c38202311bd8e233898c3c0a4152f8240226795 100644
--- a/frontend/src/metabase/meta/types/Parameter.js
+++ b/frontend/src/metabase/meta/types/Parameter.js
@@ -13,52 +13,55 @@ export type ParameterValue = string;
 export type ParameterValueOrArray = string | Array<string>;
 
 export type Parameter = {
-    id: ParameterId,
-    name: string,
-    type: ParameterType,
-    slug: string,
-    default?: string,
+  id: ParameterId,
+  name: string,
+  type: ParameterType,
+  slug: string,
+  default?: string,
 
-    target?: ParameterTarget
+  target?: ParameterTarget,
 };
 
 export type VariableTarget = ["template-tag", string];
-export type DimensionTarget = ["template-tag", string] | LocalFieldReference | ForeignFieldReference
+export type DimensionTarget =
+  | ["template-tag", string]
+  | LocalFieldReference
+  | ForeignFieldReference;
 
 export type ParameterTarget =
-    ["variable", VariableTarget] |
-    ["dimension", DimensionTarget];
+  | ["variable", VariableTarget]
+  | ["dimension", DimensionTarget];
 
 export type ParameterMappingOption = {
-    name: string,
-    target: ParameterTarget,
+  name: string,
+  target: ParameterTarget,
 };
 
 export type ParameterMapping = {
-    card_id: CardId,
-    parameter_id: ParameterId,
-    target: ParameterTarget
+  card_id: CardId,
+  parameter_id: ParameterId,
+  target: ParameterTarget,
 };
 
 export type ParameterOption = {
-    name: string,
-    description?: string,
-    type: ParameterType
+  name: string,
+  description?: string,
+  type: ParameterType,
 };
 
 export type ParameterInstance = {
-    type: ParameterType,
-    target: ParameterTarget,
-    value: ParameterValue
+  type: ParameterType,
+  target: ParameterTarget,
+  value: ParameterValue,
 };
 
 export type ParameterMappingUIOption = ParameterMappingOption & {
-    icon: ?string,
-    sectionName: string,
-    isFk?: boolean,
-    isVariable?: boolean,
+  icon: ?string,
+  sectionName: string,
+  isFk?: boolean,
+  isVariable?: boolean,
 };
 
 export type ParameterValues = {
-    [id: ParameterId]: ParameterValue
+  [id: ParameterId]: ParameterValue,
 };
diff --git a/frontend/src/metabase/meta/types/Permissions.js b/frontend/src/metabase/meta/types/Permissions.js
index 5cb7035da3de268cc7d9861af92e793e8e82d85c..3fd770099de8067a5866ee7fb58f5a8517285831 100644
--- a/frontend/src/metabase/meta/types/Permissions.js
+++ b/frontend/src/metabase/meta/types/Permissions.js
@@ -6,36 +6,42 @@ import type { SchemaName, TableId } from "metabase/meta/types/Table";
 export type GroupId = number;
 
 export type Group = {
-    id: GroupId,
-    name: string
+  id: GroupId,
+  name: string,
 };
 
 export type PermissionsGraph = {
-    groups: GroupsPermissions,
-    revision: number
-}
+  groups: GroupsPermissions,
+  revision: number,
+};
 
 export type GroupsPermissions = {
-    [key: GroupId]: GroupPermissions
-}
+  [key: GroupId]: GroupPermissions,
+};
 
 export type GroupPermissions = {
-    [key: DatabaseId]: DatabasePermissions
-}
+  [key: DatabaseId]: DatabasePermissions,
+};
 
 export type DatabasePermissions = {
-    native: NativePermissions,
-    schemas: SchemasPermissions
-}
+  native: NativePermissions,
+  schemas: SchemasPermissions,
+};
 
 export type NativePermissions = "read" | "write" | "none";
 
-export type SchemasPermissions = "all" | "none" | {
-    [key: SchemaName]: TablesPermissions
-};
-
-export type TablesPermissions = "all" | "none" | {
-    [key: TableId]: FieldsPermissions
-};
+export type SchemasPermissions =
+  | "all"
+  | "none"
+  | {
+      [key: SchemaName]: TablesPermissions,
+    };
+
+export type TablesPermissions =
+  | "all"
+  | "none"
+  | {
+      [key: TableId]: FieldsPermissions,
+    };
 
 export type FieldsPermissions = "all" | "none";
diff --git a/frontend/src/metabase/meta/types/Query.js b/frontend/src/metabase/meta/types/Query.js
index ca1b1e392049cc91a3d54aa60e902a62a688cad9..abf0897ab6d30b3f6e480b67f203f64e25b4c517 100644
--- a/frontend/src/metabase/meta/types/Query.js
+++ b/frontend/src/metabase/meta/types/Query.js
@@ -12,179 +12,263 @@ export type StringLiteral = string;
 export type NumericLiteral = number;
 export type DatetimeLiteral = string;
 
-export type Value = null | boolean | StringLiteral | NumericLiteral | DatetimeLiteral;
+export type Value =
+  | null
+  | boolean
+  | StringLiteral
+  | NumericLiteral
+  | DatetimeLiteral;
 export type OrderableValue = NumericLiteral | DatetimeLiteral;
 
 export type RelativeDatetimePeriod = "current" | "last" | "next" | number;
-export type RelativeDatetimeUnit = "minute" | "hour" | "day" | "week" | "month" | "quarter" | "year";
-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 RelativeDatetimeUnit =
+  | "minute"
+  | "hour"
+  | "day"
+  | "week"
+  | "month"
+  | "quarter"
+  | "year";
+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 TemplateTagType = "text" | "number" | "date" | "dimension";
 
 export type TemplateTag = {
-    id:           TemplateTagId,
-    name:         TemplateTagName,
-    display_name: string,
-    type:         TemplateTagType,
-    dimension?:   LocalFieldReference,
-    widget_type?: ParameterType,
-    required?:    boolean,
-    default?:     string,
+  id: TemplateTagId,
+  name: TemplateTagName,
+  display_name: string,
+  type: TemplateTagType,
+  dimension?: LocalFieldReference,
+  widget_type?: ParameterType,
+  required?: boolean,
+  default?: string,
 };
 
 export type TemplateTags = { [key: TemplateTagName]: TemplateTag };
 
 export type NativeQuery = {
-    query: string,
-    template_tags: TemplateTags
+  query: string,
+  template_tags: TemplateTags,
 };
 
 export type StructuredQuery = {
-    source_table: ?TableId,
-    aggregation?: AggregationClause,
-    breakout?:    BreakoutClause,
-    filter?:      FilterClause,
-    order_by?:    OrderByClause,
-    limit?:       LimitClause,
-    expressions?: ExpressionClause,
-    fields?:      FieldsClause,
+  source_table: ?TableId,
+  aggregation?: AggregationClause,
+  breakout?: BreakoutClause,
+  filter?: FilterClause,
+  order_by?: OrderByClause,
+  limit?: LimitClause,
+  expressions?: ExpressionClause,
+  fields?: FieldsClause,
 };
 
 export type AggregationClause =
-    Aggregation | // @deprecated: aggregation clause is now an array
-    Array<Aggregation>;
+  | Aggregation // @deprecated: aggregation clause is now an array
+  | Array<Aggregation>;
 
 /**
  * An aggregation MBQL clause
  */
 export type Aggregation =
-    Rows | // @deprecated: implicit when there are no aggregations
-    CountAgg |
-    CountFieldAgg |
-    AvgAgg |
-    CumSumAgg |
-    DistinctAgg |
-    StdDevAgg |
-    SumAgg |
-    MinAgg |
-    MaxAgg |
-    MetricAgg;
-
+  | Rows // @deprecated: implicit when there are no aggregations
+  | CountAgg
+  | CountFieldAgg
+  | AvgAgg
+  | CumSumAgg
+  | DistinctAgg
+  | StdDevAgg
+  | SumAgg
+  | MinAgg
+  | MaxAgg
+  | MetricAgg;
 
 /**
  * @deprecated: implicit when there are no aggregations
  */
-type Rows           = ["rows"];
+type Rows = ["rows"];
 
-type CountAgg       = ["count"];
+type CountAgg = ["count"];
 
-type CountFieldAgg  = ["count", ConcreteField];
-type AvgAgg         = ["avg", ConcreteField];
-type CumSumAgg      = ["cum_sum", ConcreteField];
-type DistinctAgg    = ["distinct", ConcreteField];
-type StdDevAgg      = ["stddev", ConcreteField];
-type SumAgg         = ["sum", ConcreteField];
-type MinAgg         = ["min", ConcreteField];
-type MaxAgg         = ["max", ConcreteField];
+type CountFieldAgg = ["count", ConcreteField];
+type AvgAgg = ["avg", ConcreteField];
+type CumSumAgg = ["cum_sum", ConcreteField];
+type DistinctAgg = ["distinct", ConcreteField];
+type StdDevAgg = ["stddev", ConcreteField];
+type SumAgg = ["sum", ConcreteField];
+type MinAgg = ["min", ConcreteField];
+type MaxAgg = ["max", ConcreteField];
 
 // NOTE: currently the backend expects METRIC to be uppercase
-type MetricAgg      = ["METRIC", MetricId];
+type MetricAgg = ["METRIC", MetricId];
 
 export type BreakoutClause = Array<Breakout>;
-export type Breakout =
-    ConcreteField;
+export type Breakout = ConcreteField;
 
 export type FilterClause = Filter;
 export type Filter = FieldFilter | CompoundFilter | NotFilter | SegmentFilter;
 
-export type CompoundFilter =
-    AndFilter          |
-    OrFilter;
+export type CompoundFilter = AndFilter | OrFilter;
 
 export type FieldFilter =
-    EqualityFilter     |
-    ComparisonFilter   |
-    BetweenFilter      |
-    StringFilter       |
-    NullFilter         |
-    NotNullFilter      |
-    InsideFilter       |
-    TimeIntervalFilter;
-
-export type AndFilter          = ["and", Filter, Filter];
-export type OrFilter           = ["or", Filter, Filter];
-
-export type NotFilter          = ["not", Filter];
-
-export type EqualityFilter     = ["="|"!=", ConcreteField, Value];
-export type ComparisonFilter   = ["<"|"<="|">="|">", ConcreteField, OrderableValue];
-export type BetweenFilter      = ["between", ConcreteField, OrderableValue, OrderableValue];
-export type StringFilter       = ["starts-with"|"contains"|"does-not-contain"|"ends-with", ConcreteField, StringLiteral];
-
-export type NullFilter         = ["is-null", ConcreteField];
-export type NotNullFilter      = ["not-null", ConcreteField];
-export type InsideFilter       = ["inside", ConcreteField, ConcreteField, NumericLiteral, NumericLiteral, NumericLiteral, NumericLiteral];
-export type TimeIntervalFilter = ["time-interval", ConcreteField, RelativeDatetimePeriod, RelativeDatetimeUnit] |
-                                 ["time-interval", ConcreteField, RelativeDatetimePeriod, RelativeDatetimeUnit, FilterOptions];
-
-export type FilterOptions = {
-  "include-current"?: bool
-}
+  | EqualityFilter
+  | ComparisonFilter
+  | BetweenFilter
+  | StringFilter
+  | NullFilter
+  | NotNullFilter
+  | InsideFilter
+  | TimeIntervalFilter;
+
+export type AndFilter = ["and", Filter, Filter];
+export type OrFilter = ["or", Filter, Filter];
+
+export type NotFilter = ["not", Filter];
+
+export type EqualityFilter = ["=" | "!=", ConcreteField, Value];
+export type ComparisonFilter = [
+  "<" | "<=" | ">=" | ">",
+  ConcreteField,
+  OrderableValue,
+];
+export type BetweenFilter = [
+  "between",
+  ConcreteField,
+  OrderableValue,
+  OrderableValue,
+];
+export type StringFilter =
+  | [
+      "starts-with" | "contains" | "does-not-contain" | "ends-with",
+      ConcreteField,
+      StringLiteral,
+    ]
+  | [
+      "starts-with" | "contains" | "does-not-contain" | "ends-with",
+      ConcreteField,
+      StringLiteral,
+      StringFilterOptions,
+    ];
+
+export type StringFilterOptions = {
+  "case-sensitive"?: false,
+};
+
+export type NullFilter = ["is-null", ConcreteField];
+export type NotNullFilter = ["not-null", ConcreteField];
+export type InsideFilter = [
+  "inside",
+  ConcreteField,
+  ConcreteField,
+  NumericLiteral,
+  NumericLiteral,
+  NumericLiteral,
+  NumericLiteral,
+];
+export type TimeIntervalFilter =
+  | [
+      "time-interval",
+      ConcreteField,
+      RelativeDatetimePeriod,
+      RelativeDatetimeUnit,
+    ]
+  | [
+      "time-interval",
+      ConcreteField,
+      RelativeDatetimePeriod,
+      RelativeDatetimeUnit,
+      FilterOptions,
+    ];
+
+export type TimeIntervalFilterOptions = {
+  "include-current"?: boolean,
+};
+
+export type FilterOptions = StringFilterOptions | TimeIntervalFilterOptions;
 
 // NOTE: currently the backend expects SEGMENT to be uppercase
-export type SegmentFilter      = ["SEGMENT", SegmentId];
+export type SegmentFilter = ["SEGMENT", SegmentId];
 
 export type OrderByClause = Array<OrderBy>;
-export type OrderBy = [Field, "ascending"|"descending"|"asc"|"desc"];
+export type OrderBy = [Field, "descending" | "ascending"];
 
 export type LimitClause = number;
 
-export type Field =
-    ConcreteField |
-    AggregateField;
+export type Field = ConcreteField | AggregateField;
 
 export type ConcreteField =
-    LocalFieldReference |
-    ForeignFieldReference |
-    ExpressionReference |
-    DatetimeField |
-    BinnedField;
+  | LocalFieldReference
+  | ForeignFieldReference
+  | ExpressionReference
+  | DatetimeField
+  | BinnedField;
 
-export type LocalFieldReference =
-    ["field-id", FieldId] |
-    FieldId; // @deprecated: use ["field-id", FieldId]
+export type LocalFieldReference = ["field-id", FieldId] | FieldId; // @deprecated: use ["field-id", FieldId]
 
-export type ForeignFieldReference =
-    ["fk->", FieldId, FieldId];
+export type ForeignFieldReference = ["fk->", FieldId, FieldId];
 
-export type ExpressionReference =
-    ["expression", ExpressionName];
+export type ExpressionReference = ["expression", ExpressionName];
 
-export type FieldLiteral =
-    ["field-literal", string, BaseType]; // ["field-literal", name, base-type]
+export type FieldLiteral = ["field-literal", string, BaseType]; // ["field-literal", name, base-type]
 
 export type DatetimeField =
-    ["datetime-field", LocalFieldReference | ForeignFieldReference, DatetimeUnit] |
-    ["datetime-field", LocalFieldReference | ForeignFieldReference, "as", DatetimeUnit]; // @deprecated: don't include the "as" element
+  | [
+      "datetime-field",
+      LocalFieldReference | ForeignFieldReference,
+      DatetimeUnit,
+    ]
+  | [
+      "datetime-field",
+      LocalFieldReference | ForeignFieldReference,
+      "as",
+      DatetimeUnit,
+    ]; // @deprecated: don't include the "as" element
 
 export type BinnedField =
-    ["binning-strategy", LocalFieldReference | ForeignFieldReference, "default"] | // default binning (as defined by backend)
-    ["binning-strategy", LocalFieldReference | ForeignFieldReference, "num-bins", number] | // number of bins
-    ["binning-strategy", LocalFieldReference | ForeignFieldReference, "bin-width", number]; // width of each bin
+  | ["binning-strategy", LocalFieldReference | ForeignFieldReference, "default"] // default binning (as defined by backend)
+  | [
+      "binning-strategy",
+      LocalFieldReference | ForeignFieldReference,
+      "num-bins",
+      number,
+    ] // number of bins
+  | [
+      "binning-strategy",
+      LocalFieldReference | ForeignFieldReference,
+      "bin-width",
+      number,
+    ]; // width of each bin
 
 export type AggregateField = ["aggregation", number];
 
-
 export type ExpressionClause = {
-    [key: ExpressionName]: Expression
+  [key: ExpressionName]: Expression,
 };
 
-export type Expression =
-    [ExpressionOperator, ExpressionOperand, ExpressionOperand];
+export type Expression = [
+  ExpressionOperator,
+  ExpressionOperand,
+  ExpressionOperand,
+];
 
 export type ExpressionOperator = "+" | "-" | "*" | "/";
 export type ExpressionOperand = ConcreteField | NumericLiteral | Expression;
 
-export type FieldsClause = FieldId[];
+export type FieldsClause = Field[];
diff --git a/frontend/src/metabase/meta/types/Revision.js b/frontend/src/metabase/meta/types/Revision.js
index 9155fc83e33b376da71053414e913742caa0af75..fca5b42d2a5c437b8a5cc0b75e5552bf421803c8 100644
--- a/frontend/src/metabase/meta/types/Revision.js
+++ b/frontend/src/metabase/meta/types/Revision.js
@@ -3,6 +3,6 @@
 export type RevisionId = string;
 
 export type Revision = {
-    // TODO: incomplete
-    id: RevisionId
-}
+  // TODO: incomplete
+  id: RevisionId,
+};
diff --git a/frontend/src/metabase/meta/types/Segment.js b/frontend/src/metabase/meta/types/Segment.js
index 37d1101ad7430491e1f41b97dd1c1b684d014a64..c68a994dc5b0cb60a2a7c3c178354fc89e620821 100644
--- a/frontend/src/metabase/meta/types/Segment.js
+++ b/frontend/src/metabase/meta/types/Segment.js
@@ -6,9 +6,9 @@ export type SegmentId = number;
 
 // TODO: incomplete
 export type Segment = {
-    name: string,
-    id: SegmentId,
-    table_id: TableId,
-    is_active: bool,
-    description: string
+  name: string,
+  id: SegmentId,
+  table_id: TableId,
+  is_active: boolean,
+  description: string,
 };
diff --git a/frontend/src/metabase/meta/types/Table.js b/frontend/src/metabase/meta/types/Table.js
index 4b2be18cf8ce0962ec7818d41f83995db360dec6..76d337b964e105edf9afac12a30cfb42f2b7b623 100644
--- a/frontend/src/metabase/meta/types/Table.js
+++ b/frontend/src/metabase/meta/types/Table.js
@@ -1,4 +1,3 @@
-
 import type { ISO8601Time } from ".";
 
 import type { Field } from "./Field";
@@ -13,31 +12,31 @@ type TableVisibilityType = string; // FIXME
 
 // TODO: incomplete
 export type Table = {
-    id:                      TableId,
-    db_id:                   DatabaseId,
+  id: TableId,
+  db_id: DatabaseId,
 
-    schema:                  ?SchemaName,
-    name:                    string,
-    display_name:            string,
+  schema: ?SchemaName,
+  name: string,
+  display_name: string,
 
-    description:             string,
-    active:                  boolean,
-    visibility_type:         TableVisibilityType,
+  description: string,
+  active: boolean,
+  visibility_type: TableVisibilityType,
 
-    // entity_name:          null // unused?
-    // entity_type:          null // unused?
-    // raw_table_id:         number, // unused?
+  // entity_name:          null // unused?
+  // entity_type:          null // unused?
+  // raw_table_id:         number, // unused?
 
-    fields:                  Field[],
-    segments:                Segment[],
-    metrics:                 Metric[],
+  fields: Field[],
+  segments: Segment[],
+  metrics: Metric[],
 
-    rows:                    number,
+  rows: number,
 
-    caveats:                 ?string,
-    points_of_interest:      ?string,
-    show_in_getting_started: boolean,
+  caveats: ?string,
+  points_of_interest: ?string,
+  show_in_getting_started: boolean,
 
-    updated_at:              ISO8601Time,
-    created_at:              ISO8601Time,
-}
+  updated_at: ISO8601Time,
+  created_at: ISO8601Time,
+};
diff --git a/frontend/src/metabase/meta/types/User.js b/frontend/src/metabase/meta/types/User.js
index 2b1d4440ecfc52d02d3cb09aca05bbfbd54e8b70..dd531c252cb64a667ac27102f72bcccd393067fc 100644
--- a/frontend/src/metabase/meta/types/User.js
+++ b/frontend/src/metabase/meta/types/User.js
@@ -1,13 +1,13 @@
 export type User = {
-    common_name: string,
-    date_joined: string,
-    email: string,
-    first_name: string,
-    google_auth: boolean,
-    id: number,
-    is_active: boolean,
-    is_qbnewb: false,
-    is_superuser: true,
-    last_login: string,
-    last_name: string
-}
\ No newline at end of file
+  common_name: string,
+  date_joined: string,
+  email: string,
+  first_name: string,
+  google_auth: boolean,
+  id: number,
+  is_active: boolean,
+  is_qbnewb: false,
+  is_superuser: true,
+  last_login: string,
+  last_name: string,
+};
diff --git a/frontend/src/metabase/meta/types/Visualization.js b/frontend/src/metabase/meta/types/Visualization.js
index e56f6bbe58b40bbf09c64bb62df174698df4efd7..e59453f964c0249a8b9b1deaaeb96f66ad3f5ec4 100644
--- a/frontend/src/metabase/meta/types/Visualization.js
+++ b/frontend/src/metabase/meta/types/Visualization.js
@@ -6,116 +6,119 @@ import type { TableMetadata } from "metabase/meta/types/Metadata";
 import type { Field, FieldId } from "metabase/meta/types/Field";
 import Question from "metabase-lib/lib/Question";
 
-export type ActionCreator = (props: ClickActionProps) => ClickAction[]
+export type ActionCreator = (props: ClickActionProps) => ClickAction[];
 
 export type QueryMode = {
-    name: string,
-    actions: ActionCreator[],
-    drills: ActionCreator[]
-}
+  name: string,
+  actions: ActionCreator[],
+  drills: ActionCreator[],
+};
 
 export type HoverData = Array<{ key: string, value: any, col?: Column }>;
 
 export type HoverObject = {
-    index?: number,
-    axisIndex?: number,
-    data?: HoverData,
-    element?: ?HTMLElement,
-    event?: MouseEvent,
-}
+  index?: number,
+  axisIndex?: number,
+  data?: HoverData,
+  element?: ?HTMLElement,
+  event?: MouseEvent,
+};
 
 export type DimensionValue = {
-    value: Value,
-    column: Column
+  value: Value,
+  column: Column,
 };
 
 export type ClickObject = {
-    value?: Value,
-    column?: Column,
-    dimensions?: DimensionValue[],
-    event?: MouseEvent,
-    element?: HTMLElement,
-    seriesIndex?: number,
-}
+  value?: Value,
+  column?: Column,
+  dimensions?: DimensionValue[],
+  event?: MouseEvent,
+  element?: HTMLElement,
+  seriesIndex?: number,
+};
 
 export type ClickAction = {
-    title: any, // React Element
-    icon?: string,
-    popover?: (props: ClickActionPopoverProps) => any, // React Element
-    question?: () => ?Question,
-    url?: () => string,
-    section?: string,
-    name?: string,
-}
+  title: any, // React Element
+  icon?: string,
+  popover?: (props: ClickActionPopoverProps) => any, // React Element
+  question?: () => ?Question,
+  url?: () => string,
+  section?: string,
+  name?: string,
+};
 
 export type ClickActionProps = {
-    question: Question,
-    clicked?: ClickObject,
-    settings: {
-        'enable_xrays': boolean,
-        'xray_max_cost': string
-    }
-}
+  question: Question,
+  clicked?: ClickObject,
+  settings: {
+    enable_xrays: boolean,
+    xray_max_cost: string,
+  },
+};
 
-export type OnChangeCardAndRun = ({ nextCard: Card, previousCard?: ?Card }) => void
+export type OnChangeCardAndRun = ({
+  nextCard: Card,
+  previousCard?: ?Card,
+}) => void;
 
 export type ClickActionPopoverProps = {
-    onChangeCardAndRun: OnChangeCardAndRun,
-    onClose: () => void,
-}
+  onChangeCardAndRun: OnChangeCardAndRun,
+  onClose: () => void,
+};
 
 export type SingleSeries = { card: Card, data: DatasetData };
-export type Series = SingleSeries[] & { _raw: Series }
+export type Series = SingleSeries[] & { _raw: Series };
 
 export type VisualizationProps = {
-    series: Series,
-    card: Card,
-    data: DatasetData,
-    settings: VisualizationSettings,
-
-    className?: string,
-    gridSize: ?{
-        width: number,
-        height: number
-    },
-
-    showTitle: boolean,
-    isDashboard: boolean,
-    isEditing: boolean,
-    actionButtons: Node,
-
-    onRender: ({
-        yAxisSplit?: number[][],
-        warnings?: string[]
-    }) => void,
-
-    hovered: ?HoverObject,
-    onHoverChange: (?HoverObject) => void,
-    onVisualizationClick: (?ClickObject) => void,
-    visualizationIsClickable: (?ClickObject) => boolean,
-    onChangeCardAndRun: OnChangeCardAndRun,
-
-    onUpdateVisualizationSettings: ({ [key: string]: any }) => void,
-
-    // object detail
-    tableMetadata: ?TableMetadata,
-    tableForeignKeys: ?ForeignKey[],
-    tableForeignKeyReferences: { [id: ForeignKeyId]: ForeignKeyCountInfo },
-    loadObjectDetailFKReferences: () => void,
-    followForeignKey: (fk: any) => void,
-}
+  series: Series,
+  card: Card,
+  data: DatasetData,
+  settings: VisualizationSettings,
+
+  className?: string,
+  gridSize: ?{
+    width: number,
+    height: number,
+  },
+
+  showTitle: boolean,
+  isDashboard: boolean,
+  isEditing: boolean,
+  actionButtons: Node,
+
+  onRender: ({
+    yAxisSplit?: number[][],
+    warnings?: string[],
+  }) => void,
+
+  hovered: ?HoverObject,
+  onHoverChange: (?HoverObject) => void,
+  onVisualizationClick: (?ClickObject) => void,
+  visualizationIsClickable: (?ClickObject) => boolean,
+  onChangeCardAndRun: OnChangeCardAndRun,
+
+  onUpdateVisualizationSettings: ({ [key: string]: any }) => void,
+
+  // object detail
+  tableMetadata: ?TableMetadata,
+  tableForeignKeys: ?(ForeignKey[]),
+  tableForeignKeyReferences: { [id: ForeignKeyId]: ForeignKeyCountInfo },
+  loadObjectDetailFKReferences: () => void,
+  followForeignKey: (fk: any) => void,
+};
 
 type ForeignKeyId = number;
 type ForeignKey = {
-    id: ForeignKeyId,
-    relationship: string,
-    origin: Field,
-    origin_id: FieldId,
-    destination: Field,
-    destination_id: FieldId,
-}
+  id: ForeignKeyId,
+  relationship: string,
+  origin: Field,
+  origin_id: FieldId,
+  destination: Field,
+  destination_id: FieldId,
+};
 
 type ForeignKeyCountInfo = {
-    status: number,
-    value: number,
+  status: number,
+  value: number,
 };
diff --git a/frontend/src/metabase/meta/types/index.js b/frontend/src/metabase/meta/types/index.js
index 01a75a8e57446d8293ea449ecd88b19457028888..1f18187d13e5799b5b8576ec4a1a44bc454894c3 100644
--- a/frontend/src/metabase/meta/types/index.js
+++ b/frontend/src/metabase/meta/types/index.js
@@ -14,25 +14,25 @@ export type IconName = string;
 
 /* Location descriptor used by react-router and history */
 export type LocationDescriptor = {
-    hash: string,
-    pathname: string,
-    search?: string,
-    query?: { [key: string]: string }
+  hash: string,
+  pathname: string,
+  search?: string,
+  query?: { [key: string]: string },
 };
 
 /* Map of query string names to string values */
 export type QueryParams = {
-    [key: string]: string
+  [key: string]: string,
 };
 
 /* Metabase API error object returned by the backend */
 export type ApiError = {
-    status: number, // HTTP status
-    // TODO: incomplete
-}
+  status: number, // HTTP status
+  // TODO: incomplete
+};
 
 // FIXME: actual moment.js type
 export type Moment = {
-    locale: () => Moment,
-    format: (format: string) => string
+  locale: () => Moment,
+  format: (format: string) => string,
 };
diff --git a/frontend/src/metabase/nav/components/ProfileLink.jsx b/frontend/src/metabase/nav/components/ProfileLink.jsx
index b90d8b51516ed5a56c3370c09fc9b410f66d6273..de888525c991d2e1ecf80d889a288bbc7d1d5551 100644
--- a/frontend/src/metabase/nav/components/ProfileLink.jsx
+++ b/frontend/src/metabase/nav/components/ProfileLink.jsx
@@ -1,10 +1,10 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 
-import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper';
-import { t } from 'c-3po';
-import cx from 'classnames';
+import OnClickOutsideWrapper from "metabase/components/OnClickOutsideWrapper";
+import { t } from "c-3po";
+import cx from "classnames";
 import _ from "underscore";
 import { capitalize } from "metabase/lib/formatting";
 
@@ -12,162 +12,229 @@ import MetabaseSettings from "metabase/lib/settings";
 import Modal from "metabase/components/Modal.jsx";
 import Logs from "metabase/components/Logs.jsx";
 
-import UserAvatar from 'metabase/components/UserAvatar.jsx';
-import Icon from 'metabase/components/Icon.jsx';
-import LogoIcon from 'metabase/components/LogoIcon.jsx';
+import UserAvatar from "metabase/components/UserAvatar.jsx";
+import Icon from "metabase/components/Icon.jsx";
+import LogoIcon from "metabase/components/LogoIcon.jsx";
 
 export default class ProfileLink extends Component {
+  constructor(props, context) {
+    super(props, context);
 
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            dropdownOpen: false,
-            modalOpen: null
-        };
-
-        _.bindAll(this, "toggleDropdown", "closeDropdown", "openModal", "closeModal");
-    }
-
-    static propTypes = {
-        user: PropTypes.object.isRequired,
-        context: PropTypes.string.isRequired,
+    this.state = {
+      dropdownOpen: false,
+      modalOpen: null,
     };
 
-    toggleDropdown() {
-        this.setState({ dropdownOpen: !this.state.dropdownOpen });
-    }
-
-    closeDropdown() {
-        this.setState({ dropdownOpen: false });
-    }
-
-    openModal(modalName) {
-        this.setState({ dropdownOpen: false, modalOpen: modalName });
-    }
-
-    closeModal() {
-        this.setState({ modalOpen: null });
-    }
-
-    render() {
-        const { user, context } = this.props;
-        const { modalOpen, dropdownOpen } = this.state;
-        const { tag, date, ...versionExtra } = MetabaseSettings.get('version');
-
-        let dropDownClasses = cx({
-            'NavDropdown': true,
-            'inline-block': true,
-            'cursor-pointer': true,
-            'open': dropdownOpen,
-        });
-
-        return (
-            <div className={dropDownClasses}>
-                <a data-metabase-event={"Navbar;Profile Dropdown;Toggle"} className="NavDropdown-button NavItem flex align-center p2 transition-background" onClick={this.toggleDropdown}>
-                    <div className="NavDropdown-button-layer">
-                        <div className="flex align-center">
-                            <UserAvatar user={user} style={{backgroundColor: 'transparent'}}/>
-                            <Icon name="chevrondown" className="Dropdown-chevron ml1" size={8} />
-                        </div>
-                    </div>
-                </a>
-
-                { dropdownOpen ?
-                    <OnClickOutsideWrapper handleDismissal={this.closeDropdown}>
-                        <div className="NavDropdown-content right">
-                            <ul className="NavDropdown-content-layer">
-                                { !user.google_auth && !user.ldap_auth ?
-                                    <li>
-                                        <Link to="/user/edit_current" data-metabase-event={"Navbar;Profile Dropdown;Edit Profile"} onClick={this.closeDropdown} className="Dropdown-item block text-white no-decoration">
-                                            {t`Account Settings`}
-                                        </Link>
-                                    </li>
-                                : null }
-
-                                { user.is_superuser && context !== 'admin' ?
-                                    <li>
-                                        <Link to="/admin" data-metabase-event={"Navbar;Profile Dropdown;Enter Admin"} onClick={this.closeDropdown} className="Dropdown-item block text-white no-decoration">
-                                            {t`Admin Panel`}
-                                        </Link>
-                                    </li>
-                                : null }
-
-                                { user.is_superuser && context === 'admin' ?
-                                    <li>
-                                        <Link to="/" data-metabase-event={"Navbar;Profile Dropdown;Exit Admin"} onClick={this.closeDropdown} className="Dropdown-item block text-white no-decoration">
-                                            {t`Exit Admin`}
-                                        </Link>
-                                    </li>
-                                : null }
-
-                                <li>
-                                    <a data-metabase-event={"Navbar;Profile Dropdown;Help "+tag} className="Dropdown-item block text-white no-decoration" href={"http://www.metabase.com/docs/"+tag} target="_blank">
-                                        {t`Help`}
-                                    </a>
-                                </li>
-
-                                { user.is_superuser &&
-                                    <li>
-                                        <a data-metabase-event={"Navbar;Profile Dropdown;Debugging "+tag} onClick={this.openModal.bind(this, "logs")} className="Dropdown-item block text-white no-decoration">
-                                            {t`Logs`}
-                                        </a>
-                                    </li>
-                                }
-
-                                <li>
-                                    <a data-metabase-event={"Navbar;Profile Dropdown;About "+tag} onClick={this.openModal.bind(this, "about")} className="Dropdown-item block text-white no-decoration">
-                                        {t`About Metabase`}
-                                    </a>
-                                </li>
-
-                                <li className="border-top border-light">
-                                    <Link
-                                        to="/auth/logout"
-                                        data-metabase-event={"Navbar;Profile Dropdown;Logout"}
-                                        className="Dropdown-item block text-white no-decoration"
-                                    >
-                                        {t`Sign out`}
-                                    </Link>
-                                </li>
-                            </ul>
-                        </div>
-                    </OnClickOutsideWrapper>
-                : null }
-
-                { modalOpen === "about" ?
-                    <Modal small onClose={this.closeModal}>
-                        <div className="px4 pt4 pb2 text-centered relative">
-                            <span className="absolute top right p4 text-normal text-grey-3 cursor-pointer" onClick={this.closeModal}>
-                                <Icon name={'close'} size={16} />
-                            </span>
-                            <div className="text-brand pb2">
-                                <LogoIcon width={48} height={48} />
-                            </div>
-                            <h2 style={{fontSize: "1.75em"}} className="text-dark">{t`Thanks for using`} Metabase!</h2>
-                            <div className="pt2">
-                                <h3 className="text-dark mb1">{t`You're on version`} {tag}</h3>
-                                <p className="text-grey-3 text-bold">{t`Built on`} {date}</p>
-                                { !/^v\d+\.\d+\.\d+$/.test(tag) &&
-                                    <div>
-                                    { _.map(versionExtra, (value, key) =>
-                                        <p key={key} className="text-grey-3 text-bold">{capitalize(key)}: {value}</p>
-                                    ) }
-                                    </div>
-                                }
-                            </div>
-                        </div>
-                        <div style={{borderWidth: "2px"}} className="p2 h5 text-centered text-grey-3 border-top">
-                            <span className="block"><span className="text-bold">Metabase</span> {t`is a Trademark of`} Metabase, Inc</span>
-                            <span>{t`and is built with care in San Francisco, CA`}</span>
-                        </div>
-                    </Modal>
-                : modalOpen === "logs" ?
-                    <Modal wide onClose={this.closeModal}>
-                        <Logs onClose={this.closeModal} />
-                    </Modal>
-                : null }
+    _.bindAll(
+      this,
+      "toggleDropdown",
+      "closeDropdown",
+      "openModal",
+      "closeModal",
+    );
+  }
+
+  static propTypes = {
+    user: PropTypes.object.isRequired,
+    context: PropTypes.string.isRequired,
+  };
+
+  toggleDropdown() {
+    this.setState({ dropdownOpen: !this.state.dropdownOpen });
+  }
+
+  closeDropdown() {
+    this.setState({ dropdownOpen: false });
+  }
+
+  openModal(modalName) {
+    this.setState({ dropdownOpen: false, modalOpen: modalName });
+  }
+
+  closeModal() {
+    this.setState({ modalOpen: null });
+  }
+
+  render() {
+    const { user, context } = this.props;
+    const { modalOpen, dropdownOpen } = this.state;
+    const { tag, date, ...versionExtra } = MetabaseSettings.get("version");
+
+    let dropDownClasses = cx({
+      NavDropdown: true,
+      "inline-block": true,
+      "cursor-pointer": true,
+      open: dropdownOpen,
+    });
+
+    return (
+      <div className={dropDownClasses}>
+        <a
+          data-metabase-event={"Navbar;Profile Dropdown;Toggle"}
+          className="NavDropdown-button NavItem flex align-center p2 transition-background"
+          onClick={this.toggleDropdown}
+        >
+          <div className="NavDropdown-button-layer">
+            <div className="flex align-center">
+              <UserAvatar
+                user={user}
+                style={{ backgroundColor: "transparent" }}
+              />
+              <Icon
+                name="chevrondown"
+                className="Dropdown-chevron ml1"
+                size={8}
+              />
+            </div>
+          </div>
+        </a>
+
+        {dropdownOpen ? (
+          <OnClickOutsideWrapper handleDismissal={this.closeDropdown}>
+            <div className="NavDropdown-content right">
+              <ul className="NavDropdown-content-layer">
+                {!user.google_auth && !user.ldap_auth ? (
+                  <li>
+                    <Link
+                      to="/user/edit_current"
+                      data-metabase-event={
+                        "Navbar;Profile Dropdown;Edit Profile"
+                      }
+                      onClick={this.closeDropdown}
+                      className="Dropdown-item block text-white no-decoration"
+                    >
+                      {t`Account Settings`}
+                    </Link>
+                  </li>
+                ) : null}
+
+                {user.is_superuser && context !== "admin" ? (
+                  <li>
+                    <Link
+                      to="/admin"
+                      data-metabase-event={
+                        "Navbar;Profile Dropdown;Enter Admin"
+                      }
+                      onClick={this.closeDropdown}
+                      className="Dropdown-item block text-white no-decoration"
+                    >
+                      {t`Admin Panel`}
+                    </Link>
+                  </li>
+                ) : null}
+
+                {user.is_superuser && context === "admin" ? (
+                  <li>
+                    <Link
+                      to="/"
+                      data-metabase-event={"Navbar;Profile Dropdown;Exit Admin"}
+                      onClick={this.closeDropdown}
+                      className="Dropdown-item block text-white no-decoration"
+                    >
+                      {t`Exit Admin`}
+                    </Link>
+                  </li>
+                ) : null}
+
+                <li>
+                  <a
+                    data-metabase-event={"Navbar;Profile Dropdown;Help " + tag}
+                    className="Dropdown-item block text-white no-decoration"
+                    href={"http://www.metabase.com/docs/" + tag}
+                    target="_blank"
+                  >
+                    {t`Help`}
+                  </a>
+                </li>
+
+                {user.is_superuser && (
+                  <li>
+                    <a
+                      data-metabase-event={
+                        "Navbar;Profile Dropdown;Debugging " + tag
+                      }
+                      onClick={this.openModal.bind(this, "logs")}
+                      className="Dropdown-item block text-white no-decoration"
+                    >
+                      {t`Logs`}
+                    </a>
+                  </li>
+                )}
+
+                <li>
+                  <a
+                    data-metabase-event={"Navbar;Profile Dropdown;About " + tag}
+                    onClick={this.openModal.bind(this, "about")}
+                    className="Dropdown-item block text-white no-decoration"
+                  >
+                    {t`About Metabase`}
+                  </a>
+                </li>
+
+                <li className="border-top border-light">
+                  <Link
+                    to="/auth/logout"
+                    data-metabase-event={"Navbar;Profile Dropdown;Logout"}
+                    className="Dropdown-item block text-white no-decoration"
+                  >
+                    {t`Sign out`}
+                  </Link>
+                </li>
+              </ul>
+            </div>
+          </OnClickOutsideWrapper>
+        ) : null}
+
+        {modalOpen === "about" ? (
+          <Modal small onClose={this.closeModal}>
+            <div className="px4 pt4 pb2 text-centered relative">
+              <span
+                className="absolute top right p4 text-normal text-grey-3 cursor-pointer"
+                onClick={this.closeModal}
+              >
+                <Icon name={"close"} size={16} />
+              </span>
+              <div className="text-brand pb2">
+                <LogoIcon width={48} height={48} />
+              </div>
+              <h2 style={{ fontSize: "1.75em" }} className="text-dark">
+                {t`Thanks for using`} Metabase!
+              </h2>
+              <div className="pt2">
+                <h3 className="text-dark mb1">
+                  {t`You're on version`} {tag}
+                </h3>
+                <p className="text-grey-3 text-bold">
+                  {t`Built on`} {date}
+                </p>
+                {!/^v\d+\.\d+\.\d+$/.test(tag) && (
+                  <div>
+                    {_.map(versionExtra, (value, key) => (
+                      <p key={key} className="text-grey-3 text-bold">
+                        {capitalize(key)}: {value}
+                      </p>
+                    ))}
+                  </div>
+                )}
+              </div>
+            </div>
+            <div
+              style={{ borderWidth: "2px" }}
+              className="p2 h5 text-centered text-grey-3 border-top"
+            >
+              <span className="block">
+                <span className="text-bold">Metabase</span>{" "}
+                {t`is a Trademark of`} Metabase, Inc
+              </span>
+              <span>{t`and is built with care in San Francisco, CA`}</span>
             </div>
-        );
-    }
+          </Modal>
+        ) : modalOpen === "logs" ? (
+          <Modal wide onClose={this.closeModal}>
+            <Logs onClose={this.closeModal} />
+          </Modal>
+        ) : null}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/nav/containers/Navbar.jsx b/frontend/src/metabase/nav/containers/Navbar.jsx
index f4a1c971266f4aba3901d4ed25d7fc8882d8556d..8c24fe6f8b3070b711c71afc168d57419e896420 100644
--- a/frontend/src/metabase/nav/containers/Navbar.jsx
+++ b/frontend/src/metabase/nav/containers/Navbar.jsx
@@ -1,7 +1,7 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import { t } from 'c-3po'
+import { t } from "c-3po";
 
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
@@ -16,153 +16,215 @@ import ProfileLink from "metabase/nav/components/ProfileLink.jsx";
 import { getPath, getContext, getUser } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    path:       getPath(state, props),
-    context:    getContext(state, props),
-    user:       getUser(state)
+  path: getPath(state, props),
+  context: getContext(state, props),
+  user: getUser(state),
 });
 
 const mapDispatchToProps = {
-    onChangeLocation: push
+  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",
-    }
+  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}`}
-            className={cx("NavItem py1 px2 no-decoration", {"is--selected": currentPath.startsWith(path) })}
-        >
-            {name}
-        </Link>
-    </li>
-
-const MainNavLink = ({ to, name, eventName, icon }) =>
+const AdminNavItem = ({ name, path, currentPath }) => (
+  <li>
     <Link
-        to={to}
-        data-metabase-event={`NavBar;${eventName}`}
-        style={BUTTON_PADDING_STYLES.navButton}
-        className={"NavItem cursor-pointer flex-full text-white text-bold no-decoration flex align-center px2 transition-background"}
-        activeClassName="NavItem--selected"
+      to={path}
+      data-metabase-event={`NavBar;${name}`}
+      className={cx("NavItem py1 px2 no-decoration", {
+        "is--selected": currentPath.startsWith(path),
+      })}
     >
-        <Icon name={icon} className="md-hide" />
-        <span className="hide md-show">{name}</span>
+      {name}
     </Link>
+  </li>
+);
+
+const MainNavLink = ({ to, name, eventName, icon }) => (
+  <Link
+    to={to}
+    data-metabase-event={`NavBar;${eventName}`}
+    style={BUTTON_PADDING_STYLES.navButton}
+    className={
+      "NavItem cursor-pointer flex-full text-white text-bold no-decoration flex align-center px2 transition-background"
+    }
+    activeClassName="NavItem--selected"
+  >
+    <Icon name={icon} className="md-hide" />
+    <span className="hide md-show">{name}</span>
+  </Link>
+);
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class Navbar extends Component {
-    static propTypes = {
-        context: PropTypes.string.isRequired,
-        path: PropTypes.string.isRequired,
-        user: PropTypes.object
-    };
-
-    isActive(path) {
-        return this.props.path.startsWith(path);
-    }
-
-    renderAdminNav() {
-        return (
-            <nav className={cx("Nav AdminNav sm-py1")}>
-                <div className="sm-pl4 flex align-center pr1">
-                    <div className="NavTitle flex align-center">
-                        <Icon name={'gear'} className="AdminGear" size={22}></Icon>
-                        <span className="NavItem-text ml1 hide sm-show text-bold">{t`Metabase Admin`}</span>
-                    </div>
-
-                    <ul className="sm-ml4 flex flex-full">
-                        <AdminNavItem name="Settings"    path="/admin/settings"     currentPath={this.props.path} />
-                        <AdminNavItem name="People"      path="/admin/people"       currentPath={this.props.path} />
-                        <AdminNavItem name="Data Model"  path="/admin/datamodel"    currentPath={this.props.path} />
-                        <AdminNavItem name="Databases"   path="/admin/databases"    currentPath={this.props.path} />
-                        <AdminNavItem name="Permissions" path="/admin/permissions"  currentPath={this.props.path} />
-                    </ul>
-
-                    <ProfileLink {...this.props} />
-                </div>
-            </nav>
-        );
-    }
-
-    renderEmptyNav() {
-        return (
-            <nav className="Nav sm-py1 relative">
-                <ul className="wrapper flex align-center">
-                    <li>
-                        <Link to="/" data-metabase-event={"Navbar;Logo"} className="NavItem cursor-pointer flex align-center">
-                            <LogoIcon className="text-brand my2"></LogoIcon>
-                        </Link>
-                    </li>
-                </ul>
-            </nav>
-        );
-    }
-
-    renderMainNav() {
-        return (
-            <nav className="Nav relative bg-brand">
-                <ul className="md-pl4 flex align-center md-pr1">
-                    <li>
-                        <Link
-                            to="/"
-                            data-metabase-event={"Navbar;Logo"}
-                            className="LogoNavItem NavItem cursor-pointer text-white flex align-center transition-background justify-center"
-                            activeClassName="NavItem--selected"
-                        >
-                            <LogoIcon dark={true}></LogoIcon>
-                        </Link>
-                    </li>
-                    <li className="md-pl3 hide xs-show">
-                        <MainNavLink to="/dashboards" name={t`Dashboards`} eventName="Dashboards" icon="dashboard" />
-                    </li>
-                    <li className="md-pl1 hide xs-show">
-                        <MainNavLink to="/questions" name={t`Questions`} eventName="Questions" icon="all" />
-                    </li>
-                    <li className="md-pl1 hide xs-show">
-                        <MainNavLink to="/pulse" name={t`Pulses`} eventName="Pulses" icon="pulse" />
-                    </li>
-                    <li className="md-pl1 hide xs-show">
-                        <MainNavLink to="/reference/guide" name={t`Data Reference`} eventName="DataReference" icon="reference" />
-                    </li>
-                    <li className="md-pl3 hide sm-show">
-                        <Link to={Urls.newQuestion()} 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">
-                            {t`New Question`}
-                        </Link>
-                    </li>
-                    <li className="flex-align-right transition-background hide sm-show">
-                        <div className="inline-block text-white"><ProfileLink {...this.props}></ProfileLink></div>
-                    </li>
-                </ul>
-            </nav>
-        );
-    }
-
-    render() {
-        const { context, user } = this.props;
-
-        if (!user) return null;
-
-        switch (context) {
-            case "admin": return this.renderAdminNav();
-            case "auth": return null;
-            case "none": return this.renderEmptyNav();
-            case "setup": return null;
-            default: return this.renderMainNav();
-        }
+  static propTypes = {
+    context: PropTypes.string.isRequired,
+    path: PropTypes.string.isRequired,
+    user: PropTypes.object,
+  };
+
+  isActive(path) {
+    return this.props.path.startsWith(path);
+  }
+
+  renderAdminNav() {
+    return (
+      <nav className={cx("Nav AdminNav sm-py1")}>
+        <div className="sm-pl4 flex align-center pr1">
+          <div className="NavTitle flex align-center">
+            <Icon name={"gear"} className="AdminGear" size={22} />
+            <span className="NavItem-text ml1 hide sm-show text-bold">{t`Metabase Admin`}</span>
+          </div>
+
+          <ul className="sm-ml4 flex flex-full">
+            <AdminNavItem
+              name="Settings"
+              path="/admin/settings"
+              currentPath={this.props.path}
+            />
+            <AdminNavItem
+              name="People"
+              path="/admin/people"
+              currentPath={this.props.path}
+            />
+            <AdminNavItem
+              name="Data Model"
+              path="/admin/datamodel"
+              currentPath={this.props.path}
+            />
+            <AdminNavItem
+              name="Databases"
+              path="/admin/databases"
+              currentPath={this.props.path}
+            />
+            <AdminNavItem
+              name="Permissions"
+              path="/admin/permissions"
+              currentPath={this.props.path}
+            />
+          </ul>
+
+          <ProfileLink {...this.props} />
+        </div>
+      </nav>
+    );
+  }
+
+  renderEmptyNav() {
+    return (
+      <nav className="Nav sm-py1 relative">
+        <ul className="wrapper flex align-center">
+          <li>
+            <Link
+              to="/"
+              data-metabase-event={"Navbar;Logo"}
+              className="NavItem cursor-pointer flex align-center"
+            >
+              <LogoIcon className="text-brand my2" />
+            </Link>
+          </li>
+        </ul>
+      </nav>
+    );
+  }
+
+  renderMainNav() {
+    return (
+      <nav className="Nav relative bg-brand">
+        <ul className="md-pl4 flex align-center md-pr1">
+          <li>
+            <Link
+              to="/"
+              data-metabase-event={"Navbar;Logo"}
+              className="LogoNavItem NavItem cursor-pointer text-white flex align-center transition-background justify-center"
+              activeClassName="NavItem--selected"
+            >
+              <LogoIcon dark={true} />
+            </Link>
+          </li>
+          <li className="md-pl3 hide xs-show">
+            <MainNavLink
+              to="/dashboards"
+              name={t`Dashboards`}
+              eventName="Dashboards"
+              icon="dashboard"
+            />
+          </li>
+          <li className="md-pl1 hide xs-show">
+            <MainNavLink
+              to="/questions"
+              name={t`Questions`}
+              eventName="Questions"
+              icon="all"
+            />
+          </li>
+          <li className="md-pl1 hide xs-show">
+            <MainNavLink
+              to="/pulse"
+              name={t`Pulses`}
+              eventName="Pulses"
+              icon="pulse"
+            />
+          </li>
+          <li className="md-pl1 hide xs-show">
+            <MainNavLink
+              to="/reference/guide"
+              name={t`Data Reference`}
+              eventName="DataReference"
+              icon="reference"
+            />
+          </li>
+          <li className="md-pl3 hide sm-show">
+            <Link
+              to={Urls.newQuestion()}
+              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"
+            >
+              {t`New Question`}
+            </Link>
+          </li>
+          <li className="flex-align-right transition-background hide sm-show">
+            <div className="inline-block text-white">
+              <ProfileLink {...this.props} />
+            </div>
+          </li>
+        </ul>
+      </nav>
+    );
+  }
+
+  render() {
+    const { context, user } = this.props;
+
+    if (!user) return null;
+
+    switch (context) {
+      case "admin":
+        return this.renderAdminNav();
+      case "auth":
+        return null;
+      case "none":
+        return this.renderEmptyNav();
+      case "setup":
+        return null;
+      default:
+        return this.renderMainNav();
     }
+  }
 }
diff --git a/frontend/src/metabase/nav/selectors.js b/frontend/src/metabase/nav/selectors.js
index 4b958f0fca83163976e82a5d43be55ff391dbff0..3ef45d003918f445c0a55e5fb18a3c71ba8a2049 100644
--- a/frontend/src/metabase/nav/selectors.js
+++ b/frontend/src/metabase/nav/selectors.js
@@ -1,21 +1,15 @@
-
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 
 export { getUser } from "metabase/selectors/user";
 
 export const getPath = (state, props) => props.location.pathname;
 
 export const getContext = createSelector(
-    [getPath],
-    (path) =>
-        path.startsWith('/auth/') ?
-            'auth'
-        : path.startsWith('/setup/') ?
-            'setup'
-        : path.startsWith('/admin/') ?
-            'admin'
-        : path === '/' ?
-            'home'
-        :
-            'main'
+  [getPath],
+  path =>
+    path.startsWith("/auth/")
+      ? "auth"
+      : path.startsWith("/setup/")
+        ? "setup"
+        : path.startsWith("/admin/") ? "admin" : path === "/" ? "home" : "main",
 );
diff --git a/frontend/src/metabase/new_query/components/NewQueryOption.jsx b/frontend/src/metabase/new_query/components/NewQueryOption.jsx
index cc1503208943485327ebba2c5521f91bf407bc93..cb4c6f322e691e7a40b6333b9594139803f93bc8 100644
--- a/frontend/src/metabase/new_query/components/NewQueryOption.jsx
+++ b/frontend/src/metabase/new_query/components/NewQueryOption.jsx
@@ -3,46 +3,55 @@ import cx from "classnames";
 import { Link } from "react-router";
 
 export default class NewQueryOption extends Component {
-   props: {
-       image: string,
-       title: string,
-       description: string,
-       to: string
-   };
+  props: {
+    image: string,
+    title: string,
+    description: string,
+    to: string,
+  };
 
-   state = {
-       hover: false
-   };
+  state = {
+    hover: false,
+  };
 
-   render() {
-       const { width, image, title, description, to } = this.props;
-       const { hover } = this.state;
+  render() {
+    const { width, image, title, description, to } = this.props;
+    const { hover } = this.state;
 
-       return (
-           <Link
-               className="block no-decoration bg-white px3 pt4 align-center bordered rounded cursor-pointer transition-all text-centered"
-               style={{
-                   boxSizing: "border-box",
-                   boxShadow: hover ? "0 3px 8px 0 rgba(220,220,220,0.50)" : "0 1px 3px 0 rgba(220,220,220,0.50)",
-                   height: 340
-               }}
-               onMouseOver={() => this.setState({hover: true})}
-               onMouseLeave={() => this.setState({hover: false})}
-               to={to}
-           >
-               <div className="flex align-center layout-centered" style={{ height: "160px" }}>
-                   <img
-                       src={`${image}.png`}
-                       style={{ width: width ? `${width}px` : "210px" }}
-                       srcSet={`${image}@2x.png 2x`}
-                   />
-
-               </div>
-               <div className="text-normal mt2 mb2 text-paragraph" style={{lineHeight: "1.25em"}}>
-                   <h2 className={cx("transition-all", {"text-brand": hover})}>{title}</h2>
-                   <p className={"text-grey-4 text-small"}>{description}</p>
-               </div>
-           </Link>
-       );
-   }
+    return (
+      <Link
+        className="block no-decoration bg-white px3 pt4 align-center bordered rounded cursor-pointer transition-all text-centered"
+        style={{
+          boxSizing: "border-box",
+          boxShadow: hover
+            ? "0 3px 8px 0 rgba(220,220,220,0.50)"
+            : "0 1px 3px 0 rgba(220,220,220,0.50)",
+          height: 340,
+        }}
+        onMouseOver={() => this.setState({ hover: true })}
+        onMouseLeave={() => this.setState({ hover: false })}
+        to={to}
+      >
+        <div
+          className="flex align-center layout-centered"
+          style={{ height: "160px" }}
+        >
+          <img
+            src={`${image}.png`}
+            style={{ width: width ? `${width}px` : "210px" }}
+            srcSet={`${image}@2x.png 2x`}
+          />
+        </div>
+        <div
+          className="text-normal mt2 mb2 text-paragraph"
+          style={{ lineHeight: "1.25em" }}
+        >
+          <h2 className={cx("transition-all", { "text-brand": hover })}>
+            {title}
+          </h2>
+          <p className={"text-grey-4 text-small"}>{description}</p>
+        </div>
+      </Link>
+    );
+  }
 }
diff --git a/frontend/src/metabase/new_query/containers/MetricSearch.jsx b/frontend/src/metabase/new_query/containers/MetricSearch.jsx
index a5387701b192bfbfef6582f5e4d45ea259b46fae..138de77584ba47237a1dfd7a29123fb7b33ab5d3 100644
--- a/frontend/src/metabase/new_query/containers/MetricSearch.jsx
+++ b/frontend/src/metabase/new_query/containers/MetricSearch.jsx
@@ -1,101 +1,104 @@
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
+import React, { Component } from "react";
+import { connect } from "react-redux";
 import { fetchMetrics, fetchDatabases } from "metabase/redux/metadata";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 import EntitySearch from "metabase/containers/EntitySearch";
 import { getMetadata, getMetadataFetched } from "metabase/selectors/metadata";
-import _ from 'underscore'
-import { t } from 'c-3po'
+import _ from "underscore";
+import { t } from "c-3po";
 import type { Metric } from "metabase/meta/types/Metric";
 import type Metadata from "metabase-lib/lib/metadata/Metadata";
 import EmptyState from "metabase/components/EmptyState";
 
 import type { StructuredQuery } from "metabase/meta/types/Query";
 import { getCurrentQuery } from "metabase/new_query/selectors";
-import { resetQuery } from '../new_query'
+import { resetQuery } from "../new_query";
 
 const mapStateToProps = state => ({
-    query: getCurrentQuery(state),
-    metadata: getMetadata(state),
-    metadataFetched: getMetadataFetched(state)
-})
+  query: getCurrentQuery(state),
+  metadata: getMetadata(state),
+  metadataFetched: getMetadataFetched(state),
+});
 const mapDispatchToProps = {
-    fetchMetrics,
-    fetchDatabases,
-    resetQuery
-}
+  fetchMetrics,
+  fetchDatabases,
+  resetQuery,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricSearch extends Component {
-    props: {
-        getUrlForQuery: (StructuredQuery) => void,
-        backButtonUrl: string,
+  props: {
+    getUrlForQuery: StructuredQuery => void,
+    backButtonUrl: string,
 
-        query: StructuredQuery,
-        metadata: Metadata,
-        metadataFetched: any,
-        fetchMetrics: () => void,
-        fetchDatabases: () => void,
-        resetQuery: () => void,
-    }
+    query: StructuredQuery,
+    metadata: Metadata,
+    metadataFetched: any,
+    fetchMetrics: () => void,
+    fetchDatabases: () => void,
+    resetQuery: () => void,
+  };
 
-    componentDidMount() {
-        this.props.fetchDatabases() // load databases if not loaded yet
-        this.props.fetchMetrics(true) // metrics may change more often so always reload them
-        this.props.resetQuery();
-    }
+  componentDidMount() {
+    this.props.fetchDatabases(); // load databases if not loaded yet
+    this.props.fetchMetrics(true); // metrics may change more often so always reload them
+    this.props.resetQuery();
+  }
 
-    getUrlForMetric = (metric: Metric) => {
-        const updatedQuery = this.props.query
-            .setDatabase(metric.table.db)
-            .setTable(metric.table)
-            .addAggregation(metric.aggregationClause())
+  getUrlForMetric = (metric: Metric) => {
+    const updatedQuery = this.props.query
+      .setDatabase(metric.table.db)
+      .setTable(metric.table)
+      .addAggregation(metric.aggregationClause());
 
-        return this.props.getUrlForQuery(updatedQuery);
-    }
+    return this.props.getUrlForQuery(updatedQuery);
+  };
 
-    render() {
-        const { backButtonUrl, metadataFetched, metadata } = this.props;
-        const isLoading = !metadataFetched.metrics || !metadataFetched.databases
+  render() {
+    const { backButtonUrl, metadataFetched, metadata } = this.props;
+    const isLoading = !metadataFetched.metrics || !metadataFetched.databases;
 
-        return (
-            <LoadingAndErrorWrapper loading={isLoading}>
-                {() => {
-                    const sortedActiveMetrics = _.chain(metadata.metricsList())
-                        // Metric shouldn't be retired and it should refer to an existing table
-                        .filter((metric) => metric.isActive() && metric.table)
-                        .sortBy(({name}) => name.toLowerCase())
-                        .value()
+    return (
+      <LoadingAndErrorWrapper loading={isLoading}>
+        {() => {
+          const sortedActiveMetrics = _.chain(metadata.metricsList())
+            // Metric shouldn't be retired and it should refer to an existing table
+            .filter(metric => metric.isActive() && metric.table)
+            .sortBy(({ name }) => name.toLowerCase())
+            .value();
 
-                    if (sortedActiveMetrics.length > 0) {
-                        return (
-                            <EntitySearch
-                                title={t`Which metric?`}
-                                // TODO Atte Keinänen 8/22/17: If you call `/api/table/:id/table_metadata` it returns
-                                // all metrics (also retired ones) and is missing `is_active` prop. Currently this
-                                // filters them out but we should definitely update the endpoints in the upcoming metadata API refactoring.
-                                entities={sortedActiveMetrics}
-                                getUrlForEntity={this.getUrlForMetric}
-                                backButtonUrl={backButtonUrl}
-                            />
-                        )
-                    } else {
-                        return (
-                            <div className="mt2 flex-full flex align-center justify-center">
-                                <EmptyState
-                                    message={<span>${t`Defining common metrics for your team makes it even easier to ask questions`}</span>}
-                                    image="/app/img/metrics_illustration"
-                                    action={t`How to create metrics`}
-                                    link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
-                                    className="mt2"
-                                    imageClassName="mln2"
-                                />
-                            </div>
-                        )
-                    }
-                }}
-            </LoadingAndErrorWrapper>
-        )
-    }
+          if (sortedActiveMetrics.length > 0) {
+            return (
+              <EntitySearch
+                title={t`Which metric?`}
+                // TODO Atte Keinänen 8/22/17: If you call `/api/table/:id/table_metadata` it returns
+                // all metrics (also retired ones) and is missing `is_active` prop. Currently this
+                // filters them out but we should definitely update the endpoints in the upcoming metadata API refactoring.
+                entities={sortedActiveMetrics}
+                getUrlForEntity={this.getUrlForMetric}
+                backButtonUrl={backButtonUrl}
+              />
+            );
+          } else {
+            return (
+              <div className="mt2 flex-full flex align-center justify-center">
+                <EmptyState
+                  message={
+                    <span>
+                      ${t`Defining common metrics for your team makes it even easier to ask questions`}
+                    </span>
+                  }
+                  image="/app/img/metrics_illustration"
+                  action={t`How to create metrics`}
+                  link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
+                  className="mt2"
+                  imageClassName="mln2"
+                />
+              </div>
+            );
+          }
+        }}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
-
diff --git a/frontend/src/metabase/new_query/containers/NewQueryOptions.jsx b/frontend/src/metabase/new_query/containers/NewQueryOptions.jsx
index c92e97ed01742211b79a928f308a6af953683a17..fcbc60dd42a232dd5c0f276639cbdd13b56d9fdd 100644
--- a/frontend/src/metabase/new_query/containers/NewQueryOptions.jsx
+++ b/frontend/src/metabase/new_query/containers/NewQueryOptions.jsx
@@ -1,159 +1,179 @@
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
+import React, { Component } from "react";
+import { connect } from "react-redux";
 
 import {
-    fetchDatabases,
-    fetchMetrics,
-    fetchSegments,
-} from 'metabase/redux/metadata'
-
-import { withBackground } from 'metabase/hoc/Background'
-import { determineWhichOptionsToShow, resetQuery } from '../new_query'
-import { t } from 'c-3po'
+  fetchDatabases,
+  fetchMetrics,
+  fetchSegments,
+} from "metabase/redux/metadata";
+
+import { withBackground } from "metabase/hoc/Background";
+import { determineWhichOptionsToShow, resetQuery } from "../new_query";
+import { t } from "c-3po";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery"
+import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 import { getMetadata, getMetadataFetched } from "metabase/selectors/metadata";
 import NewQueryOption from "metabase/new_query/components/NewQueryOption";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
-import { getCurrentQuery, getNewQueryOptions, getPlainNativeQuery } from "metabase/new_query/selectors";
+import {
+  getCurrentQuery,
+  getNewQueryOptions,
+  getPlainNativeQuery,
+} from "metabase/new_query/selectors";
 import { getUserIsAdmin } from "metabase/selectors/user";
 import { push } from "react-router-redux";
 import NoDatabasesEmptyState from "metabase/reference/databases/NoDatabasesEmptyState";
 
 const mapStateToProps = state => ({
-    query: getCurrentQuery(state),
-    plainNativeQuery: getPlainNativeQuery(state),
-    metadata: getMetadata(state),
-    metadataFetched: getMetadataFetched(state),
-    isAdmin: getUserIsAdmin(state),
-    newQueryOptions: getNewQueryOptions(state)
-})
+  query: getCurrentQuery(state),
+  plainNativeQuery: getPlainNativeQuery(state),
+  metadata: getMetadata(state),
+  metadataFetched: getMetadataFetched(state),
+  isAdmin: getUserIsAdmin(state),
+  newQueryOptions: getNewQueryOptions(state),
+});
 
 const mapDispatchToProps = {
-    determineWhichOptionsToShow,
-    fetchDatabases,
-    fetchMetrics,
-    fetchSegments,
-    resetQuery,
-    push
-}
+  determineWhichOptionsToShow,
+  fetchDatabases,
+  fetchMetrics,
+  fetchSegments,
+  resetQuery,
+  push,
+};
 
 type Props = {
-    // Component parameters
-    getUrlForQuery: (StructuredQuery) => void,
-    metricSearchUrl: string,
-    segmentSearchUrl: string,
-
-    // Properties injected with redux connect
-    query: StructuredQuery,
-    plainNativeQuery: NativeQuery,
-    metadata: Metadata,
-    isAdmin: boolean,
-
-    resetQuery: () => void,
-    determineWhichOptionsToShow: () => void,
-
-    fetchDatabases: () => void,
-    fetchMetrics: () => void,
-    fetchSegments: () => void,
-}
+  // Component parameters
+  getUrlForQuery: StructuredQuery => void,
+  metricSearchUrl: string,
+  segmentSearchUrl: string,
 
-const allOptionsVisibleState = {
-    loaded: true,
-    hasDatabases: true,
-    showMetricOption: true,
-    showTableOption: true,
-    showSQLOption: true
-}
+  // Properties injected with redux connect
+  query: StructuredQuery,
+  plainNativeQuery: NativeQuery,
+  metadata: Metadata,
+  isAdmin: boolean,
 
-export class NewQueryOptions extends Component {
-    props: Props
-
-    constructor(props) {
-        super(props)
-
-        // By default, show all options instantly to admins
-        this.state = props.isAdmin ? allOptionsVisibleState : {
-            loaded: false,
-            hasDatabases: false,
-            showMetricOption: false,
-            showTableOption: false,
-            showSQLOption: false
-        }
-    }
+  resetQuery: () => void,
+  determineWhichOptionsToShow: () => void,
 
-    async componentWillMount() {
-        this.props.resetQuery();
-        this.props.determineWhichOptionsToShow(this.getGuiQueryUrl);
-    }
+  fetchDatabases: () => void,
+  fetchMetrics: () => void,
+  fetchSegments: () => void,
+};
 
-    getGuiQueryUrl = () => {
-        return this.props.getUrlForQuery(this.props.query);
-    }
+const allOptionsVisibleState = {
+  loaded: true,
+  hasDatabases: true,
+  showMetricOption: true,
+  showTableOption: true,
+  showSQLOption: true,
+};
 
-    getNativeQueryUrl = () => {
-        return this.props.getUrlForQuery(this.props.plainNativeQuery);
+export class NewQueryOptions extends Component {
+  props: Props;
+
+  constructor(props) {
+    super(props);
+
+    // By default, show all options instantly to admins
+    this.state = props.isAdmin
+      ? allOptionsVisibleState
+      : {
+          loaded: false,
+          hasDatabases: false,
+          showMetricOption: false,
+          showTableOption: false,
+          showSQLOption: false,
+        };
+  }
+
+  async componentWillMount() {
+    this.props.resetQuery();
+    this.props.determineWhichOptionsToShow(this.getGuiQueryUrl);
+  }
+
+  getGuiQueryUrl = () => {
+    return this.props.getUrlForQuery(this.props.query);
+  };
+
+  getNativeQueryUrl = () => {
+    return this.props.getUrlForQuery(this.props.plainNativeQuery);
+  };
+
+  render() {
+    const { isAdmin, metricSearchUrl, newQueryOptions } = this.props;
+    const {
+      loaded,
+      hasDatabases,
+      showMetricOption,
+      showSQLOption,
+    } = newQueryOptions;
+    const showCustomInsteadOfNewQuestionText = showMetricOption || isAdmin;
+
+    if (!loaded) {
+      return <LoadingAndErrorWrapper loading={true} />;
     }
 
-    render() {
-        const { isAdmin, metricSearchUrl, newQueryOptions } = this.props
-        const { loaded, hasDatabases, showMetricOption, showSQLOption } = newQueryOptions
-        const showCustomInsteadOfNewQuestionText = showMetricOption || isAdmin
-
-        if (!loaded) {
-            return <LoadingAndErrorWrapper loading={true}/>
-        }
-
-        if (!hasDatabases) {
-            return (
-                <div className="full-height flex align-center justify-center">
-                    <NoDatabasesEmptyState/>
-                </div>
-            )
-        }
-
-        return (
-            <div className="full-height flex">
-                <div className="wrapper wrapper--trim lg-wrapper--trim xl-wrapper--trim flex-full px1 mt4 mb2 align-center">
-                     <div className="flex align-center justify-center" style={{minHeight: "100%"}}>
-                        <ol className="flex-full Grid Grid--guttersXl Grid--full sm-Grid--normal">
-                            { showMetricOption &&
-                                <li className="Grid-cell">
-                                    <NewQueryOption
-                                        image="/app/img/questions_illustration"
-                                        title={t`Metrics`}
-                                        description={t`See data over time, as a map, or pivoted to help you understand trends or changes.`}
-                                        to={metricSearchUrl}
-                                    />
-                                </li>
-                            }
-                            <li className="Grid-cell">
-                                {/*TODO: Move illustrations to the new location in file hierarchy. At the same time put an end to the equal-size-@2x ridicule. */}
-                                <NewQueryOption
-                                    image="/app/img/query_builder_illustration"
-                                    title={ showCustomInsteadOfNewQuestionText ? t`Custom` : t`New question`}
-                                    description={t`Use the simple question builder to see trends, lists of things, or to create your own metrics.`}
-                                    width={180}
-                                    to={this.getGuiQueryUrl}
-                                />
-                            </li>
-                            { showSQLOption &&
-                                <li className="Grid-cell">
-                                    <NewQueryOption
-                                        image="/app/img/sql_illustration"
-                                        title={t`Native query`}
-                                        description={t`For more complicated questions, you can write your own SQL or native query.`}
-                                        to={this.getNativeQueryUrl}
-                                    />
-                                </li>
-                            }
-                        </ol>
-                    </div>
-                </div>
-            </div>
-        )
+    if (!hasDatabases) {
+      return (
+        <div className="full-height flex align-center justify-center">
+          <NoDatabasesEmptyState />
+        </div>
+      );
     }
+
+    return (
+      <div className="full-height flex">
+        <div className="wrapper wrapper--trim lg-wrapper--trim xl-wrapper--trim flex-full px1 mt4 mb2 align-center">
+          <div
+            className="flex align-center justify-center"
+            style={{ minHeight: "100%" }}
+          >
+            <ol className="flex-full Grid Grid--guttersXl Grid--full sm-Grid--normal">
+              {showMetricOption && (
+                <li className="Grid-cell">
+                  <NewQueryOption
+                    image="/app/img/questions_illustration"
+                    title={t`Metrics`}
+                    description={t`See data over time, as a map, or pivoted to help you understand trends or changes.`}
+                    to={metricSearchUrl}
+                  />
+                </li>
+              )}
+              <li className="Grid-cell">
+                {/*TODO: Move illustrations to the new location in file hierarchy. At the same time put an end to the equal-size-@2x ridicule. */}
+                <NewQueryOption
+                  image="/app/img/query_builder_illustration"
+                  title={
+                    showCustomInsteadOfNewQuestionText
+                      ? t`Custom`
+                      : t`New question`
+                  }
+                  description={t`Use the simple question builder to see trends, lists of things, or to create your own metrics.`}
+                  width={180}
+                  to={this.getGuiQueryUrl}
+                />
+              </li>
+              {showSQLOption && (
+                <li className="Grid-cell">
+                  <NewQueryOption
+                    image="/app/img/sql_illustration"
+                    title={t`Native query`}
+                    description={t`For more complicated questions, you can write your own SQL or native query.`}
+                    to={this.getNativeQueryUrl}
+                  />
+                </li>
+              )}
+            </ol>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(withBackground('bg-slate-extra-light')(NewQueryOptions))
+export default connect(mapStateToProps, mapDispatchToProps)(
+  withBackground("bg-slate-extra-light")(NewQueryOptions),
+);
diff --git a/frontend/src/metabase/new_query/new_query.js b/frontend/src/metabase/new_query/new_query.js
index 3b30a4cb4e4757be8534ea108f0b0fff2b634396..27677de926d3dedf8b84e524f0366afc89871e04 100644
--- a/frontend/src/metabase/new_query/new_query.js
+++ b/frontend/src/metabase/new_query/new_query.js
@@ -5,10 +5,10 @@
 
 import { handleActions, combineReducers } from "metabase/lib/redux";
 import {
-    fetchDatabases,
-    fetchMetrics,
-    fetchSegments,
-} from 'metabase/redux/metadata'
+  fetchDatabases,
+  fetchMetrics,
+  fetchSegments,
+} from "metabase/redux/metadata";
 
 import { STRUCTURED_QUERY_TEMPLATE } from "metabase-lib/lib/queries/StructuredQuery";
 import type { DatasetQuery } from "metabase/meta/types/Card";
@@ -21,93 +21,108 @@ import { push } from "react-router-redux";
  */
 export const RESET_QUERY = "metabase/new_query/RESET_QUERY";
 export function resetQuery() {
-    return function(dispatch, getState) {
-        dispatch.action(RESET_QUERY, STRUCTURED_QUERY_TEMPLATE)
-    }
+  return function(dispatch, getState) {
+    dispatch.action(RESET_QUERY, STRUCTURED_QUERY_TEMPLATE);
+  };
 }
 
 const newQueryOptionsDefault = {
-    loaded: false,
-    hasDatabases: false,
-    showMetricOption: false,
-    showTableOption: false,
-    showSQLOption: false
-}
+  loaded: false,
+  hasDatabases: false,
+  showMetricOption: false,
+  showTableOption: false,
+  showSQLOption: false,
+};
 
 const newQueryOptionsAllVisible = {
-    loaded: true,
-    hasDatabases: true,
-    showMetricOption: true,
-    showTableOption: true,
-    showSQLOption: true
-}
-
-export const DETERMINE_OPTIONS_STARTED = "metabase/new_query/DETERMINE_OPTIONS_STARTED"
-export const DETERMINE_OPTIONS = "metabase/new_query/DETERMINE_OPTIONS"
+  loaded: true,
+  hasDatabases: true,
+  showMetricOption: true,
+  showTableOption: true,
+  showSQLOption: true,
+};
+
+export const DETERMINE_OPTIONS_STARTED =
+  "metabase/new_query/DETERMINE_OPTIONS_STARTED";
+export const DETERMINE_OPTIONS = "metabase/new_query/DETERMINE_OPTIONS";
 export function determineWhichOptionsToShow(getGuiQueryUrl) {
-    return async (dispatch, getState) => {
-        // By default, show all options instantly to admins
-        const isAdmin = getUserIsAdmin(getState())
-        dispatch.action(DETERMINE_OPTIONS_STARTED, isAdmin ? newQueryOptionsAllVisible : {
+  return async (dispatch, getState) => {
+    // By default, show all options instantly to admins
+    const isAdmin = getUserIsAdmin(getState());
+    dispatch.action(
+      DETERMINE_OPTIONS_STARTED,
+      isAdmin
+        ? newQueryOptionsAllVisible
+        : {
             loaded: false,
             hasDatabases: false,
             showMetricOption: false,
             showTableOption: false,
-            showSQLOption: false
-        })
-
-        await Promise.all([
-            dispatch(fetchDatabases()),
-            dispatch(fetchMetrics()),
-            dispatch(fetchSegments())
-        ])
-
-        const metadata = getMetadata(getState())
-        const hasDatabases = metadata.databasesList().length > 0
-
-        if (!hasDatabases) {
-            return dispatch.action(DETERMINE_OPTIONS, { loaded: true, hasDatabases: false })
-        } else if (isAdmin) {
-            return dispatch.action(DETERMINE_OPTIONS, newQueryOptionsAllVisible)
-        } else {
-            const showMetricOption = metadata.metricsList().length > 0
-
-            // to be able to use SQL the user must have write permissions on at least one db
-            const hasSQLPermission = (db) => db.native_permissions === "write"
-            const showSQLOption = metadata.databasesList().filter(hasSQLPermission).length > 0
-
-            // if we can only show one option then we should just redirect
-            const redirectToQueryBuilder =
-                !showMetricOption && !showSQLOption
-
-            if (redirectToQueryBuilder) {
-                dispatch(push(getGuiQueryUrl()))
-            } else {
-                return dispatch.action(DETERMINE_OPTIONS, {
-                    loaded: true,
-                    hasDatabases: true,
-                    showMetricOption,
-                    showSQLOption,
-                })
-            }
-        }
+            showSQLOption: false,
+          },
+    );
+
+    await Promise.all([
+      dispatch(fetchDatabases()),
+      dispatch(fetchMetrics()),
+      dispatch(fetchSegments()),
+    ]);
+
+    const metadata = getMetadata(getState());
+    const hasDatabases = metadata.databasesList().length > 0;
+
+    if (!hasDatabases) {
+      return dispatch.action(DETERMINE_OPTIONS, {
+        loaded: true,
+        hasDatabases: false,
+      });
+    } else if (isAdmin) {
+      return dispatch.action(DETERMINE_OPTIONS, newQueryOptionsAllVisible);
+    } else {
+      const showMetricOption = metadata.metricsList().length > 0;
+
+      // to be able to use SQL the user must have write permissions on at least one db
+      const hasSQLPermission = db => db.native_permissions === "write";
+      const showSQLOption =
+        metadata.databasesList().filter(hasSQLPermission).length > 0;
+
+      // if we can only show one option then we should just redirect
+      const redirectToQueryBuilder = !showMetricOption && !showSQLOption;
+
+      if (redirectToQueryBuilder) {
+        dispatch(push(getGuiQueryUrl()));
+      } else {
+        return dispatch.action(DETERMINE_OPTIONS, {
+          loaded: true,
+          hasDatabases: true,
+          showMetricOption,
+          showSQLOption,
+        });
+      }
     }
+  };
 }
 
 /**
  * The current query that we are creating
  */
 
-const newQueryOptions = handleActions({
+const newQueryOptions = handleActions(
+  {
     [DETERMINE_OPTIONS_STARTED]: (state, { payload }): DatasetQuery => payload,
-    [DETERMINE_OPTIONS]: (state, { payload }): DatasetQuery => payload
-}, newQueryOptionsDefault)
+    [DETERMINE_OPTIONS]: (state, { payload }): DatasetQuery => payload,
+  },
+  newQueryOptionsDefault,
+);
 
-const datasetQuery = handleActions({
+const datasetQuery = handleActions(
+  {
     [RESET_QUERY]: (state, { payload }): DatasetQuery => payload,
-}, STRUCTURED_QUERY_TEMPLATE);
+  },
+  STRUCTURED_QUERY_TEMPLATE,
+);
 
 export default combineReducers({
-    newQueryOptions,
-    datasetQuery
+  newQueryOptions,
+  datasetQuery,
 });
diff --git a/frontend/src/metabase/new_query/router_wrappers.js b/frontend/src/metabase/new_query/router_wrappers.js
index 3176c08daae86addc35488922992b0206dc9fe4d..43f33cb7f0d847fa90f3e95b5b96064ada65dc62 100644
--- a/frontend/src/metabase/new_query/router_wrappers.js
+++ b/frontend/src/metabase/new_query/router_wrappers.js
@@ -2,42 +2,42 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
 
-import { withBackground } from 'metabase/hoc/Background'
+import { withBackground } from "metabase/hoc/Background";
 
 import NewQueryOptions from "./containers/NewQueryOptions";
 import MetricSearch from "./containers/MetricSearch";
 
 @connect(null, { onChangeLocation: push })
-@withBackground('bg-slate-extra-light')
+@withBackground("bg-slate-extra-light")
 export class NewQuestionStart extends Component {
-    getUrlForQuery = (query) => {
-        return query.question().getUrl()
-    }
+  getUrlForQuery = query => {
+    return query.question().getUrl();
+  };
 
-    render() {
-        return (
-            <NewQueryOptions
-                getUrlForQuery={this.getUrlForQuery}
-                metricSearchUrl="/question/new/metric"
-                segmentSearchUrl="/question/new/segment"
-            />
-        )
-    }
+  render() {
+    return (
+      <NewQueryOptions
+        getUrlForQuery={this.getUrlForQuery}
+        metricSearchUrl="/question/new/metric"
+        segmentSearchUrl="/question/new/segment"
+      />
+    );
+  }
 }
 
 @connect(null, { onChangeLocation: push })
-@withBackground('bg-slate-extra-light')
+@withBackground("bg-slate-extra-light")
 export class NewQuestionMetricSearch extends Component {
-    getUrlForQuery = (query) => {
-        return query.question().getUrl()
-    }
+  getUrlForQuery = query => {
+    return query.question().getUrl();
+  };
 
-    render() {
-        return (
-            <MetricSearch
-                getUrlForQuery={this.getUrlForQuery}
-                backButtonUrl="/question/new"
-            />
-        )
-    }
+  render() {
+    return (
+      <MetricSearch
+        getUrlForQuery={this.getUrlForQuery}
+        backButtonUrl="/question/new"
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/new_query/selectors.js b/frontend/src/metabase/new_query/selectors.js
index 56e7cb40d83e96930b12067e5dbaca8b1a9d098b..a2e2bc195f48cbcba9995d2ff330a0d56b5a9cb9 100644
--- a/frontend/src/metabase/new_query/selectors.js
+++ b/frontend/src/metabase/new_query/selectors.js
@@ -9,25 +9,26 @@ import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import Question from "metabase-lib/lib/Question";
 
 export const getCurrentQuery = state => {
-    // NOTE Atte Keinänen 8/14/17: This is a useless question that will go away after query lib refactoring
-    const question = Question.create({ metadata: getMetadata(state) })
-    const datasetQuery = state.new_query.datasetQuery;
-    return new StructuredQuery(question, datasetQuery)
-}
+  // NOTE Atte Keinänen 8/14/17: This is a useless question that will go away after query lib refactoring
+  const question = Question.create({ metadata: getMetadata(state) });
+  const datasetQuery = state.new_query.datasetQuery;
+  return new StructuredQuery(question, datasetQuery);
+};
 
 export const getPlainNativeQuery = state => {
-    const metadata = getMetadata(state)
-    const question = Question.create({ metadata: getMetadata(state) })
-    const databases = metadata.databasesList().filter(db => !db.is_saved_questions && db.native_permissions === "write")
+  const metadata = getMetadata(state);
+  const question = Question.create({ metadata: getMetadata(state) });
+  const databases = metadata
+    .databasesList()
+    .filter(db => !db.is_saved_questions && db.native_permissions === "write");
 
-    // If we only have a single database, then default to that
-    // (native query editor doesn't currently show the db selector if there is only one database available)
-    if (databases.length === 1) {
-        return new NativeQuery(question).setDatabase(databases[0])
-    } else {
-        return new NativeQuery(question)
-    }
-}
+  // If we only have a single database, then default to that
+  // (native query editor doesn't currently show the db selector if there is only one database available)
+  if (databases.length === 1) {
+    return new NativeQuery(question).setDatabase(databases[0]);
+  } else {
+    return new NativeQuery(question);
+  }
+};
 
-export const getNewQueryOptions = state =>
-    state.new_query.newQueryOptions
+export const getNewQueryOptions = state => state.new_query.newQueryOptions;
diff --git a/frontend/src/metabase/parameters/components/ParameterValueWidget.jsx b/frontend/src/metabase/parameters/components/ParameterValueWidget.jsx
index 1e46335ac0067dfe86b67fdc111f1dc55d4347df..ccd2afeef984c8e1912e57d9c97363e27b10a67d 100644
--- a/frontend/src/metabase/parameters/components/ParameterValueWidget.jsx
+++ b/frontend/src/metabase/parameters/components/ParameterValueWidget.jsx
@@ -1,12 +1,12 @@
 /* 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";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import DateSingleWidget from "./widgets/DateSingleWidget.jsx";
 import DateRangeWidget from "./widgets/DateRangeWidget.jsx";
 import DateRelativeWidget from "./widgets/DateRelativeWidget.jsx";
@@ -15,174 +15,261 @@ import DateQuarterYearWidget from "./widgets/DateQuarterYearWidget.jsx";
 import DateAllOptionsWidget from "./widgets/DateAllOptionsWidget.jsx";
 import CategoryWidget from "./widgets/CategoryWidget.jsx";
 import TextWidget from "./widgets/TextWidget.jsx";
+import ParameterFieldWidget from "./widgets/ParameterFieldWidget";
+
+import { fetchField, fetchFieldValues } from "metabase/redux/metadata";
+import {
+  getMetadata,
+  makeGetMergedParameterFieldValues,
+} from "metabase/selectors/metadata";
 
 import S from "./ParameterWidget.css";
 
 import cx from "classnames";
-import _ from "underscore"
+import _ from "underscore";
 
 const DATE_WIDGETS = {
-    "date/single": DateSingleWidget,
-    "date/range": DateRangeWidget,
-    "date/relative": DateRelativeWidget,
-    "date/month-year": DateMonthYearWidget,
-    "date/quarter-year": DateQuarterYearWidget,
-    "date/all-options": DateAllOptionsWidget
-}
-
-import { fetchFieldValues } from "metabase/redux/metadata";
-import { makeGetMergedParameterFieldValues } from "metabase/selectors/metadata";
+  "date/single": DateSingleWidget,
+  "date/range": DateRangeWidget,
+  "date/relative": DateRelativeWidget,
+  "date/month-year": DateMonthYearWidget,
+  "date/quarter-year": DateQuarterYearWidget,
+  "date/all-options": DateAllOptionsWidget,
+};
 
 const makeMapStateToProps = () => {
-    const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
-    const mapStateToProps = (state, props) => ({
-        values: getMergedParameterFieldValues(state, props),
-    })
-    return mapStateToProps;
-}
+  const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
+  const mapStateToProps = (state, props) => ({
+    metadata: getMetadata(state),
+    values: getMergedParameterFieldValues(state, props),
+  });
+  return mapStateToProps;
+};
 
 const mapDispatchToProps = {
-    fetchFieldValues
-}
+  fetchFieldValues,
+  fetchField,
+};
 
 @connect(makeMapStateToProps, mapDispatchToProps)
 export default class ParameterValueWidget extends Component {
+  static propTypes = {
+    parameter: PropTypes.object.isRequired,
+    name: PropTypes.string,
+    value: PropTypes.any,
+    setValue: PropTypes.func.isRequired,
+    placeholder: PropTypes.string,
+    isEditing: PropTypes.bool,
+    noReset: PropTypes.bool,
+    commitImmediately: PropTypes.bool,
+    focusChanged: PropTypes.func,
+    isFullscreen: PropTypes.bool,
+    className: PropTypes.string,
 
-    static propTypes = {
-        parameter: PropTypes.object.isRequired,
-        name: PropTypes.string,
-        value: PropTypes.any,
-        setValue: PropTypes.func.isRequired,
-        placeholder: PropTypes.string,
-        values: PropTypes.array,
-        isEditing: PropTypes.bool,
-        noReset: PropTypes.bool,
-        commitImmediately: PropTypes.bool,
-        focusChanged: PropTypes.func,
-        isFullscreen: PropTypes.bool,
-        className: PropTypes.string
-    };
+    // provided by @connect
+    values: PropTypes.array,
+    metadata: PropTypes.object.isRequired,
+  };
 
-    static defaultProps = {
-        values: [],
-        isEditing: false,
-        noReset: false,
-        commitImmediately: false,
-        className: ""
-    };
+  static defaultProps = {
+    values: [],
+    isEditing: false,
+    noReset: false,
+    commitImmediately: false,
+    className: "",
+  };
 
-    static getWidget(parameter, values) {
-        if (DATE_WIDGETS[parameter.type]) {
-            return DATE_WIDGETS[parameter.type];
-        } else if (values && values.length > 0) {
-            return CategoryWidget;
-        } else {
-            return TextWidget;
-        }
-    }
+  getField() {
+    const { parameter, metadata } = this.props;
+    return parameter.field_id != null
+      ? metadata.fields[parameter.field_id]
+      : null;
+  }
 
-    static getParameterIconName(parameterType) {
-        if (parameterType.search(/date/) !== -1) return "calendar";
-        if (parameterType.search(/location/) !== -1) return "location";
-        if (parameterType.search(/id/) !== -1) return "label";
-        return "clipboard";
+  getWidget() {
+    const { parameter, values } = this.props;
+    if (DATE_WIDGETS[parameter.type]) {
+      return DATE_WIDGETS[parameter.type];
+    } else if (this.getField()) {
+      return ParameterFieldWidget;
+    } else if (values && values.length > 0) {
+      return CategoryWidget;
+    } else {
+      return TextWidget;
     }
+  }
 
-    state = { isFocused: false };
+  static getParameterIconName(parameterType) {
+    if (parameterType.search(/date/) !== -1) return "calendar";
+    if (parameterType.search(/location/) !== -1) return "location";
+    if (parameterType.search(/id/) !== -1) return "label";
+    return "clipboard";
+  }
 
-    componentWillMount() {
-        // In public dashboards we receive field values before mounting this component and
-        // without need to call `fetchFieldValues` separately
-        if (_.isEmpty(this.props.values)) {
-            this.updateFieldValues(this.props);
-        }
-    }
+  state = { isFocused: false };
 
-    componentWillReceiveProps(nextProps) {
-        if (nextProps.parameter.field_id != null && nextProps.parameter.field_id !== this.props.parameter.field_id) {
-            this.updateFieldValues(nextProps);
-        }
+  componentWillMount() {
+    // In public dashboards we receive field values before mounting this component and
+    // without need to call `fetchFieldValues` separately
+    if (_.isEmpty(this.props.values)) {
+      this.updateFieldValues(this.props);
     }
+  }
 
-    updateFieldValues(props) {
-        if (props.parameter.field_id != null) {
-            props.fetchFieldValues(props.parameter.field_id)
-        }
+  componentWillReceiveProps(nextProps) {
+    if (
+      nextProps.parameter.field_id != null &&
+      nextProps.parameter.field_id !== this.props.parameter.field_id
+    ) {
+      this.updateFieldValues(nextProps);
     }
+  }
 
-    render() {
-        const {parameter, value, values, setValue, isEditing, placeholder, isFullscreen,
-               noReset, commitImmediately, className, focusChanged: parentFocusChanged} = this.props;
-
-        let hasValue = value != null;
-
-        let Widget = ParameterValueWidget.getWidget(parameter, values);
-
-        const focusChanged = (isFocused) => {
-            if (parentFocusChanged) {
-                parentFocusChanged(isFocused);
-            }
-            this.setState({isFocused})
-        };
-
-        const getParameterTypeIcon = () => {
-            if (!isEditing && !hasValue && !this.state.isFocused) {
-                return <Icon name={ParameterValueWidget.getParameterIconName(parameter.type)} className="flex-align-left mr1 flex-no-shrink" size={14} />
-            } else {
-                return null;
-            }
-        };
-
-        const getWidgetStatusIcon = () => {
-            if (isFullscreen) return null;
-
-            if (hasValue && !noReset) {
-                return <Icon name="close" className="flex-align-right cursor-pointer flex-no-shrink" size={12} onClick={(e) => {
-                            if (hasValue) {
-                                e.stopPropagation();
-                                setValue(null);
-                            }
-                        }}/>
-            } else if (Widget.noPopover && this.state.isFocused) {
-                return <Icon name="enterorreturn" className="flex-align-right flex-no-shrink" size={12}/>
-            } else if (Widget.noPopover) {
-                return <Icon name="empty" className="flex-align-right cursor-pointer flex-no-shrink" size={12}/>
-            } else if (!Widget.noPopover) {
-                return <Icon name="chevrondown" className="flex-align-right flex-no-shrink" size={12}/>
-            }
-        };
-
-        if (Widget.noPopover) {
-            return (
-                <div className={cx(S.parameter, S.noPopover, className, { [S.selected]: hasValue, [S.isEditing]: isEditing})}>
-                    { getParameterTypeIcon() }
-                    <Widget placeholder={placeholder} value={value} values={values} setValue={setValue}
-                            isEditing={isEditing} commitImmediately={commitImmediately} focusChanged={focusChanged}/>
-                    { getWidgetStatusIcon() }
-                </div>
-            );
-        } else {
-            let placeholderText = isEditing ? t`Select a default value…` : (placeholder || t`Select…`);
-
-            return (
-                <PopoverWithTrigger
-                    ref="valuePopover"
-                    triggerElement={
-                        <div ref="trigger" className={cx(S.parameter, className, { [S.selected]: hasValue })}>
-                            { getParameterTypeIcon() }
-                            <div className="mr1 text-nowrap">{ hasValue ? Widget.format(value, values) : placeholderText }</div>
-                            { getWidgetStatusIcon() }
-                        </div>
-                    }
-                    target={() => this.refs.trigger} // not sure why this is necessary
-                    // make sure the full date picker will expand to fit the dual calendars
-                    autoWidth={parameter.type === "date/all-options"}
-                >
-                    <Widget value={value} values={values} setValue={setValue}
-                            onClose={() => this.refs.valuePopover.close()}/>
-                </PopoverWithTrigger>
-            );
-        }
+  updateFieldValues(props) {
+    if (props.parameter.field_id != null) {
+      props.fetchField(props.parameter.field_id);
+      props.fetchFieldValues(props.parameter.field_id);
     }
+  }
+
+  render() {
+    const {
+      parameter,
+      value,
+      values,
+      setValue,
+      isEditing,
+      placeholder,
+      isFullscreen,
+      noReset,
+      commitImmediately,
+      className,
+      focusChanged: parentFocusChanged,
+    } = this.props;
+
+    let hasValue = value != null;
 
+    let Widget = this.getWidget();
+
+    const focusChanged = isFocused => {
+      if (parentFocusChanged) {
+        parentFocusChanged(isFocused);
+      }
+      this.setState({ isFocused });
+    };
+
+    const getParameterTypeIcon = () => {
+      if (!isEditing && !hasValue && !this.state.isFocused) {
+        return (
+          <Icon
+            name={ParameterValueWidget.getParameterIconName(parameter.type)}
+            className="flex-align-left mr1 flex-no-shrink"
+            size={14}
+          />
+        );
+      } else {
+        return null;
+      }
+    };
+
+    const getWidgetStatusIcon = () => {
+      if (isFullscreen) return null;
+
+      if (hasValue && !noReset) {
+        return (
+          <Icon
+            name="close"
+            className="flex-align-right cursor-pointer flex-no-shrink"
+            size={12}
+            onClick={e => {
+              if (hasValue) {
+                e.stopPropagation();
+                setValue(null);
+              }
+            }}
+          />
+        );
+      } else if (Widget.noPopover && this.state.isFocused) {
+        return (
+          <Icon
+            name="enterorreturn"
+            className="flex-align-right flex-no-shrink"
+            size={12}
+          />
+        );
+      } else if (Widget.noPopover) {
+        return (
+          <Icon
+            name="empty"
+            className="flex-align-right cursor-pointer flex-no-shrink"
+            size={12}
+          />
+        );
+      } else if (!Widget.noPopover) {
+        return (
+          <Icon
+            name="chevrondown"
+            className="flex-align-right flex-no-shrink"
+            size={12}
+          />
+        );
+      }
+    };
+
+    if (Widget.noPopover) {
+      return (
+        <div
+          className={cx(S.parameter, S.noPopover, className, {
+            [S.selected]: hasValue,
+            [S.isEditing]: isEditing,
+          })}
+        >
+          {getParameterTypeIcon()}
+          <Widget
+            placeholder={placeholder}
+            value={value}
+            values={values}
+            field={this.getField()}
+            setValue={setValue}
+            isEditing={isEditing}
+            commitImmediately={commitImmediately}
+            focusChanged={focusChanged}
+          />
+          {getWidgetStatusIcon()}
+        </div>
+      );
+    } else {
+      let placeholderText = isEditing
+        ? t`Select a default value…`
+        : placeholder || t`Select…`;
+
+      return (
+        <PopoverWithTrigger
+          ref="valuePopover"
+          triggerElement={
+            <div
+              ref="trigger"
+              className={cx(S.parameter, className, { [S.selected]: hasValue })}
+            >
+              {getParameterTypeIcon()}
+              <div className="mr1 text-nowrap">
+                {hasValue ? Widget.format(value, values) : placeholderText}
+              </div>
+              {getWidgetStatusIcon()}
+            </div>
+          }
+          target={() => this.refs.trigger} // not sure why this is necessary
+          // make sure the full date picker will expand to fit the dual calendars
+          autoWidth={parameter.type === "date/all-options"}
+        >
+          <Widget
+            value={value}
+            values={values}
+            setValue={setValue}
+            onClose={() => this.refs.valuePopover.close()}
+          />
+        </PopoverWithTrigger>
+      );
+    }
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/ParameterWidget.css b/frontend/src/metabase/parameters/components/ParameterWidget.css
index 87364a74632a16a46228008f9009d83bb90a5d3b..154d2d46d3e70ac6f7542b480b4c668c281f6d69 100644
--- a/frontend/src/metabase/parameters/components/ParameterWidget.css
+++ b/frontend/src/metabase/parameters/components/ParameterWidget.css
@@ -1,102 +1,102 @@
 :local(.container) {
-    composes: flex align-center from "style";
-    transition: opacity 500ms linear;
-    border: 2px solid var(--grey-1);
-    margin-right: 0.85em;
-    margin-bottom: 0.5em;
-    padding: 0.25em 1em 0.25em 1em;
+  composes: flex align-center from "style";
+  transition: opacity 500ms linear;
+  border: 2px solid var(--grey-1);
+  margin-right: 0.85em;
+  margin-bottom: 0.5em;
+  padding: 0.25em 1em 0.25em 1em;
 }
 
 :local(.container) legend {
-    text-transform: none;
-    position: relative;
-    height: 2px;
-    line-height: 0;
-    margin-left: -0.45em;
-    padding: 0 0.5em;
+  text-transform: none;
+  position: relative;
+  height: 2px;
+  line-height: 0;
+  margin-left: -0.45em;
+  padding: 0 0.5em;
 }
 
 :local(.container.deemphasized) {
-    opacity: 0.4;
+  opacity: 0.4;
 }
 :local(.container.deemphasized:hover) {
-    opacity: 1;
+  opacity: 1;
 }
 
 :local(.parameter) {
-    composes: flex align-center from "style";
-    font-weight: 600;
-    min-height: 30px;
-    min-width: 150px;
-    color: var(--grey-4);
+  composes: flex align-center from "style";
+  font-weight: 600;
+  min-height: 30px;
+  min-width: 150px;
+  color: var(--grey-4);
 }
 
 :local(.nameInput) {
-    composes: flex align-center from "style";
-    min-height: 30px;
-    min-width: 150px;
-    color: var(--grey-4);
-    border: none;
-    font-size: 1em;
-    font-weight: 600;
-    border: none;
+  composes: flex align-center from "style";
+  min-height: 30px;
+  min-width: 150px;
+  color: var(--grey-4);
+  border: none;
+  font-size: 1em;
+  font-weight: 600;
+  border: none;
 }
 
 :local(.fullscreen) {
-    margin-right: 0;
-    margin-left: 0;
+  margin-right: 0;
+  margin-left: 0;
 }
 :local(.fullscreen .name) {
-    font-size: 14px;
+  font-size: 14px;
 }
 :local(.fullscreen .parameter) {
-    min-width: 0;
-    min-height: 0;
-    background-color: transparent;
-    font-size: 14px;
+  min-width: 0;
+  min-height: 0;
+  background-color: transparent;
+  font-size: 14px;
 }
 
 :local(.parameter.selected) {
-    font-weight: bold;
-    color: var(--brand-color);
-    border-color: var(--brand-color);
+  font-weight: bold;
+  color: var(--brand-color);
+  border-color: var(--brand-color);
 }
 
 :local(.parameter.noPopover) input {
-    /* NOTE: Fixed with to circumvent issues with flexbox with container having a min-width */
-    width: 115px;
-    font-size: 1em;
-    font-weight: 600;
-    border: none;
-    background: none;
+  /* NOTE: Fixed with to circumvent issues with flexbox with container having a min-width */
+  width: 115px;
+  font-size: 1em;
+  font-weight: 600;
+  border: none;
+  background: none;
 }
 
 :local(.parameter.noPopover.isEditing) input {
-    width: 138px;
+  width: 138px;
 }
 
 :local(.parameter.noPopover.selected) input {
-    width: 127px;
-    font-weight: bold;
-    color: var(--brand-color);
+  width: 127px;
+  font-weight: bold;
+  color: var(--brand-color);
 }
 
 :local(.parameter.noPopover) input:focus {
-    outline: none;
-    color: var(--default-font-color);
-    width: 127px;
+  outline: none;
+  color: var(--default-font-color);
+  width: 127px;
 }
 :local(.parameter.noPopover) input::-webkit-input-placeholder {
-    color: var(--grey-4);
+  color: var(--grey-4);
 }
 :local(.parameter.noPopover) input:-moz-placeholder {
-    color: var(--grey-4);
+  color: var(--grey-4);
 }
 :local(.parameter.noPopover) input::-moz-placeholder {
-    color: var(--grey-4);
+  color: var(--grey-4);
 }
 :local(.parameter.noPopover) input:-ms-input-placeholder {
-    color: var(--grey-4);
+  color: var(--grey-4);
 }
 
 :local(.input) {
@@ -104,44 +104,43 @@
 
 :local(.nameInput:focus),
 :local(.input:focus) {
-    outline: none;
+  outline: none;
 }
 
 :local(.name) {
-    composes: mr1 from "style";
-    font-size: 16px;
-    font-weight: bold;
-    color: var(--grey-4);
+  composes: mr1 from "style";
+  font-size: 16px;
+  font-weight: bold;
+  color: var(--grey-4);
 }
 
-
 :local(.parameterButtons) {
-    display: flex;
-    justify-content: space-around;
-    font-size: smaller;
+  display: flex;
+  justify-content: space-around;
+  font-size: smaller;
 }
 
 :local(.editButton),
 :local(.removeButton) {
-    composes: flex align-center cursor-pointer from "style";
+  composes: flex align-center cursor-pointer from "style";
 }
 
 :local(.editButton:hover) {
-    color: var(--brand-color);
+  color: var(--brand-color);
 }
 
 :local(.removeButton:hover) {
-    color: var(--warning-color);
+  color: var(--warning-color);
 }
 
 :local(.editNameIconContainer) {
-    display: inline-block;
-    height: 0;
-    margin-left: 0.25em;
-    width: 10px;
+  display: inline-block;
+  height: 0;
+  margin-left: 0.25em;
+  width: 10px;
 }
 
 :local(.editNameIconContainer) > svg {
-    position: absolute;
-    top: -6px;
-}
\ No newline at end of file
+  position: absolute;
+  top: -6px;
+}
diff --git a/frontend/src/metabase/parameters/components/ParameterWidget.jsx b/frontend/src/metabase/parameters/components/ParameterWidget.jsx
index b2738ccb5593a18ca0ac2ce4ad7942771f87b666..14ba7fcce10806b1e54b67fbd3e2c961e4fa5e45 100644
--- a/frontend/src/metabase/parameters/components/ParameterWidget.jsx
+++ b/frontend/src/metabase/parameters/components/ParameterWidget.jsx
@@ -1,7 +1,6 @@
-
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ParameterValueWidget from "./ParameterValueWidget.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
@@ -14,131 +13,192 @@ import FieldSet from "../../components/FieldSet";
 import { KEYCODE_ENTER, KEYCODE_ESCAPE } from "metabase/lib/keyboard";
 
 export default class ParameterWidget extends Component {
-    state = {
-        isEditingName: false,
-        isFocused: false
+  state = {
+    isEditingName: false,
+    isFocused: false,
+  };
+
+  static propTypes = {
+    parameter: PropTypes.object,
+    commitImmediately: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    parameter: null,
+    commitImmediately: false,
+  };
+
+  renderPopover(value, setValue, placeholder, isFullscreen) {
+    const { parameter, editingParameter, commitImmediately } = this.props;
+    const isEditingParameter = !!(
+      editingParameter && editingParameter.id === parameter.id
+    );
+    return (
+      <ParameterValueWidget
+        parameter={parameter}
+        name={name}
+        value={value}
+        setValue={setValue}
+        isEditing={isEditingParameter}
+        placeholder={placeholder}
+        focusChanged={this.focusChanged}
+        isFullscreen={isFullscreen}
+        commitImmediately={commitImmediately}
+      />
+    );
+  }
+
+  focusChanged = isFocused => {
+    this.setState({ isFocused });
+  };
+
+  render() {
+    const {
+      className,
+      parameter,
+      parameters,
+      isEditing,
+      isFullscreen,
+      editingParameter,
+      setEditingParameter,
+      setName,
+      setValue,
+      setDefaultValue,
+      remove,
+    } = this.props;
+
+    const isEditingDashboard = isEditing;
+    const isEditingParameter =
+      editingParameter && editingParameter.id === parameter.id;
+
+    const renderFieldInNormalMode = () => {
+      const fieldHasValueOrFocus =
+        parameter.value != null || this.state.isFocused;
+      const legend = fieldHasValueOrFocus ? parameter.name : "";
+
+      return (
+        <FieldSet
+          legend={legend}
+          noPadding={true}
+          className={cx(className, S.container, {
+            "border-brand": fieldHasValueOrFocus,
+          })}
+        >
+          {this.renderPopover(
+            parameter.value,
+            value => setValue(value),
+            parameter.name,
+            isFullscreen,
+          )}
+        </FieldSet>
+      );
     };
 
-    static propTypes = {
-        parameter: PropTypes.object,
-        commitImmediately: PropTypes.bool
+    const renderEditFieldNameUI = () => {
+      return (
+        <FieldSet
+          legend=""
+          noPadding={true}
+          className={cx(className, S.container)}
+        >
+          <input
+            type="text"
+            className={cx(S.nameInput, {
+              "border-error": _.any(
+                parameters,
+                p => p.name === parameter.name && p.id !== parameter.id,
+              ),
+            })}
+            value={parameter.name}
+            onChange={e => setName(e.target.value)}
+            onBlur={() => this.setState({ isEditingName: false })}
+            onKeyUp={e => {
+              if (e.keyCode === KEYCODE_ESCAPE || e.keyCode === KEYCODE_ENTER) {
+                e.target.blur();
+              }
+            }}
+            autoFocus
+          />
+        </FieldSet>
+      );
     };
 
-    static defaultProps = {
-        parameter: null,
-        commitImmediately: false
-    }
+    const renderSetDefaultFieldValueUI = () => {
+      const editNameButton = (
+        <span className={S.editNameIconContainer}>
+          <Icon
+            name="pencil"
+            size={12}
+            className="text-brand cursor-pointer"
+            onClick={() => this.setState({ isEditingName: true })}
+          />
+        </span>
+      );
+
+      const legend = (
+        <span>
+          {parameter.name} {editNameButton}
+        </span>
+      );
+
+      return (
+        <FieldSet
+          legend={legend}
+          noPadding={true}
+          className={cx(className, S.container)}
+        >
+          {this.renderPopover(
+            parameter.default,
+            value => setDefaultValue(value),
+            parameter.name,
+            isFullscreen,
+          )}
+        </FieldSet>
+      );
+    };
+
+    const renderFieldEditingButtons = () => {
+      return (
+        <FieldSet
+          legend={parameter.name}
+          noPadding={true}
+          className={cx(className, S.container)}
+        >
+          <div className={cx(S.parameter, S.parameterButtons)}>
+            <div
+              className={S.editButton}
+              onClick={() => setEditingParameter(parameter.id)}
+            >
+              <Icon name="pencil" />
+              <span className="ml1">{t`Edit`}</span>
+            </div>
+            <div className={S.removeButton} onClick={() => remove()}>
+              <Icon name="close" />
+              <span className="ml1">{t`Remove`}</span>
+            </div>
+          </div>
+        </FieldSet>
+      );
+    };
 
-    renderPopover(value, setValue, placeholder, isFullscreen) {
-        const {parameter, editingParameter, commitImmediately} = this.props;
-        const isEditingParameter = !!(editingParameter && editingParameter.id === parameter.id);
+    if (isFullscreen) {
+      if (parameter.value != null) {
         return (
-            <ParameterValueWidget
-                parameter={parameter}
-                name={name}
-                value={value}
-                setValue={setValue}
-                isEditing={isEditingParameter}
-                placeholder={placeholder}
-                focusChanged={this.focusChanged}
-                isFullscreen={isFullscreen}
-                commitImmediately={commitImmediately}
-            />
+          <div style={{ fontSize: "0.833em" }}>{renderFieldInNormalMode()}</div>
         );
+      } else {
+        return <span className="hide" />;
+      }
+    } else if (!isEditingDashboard) {
+      return renderFieldInNormalMode();
+    } else if (isEditingParameter) {
+      if (this.state.isEditingName) {
+        return renderEditFieldNameUI();
+      } else {
+        return renderSetDefaultFieldValueUI();
+      }
+    } else {
+      return renderFieldEditingButtons();
     }
-
-    focusChanged = (isFocused) => {
-        this.setState({isFocused})
-    }
-
-    render() {
-        const {className, parameter, parameters, isEditing, isFullscreen, editingParameter, setEditingParameter, setName, setValue, setDefaultValue, remove} = this.props;
-
-        const isEditingDashboard = isEditing;
-        const isEditingParameter = editingParameter && editingParameter.id === parameter.id;
-
-        const renderFieldInNormalMode = () => {
-            const fieldHasValueOrFocus = parameter.value != null || this.state.isFocused;
-            const legend = fieldHasValueOrFocus ? parameter.name : "";
-
-            return (
-                <FieldSet legend={legend} noPadding={true}
-                          className={cx(className, S.container, {"border-brand": fieldHasValueOrFocus})}>
-                    {this.renderPopover(parameter.value, (value) => setValue(value), parameter.name, isFullscreen)}
-                </FieldSet>
-            );
-        };
-
-        const renderEditFieldNameUI = () => {
-            return (
-                <FieldSet legend="" noPadding={true} className={cx(className, S.container)}>
-                    <input
-                        type="text"
-                        className={cx(S.nameInput, { "border-error": _.any(parameters, (p) => p.name === parameter.name && p.id !== parameter.id) })}
-                        value={parameter.name}
-                        onChange={(e) => setName(e.target.value)}
-                        onBlur={() => this.setState({ isEditingName: false })}
-                        onKeyUp={(e) => {
-                                if (e.keyCode === KEYCODE_ESCAPE || e.keyCode === KEYCODE_ENTER) {
-                                    e.target.blur();
-                                }
-                            }}
-                        autoFocus
-                    />
-                </FieldSet>
-            )
-        };
-
-        const renderSetDefaultFieldValueUI = () => {
-            const editNameButton = (
-                <span className={S.editNameIconContainer}>
-                <Icon name="pencil" size={12} className="text-brand cursor-pointer"
-                      onClick={() => this.setState({ isEditingName: true })}/>
-                </span>
-            )
-
-            const legend = <span>{parameter.name} {editNameButton}</span>
-
-            return (
-                <FieldSet legend={legend} noPadding={true} className={cx(className, S.container)}>
-                    {this.renderPopover(parameter.default, (value) => setDefaultValue(value), parameter.name, isFullscreen)}
-                </FieldSet>
-            );
-        };
-
-        const renderFieldEditingButtons = () => {
-            return (
-                <FieldSet legend={parameter.name} noPadding={true} className={cx(className, S.container)}>
-                    <div className={cx(S.parameter, S.parameterButtons)}>
-                        <div className={S.editButton} onClick={() => setEditingParameter(parameter.id)}>
-                            <Icon name="pencil"/>
-                            <span className="ml1">{t`Edit`}</span>
-                        </div>
-                        <div className={S.removeButton} onClick={() => remove()}>
-                            <Icon name="close"/>
-                            <span className="ml1">{t`Remove`}</span>
-                        </div>
-                    </div>
-                </FieldSet>
-            );
-        };
-
-        if (isFullscreen) {
-            if (parameter.value != null) {
-                return <div style={{fontSize: "0.833em"}}>{renderFieldInNormalMode()}</div>;
-            } else {
-                return <span className="hide"/>;
-            }
-        } else if (!isEditingDashboard) {
-            return renderFieldInNormalMode();
-        } else if (isEditingParameter) {
-            if (this.state.isEditingName) {
-                return renderEditFieldNameUI()
-            } else {
-                return renderSetDefaultFieldValueUI()
-            }
-        } else {
-            return renderFieldEditingButtons()
-        }
-    }
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/Parameters.jsx b/frontend/src/metabase/parameters/components/Parameters.jsx
index d221bc070a6447184e265b535a07c0eebc809730..93e31ca306c139ae86d2bdb4615137cb455c788a 100644
--- a/frontend/src/metabase/parameters/components/Parameters.jsx
+++ b/frontend/src/metabase/parameters/components/Parameters.jsx
@@ -8,123 +8,151 @@ import querystring from "querystring";
 import cx from "classnames";
 
 import type { QueryParams } from "metabase/meta/types";
-import type { ParameterId, Parameter, ParameterValues } from "metabase/meta/types/Parameter";
+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,
-}
+  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: Props;
-
-    defaultProps = {
-        syncQueryString: false,
-        vertical: false,
-        commitImmediately: false
-    }
-
-    componentWillMount() {
-        // sync parameters from URL query string
-        const { parameters, setParameterValue, query } = this.props;
-        if (setParameterValue) {
-            for (const parameter of parameters) {
-                if (query && query[parameter.slug] != null) {
-                    setParameterValue(parameter.id, query[parameter.slug]);
-                } else if (parameter.default != null) {
-                    setParameterValue(parameter.id, parameter.default);
-                }
-            }
+  props: Props;
+
+  defaultProps = {
+    syncQueryString: false,
+    vertical: false,
+    commitImmediately: false,
+  };
+
+  componentWillMount() {
+    // sync parameters from URL query string
+    const { parameters, setParameterValue, query } = this.props;
+    if (setParameterValue) {
+      for (const parameter of parameters) {
+        if (query && query[parameter.slug] != null) {
+          setParameterValue(parameter.id, query[parameter.slug]);
+        } else if (parameter.default != null) {
+          setParameterValue(parameter.id, parameter.default);
         }
+      }
     }
-
-    componentDidUpdate() {
-        if (this.props.syncQueryString) {
-            // sync parameters to URL query string
-            const queryParams = {};
-            for (const parameter of this._parametersWithValues()) {
-                if (parameter.value) {
-                    queryParams[parameter.slug] = parameter.value;
-                }
-            }
-
-            let search = querystring.stringify(queryParams);
-            search = (search ? "?" + search : "");
-
-            if (search !== window.location.search) {
-                history.replaceState(null, document.title, window.location.pathname + search + window.location.hash);
-            }
+  }
+
+  componentDidUpdate() {
+    if (this.props.syncQueryString) {
+      // sync parameters to URL query string
+      const queryParams = {};
+      for (const parameter of this._parametersWithValues()) {
+        if (parameter.value) {
+          queryParams[parameter.slug] = parameter.value;
         }
-    }
+      }
 
-    _parametersWithValues() {
-        const { parameters, parameterValues } = this.props;
-        if (parameterValues) {
-            return parameters.map(p => ({
-                ...p,
-                value: parameterValues[p.id]
-            }));
-        } else {
-            return parameters;
-        }
-    }
+      let search = querystring.stringify(queryParams);
+      search = search ? "?" + search : "";
 
-    render() {
-        const {
-            className,
-            editingParameter, setEditingParameter,
-            isEditing, isFullscreen, isNightMode, isQB,
-            setParameterName, setParameterValue, setParameterDefaultValue, removeParameter,
-            vertical,
-            commitImmediately
-        } = this.props;
-
-        const parameters = this._parametersWithValues();
-
-        return (
-            <div className={cx(className, "flex align-end flex-wrap", vertical ? "flex-column" : "flex-row", {"mt1": isQB})}>
-                { parameters.map(parameter =>
-                    <ParameterWidget
-                        className={vertical ? "mb2" : null}
-                        key={parameter.id}
-
-                        isEditing={isEditing}
-                        isFullscreen={isFullscreen}
-                        isNightMode={isNightMode}
-
-                        parameter={parameter}
-                        parameters={parameters}
-
-                        editingParameter={editingParameter}
-                        setEditingParameter={setEditingParameter}
-
-                        setName={setParameterName && ((name) => setParameterName(parameter.id, name))}
-                        setValue={setParameterValue && ((value) => setParameterValue(parameter.id, value))}
-                        setDefaultValue={setParameterDefaultValue && ((value) => setParameterDefaultValue(parameter.id, value))}
-                        remove={removeParameter && (() => removeParameter(parameter.id))}
-
-                        commitImmediately={commitImmediately}
-                    />
-                ) }
-            </div>
+      if (search !== window.location.search) {
+        history.replaceState(
+          null,
+          document.title,
+          window.location.pathname + search + window.location.hash,
         );
+      }
     }
+  }
+
+  _parametersWithValues() {
+    const { parameters, parameterValues } = this.props;
+    if (parameterValues) {
+      return parameters.map(p => ({
+        ...p,
+        value: parameterValues[p.id],
+      }));
+    } else {
+      return parameters;
+    }
+  }
+
+  render() {
+    const {
+      className,
+      editingParameter,
+      setEditingParameter,
+      isEditing,
+      isFullscreen,
+      isNightMode,
+      isQB,
+      setParameterName,
+      setParameterValue,
+      setParameterDefaultValue,
+      removeParameter,
+      vertical,
+      commitImmediately,
+    } = this.props;
+
+    const parameters = this._parametersWithValues();
+
+    return (
+      <div
+        className={cx(
+          className,
+          "flex align-end flex-wrap",
+          vertical ? "flex-column" : "flex-row",
+          { mt1: isQB },
+        )}
+      >
+        {parameters.map(parameter => (
+          <ParameterWidget
+            className={vertical ? "mb2" : null}
+            key={parameter.id}
+            isEditing={isEditing}
+            isFullscreen={isFullscreen}
+            isNightMode={isNightMode}
+            parameter={parameter}
+            parameters={parameters}
+            editingParameter={editingParameter}
+            setEditingParameter={setEditingParameter}
+            setName={
+              setParameterName && (name => setParameterName(parameter.id, name))
+            }
+            setValue={
+              setParameterValue &&
+              (value => setParameterValue(parameter.id, value))
+            }
+            setDefaultValue={
+              setParameterDefaultValue &&
+              (value => setParameterDefaultValue(parameter.id, value))
+            }
+            remove={removeParameter && (() => removeParameter(parameter.id))}
+            commitImmediately={commitImmediately}
+          />
+        ))}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/widgets/CategoryWidget.jsx b/frontend/src/metabase/parameters/components/widgets/CategoryWidget.jsx
index 2291d1e71f795db62b61d310f5dd943af97f62b6..2e09d803b9258afb544ac1cbe0ac3d39d868a247 100644
--- a/frontend/src/metabase/parameters/components/widgets/CategoryWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/CategoryWidget.jsx
@@ -8,109 +8,105 @@ import { t } from "c-3po";
 import { createMultiwordSearchRegex } from "metabase/lib/string";
 import { getHumanReadableValue } from "metabase/lib/query/field";
 
-import SelectPicker
-    from "../../../query_builder/components/filters/pickers/SelectPicker.jsx";
+import SelectPicker from "../../../query_builder/components/filters/pickers/SelectPicker.jsx";
 
 type Props = {
-    value: any,
-    values: any[],
-    setValue: () => void,
-    onClose: () => void
+  value: any,
+  values: any[],
+  setValue: () => void,
+  onClose: () => void,
 };
 type State = {
-    searchText: string,
-    searchRegex: ?RegExp,
-    selectedValues: Array<string>
+  searchText: string,
+  searchRegex: ?RegExp,
+  selectedValues: Array<string>,
 };
 
 export default class CategoryWidget extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            searchText: "",
-            searchRegex: null,
-            selectedValues: Array.isArray(props.value)
-                ? props.value
-                : [props.value]
-        };
-    }
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
 
-    static propTypes = {
-        value: PropTypes.any,
-        values: PropTypes.array.isRequired,
-        setValue: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired
+    this.state = {
+      searchText: "",
+      searchRegex: null,
+      selectedValues: Array.isArray(props.value) ? props.value : [props.value],
     };
+  }
 
-    updateSearchText = (value: string) => {
-        let regex = null;
+  static propTypes = {
+    value: PropTypes.any,
+    values: PropTypes.array.isRequired,
+    setValue: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+  };
 
-        if (value) {
-            regex = createMultiwordSearchRegex(value);
-        }
+  updateSearchText = (value: string) => {
+    let regex = null;
 
-        this.setState({
-            searchText: value,
-            searchRegex: regex
-        });
-    };
-
-    static format(values, fieldValues) {
-        if (Array.isArray(values) && values.length > 1) {
-            return `${values.length} selections`;
-        } else {
-            return getHumanReadableValue(values, fieldValues);
-        }
+    if (value) {
+      regex = createMultiwordSearchRegex(value);
     }
 
-    getOptions() {
-        return this.props.values.slice().map(value => {
-            return {
-                name: value[0],
-                key: value[0]
-            };
-        });
+    this.setState({
+      searchText: value,
+      searchRegex: regex,
+    });
+  };
+
+  static format(values, fieldValues) {
+    if (Array.isArray(values) && values.length > 1) {
+      return `${values.length} selections`;
+    } else {
+      return getHumanReadableValue(values, fieldValues);
     }
-
-    commitValues = (values: ?Array<string>) => {
-        if (values && values.length === 0) {
-            values = null;
-        }
-        this.props.setValue(values);
-        this.props.onClose();
-    };
-
-    onSelectedValuesChange = (values: Array<string>) => {
-        this.setState({ selectedValues: values });
-    };
-
-    render() {
-        const options = this.getOptions();
-        const selectedValues = this.state.selectedValues;
-
-        return (
-            <div style={{ minWidth: 182 }}>
-                <SelectPicker
-                    options={options}
-                    values={(selectedValues: Array<string>)}
-                    onValuesChange={this.onSelectedValuesChange}
-                    multi={true}
-                />
-                <div className="p1">
-                    <button
-                        data-ui-tag="add-category-filter"
-                        className="Button Button--purple full"
-                        onClick={() =>
-                            this.commitValues(this.state.selectedValues)}
-                    >
-                        {t`Done`}
-                    </button>
-                </div>
-            </div>
-        );
+  }
+
+  getOptions() {
+    return this.props.values.slice().map(value => {
+      return {
+        name: value[0],
+        key: value[0],
+      };
+    });
+  }
+
+  commitValues = (values: ?Array<string>) => {
+    if (values && values.length === 0) {
+      values = null;
     }
+    this.props.setValue(values);
+    this.props.onClose();
+  };
+
+  onSelectedValuesChange = (values: Array<string>) => {
+    this.setState({ selectedValues: values });
+  };
+
+  render() {
+    const options = this.getOptions();
+    const selectedValues = this.state.selectedValues;
+
+    return (
+      <div style={{ minWidth: 182 }}>
+        <SelectPicker
+          options={options}
+          values={(selectedValues: Array<string>)}
+          onValuesChange={this.onSelectedValuesChange}
+          multi={true}
+        />
+        <div className="p1">
+          <button
+            data-ui-tag="add-category-filter"
+            className="Button Button--purple full"
+            onClick={() => this.commitValues(this.state.selectedValues)}
+          >
+            {t`Done`}
+          </button>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/widgets/DateAllOptionsWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateAllOptionsWidget.jsx
index 553b70822d24cf9f901e6a4247d7bd74feec645e..b65df6532d75c794412b5c59534f930e9f08e3f2 100644
--- a/frontend/src/metabase/parameters/components/widgets/DateAllOptionsWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/DateAllOptionsWidget.jsx
@@ -1,15 +1,18 @@
 /* @flow */
 
-import React, {Component} from "react";
+import React, { Component } from "react";
 import cx from "classnames";
-import { t } from 'c-3po';
-import DatePicker, {DATE_OPERATORS} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
+import { t } from "c-3po";
+import DatePicker, {
+  DATE_OPERATORS,
+  getOperator,
+} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
 import FilterOptions from "metabase/query_builder/components/filters/FilterOptions.jsx";
-import {generateTimeFilterValuesDescriptions} from "metabase/lib/query_time";
+import { generateTimeFilterValuesDescriptions } from "metabase/lib/query_time";
 import { dateParameterValueToMBQL } from "metabase/meta/Parameter";
 
-import type {OperatorName} from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
-import type {FieldFilter} from "metabase/meta/types/Query";
+import type { OperatorName } from "metabase/query_builder/components/filters/pickers/DatePicker.jsx";
+import type { FieldFilter } from "metabase/meta/types/Query";
 
 type UrlEncoded = string;
 
@@ -17,106 +20,134 @@ type UrlEncoded = string;
 // $FlowFixMe
 const noopRef: LocalFieldReference = null;
 
-function getFilterValueSerializer(func: ((val1: string, val2: string) => UrlEncoded)) {
-    // $FlowFixMe
-    return filter => func(filter[2], filter[3], filter[4] || {})
+function getFilterValueSerializer(
+  func: (val1: string, val2: string) => UrlEncoded,
+) {
+  // $FlowFixMe
+  return filter => func(filter[2], filter[3], filter[4] || {});
 }
 
-const serializersByOperatorName: { [id: OperatorName]: (FieldFilter) => UrlEncoded } = {
-    // $FlowFixMe
-    "previous": getFilterValueSerializer((value, unit, options = {}) => `past${-value}${unit}s${options['include-current'] ? "~" : ""}`),
-    "next": getFilterValueSerializer((value, unit, options = {}) => `next${value}${unit}s${options['include-current'] ? "~" : ""}`),
-    "current": getFilterValueSerializer((_, unit) => `this${unit}`),
-    "before": getFilterValueSerializer((value) => `~${value}`),
-    "after": getFilterValueSerializer((value) => `${value}~`),
-    "on": getFilterValueSerializer((value) => `${value}`),
-    "between": getFilterValueSerializer((from, to) => `${from}~${to}`)
+const serializersByOperatorName: {
+  [id: OperatorName]: (FieldFilter) => UrlEncoded,
+} = {
+  previous: getFilterValueSerializer(
+    (value, unit, options = {}) =>
+      // $FlowFixMe
+      `past${-value}${unit}s${options["include-current"] ? "~" : ""}`,
+  ),
+  next: getFilterValueSerializer(
+    (value, unit, options = {}) =>
+      `next${value}${unit}s${options["include-current"] ? "~" : ""}`,
+  ),
+  current: getFilterValueSerializer((_, unit) => `this${unit}`),
+  before: getFilterValueSerializer(value => `~${value}`),
+  after: getFilterValueSerializer(value => `${value}~`),
+  on: getFilterValueSerializer(value => `${value}`),
+  between: getFilterValueSerializer((from, to) => `${from}~${to}`),
 };
 
 function getFilterOperator(filter) {
-    return DATE_OPERATORS.find((op) => op.test(filter));
+  return DATE_OPERATORS.find(op => op.test(filter));
 }
 function filterToUrlEncoded(filter: FieldFilter): ?UrlEncoded {
-    const operator = getFilterOperator(filter)
+  const operator = getFilterOperator(filter);
 
-    if (operator) {
-        return serializersByOperatorName[operator.name](filter);
-    } else {
-        return null;
-    }
+  if (operator) {
+    return serializersByOperatorName[operator.name](filter);
+  } else {
+    return null;
+  }
 }
 
-
-const prefixedOperators: Set<OperatorName> = new Set(["before", "after", "on", "empty", "not-empty"]);
+const prefixedOperators: Set<OperatorName> = new Set([
+  "before",
+  "after",
+  "on",
+  "empty",
+  "not-empty",
+]);
 function getFilterTitle(filter) {
-    const desc = generateTimeFilterValuesDescriptions(filter).join(" - ")
-    const op = getFilterOperator(filter);
-    const prefix = op && prefixedOperators.has(op.name) ? `${op.displayName} ` : "";
-    return prefix + desc;
+  const desc = generateTimeFilterValuesDescriptions(filter).join(" - ");
+  const op = getFilterOperator(filter);
+  const prefix =
+    op && prefixedOperators.has(op.name) ? `${op.displayName} ` : "";
+  return prefix + desc;
 }
 
 type Props = {
-    setValue: (value: ?string) => void,
-    onClose: () => void
+  setValue: (value: ?string) => void,
+  onClose: () => void,
 };
 
 type State = { filter: FieldFilter };
 
 export default class DateAllOptionsWidget extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            // $FlowFixMe
-            filter: props.value != null ? dateParameterValueToMBQL(props.value, noopRef) || [] : []
-        }
-    }
-
-    static propTypes = {};
-    static defaultProps = {};
-
-    static format = (urlEncoded: ?string) => {
-        if (urlEncoded == null) return null;
-        const filter = dateParameterValueToMBQL(urlEncoded, noopRef);
-
-        return filter ? getFilterTitle(filter) : null;
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      filter:
+        props.value != null
+          ? // $FlowFixMe
+            dateParameterValueToMBQL(props.value, noopRef) || []
+          : // $FlowFixMe
+            [],
     };
-
-    commitAndClose = () => {
-        this.props.setValue(filterToUrlEncoded(this.state.filter));
-        this.props.onClose()
-    }
-
-    setFilter = (filter: FieldFilter) => {
-        this.setState({filter});
-    }
-
-    isValid() {
-        const filterValues = this.state.filter.slice(2);
-        return filterValues.every((value) => value != null);
-    }
-
-    render() {
-        const { filter } = this.state;
-        return (<div style={{minWidth: "300px"}}>
-            <DatePicker
-                filter={this.state.filter}
-                onFilterChange={this.setFilter}
-                hideEmptinessOperators
-                hideTimeSelectors
-            />
-            <div className="FilterPopover-footer border-top flex align-center p2">
-                <FilterOptions filter={filter} onFilterChange={this.setFilter} />
-                <button
-                    className={cx("Button Button--purple ml-auto", {"disabled": !this.isValid()})}
-                    onClick={this.commitAndClose}
-                >
-                    {t`Update filter`}
-                </button>
-            </div>
-        </div>)
-    }
+  }
+
+  static propTypes = {};
+  static defaultProps = {};
+
+  static format = (urlEncoded: ?string) => {
+    if (urlEncoded == null) return null;
+    const filter = dateParameterValueToMBQL(urlEncoded, noopRef);
+
+    return filter ? getFilterTitle(filter) : null;
+  };
+
+  commitAndClose = () => {
+    this.props.setValue(filterToUrlEncoded(this.state.filter));
+    this.props.onClose();
+  };
+
+  setFilter = (filter: FieldFilter) => {
+    this.setState({ filter });
+  };
+
+  isValid() {
+    const filterValues = this.state.filter.slice(2);
+    return filterValues.every(value => value != null);
+  }
+
+  render() {
+    const { filter } = this.state;
+    return (
+      <div style={{ minWidth: "300px" }}>
+        <DatePicker
+          filter={this.state.filter}
+          onFilterChange={this.setFilter}
+          hideEmptinessOperators
+          hideTimeSelectors
+        />
+        <div className="FilterPopover-footer border-top flex align-center p2">
+          <FilterOptions
+            filter={filter}
+            onFilterChange={this.setFilter}
+            operator={getOperator(filter)}
+          />
+          <button
+            className={cx("Button Button--purple ml-auto", {
+              disabled: !this.isValid(),
+            })}
+            onClick={this.commitAndClose}
+          >
+            {t`Update filter`}
+          </button>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/widgets/DateMonthYearWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateMonthYearWidget.jsx
index a037e3576f3cc4316021a3377d7b81304c62bc00..4c2fc9412e01c52d7c47a8fb58de442fbd5cb2ab 100644
--- a/frontend/src/metabase/parameters/components/widgets/DateMonthYearWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/DateMonthYearWidget.jsx
@@ -7,70 +7,91 @@ import _ from "underscore";
 import cx from "classnames";
 
 export default class DateMonthYearWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        let initial = moment(this.props.value, "YYYY-MM");
-        if (initial.isValid()) {
-            this.state = {
-                month: initial.month(),
-                year: initial.year()
-            };
-        } else {
-            this.state = {
-                month: null,
-                year: moment().year()
-            };
-        }
+    let initial = moment(this.props.value, "YYYY-MM");
+    if (initial.isValid()) {
+      this.state = {
+        month: initial.month(),
+        year: initial.year(),
+      };
+    } else {
+      this.state = {
+        month: null,
+        year: moment().year(),
+      };
     }
+  }
 
-    static propTypes = {};
-    static defaultProps = {};
+  static propTypes = {};
+  static defaultProps = {};
 
-    static format = (value) => {
-        const m = moment(value, "YYYY-MM");
-        return m.isValid() ? m.format("MMMM, YYYY") : "";
-    }
+  static format = value => {
+    const m = moment(value, "YYYY-MM");
+    return m.isValid() ? m.format("MMMM, YYYY") : "";
+  };
 
-    componentWillUnmount() {
-        const { month, year } = this.state;
-        if (month != null && year != null) {
-            let value = moment().year(year).month(month).format("YYYY-MM");
-            if (this.props.value !== value) {
-                this.props.setValue(value);
-            }
-        }
+  componentWillUnmount() {
+    const { month, year } = this.state;
+    if (month != null && year != null) {
+      let value = moment()
+        .year(year)
+        .month(month)
+        .format("YYYY-MM");
+      if (this.props.value !== value) {
+        this.props.setValue(value);
+      }
     }
+  }
 
-    render() {
-        const { onClose } = this.props;
-        const { month, year } = this.state;
-        return (
-            <div className="py2">
-                <div className="flex flex-column align-center px1">
-                    <YearPicker value={year} onChange={(year) => this.setState({ year: year })} />
-                </div>
-                <div className="flex">
-                    <ol className="flex flex-column">
-                    { _.range(0,6).map(m =>
-                        <Month key={m} month={m} selected={m === month} onClick={() => this.setState({ month: m }, onClose)} />
-                    )}
-                    </ol>
-                    <ol className="flex flex-column">
-                    { _.range(6,12).map(m =>
-                        <Month key={m} month={m} selected={m === month} onClick={() => this.setState({ month: m }, onClose)} />
-                    )}
-                    </ol>
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const { onClose } = this.props;
+    const { month, year } = this.state;
+    return (
+      <div className="py2">
+        <div className="flex flex-column align-center px1">
+          <YearPicker
+            value={year}
+            onChange={year => this.setState({ year: year })}
+          />
+        </div>
+        <div className="flex">
+          <ol className="flex flex-column">
+            {_.range(0, 6).map(m => (
+              <Month
+                key={m}
+                month={m}
+                selected={m === month}
+                onClick={() => this.setState({ month: m }, onClose)}
+              />
+            ))}
+          </ol>
+          <ol className="flex flex-column">
+            {_.range(6, 12).map(m => (
+              <Month
+                key={m}
+                month={m}
+                selected={m === month}
+                onClick={() => this.setState({ month: m }, onClose)}
+              />
+            ))}
+          </ol>
+        </div>
+      </div>
+    );
+  }
 }
 
-const Month = ({ month, selected, onClick}) =>
-    <li
-        className={cx("cursor-pointer px3 py1 text-bold text-brand-hover", { "text-brand": selected })}
-        onClick={onClick}
-    >
-        {moment().month(month).format("MMMM")}
-    </li>
+const Month = ({ month, selected, onClick }) => (
+  <li
+    className={cx("cursor-pointer px3 py1 text-bold text-brand-hover", {
+      "text-brand": selected,
+    })}
+    onClick={onClick}
+  >
+    {moment()
+      .month(month)
+      .format("MMMM")}
+  </li>
+);
diff --git a/frontend/src/metabase/parameters/components/widgets/DateQuarterYearWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateQuarterYearWidget.jsx
index 7fb3c4549b39968ed6fb819ff59099776cdddeab..2ce676ec775b542cadf4652feda08fe499b943e8 100644
--- a/frontend/src/metabase/parameters/components/widgets/DateQuarterYearWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/DateQuarterYearWidget.jsx
@@ -7,64 +7,83 @@ import _ from "underscore";
 import cx from "classnames";
 
 export default class DateQuarterYearWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        let initial = moment(this.props.value, "[Q]Q-YYYY");
-        if (initial.isValid()) {
-            this.state = {
-                quarter: initial.quarter(),
-                year: initial.year()
-            };
-        } else {
-            this.state = {
-                quarter: null,
-                year: moment().year()
-            };
-        }
+    let initial = moment(this.props.value, "[Q]Q-YYYY");
+    if (initial.isValid()) {
+      this.state = {
+        quarter: initial.quarter(),
+        year: initial.year(),
+      };
+    } else {
+      this.state = {
+        quarter: null,
+        year: moment().year(),
+      };
     }
+  }
 
-    static propTypes = {};
-    static defaultProps = {};
+  static propTypes = {};
+  static defaultProps = {};
 
-    static format = (value) => {
-        const m = moment(value, "[Q]Q-YYYY");
-        return m.isValid() ? m.format("[Q]Q, YYYY") : "";
-    }
+  static format = value => {
+    const m = moment(value, "[Q]Q-YYYY");
+    return m.isValid() ? m.format("[Q]Q, YYYY") : "";
+  };
 
-    componentWillUnmount() {
-        const { quarter, year } = this.state;
-        if (quarter != null && year != null) {
-            let value = moment().year(year).quarter(quarter).format("[Q]Q-YYYY");
-            if (this.props.value !== value) {
-                this.props.setValue(value);
-            }
-        }
+  componentWillUnmount() {
+    const { quarter, year } = this.state;
+    if (quarter != null && year != null) {
+      let value = moment()
+        .year(year)
+        .quarter(quarter)
+        .format("[Q]Q-YYYY");
+      if (this.props.value !== value) {
+        this.props.setValue(value);
+      }
     }
+  }
 
-    render() {
-        const { onClose } = this.props;
-        const { quarter, year } = this.state;
-        return (
-            <div className="py2">
-                <div className="flex flex-column align-center px1">
-                    <YearPicker value={year} onChange={(year) => this.setState({ year: year })} />
-                </div>
-                <ol className="flex flex-wrap bordered mx2 text-bold rounded" style={{ width: 150 }}>
-                    {_.range(1,5).map(q =>
-                        <Quarter quarter={q} selected={q === quarter} onClick={() => this.setState({ quarter: q }, onClose)} />
-                    )}
-                </ol>
-            </div>
-        );
-    }
+  render() {
+    const { onClose } = this.props;
+    const { quarter, year } = this.state;
+    return (
+      <div className="py2">
+        <div className="flex flex-column align-center px1">
+          <YearPicker
+            value={year}
+            onChange={year => this.setState({ year: year })}
+          />
+        </div>
+        <ol
+          className="flex flex-wrap bordered mx2 text-bold rounded"
+          style={{ width: 150 }}
+        >
+          {_.range(1, 5).map(q => (
+            <Quarter
+              quarter={q}
+              selected={q === quarter}
+              onClick={() => this.setState({ quarter: q }, onClose)}
+            />
+          ))}
+        </ol>
+      </div>
+    );
+  }
 }
 
-const Quarter = ({ quarter, selected, onClick}) =>
-    <li
-        className={cx("cursor-pointer bg-brand-hover text-white-hover flex layout-centered", { "bg-brand text-white": selected })}
-        style={{ width: 75, height: 75 }}
-        onClick={onClick}
-    >
-        {moment().quarter(quarter).format("[Q]Q")}
-    </li>
+const Quarter = ({ quarter, selected, onClick }) => (
+  <li
+    className={cx(
+      "cursor-pointer bg-brand-hover text-white-hover flex layout-centered",
+      { "bg-brand text-white": selected },
+    )}
+    style={{ width: 75, height: 75 }}
+    onClick={onClick}
+  >
+    {moment()
+      .quarter(quarter)
+      .format("[Q]Q")}
+  </li>
+);
diff --git a/frontend/src/metabase/parameters/components/widgets/DateRangeWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateRangeWidget.jsx
index b3c777f50653f537749d8182cbdaebbc40775e6a..b311447da4609eb7bad5efed14f7bc972c42e997 100644
--- a/frontend/src/metabase/parameters/components/widgets/DateRangeWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/DateRangeWidget.jsx
@@ -6,48 +6,52 @@ import moment from "moment";
 const SEPARATOR = "~"; // URL-safe
 
 export default class DateRangeWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            start: null,
-            end: null
-        };
-    }
-
-    static propTypes = {};
-    static defaultProps = {};
-
-    static format = (value) => {
-        const [start,end] = (value || "").split(SEPARATOR);
-        return start && end ? moment(start).format("MMMM D, YYYY") + " - " + moment(end).format("MMMM D, YYYY") : "";
-    }
-
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
-    }
-
-    componentWillReceiveProps(nextProps) {
-        const [start, end] = (nextProps.value || "").split(SEPARATOR);
-        this.setState({ start, end });
-    }
-
-    render() {
-        const { start, end } = this.state;
-        return (
-            <div className="p1">
-                <Calendar
-                    initial={start ? moment(start) : null}
-                    selected={start ? moment(start) : null}
-                    selectedEnd={end ? moment(end) : null}
-                    onChange={(start, end) => {
-                        if (end == null) {
-                            this.setState({ start, end });
-                        } else {
-                            this.props.setValue([start, end].join(SEPARATOR));
-                        }
-                    }}
-                />
-            </div>
-        )
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      start: null,
+      end: null,
+    };
+  }
+
+  static propTypes = {};
+  static defaultProps = {};
+
+  static format = value => {
+    const [start, end] = (value || "").split(SEPARATOR);
+    return start && end
+      ? moment(start).format("MMMM D, YYYY") +
+          " - " +
+          moment(end).format("MMMM D, YYYY")
+      : "";
+  };
+
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const [start, end] = (nextProps.value || "").split(SEPARATOR);
+    this.setState({ start, end });
+  }
+
+  render() {
+    const { start, end } = this.state;
+    return (
+      <div className="p1">
+        <Calendar
+          initial={start ? moment(start) : null}
+          selected={start ? moment(start) : null}
+          selectedEnd={end ? moment(end) : null}
+          onChange={(start, end) => {
+            if (end == null) {
+              this.setState({ start, end });
+            } else {
+              this.props.setValue([start, end].join(SEPARATOR));
+            }
+          }}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/widgets/DateRelativeWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateRelativeWidget.jsx
index e833b7f923642585b0b20d92efc61e0c616b179a..017550f6413de017daff4b1c1fda0e26476eccd5 100644
--- a/frontend/src/metabase/parameters/components/widgets/DateRelativeWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/DateRelativeWidget.jsx
@@ -1,173 +1,210 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 import _ from "underscore";
 
 const SHORTCUTS = [
-        { name: t`Today`,        operator: ["=", "<", ">"], values: [["relative-datetime", "current"]]},
-        { name: t`Yesterday`,    operator: ["=", "<", ">"], values: [["relative-datetime", -1, "day"]]},
-        { name: t`Past 7 days`,  operator: "time-interval", values: [-7, "day"]},
-        { name: t`Past 30 days`, operator: "time-interval", values: [-30, "day"]}
+  {
+    name: t`Today`,
+    operator: ["=", "<", ">"],
+    values: [["relative-datetime", "current"]],
+  },
+  {
+    name: t`Yesterday`,
+    operator: ["=", "<", ">"],
+    values: [["relative-datetime", -1, "day"]],
+  },
+  { name: t`Past 7 days`, operator: "time-interval", values: [-7, "day"] },
+  { name: t`Past 30 days`, operator: "time-interval", values: [-30, "day"] },
 ];
 
 const RELATIVE_SHORTCUTS = {
-        "Last": [
-            { name: t`Week`,  operator: "time-interval", values: ["last", "week"]},
-            { name: t`Month`, operator: "time-interval", values: ["last", "month"]},
-            { name: t`Year`,  operator: "time-interval", values: ["last", "year"]}
-        ],
-        "This": [
-            { name: t`Week`,  operator: "time-interval", values: ["current", "week"]},
-            { name: t`Month`, operator: "time-interval", values: ["current", "month"]},
-            { name: t`Year`,  operator: "time-interval", values: ["current", "year"]}
-        ]
+  Last: [
+    { name: t`Week`, operator: "time-interval", values: ["last", "week"] },
+    { name: t`Month`, operator: "time-interval", values: ["last", "month"] },
+    { name: t`Year`, operator: "time-interval", values: ["last", "year"] },
+  ],
+  This: [
+    { name: t`Week`, operator: "time-interval", values: ["current", "week"] },
+    { name: t`Month`, operator: "time-interval", values: ["current", "month"] },
+    { name: t`Year`, operator: "time-interval", values: ["current", "year"] },
+  ],
 };
 
 export class PredefinedRelativeDatePicker extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        _.bindAll(this, "isSelectedShortcut", "onSetShortcut");
-    }
+    _.bindAll(this, "isSelectedShortcut", "onSetShortcut");
+  }
 
-    static propTypes = {
-        filter: PropTypes.array.isRequired,
-        onFilterChange: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    filter: PropTypes.array.isRequired,
+    onFilterChange: PropTypes.func.isRequired,
+  };
 
-    isSelectedShortcut(shortcut) {
-        let { filter } = this.props;
-        return (
-            (Array.isArray(shortcut.operator) ? _.contains(shortcut.operator, filter[0]): filter[0] === shortcut.operator ) &&
-            _.isEqual(filter.slice(2), shortcut.values)
-        );
-    }
+  isSelectedShortcut(shortcut) {
+    let { filter } = this.props;
+    return (
+      (Array.isArray(shortcut.operator)
+        ? _.contains(shortcut.operator, filter[0])
+        : filter[0] === shortcut.operator) &&
+      _.isEqual(filter.slice(2), shortcut.values)
+    );
+  }
 
-    onSetShortcut(shortcut) {
-        let { filter } = this.props;
-        let operator;
-        if (Array.isArray(shortcut.operator)) {
-            if (_.contains(shortcut.operator, filter[0])) {
-                operator = filter[0];
-            } else {
-                operator = shortcut.operator[0];
-            }
-        } else {
-            operator = shortcut.operator;
-        }
-        this.props.onFilterChange([operator, filter[1], ...shortcut.values])
+  onSetShortcut(shortcut) {
+    let { filter } = this.props;
+    let operator;
+    if (Array.isArray(shortcut.operator)) {
+      if (_.contains(shortcut.operator, filter[0])) {
+        operator = filter[0];
+      } else {
+        operator = shortcut.operator[0];
+      }
+    } else {
+      operator = shortcut.operator;
     }
+    this.props.onFilterChange([operator, filter[1], ...shortcut.values]);
+  }
 
-    render() {
-        return (
-            <div className="p1 pt2">
-                <section>
-                    { SHORTCUTS.map((s, index) =>
-                        <span key={index} className={cx("inline-block half pb1", { "pr1": index % 2 === 0 })}>
-                            <button
-                                key={index}
-                                className={cx("Button Button-normal Button--medium text-normal text-centered full", { "Button--purple": this.isSelectedShortcut(s) })}
-                                onClick={() => this.onSetShortcut(s)}
-                            >
-                                {s.name}
-                            </button>
-                        </span>
-                    )}
-                </section>
-                {Object.keys(RELATIVE_SHORTCUTS).map(sectionName =>
-                    <section key={sectionName}>
-                        <div style={{}} className="border-bottom text-uppercase flex layout-centered mb2">
-                            <h6 style={{"position": "relative", "backgroundColor": "white", "top": "6px" }} className="px2">
-                                {sectionName}
-                            </h6>
-                        </div>
-                        <div className="flex">
-                            { RELATIVE_SHORTCUTS[sectionName].map((s, index) =>
-                                <button
-                                    key={index}
-                                    data-ui-tag={"relative-date-shortcut-" + sectionName.toLowerCase() + "-" + s.name.toLowerCase()}
-                                    className={cx("Button Button-normal Button--medium flex-full mb1", { "Button--purple": this.isSelectedShortcut(s), "mr1": index !== RELATIVE_SHORTCUTS[sectionName].length - 1 })}
-                                    onClick={() => this.onSetShortcut(s)}
-                                >
-                                    {s.name}
-                                </button>
-                            )}
-                        </div>
-                    </section>
+  render() {
+    return (
+      <div className="p1 pt2">
+        <section>
+          {SHORTCUTS.map((s, index) => (
+            <span
+              key={index}
+              className={cx("inline-block half pb1", { pr1: index % 2 === 0 })}
+            >
+              <button
+                key={index}
+                className={cx(
+                  "Button Button-normal Button--medium text-normal text-centered full",
+                  { "Button--purple": this.isSelectedShortcut(s) },
                 )}
+                onClick={() => this.onSetShortcut(s)}
+              >
+                {s.name}
+              </button>
+            </span>
+          ))}
+        </section>
+        {Object.keys(RELATIVE_SHORTCUTS).map(sectionName => (
+          <section key={sectionName}>
+            <div
+              style={{}}
+              className="border-bottom text-uppercase flex layout-centered mb2"
+            >
+              <h6
+                style={{
+                  position: "relative",
+                  backgroundColor: "white",
+                  top: "6px",
+                }}
+                className="px2"
+              >
+                {sectionName}
+              </h6>
             </div>
-        );
-    }
+            <div className="flex">
+              {RELATIVE_SHORTCUTS[sectionName].map((s, index) => (
+                <button
+                  key={index}
+                  data-ui-tag={
+                    "relative-date-shortcut-" +
+                    sectionName.toLowerCase() +
+                    "-" +
+                    s.name.toLowerCase()
+                  }
+                  className={cx(
+                    "Button Button-normal Button--medium flex-full mb1",
+                    {
+                      "Button--purple": this.isSelectedShortcut(s),
+                      mr1: index !== RELATIVE_SHORTCUTS[sectionName].length - 1,
+                    },
+                  )}
+                  onClick={() => this.onSetShortcut(s)}
+                >
+                  {s.name}
+                </button>
+              ))}
+            </div>
+          </section>
+        ))}
+      </div>
+    );
+  }
 }
 
 // HACK: easiest way to get working with RelativeDatePicker
 const FILTERS = {
-    "today": {
-        name: t`Today`,
-        mapping: ["=", null, ["relative-datetime", "current"]]
-    },
-    "yesterday": {
-        name: t`Yesterday`,
-        mapping: ["=", null, ["relative-datetime", -1, "day"]]
-    },
-    "past7days": {
-        name: t`Past 7 Days`,
-        mapping: ["time-interval", null, -7, "day"]
-    },
-    "past30days": {
-        name: t`Past 30 Days`,
-        mapping: ["time-interval", null, -30, "day"]
-    },
-    "lastweek": {
-        name: t`Last Week`,
-        mapping: ["time-interval", null, "last", "week"]
-    },
-    "lastmonth": {
-        name: t`Last Month`,
-        mapping: ["time-interval", null, "last", "month"]
-    },
-    "lastyear": {
-        name: t`Last Year`,
-        mapping: ["time-interval", null, "last", "year"]
-    },
-    "thisweek": {
-        name: t`This Week`,
-        mapping: ["time-interval", null, "current", "week"]
-    },
-    "thismonth": {
-        name: t`This Month`,
-        mapping: ["time-interval", null, "current", "month"]
-    },
-    "thisyear": {
-        name: t`This Year`,
-        mapping: ["time-interval", null, "current", "year"]
-    }
+  today: {
+    name: t`Today`,
+    mapping: ["=", null, ["relative-datetime", "current"]],
+  },
+  yesterday: {
+    name: t`Yesterday`,
+    mapping: ["=", null, ["relative-datetime", -1, "day"]],
+  },
+  past7days: {
+    name: t`Past 7 Days`,
+    mapping: ["time-interval", null, -7, "day"],
+  },
+  past30days: {
+    name: t`Past 30 Days`,
+    mapping: ["time-interval", null, -30, "day"],
+  },
+  lastweek: {
+    name: t`Last Week`,
+    mapping: ["time-interval", null, "last", "week"],
+  },
+  lastmonth: {
+    name: t`Last Month`,
+    mapping: ["time-interval", null, "last", "month"],
+  },
+  lastyear: {
+    name: t`Last Year`,
+    mapping: ["time-interval", null, "last", "year"],
+  },
+  thisweek: {
+    name: t`This Week`,
+    mapping: ["time-interval", null, "current", "week"],
+  },
+  thismonth: {
+    name: t`This Month`,
+    mapping: ["time-interval", null, "current", "month"],
+  },
+  thisyear: {
+    name: t`This Year`,
+    mapping: ["time-interval", null, "current", "year"],
+  },
 };
 
 export default class DateRelativeWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {};
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {};
+  }
 
-    static propTypes = {};
-    static defaultProps = {};
+  static propTypes = {};
+  static defaultProps = {};
 
-    static format = (value) => FILTERS[value] ? FILTERS[value].name : "";
+  static format = value => (FILTERS[value] ? FILTERS[value].name : "");
 
-    render() {
-        const { value, setValue, onClose } = this.props;
-        return (
-            <div className="px1" style={{ maxWidth: 300 }}>
-                <PredefinedRelativeDatePicker
-                    filter={FILTERS[value] ? FILTERS[value].mapping : [null, null]}
-                    onFilterChange={(filter) => {
-                        setValue(_.findKey(FILTERS, (f) => _.isEqual(f.mapping, filter)));
-                        onClose();
-                    }}
-                />
-            </div>
-        );
-    }
+  render() {
+    const { value, setValue, onClose } = this.props;
+    return (
+      <div className="px1" style={{ maxWidth: 300 }}>
+        <PredefinedRelativeDatePicker
+          filter={FILTERS[value] ? FILTERS[value].mapping : [null, null]}
+          onFilterChange={filter => {
+            setValue(_.findKey(FILTERS, f => _.isEqual(f.mapping, filter)));
+            onClose();
+          }}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/widgets/DateSingleWidget.jsx b/frontend/src/metabase/parameters/components/widgets/DateSingleWidget.jsx
index 09141e8f4e137bb4fe7b1dee6174021f97ac0265..674098d748e8bda257bc16ff7b62ffd08f5ddd0d 100644
--- a/frontend/src/metabase/parameters/components/widgets/DateSingleWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/DateSingleWidget.jsx
@@ -3,17 +3,21 @@ import React from "react";
 import Calendar from "metabase/components/Calendar.jsx";
 import moment from "moment";
 
-const DateSingleWidget = ({ value, setValue, onClose }) =>
-    <div className="p1">
-        <Calendar
-            initial={value ? moment(value) : null}
-            selected={value ? moment(value) : null}
-            selectedEnd={value ? moment(value) : null}
-            onChange={(value) => { setValue(value); onClose() }}
-        />
-    </div>
+const DateSingleWidget = ({ value, setValue, onClose }) => (
+  <div className="p1">
+    <Calendar
+      initial={value ? moment(value) : null}
+      selected={value ? moment(value) : null}
+      selectedEnd={value ? moment(value) : null}
+      onChange={value => {
+        setValue(value);
+        onClose();
+      }}
+    />
+  </div>
+);
 
-DateSingleWidget.format = (value) =>
-    value ? moment(value).format("MMMM D, YYYY") : "";
+DateSingleWidget.format = value =>
+  value ? moment(value).format("MMMM D, YYYY") : "";
 
 export default DateSingleWidget;
diff --git a/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx b/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx
new file mode 100644
index 0000000000000000000000000000000000000000..69ba035cb84053b281edd41dd4e61d77f5657eb4
--- /dev/null
+++ b/frontend/src/metabase/parameters/components/widgets/ParameterFieldWidget.jsx
@@ -0,0 +1,159 @@
+/* @flow */
+
+import React, { Component } from "react";
+import ReactDOM from "react-dom";
+
+import { t } from "c-3po";
+
+import FieldValuesWidget from "metabase/components/FieldValuesWidget";
+import Popover from "metabase/components/Popover";
+import Button from "metabase/components/Button";
+import RemappedValue from "metabase/containers/RemappedValue";
+
+import Field from "metabase-lib/lib/metadata/Field";
+
+type Props = {
+  value: any,
+  setValue: () => void,
+
+  isEditing: boolean,
+
+  field: Field,
+  parentFocusChanged: boolean => void,
+};
+
+type State = {
+  value: any[],
+  isFocused: boolean,
+  widgetWidth: ?number,
+};
+
+const BORDER_WIDTH = 2;
+
+const normalizeValue = value =>
+  Array.isArray(value) ? value : value != null ? [value] : [];
+
+// TODO: rename this something else since we're using it for more than searching and more than text
+export default class ParameterFieldWidget extends Component<*, Props, State> {
+  props: Props;
+  state: State;
+
+  _unfocusedElement: React$Component<any, any, any>;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      isFocused: false,
+      value: props.value,
+      widgetWidth: null,
+    };
+  }
+
+  static noPopover = true;
+
+  static format(value, field) {
+    value = normalizeValue(value);
+    if (value.length > 1) {
+      return `${value.length} selections`;
+    } else {
+      return <RemappedValue value={value[0]} column={field} />;
+    }
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    if (this.props.value !== nextProps.value) {
+      this.setState({ value: nextProps.value });
+    }
+  }
+
+  componentDidUpdate() {
+    let element = ReactDOM.findDOMNode(this._unfocusedElement);
+    if (!this.state.isFocused && element) {
+      const parameterWidgetElement = element.parentNode.parentNode.parentNode;
+      if (parameterWidgetElement.clientWidth !== this.state.widgetWidth) {
+        this.setState({ widgetWidth: parameterWidgetElement.clientWidth });
+      }
+    }
+  }
+
+  render() {
+    let { setValue, isEditing, field, parentFocusChanged } = this.props;
+    let { isFocused } = this.state;
+
+    const savedValue = normalizeValue(this.props.value);
+    const unsavedValue = normalizeValue(this.state.value);
+
+    const defaultPlaceholder = isFocused
+      ? ""
+      : this.props.placeholder || t`Enter a value...`;
+
+    const focusChanged = isFocused => {
+      if (parentFocusChanged) parentFocusChanged(isFocused);
+      this.setState({ isFocused });
+    };
+
+    const placeholder = isEditing
+      ? "Enter a default value..."
+      : defaultPlaceholder;
+
+    if (!isFocused) {
+      return (
+        <div
+          ref={_ => (this._unfocusedElement = _)}
+          className="flex-full cursor-pointer"
+          onClick={() => focusChanged(true)}
+        >
+          {savedValue.length > 0 ? (
+            ParameterFieldWidget.format(savedValue, field)
+          ) : (
+            <span>{placeholder}</span>
+          )}
+        </div>
+      );
+    } else {
+      return (
+        <Popover
+          tetherOptions={{
+            attachment: "top left",
+            targetAttachment: "top left",
+            targetOffset: "-15 -25",
+          }}
+          hasArrow={false}
+          onClose={() => focusChanged(false)}
+        >
+          <FieldValuesWidget
+            value={unsavedValue}
+            onChange={value => {
+              this.setState({ value });
+            }}
+            placeholder={placeholder}
+            field={field}
+            searchField={field.parameterSearchField()}
+            multi
+            autoFocus
+            color="brand"
+            style={{
+              borderWidth: BORDER_WIDTH,
+              minWidth: this.state.widgetWidth
+                ? this.state.widgetWidth + BORDER_WIDTH * 2
+                : null,
+            }}
+            maxWidth={400}
+          />
+          <div className="flex p1">
+            <Button
+              primary
+              className="ml-auto"
+              onClick={() => {
+                setValue(unsavedValue.length > 0 ? unsavedValue : null);
+                focusChanged(false);
+              }}
+            >
+              Done
+            </Button>
+          </div>
+        </Popover>
+      );
+    }
+  }
+}
diff --git a/frontend/src/metabase/parameters/components/widgets/TextWidget.jsx b/frontend/src/metabase/parameters/components/widgets/TextWidget.jsx
index 4a6b14772d6d4c80fa8eecc28f20ad5022204e47..2ac542e31904c99ea3d5aaf044f330a497eae80d 100644
--- a/frontend/src/metabase/parameters/components/widgets/TextWidget.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/TextWidget.jsx
@@ -2,81 +2,92 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
-import {forceRedraw} from "metabase/lib/dom";
-import { t } from 'c-3po';
+import { forceRedraw } from "metabase/lib/dom";
+import { t } from "c-3po";
 import { KEYCODE_ENTER, KEYCODE_ESCAPE } from "metabase/lib/keyboard";
 
 export default class TextWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            value: props.value,
-            isFocused: false
-        };
-    }
-
-    static propTypes = {
-        value: PropTypes.any,
-        setValue: PropTypes.func.isRequired,
-        className: PropTypes.string,
-        isEditing: PropTypes.bool,
-        commitImmediately: PropTypes.bool,
-        placeholder: PropTypes.string,
-        focusChanged: PropTypes.func
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      value: props.value,
+      isFocused: false,
     };
+  }
 
-    static defaultProps = {
-        commitImmediately: false,
-    };
+  static propTypes = {
+    value: PropTypes.any,
+    setValue: PropTypes.func.isRequired,
+    className: PropTypes.string,
+    isEditing: PropTypes.bool,
+    commitImmediately: PropTypes.bool,
+    placeholder: PropTypes.string,
+    focusChanged: PropTypes.func,
+  };
 
-    static noPopover = true;
+  static defaultProps = {
+    commitImmediately: false,
+  };
 
-    static format = (value) => value;
+  static noPopover = true;
 
-    componentWillReceiveProps(nextProps) {
-        if (this.props.value !== nextProps.value) {
-            this.setState({ value: nextProps.value }, () => {
-                // HACK: Address Safari rendering bug which causes https://github.com/metabase/metabase/issues/5335
-                forceRedraw(ReactDOM.findDOMNode(this));
-            });
-        }
+  static format = value => value;
+
+  componentWillReceiveProps(nextProps) {
+    if (this.props.value !== nextProps.value) {
+      this.setState({ value: nextProps.value }, () => {
+        // HACK: Address Safari rendering bug which causes https://github.com/metabase/metabase/issues/5335
+        forceRedraw(ReactDOM.findDOMNode(this));
+      });
     }
+  }
 
-    render() {
-        const { setValue, className, isEditing, focusChanged: parentFocusChanged } = this.props;
-        const defaultPlaceholder = this.state.isFocused ? "" : (this.props.placeholder || t`Enter a value...`);
+  render() {
+    const {
+      setValue,
+      className,
+      isEditing,
+      focusChanged: parentFocusChanged,
+    } = this.props;
+    const defaultPlaceholder = this.state.isFocused
+      ? ""
+      : this.props.placeholder || t`Enter a value...`;
 
-        const focusChanged = (isFocused) => {
-            if (parentFocusChanged) parentFocusChanged(isFocused);
-            this.setState({isFocused})
-        };
+    const focusChanged = isFocused => {
+      if (parentFocusChanged) parentFocusChanged(isFocused);
+      this.setState({ isFocused });
+    };
 
-        return (
-            <input
-                className={className}
-                type="text"
-                value={this.state.value || ""}
-                onChange={(e) => {
-                    this.setState({ value: e.target.value })
-                    if (this.props.commitImmediately) {
-                        this.props.setValue(e.target.value || null);
-                    }
-                }}
-                onKeyUp={(e) => {
-                    if (e.keyCode === KEYCODE_ESCAPE) {
-                        e.target.blur();
-                    } else if (e.keyCode === KEYCODE_ENTER) {
-                        setValue(this.state.value || null);
-                        e.target.blur();
-                    }
-                }}
-                onFocus={() => {focusChanged(true)}}
-                onBlur={() => {
-                    focusChanged(false);
-                    this.setState({ value: this.props.value });
-                }}
-                placeholder={isEditing ? t`Enter a default value...` : defaultPlaceholder}
-            />
-        );
-    }
+    return (
+      <input
+        className={className}
+        type="text"
+        value={this.state.value || ""}
+        onChange={e => {
+          this.setState({ value: e.target.value });
+          if (this.props.commitImmediately) {
+            this.props.setValue(e.target.value || null);
+          }
+        }}
+        onKeyUp={e => {
+          if (e.keyCode === KEYCODE_ESCAPE) {
+            e.target.blur();
+          } else if (e.keyCode === KEYCODE_ENTER) {
+            setValue(this.state.value || null);
+            e.target.blur();
+          }
+        }}
+        onFocus={() => {
+          focusChanged(true);
+        }}
+        onBlur={() => {
+          focusChanged(false);
+          this.setState({ value: this.props.value });
+        }}
+        placeholder={
+          isEditing ? t`Enter a default value...` : defaultPlaceholder
+        }
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/parameters/components/widgets/YearPicker.jsx b/frontend/src/metabase/parameters/components/widgets/YearPicker.jsx
index 455eeb5fc466b9623184922623e87cb497db7140..861ec358d49b87f69b3fa619a2601d034d5f66a5 100644
--- a/frontend/src/metabase/parameters/components/widgets/YearPicker.jsx
+++ b/frontend/src/metabase/parameters/components/widgets/YearPicker.jsx
@@ -5,14 +5,15 @@ import _ from "underscore";
 
 const YEARS = _.range(new Date().getFullYear(), 1900, -1);
 
-const YearPicker = ({ value, onChange }) =>
-    <Select
-        className="borderless"
-        value={value}
-        options={YEARS}
-        optionNameFn={(option) => option}
-        optionValueFn={(option) => option}
-        onChange={onChange}
-    />
+const YearPicker = ({ value, onChange }) => (
+  <Select
+    className="borderless"
+    value={value}
+    options={YEARS}
+    optionNameFn={option => option}
+    optionValueFn={option => option}
+    onChange={onChange}
+  />
+);
 
 export default YearPicker;
diff --git a/frontend/src/metabase/public/components/EmbedFrame.jsx b/frontend/src/metabase/public/components/EmbedFrame.jsx
index bac003e596e0010f6dfc3571fbf69d109f720d00..96a9b1c80890f2151a62eb538e9599503f0a41c6 100644
--- a/frontend/src/metabase/public/components/EmbedFrame.jsx
+++ b/frontend/src/metabase/public/components/EmbedFrame.jsx
@@ -16,121 +16,140 @@ import cx from "classnames";
 import "./EmbedFrame.css";
 
 const DEFAULT_OPTIONS = {
-    bordered: IFRAMED,
-    titled: true
-}
+  bordered: IFRAMED,
+  titled: true,
+};
 
 import type { Parameter } from "metabase/meta/types/Parameter";
 
 type Props = {
-    className?: string,
-    children?: any,
-    actionButtons?: any[],
-    name?: string,
-    description?: string,
-    location: { query: {[key:string]: string}, hash: string },
-    parameters?: Parameter[],
-    parameterValues?: {[key:string]: string},
-    setParameterValue: (id: string, value: string) => void
-}
+  className?: string,
+  children?: any,
+  actionButtons?: any[],
+  name?: string,
+  description?: string,
+  location: { query: { [key: string]: string }, hash: string },
+  parameters?: Parameter[],
+  parameterValues?: { [key: string]: string },
+  setParameterValue: (id: string, value: string) => void,
+};
 
 type State = {
-    innerScroll: boolean
-}
+  innerScroll: boolean,
+};
 
 @withRouter
 export default class EmbedFrame extends Component {
-    props: Props;
-    state: State = {
-        innerScroll: true
+  props: Props;
+  state: State = {
+    innerScroll: true,
+  };
+
+  componentWillMount() {
+    // Make iFrameResizer avaliable so that embed users can
+    // have their embeds autosize to their content
+    if (window.iFrameResizer) {
+      console.error("iFrameResizer resizer already defined.");
+    } else {
+      window.iFrameResizer = {
+        autoResize: true,
+        heightCalculationMethod: "bodyScroll",
+        readyCallback: () => {
+          this.setState({ innerScroll: false });
+        },
+      };
+
+      // FIXME: Crimes
+      // This is needed so the FE test framework which runs in node
+      // without the avaliability of require.ensure skips over this part
+      // which is for external purposes only.
+      //
+      // Ideally that should happen in the test config, but it doesn't
+      // seem to want to play nice when messing with require
+      if (typeof require.ensure !== "function") {
+        // $FlowFixMe: flow doesn't seem to like returning false here
+        return false;
+      }
+
+      // Make iframe-resizer avaliable to the embed
+      // We only care about contentWindow so require that minified file
+
+      require.ensure([], require => {
+        require("iframe-resizer/js/iframeResizer.contentWindow.min.js");
+      });
     }
-
-    componentWillMount() {
-        // Make iFrameResizer avaliable so that embed users can
-        // have their embeds autosize to their content
-        if (window.iFrameResizer) {
-            console.error("iFrameResizer resizer already defined.")
-        } else {
-            window.iFrameResizer = {
-                autoResize: true,
-                heightCalculationMethod: "bodyScroll",
-                readyCallback: () => {
-                    this.setState({ innerScroll: false })
-                }
-            }
-
-
-            // FIXME: Crimes
-            // This is needed so the FE test framework which runs in node
-            // without the avaliability of require.ensure skips over this part
-            // which is for external purposes only.
-            //
-            // Ideally that should happen in the test config, but it doesn't
-            // seem to want to play nice when messing with require
-            if(typeof require.ensure !== "function") {
-                // $FlowFixMe: flow doesn't seem to like returning false here
-                return false
-            }
-
-            // Make iframe-resizer avaliable to the embed
-            // We only care about contentWindow so require that minified file
-
-            require.ensure([], (require) => {
-                require('iframe-resizer/js/iframeResizer.contentWindow.min.js')
-            });
-        }
-    }
-
-    render() {
-        const { className, children, actionButtons, location, parameters, parameterValues, setParameterValue } = this.props;
-        const { innerScroll } = this.state;
-
-        const footer = true;
-
-        const { bordered, titled, theme } = { ...DEFAULT_OPTIONS, ...parseHashOptions(location.hash) };
-
-        const name = titled ? this.props.name : null;
-
-        return (
-            <div className={cx("EmbedFrame flex flex-column", className, {
-                "spread": innerScroll,
-                "bordered rounded shadowed": bordered,
-                [`Theme--${theme}`]: !!theme
-            })}>
-                <div className={cx("flex flex-column flex-full relative", { "scroll-y": innerScroll })}>
-                    { name || (parameters && parameters.length > 0) ?
-                        <div className="EmbedFrame-header flex align-center p1 sm-p2 lg-p3">
-                            { name && (
-                                <div className="h4 text-bold sm-h3 md-h2">{name}</div>
-                            )}
-                            { parameters && parameters.length > 0 ?
-                                <div className="flex ml-auto">
-                                    <Parameters
-                                        parameters={parameters.map(p => ({ ...p, value: parameterValues && parameterValues[p.id] }))}
-                                        query={location.query}
-                                        setParameterValue={setParameterValue}
-                                        syncQueryString
-                                        isQB
-                                    />
-                                </div>
-                            : null }
-                        </div>
-                    : null }
-                    <div className="flex flex-column relative full flex-full">
-                        {children}
-                    </div>
+  }
+
+  render() {
+    const {
+      className,
+      children,
+      actionButtons,
+      location,
+      parameters,
+      parameterValues,
+      setParameterValue,
+    } = this.props;
+    const { innerScroll } = this.state;
+
+    const footer = true;
+
+    const { bordered, titled, theme } = {
+      ...DEFAULT_OPTIONS,
+      ...parseHashOptions(location.hash),
+    };
+
+    const name = titled ? this.props.name : null;
+
+    return (
+      <div
+        className={cx("EmbedFrame flex flex-column", className, {
+          spread: innerScroll,
+          "bordered rounded shadowed": bordered,
+          [`Theme--${theme}`]: !!theme,
+        })}
+      >
+        <div
+          className={cx("flex flex-column flex-full relative", {
+            "scroll-y": innerScroll,
+          })}
+        >
+          {name || (parameters && parameters.length > 0) ? (
+            <div className="EmbedFrame-header flex align-center p1 sm-p2 lg-p3">
+              {name && <div className="h4 text-bold sm-h3 md-h2">{name}</div>}
+              {parameters && parameters.length > 0 ? (
+                <div className="flex ml-auto">
+                  <Parameters
+                    parameters={parameters.map(p => ({
+                      ...p,
+                      value: parameterValues && parameterValues[p.id],
+                    }))}
+                    query={location.query}
+                    setParameterValue={setParameterValue}
+                    syncQueryString
+                    isQB
+                  />
                 </div>
-                { footer &&
-                    <div className="EmbedFrame-footer p1 md-p2 lg-p3 border-top flex-no-shrink flex align-center">
-                        {!MetabaseSettings.hideEmbedBranding() &&
-                            <LogoBadge dark={theme} />
-                        }
-                        {actionButtons &&
-                            <div className="flex-align-right text-grey-3">{actionButtons}</div>
-                        }
-                    </div>
-                }
+              ) : null}
             </div>
-        )
-    }
+          ) : null}
+          <div className="flex flex-column relative full flex-full">
+            {children}
+          </div>
+        </div>
+        {footer && (
+          <div className="EmbedFrame-footer p1 md-p2 lg-p3 border-top flex-no-shrink flex align-center">
+            {!MetabaseSettings.hideEmbedBranding() && (
+              <LogoBadge dark={theme} />
+            )}
+            {actionButtons && (
+              <div className="flex-align-right text-grey-3">
+                {actionButtons}
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/components/LogoBadge.jsx b/frontend/src/metabase/public/components/LogoBadge.jsx
index d4cc175b45a258bbec9aa88474fb5464bdcc4cb0..2ccdd5dd7f83222086ac36c776da0723b01ae1b7 100644
--- a/frontend/src/metabase/public/components/LogoBadge.jsx
+++ b/frontend/src/metabase/public/components/LogoBadge.jsx
@@ -6,18 +6,23 @@ import LogoIcon from "metabase/components/LogoIcon";
 import cx from "classnames";
 
 type Props = {
-    dark: bool,
-}
+  dark: boolean,
+};
 
-const LogoBadge = ({ dark }: Props) =>
-    <a href="http://www.metabase.com/" target="_blank" className="h4 flex text-bold align-center no-decoration">
-        <LogoIcon
-            size={28}
-            dark={dark}
-        />
-        <span className="text-small">
-            <span className="ml1 text-grey-3">Powered by</span> <span className={cx({ "text-brand": !dark }, { "text-white": dark })}>Metabase</span>
-        </span>
-    </a>
+const LogoBadge = ({ dark }: Props) => (
+  <a
+    href="http://www.metabase.com/"
+    target="_blank"
+    className="h4 flex text-bold align-center no-decoration"
+  >
+    <LogoIcon size={28} dark={dark} />
+    <span className="text-small">
+      <span className="ml1 text-grey-3">Powered by</span>{" "}
+      <span className={cx({ "text-brand": !dark }, { "text-white": dark })}>
+        Metabase
+      </span>
+    </span>
+  </a>
+);
 
 export default LogoBadge;
diff --git a/frontend/src/metabase/public/components/PublicError.jsx b/frontend/src/metabase/public/components/PublicError.jsx
index 8ebb956982d251019f8daab7d19be9263cb86484..1cf1c2eb96b11eccbcda16b8ecf26ce7e54923b7 100644
--- a/frontend/src/metabase/public/components/PublicError.jsx
+++ b/frontend/src/metabase/public/components/PublicError.jsx
@@ -1,28 +1,27 @@
 /* @flow */
 
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { connect } from "react-redux";
 import { getErrorMessage } from "metabase/selectors/app";
 
 import EmbedFrame from "./EmbedFrame";
 
 const mapStateToProps = (state, props) => ({
-    message: getErrorMessage(state, props)
-})
+  message: getErrorMessage(state, props),
+});
 
 type Props = {
-    message?: string
+  message?: string,
 };
 
-const PublicError = ({ message = t`An error occurred` }: Props) =>
-    <EmbedFrame className="spread">
-        <div className="flex layout-centered flex-full flex-column">
-            <div className="QueryError-image QueryError-image--noRows"></div>
-            <div className="mt1 h4 sm-h3 md-h2 text-bold">
-                {message}
-            </div>
-        </div>
-    </EmbedFrame>;
+const PublicError = ({ message = t`An error occurred` }: Props) => (
+  <EmbedFrame className="spread">
+    <div className="flex layout-centered flex-full flex-column">
+      <div className="QueryError-image QueryError-image--noRows" />
+      <div className="mt1 h4 sm-h3 md-h2 text-bold">{message}</div>
+    </div>
+  </EmbedFrame>
+);
 
 export default connect(mapStateToProps)(PublicError);
diff --git a/frontend/src/metabase/public/components/PublicNotFound.jsx b/frontend/src/metabase/public/components/PublicNotFound.jsx
index 73a44f2d24a41c1ef2b8258f778e09a78e3e8a72..c12c35672599fbf09cb3417005c3f48e1545994e 100644
--- a/frontend/src/metabase/public/components/PublicNotFound.jsx
+++ b/frontend/src/metabase/public/components/PublicNotFound.jsx
@@ -1,17 +1,16 @@
 /* @flow */
 
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import EmbedFrame from "./EmbedFrame";
 
-const PublicNotFound = () =>
-    <EmbedFrame className="spread">
-        <div className="flex layout-centered flex-full flex-column">
-            <div className="QueryError-image QueryError-image--noRows"></div>
-            <div className="mt1 h4 sm-h3 md-h2 text-bold">
-                {t`Not found`}
-            </div>
-        </div>
-    </EmbedFrame>;
+const PublicNotFound = () => (
+  <EmbedFrame className="spread">
+    <div className="flex layout-centered flex-full flex-column">
+      <div className="QueryError-image QueryError-image--noRows" />
+      <div className="mt1 h4 sm-h3 md-h2 text-bold">{t`Not found`}</div>
+    </div>
+  </EmbedFrame>
+);
 
 export default PublicNotFound;
diff --git a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
index 869fb419dafaf9afc065d71ccf2ebb7b790deb5d..e3168d913f1d770576bcd3e107a47e8f85b42828 100644
--- a/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/AdvancedEmbedPane.jsx
@@ -5,132 +5,146 @@ import React from "react";
 import ToggleLarge from "metabase/components/ToggleLarge";
 import Button from "metabase/components/Button";
 import ActionButton from "metabase/components/ActionButton";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AdvancedSettingsPane from "./AdvancedSettingsPane";
 import PreviewPane from "./PreviewPane";
 import EmbedCodePane from "./EmbedCodePane";
 
 import type { Parameter, ParameterId } from "metabase/meta/types/Parameter";
 import type { Pane, EmbedType, DisplayOptions } from "./EmbedModalContent";
-import type { EmbeddableResource, EmbeddingParams } from "metabase/public/lib/types";
+import type {
+  EmbeddableResource,
+  EmbeddingParams,
+} from "metabase/public/lib/types";
 
 import _ from "underscore";
 
 type Props = {
-    className?: string,
+  className?: string,
 
-    pane: Pane,
-    embedType: EmbedType,
+  pane: Pane,
+  embedType: EmbedType,
 
-    resourceType: string,
-    resource: EmbeddableResource,
-    resourceParameters:  Parameter[],
+  resourceType: string,
+  resource: EmbeddableResource,
+  resourceParameters: Parameter[],
 
-    token: string,
-    iframeUrl: string,
-    siteUrl: string,
-    secretKey: string,
-    params: { [slug: string]: any },
+  token: string,
+  iframeUrl: string,
+  siteUrl: string,
+  secretKey: string,
+  params: { [slug: string]: any },
 
-    displayOptions: DisplayOptions,
-    previewParameters: Parameter[],
-    parameterValues: { [id: ParameterId]: any },
-    embeddingParams: EmbeddingParams,
+  displayOptions: DisplayOptions,
+  previewParameters: Parameter[],
+  parameterValues: { [id: ParameterId]: any },
+  embeddingParams: EmbeddingParams,
 
-    onChangeDisplayOptions: (DisplayOptions) => void,
-    onChangeEmbeddingParameters: (EmbeddingParams) => void,
-    onChangeParameterValue: (id: ParameterId, value: any) => void,
-    onChangePane: (pane: Pane) => void,
-    onSave: () => Promise<void>,
-    onUnpublish: () => Promise<void>,
-    onDiscard: () => void,
+  onChangeDisplayOptions: DisplayOptions => void,
+  onChangeEmbeddingParameters: EmbeddingParams => void,
+  onChangeParameterValue: (id: ParameterId, value: any) => void,
+  onChangePane: (pane: Pane) => void,
+  onSave: () => Promise<void>,
+  onUnpublish: () => Promise<void>,
+  onDiscard: () => void,
 };
 
 const AdvancedEmbedPane = ({
-    pane,
-    resource,
-    resourceType,
-    embedType,
-    token,
-    iframeUrl,
-    siteUrl,
-    secretKey,
-    params,
-    displayOptions,
-    previewParameters,
-    parameterValues,
-    resourceParameters,
-    embeddingParams,
-    onChangeDisplayOptions,
-    onChangeEmbeddingParameters,
-    onChangeParameterValue,
-    onChangePane,
-    onSave,
-    onUnpublish,
-    onDiscard,
-}: Props) =>
-    <div className="full flex">
-        <div className="flex-full p4 flex flex-column">
-            { !resource.enable_embedding || !_.isEqual(resource.embedding_params, embeddingParams) ?
-                <div className="mb2 p2 bordered rounded flex align-center flex-no-shrink">
-                    <div className="flex-full mr1">
-                        { resource.enable_embedding ?
-                            t`You’ve made changes that need to be published before they will be reflected in your application embed.` :
-                            t`You will need to publish this ${resourceType} before you can embed it in another application.`
-                        }
-                    </div>
-                    <div className="flex-no-shrink">
-                        { resource.enable_embedding && !_.isEqual(resource.embedding_params, embeddingParams) ?
-                            <Button className="ml1" medium onClick={onDiscard}>{t`Discard Changes`}</Button>
-                        : null }
-                        <ActionButton className="ml1" primary medium actionFn={onSave} activeText={t`Updating...`} successText={t`Updated`} failedText={t`Failed!`}>{t`Publish`}</ActionButton>
-                    </div>
-                </div>
-            : null }
-            <ToggleLarge
-                className="mb2 flex-no-shrink"
-                style={{ width: 244, height: 34 }}
-                value={pane === "preview"}
-                textLeft={t`Preview`}
-                textRight={t`Code`}
-                onChange={() => onChangePane(pane === "preview" ? "code" : "preview")}
-            />
-            { pane === "preview" ?
-                <PreviewPane
-                    className="flex-full"
-                    previewUrl={iframeUrl}
-                />
-            : pane === "code" ?
-                <EmbedCodePane
-                    className="flex-full"
-                    embedType={embedType}
-                    resource={resource}
-                    resourceType={resourceType}
-                    iframeUrl={iframeUrl}
-                    token={token}
-                    siteUrl={siteUrl}
-                    secretKey={secretKey}
-                    params={params}
-                    displayOptions={displayOptions}
-                />
-            : null }
+  pane,
+  resource,
+  resourceType,
+  embedType,
+  token,
+  iframeUrl,
+  siteUrl,
+  secretKey,
+  params,
+  displayOptions,
+  previewParameters,
+  parameterValues,
+  resourceParameters,
+  embeddingParams,
+  onChangeDisplayOptions,
+  onChangeEmbeddingParameters,
+  onChangeParameterValue,
+  onChangePane,
+  onSave,
+  onUnpublish,
+  onDiscard,
+}: Props) => (
+  <div className="full flex">
+    <div className="flex-full p4 flex flex-column">
+      {!resource.enable_embedding ||
+      !_.isEqual(resource.embedding_params, embeddingParams) ? (
+        <div className="mb2 p2 bordered rounded flex align-center flex-no-shrink">
+          <div className="flex-full mr1">
+            {resource.enable_embedding
+              ? t`You’ve made changes that need to be published before they will be reflected in your application embed.`
+              : t`You will need to publish this ${resourceType} before you can embed it in another application.`}
+          </div>
+          <div className="flex-no-shrink">
+            {resource.enable_embedding &&
+            !_.isEqual(resource.embedding_params, embeddingParams) ? (
+              <Button
+                className="ml1"
+                medium
+                onClick={onDiscard}
+              >{t`Discard Changes`}</Button>
+            ) : null}
+            <ActionButton
+              className="ml1"
+              primary
+              medium
+              actionFn={onSave}
+              activeText={t`Updating...`}
+              successText={t`Updated`}
+              failedText={t`Failed!`}
+            >{t`Publish`}</ActionButton>
+          </div>
         </div>
-        <AdvancedSettingsPane
-            pane={pane}
-            embedType={embedType}
-            onChangePane={onChangePane}
-            resource={resource}
-            resourceType={resourceType}
-            resourceParameters={resourceParameters}
-            embeddingParams={embeddingParams}
-            onChangeEmbeddingParameters={onChangeEmbeddingParameters}
-            displayOptions={displayOptions}
-            onChangeDisplayOptions={onChangeDisplayOptions}
-            previewParameters={previewParameters}
-            parameterValues={parameterValues}
-            onChangeParameterValue={onChangeParameterValue}
-            onUnpublish={onUnpublish}
+      ) : null}
+      <ToggleLarge
+        className="mb2 flex-no-shrink"
+        style={{ width: 244, height: 34 }}
+        value={pane === "preview"}
+        textLeft={t`Preview`}
+        textRight={t`Code`}
+        onChange={() => onChangePane(pane === "preview" ? "code" : "preview")}
+      />
+      {pane === "preview" ? (
+        <PreviewPane className="flex-full" previewUrl={iframeUrl} />
+      ) : pane === "code" ? (
+        <EmbedCodePane
+          className="flex-full"
+          embedType={embedType}
+          resource={resource}
+          resourceType={resourceType}
+          iframeUrl={iframeUrl}
+          token={token}
+          siteUrl={siteUrl}
+          secretKey={secretKey}
+          params={params}
+          displayOptions={displayOptions}
         />
-    </div>;
+      ) : null}
+    </div>
+    <AdvancedSettingsPane
+      pane={pane}
+      embedType={embedType}
+      onChangePane={onChangePane}
+      resource={resource}
+      resourceType={resourceType}
+      resourceParameters={resourceParameters}
+      embeddingParams={embeddingParams}
+      onChangeEmbeddingParameters={onChangeEmbeddingParameters}
+      displayOptions={displayOptions}
+      onChangeDisplayOptions={onChangeDisplayOptions}
+      previewParameters={previewParameters}
+      parameterValues={parameterValues}
+      onChangeParameterValue={onChangeParameterValue}
+      onUnpublish={onUnpublish}
+    />
+  </div>
+);
 
 export default AdvancedEmbedPane;
diff --git a/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx b/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx
index db5e4fd0f86da40390223b4295f1630adf3237d7..c1eb9c26002950991fae5be9607cc70ed02a4791 100644
--- a/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/AdvancedSettingsPane.jsx
@@ -6,108 +6,137 @@ import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
 import Parameters from "metabase/parameters/components/Parameters";
 import Select, { Option } from "metabase/components/Select";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import DisplayOptionsPane from "./DisplayOptionsPane";
 
 import cx from "classnames";
 
-const getIconForParameter = (parameter) =>
-    parameter.type === "category" ? "string" :
-    parameter.type.indexOf("date/") === 0 ? "calendar" :
-    "unknown";
+const getIconForParameter = parameter =>
+  parameter.type === "category"
+    ? "string"
+    : parameter.type.indexOf("date/") === 0 ? "calendar" : "unknown";
 
 import type { EmbedType, DisplayOptions } from "./EmbedModalContent";
-import type { EmbeddableResource, EmbeddingParams } from "metabase/public/lib/types";
+import type {
+  EmbeddableResource,
+  EmbeddingParams,
+} from "metabase/public/lib/types";
 import type { Parameter, ParameterId } from "metabase/meta/types/Parameter";
 
 type Props = {
-    className?: string,
+  className?: string,
 
-    embedType: EmbedType,
+  embedType: EmbedType,
 
-    resourceType: string,
-    resource: EmbeddableResource,
-    resourceParameters:  Parameter[],
+  resourceType: string,
+  resource: EmbeddableResource,
+  resourceParameters: Parameter[],
 
-    embeddingParams: EmbeddingParams,
-    onChangeEmbeddingParameters: (EmbeddingParams) => void,
+  embeddingParams: EmbeddingParams,
+  onChangeEmbeddingParameters: EmbeddingParams => void,
 
-    displayOptions: DisplayOptions,
-    previewParameters: Parameter[],
-    parameterValues: { [id: ParameterId]: any },
+  displayOptions: DisplayOptions,
+  previewParameters: Parameter[],
+  parameterValues: { [id: ParameterId]: any },
 
-    onChangeDisplayOptions: (DisplayOptions) => void,
-    onChangeParameterValue: (id: ParameterId, value: any) => void,
-    onUnpublish: () => Promise<void>
+  onChangeDisplayOptions: DisplayOptions => void,
+  onChangeParameterValue: (id: ParameterId, value: any) => void,
+  onUnpublish: () => Promise<void>,
 };
 
 const AdvancedSettingsPane = ({
-    className,
-    embedType,
-    resource,
-    resourceType, resourceParameters,
-    embeddingParams, onChangeEmbeddingParameters,
-    displayOptions, onChangeDisplayOptions,
-    onUnpublish,
-    pane, onChangePane,
-    previewParameters, parameterValues, onChangeParameterValue,
-}: Props) =>
-    <div className={cx(className, "p4 full-height flex flex-column bg-slate-extra-light")} style={{ width: 400 }}>
-        <Section title={t`Style`}>
-            <DisplayOptionsPane
-                className="pt1"
-                displayOptions={displayOptions}
-                onChangeDisplayOptions={onChangeDisplayOptions}
+  className,
+  embedType,
+  resource,
+  resourceType,
+  resourceParameters,
+  embeddingParams,
+  onChangeEmbeddingParameters,
+  displayOptions,
+  onChangeDisplayOptions,
+  onUnpublish,
+  pane,
+  onChangePane,
+  previewParameters,
+  parameterValues,
+  onChangeParameterValue,
+}: Props) => (
+  <div
+    className={cx(
+      className,
+      "p4 full-height flex flex-column bg-slate-extra-light",
+    )}
+    style={{ width: 400 }}
+  >
+    <Section title={t`Style`}>
+      <DisplayOptionsPane
+        className="pt1"
+        displayOptions={displayOptions}
+        onChangeDisplayOptions={onChangeDisplayOptions}
+      />
+    </Section>
+    {embedType === "application" && (
+      <Section title={t`Parameters`}>
+        {resourceParameters.length > 0 ? (
+          <p>{t`Which parameters can users of this embed use?`}</p>
+        ) : (
+          <p
+          >{t`This ${resourceType} doesn't have any parameters to configure yet.`}</p>
+        )}
+        {resourceParameters.map(parameter => (
+          <div className="flex align-center my1">
+            <Icon
+              name={getIconForParameter(parameter)}
+              className="mr2"
+              style={{ color: "#DFE8EA" }}
             />
+            <h3>{parameter.name}</h3>
+            <Select
+              className="ml-auto bg-white"
+              value={embeddingParams[parameter.slug] || "disabled"}
+              onChange={e =>
+                onChangeEmbeddingParameters({
+                  ...embeddingParams,
+                  [parameter.slug]: e.target.value,
+                })
+              }
+            >
+              <Option icon="close" value="disabled">{t`Disabled`}</Option>
+              <Option icon="pencil" value="enabled">{t`Editable`}</Option>
+              <Option icon="lock" value="locked">{t`Locked`}</Option>
+            </Select>
+          </div>
+        ))}
+      </Section>
+    )}
+    {embedType === "application" &&
+      previewParameters.length > 0 && (
+        <Section title={t`Preview Locked Parameters`}>
+          <p
+          >{t`Try passing some values to your locked parameters here. Your server will have to provide the actual values in the signed token when using this for real.`}</p>
+          <Parameters
+            className="mt2"
+            vertical
+            parameters={previewParameters}
+            parameterValues={parameterValues}
+            setParameterValue={onChangeParameterValue}
+          />
         </Section>
-        { embedType === "application" &&
-            <Section title={t`Parameters`}>
-                { resourceParameters.length > 0 ?
-                    <p>{t`Which parameters can users of this embed use?`}</p>
-                :
-                    <p>{t`This ${resourceType} doesn't have any parameters to configure yet.`}</p>
-                }
-                {resourceParameters.map(parameter =>
-                    <div className="flex align-center my1">
-                        <Icon name={getIconForParameter(parameter)} className="mr2" style={{ color: "#DFE8EA" }} />
-                        <h3>{parameter.name}</h3>
-                        <Select
-                            className="ml-auto bg-white"
-                            value={embeddingParams[parameter.slug] || "disabled"}
-                            onChange={(e) => onChangeEmbeddingParameters({ ...embeddingParams, [parameter.slug] : e.target.value })}
-                        >
-                            <Option icon="close" value="disabled">{t`Disabled`}</Option>
-                            <Option icon="pencil" value="enabled">{t`Editable`}</Option>
-                            <Option icon="lock" value="locked">{t`Locked`}</Option>
-                        </Select>
-                    </div>
-                )}
-            </Section>
-        }
-        { embedType === "application" && previewParameters.length > 0 &&
-            <Section title={t`Preview Locked Parameters`}>
-                <p>{t`Try passing some values to your locked parameters here. Your server will have to provide the actual values in the signed token when using this for real.`}</p>
-                <Parameters
-                    className="mt2"
-                    vertical
-                    parameters={previewParameters}
-                    parameterValues={parameterValues}
-                    setParameterValue={onChangeParameterValue}
-                />
-            </Section>
-        }
-        { resource.enable_embedding ?
-            <Section title={t`Danger zone`}>
-                <p>{t`This will disable embedding for this ${resourceType}.`}</p>
-                <Button medium warning onClick={onUnpublish}>{t`Unpublish`}</Button>
-            </Section>
-        : null }
-    </div>
+      )}
+    {resource.enable_embedding ? (
+      <Section title={t`Danger zone`}>
+        <p>{t`This will disable embedding for this ${resourceType}.`}</p>
+        <Button medium warning onClick={onUnpublish}>{t`Unpublish`}</Button>
+      </Section>
+    ) : null}
+  </div>
+);
 
-const Section = ({ className, title, children }) =>
-    <div className={cx(className, "mb3 pb4 border-row-divider border-med")}>
-        <h3>{title}</h3>
-        {children}
-    </div>
+const Section = ({ className, title, children }) => (
+  <div className={cx(className, "mb3 pb4 border-row-divider border-med")}>
+    <h3>{title}</h3>
+    {children}
+  </div>
+);
 
 export default AdvancedSettingsPane;
diff --git a/frontend/src/metabase/public/components/widgets/CodeSample.jsx b/frontend/src/metabase/public/components/widgets/CodeSample.jsx
index c998e9062982561d21b487a89f193e7b21200281..ae3cb7f71fc19e7e4aadac7973e3e9f5aec7b32f 100644
--- a/frontend/src/metabase/public/components/widgets/CodeSample.jsx
+++ b/frontend/src/metabase/public/components/widgets/CodeSample.jsx
@@ -12,77 +12,81 @@ import _ from "underscore";
 import type { CodeSampleOption } from "metabase/public/lib/code";
 
 type Props = {
-    className?: string,
-    title?: string,
-    options?: Array<CodeSampleOption>,
-    onChangeOption?: (option: ?CodeSampleOption) => void
+  className?: string,
+  title?: string,
+  options?: Array<CodeSampleOption>,
+  onChangeOption?: (option: ?CodeSampleOption) => void,
 };
 
 type State = {
-    name: ?string,
+  name: ?string,
 };
 
 export default class CodeSample extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            name: Array.isArray(props.options) && props.options.length > 0 ? props.options[0].name : null
-        };
-    }
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      name:
+        Array.isArray(props.options) && props.options.length > 0
+          ? props.options[0].name
+          : null,
+    };
+  }
 
-    setOption(name: string) {
-        this.setState({ name })
-    }
+  setOption(name: string) {
+    this.setState({ name });
+  }
 
-    handleChange(name: string) {
-        const { options, onChangeOption } = this.props;
-        this.setState({ name });
-        if (onChangeOption) {
-            onChangeOption(_.findWhere(options, { name }))
-        }
+  handleChange(name: string) {
+    const { options, onChangeOption } = this.props;
+    this.setState({ name });
+    if (onChangeOption) {
+      onChangeOption(_.findWhere(options, { name }));
     }
+  }
 
-    render() {
-        const { className, title, options } = this.props;
-        const { name } = this.state;
-        const selected = _.findWhere(options, { name });
-        const source = selected && selected.source()
-        return (
-            <div className={className}>
-                { (title || (options && options.length > 1)) &&
-                    <div className="flex align-center">
-                        <h4>{title}</h4>
-                        { options && options.length > 1 ?
-                            <Select
-                                className="AdminSelect--borderless ml-auto pt1 pb1"
-                                value={name}
-                                onChange={(e) => this.handleChange(e.target.value)}
-                            >
-                                { options.map(option =>
-                                    <Option value={option.name}>{option.name}</Option>
-                                )}
-                            </Select>
-                        : null }
-                    </div>
-                }
-                <div className="bordered rounded shadowed relative">
-                    <AceEditor
-                        className="z1"
-                        value={source}
-                        mode={selected && selected.mode}
-                        theme="ace/theme/metabase"
-                        sizeToFit readOnly
-                    />
-                    { source &&
-                        <div className="absolute top right text-brand-hover cursor-pointer z2">
-                            <CopyButton className="p1" value={source} />
-                        </div>
-                    }
-                </div>
+  render() {
+    const { className, title, options } = this.props;
+    const { name } = this.state;
+    const selected = _.findWhere(options, { name });
+    const source = selected && selected.source();
+    return (
+      <div className={className}>
+        {(title || (options && options.length > 1)) && (
+          <div className="flex align-center">
+            <h4>{title}</h4>
+            {options && options.length > 1 ? (
+              <Select
+                className="AdminSelect--borderless ml-auto pt1 pb1"
+                value={name}
+                onChange={e => this.handleChange(e.target.value)}
+              >
+                {options.map(option => (
+                  <Option value={option.name}>{option.name}</Option>
+                ))}
+              </Select>
+            ) : null}
+          </div>
+        )}
+        <div className="bordered rounded shadowed relative">
+          <AceEditor
+            className="z1"
+            value={source}
+            mode={selected && selected.mode}
+            theme="ace/theme/metabase"
+            sizeToFit
+            readOnly
+          />
+          {source && (
+            <div className="absolute top right text-brand-hover cursor-pointer z2">
+              <CopyButton className="p1" value={source} />
             </div>
-        )
-    }
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx b/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx
index ba296039efee03893d70c4716f6f3d746b3eef47..0150c027daf2adbaa25cb3baacb44f0730553385 100644
--- a/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/DisplayOptionsPane.jsx
@@ -4,41 +4,58 @@ import React from "react";
 
 import EmbedSelect from "./EmbedSelect";
 import CheckBox from "metabase/components/CheckBox";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import type { DisplayOptions } from "./EmbedModalContent";
 
 type Props = {
-    className?: string,
-    displayOptions: DisplayOptions,
-    onChangeDisplayOptions: (displayOptions: DisplayOptions) => void
-}
+  className?: string,
+  displayOptions: DisplayOptions,
+  onChangeDisplayOptions: (displayOptions: DisplayOptions) => void,
+};
 
 const THEME_OPTIONS = [
-    { name: t`Light`, value: null, icon: "sun" },
-    { name: t`Dark`, value: "night", icon: "moon" }
+  { name: t`Light`, value: null, icon: "sun" },
+  { name: t`Dark`, value: "night", icon: "moon" },
 ];
 
-const DisplayOptionsPane = ({ className, displayOptions, onChangeDisplayOptions }: Props) =>
-    <div className={className}>
-        <div className="flex align-center my1">
-            <CheckBox
-                checked={displayOptions.bordered}
-                onChange={(e) => onChangeDisplayOptions({ ...displayOptions, bordered: e.target.checked })}
-            />
-            <span className="ml1">{t`Border`}</span>
-        </div>
-        <div className="flex align-center my1">
-            <CheckBox
-                checked={displayOptions.titled}
-                onChange={(e) => onChangeDisplayOptions({ ...displayOptions, titled: e.target.checked })}
-            />
-            <span className="ml1">{t`Title`}</span>
-        </div>
-        <EmbedSelect
-            value={displayOptions.theme}
-            options={THEME_OPTIONS}
-            onChange={(value) => onChangeDisplayOptions({ ...displayOptions, theme: value })}
-        />
-    </div>;
+const DisplayOptionsPane = ({
+  className,
+  displayOptions,
+  onChangeDisplayOptions,
+}: Props) => (
+  <div className={className}>
+    <div className="flex align-center my1">
+      <CheckBox
+        checked={displayOptions.bordered}
+        onChange={e =>
+          onChangeDisplayOptions({
+            ...displayOptions,
+            bordered: e.target.checked,
+          })
+        }
+      />
+      <span className="ml1">{t`Border`}</span>
+    </div>
+    <div className="flex align-center my1">
+      <CheckBox
+        checked={displayOptions.titled}
+        onChange={e =>
+          onChangeDisplayOptions({
+            ...displayOptions,
+            titled: e.target.checked,
+          })
+        }
+      />
+      <span className="ml1">{t`Title`}</span>
+    </div>
+    <EmbedSelect
+      value={displayOptions.theme}
+      options={THEME_OPTIONS}
+      onChange={value =>
+        onChangeDisplayOptions({ ...displayOptions, theme: value })
+      }
+    />
+  </div>
+);
 
 export default DisplayOptionsPane;
diff --git a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx
index 6059b06ca292164bf1a3bef145e7e2ba545e0c11..d7b471cb010a8abeb5d4d82c4e488d09b7fde4b3 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedCodePane.jsx
@@ -4,8 +4,12 @@ import React, { Component } from "react";
 
 import ExternalLink from "metabase/components/ExternalLink";
 import CodeSample from "./CodeSample";
-import { t, jt } from 'c-3po';
-import { getPublicEmbedOptions, getSignedEmbedOptions, getSignTokenOptions } from "../../lib/code"
+import { t, jt } from "c-3po";
+import {
+  getPublicEmbedOptions,
+  getSignedEmbedOptions,
+  getSignTokenOptions,
+} from "../../lib/code";
 
 import "metabase/lib/ace/theme-metabase";
 
@@ -16,62 +20,91 @@ import "ace/mode-html";
 import "ace/mode-jsx";
 
 import type { EmbedType, DisplayOptions } from "./EmbedModalContent";
-import type { EmbeddableResource, EmbeddingParams } from "metabase/public/lib/types";
+import type {
+  EmbeddableResource,
+  EmbeddingParams,
+} from "metabase/public/lib/types";
 
 type Props = {
-    className: string,
-    embedType: EmbedType,
-    iframeUrl: string,
-    token: string,
-    siteUrl: string,
-    secretKey: string,
-    resource: EmbeddableResource,
-    resourceType: string,
-    params: EmbeddingParams,
-    displayOptions: DisplayOptions
-}
+  className: string,
+  embedType: EmbedType,
+  iframeUrl: string,
+  token: string,
+  siteUrl: string,
+  secretKey: string,
+  resource: EmbeddableResource,
+  resourceType: string,
+  params: EmbeddingParams,
+  displayOptions: DisplayOptions,
+};
 
 export default class EmbedCodePane extends Component {
-    props: Props;
+  props: Props;
 
-    _embedSample: ?CodeSample;
+  _embedSample: ?CodeSample;
 
-    render() {
-        const { className, embedType, iframeUrl, siteUrl, secretKey, resource, resourceType, params, displayOptions } = this.props;
-        return (
-            <div className={className}>
-                { embedType === "application" ?
-                    <div key="application">
-                    <p>{t`To embed this ${resourceType} in your application:`}</p>
-                        <CodeSample
-                            title={t`Insert this code snippet in your server code to generate the signed embedding URL `}
-                            options={getSignTokenOptions({ siteUrl, secretKey, resourceType, resourceId: resource.id, params, displayOptions })}
-                            onChangeOption={(option) => {
-                                if (option && option.embedOption && this._embedSample && this._embedSample.setOption) {
-                                    this._embedSample.setOption(option.embedOption);
-                                }
-                            }}
-                        />
-                        <CodeSample
-                            className="mt2"
-                            ref={embedSample => this._embedSample = embedSample}
-                            title={t`Then insert this code snippet in your HTML template or single page app.`}
-                            options={getSignedEmbedOptions({ iframeUrl })}
-                        />
-                    </div>
-                :
-                    <div key="public">
-                        <CodeSample
-                            title={t`Embed code snippet for your HTML or Frontend Application`}
-                            options={getPublicEmbedOptions({ iframeUrl })}
-                        />
-                    </div>
+  render() {
+    const {
+      className,
+      embedType,
+      iframeUrl,
+      siteUrl,
+      secretKey,
+      resource,
+      resourceType,
+      params,
+      displayOptions,
+    } = this.props;
+    return (
+      <div className={className}>
+        {embedType === "application" ? (
+          <div key="application">
+            <p>{t`To embed this ${resourceType} in your application:`}</p>
+            <CodeSample
+              title={t`Insert this code snippet in your server code to generate the signed embedding URL `}
+              options={getSignTokenOptions({
+                siteUrl,
+                secretKey,
+                resourceType,
+                resourceId: resource.id,
+                params,
+                displayOptions,
+              })}
+              onChangeOption={option => {
+                if (
+                  option &&
+                  option.embedOption &&
+                  this._embedSample &&
+                  this._embedSample.setOption
+                ) {
+                  this._embedSample.setOption(option.embedOption);
                 }
+              }}
+            />
+            <CodeSample
+              className="mt2"
+              ref={embedSample => (this._embedSample = embedSample)}
+              title={t`Then insert this code snippet in your HTML template or single page app.`}
+              options={getSignedEmbedOptions({ iframeUrl })}
+            />
+          </div>
+        ) : (
+          <div key="public">
+            <CodeSample
+              title={t`Embed code snippet for your HTML or Frontend Application`}
+              options={getPublicEmbedOptions({ iframeUrl })}
+            />
+          </div>
+        )}
 
-                <div className="text-centered my2">
-                    <h4>{jt`More ${<ExternalLink href="https://github.com/metabase/embedding_reference_apps">examples on GitHub</ExternalLink>}`}</h4>
-                </div>
-            </div>
-        );
-    }
+        <div className="text-centered my2">
+          <h4>{jt`More ${(
+            <ExternalLink href="https://github.com/metabase/embedding_reference_apps">
+              examples on GitHub
+            </ExternalLink>
+          )}`}</h4>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
index 874137c8c41443cddf023510a50a0a24fdd09049..0347fe3a44f919cb9734d88b7ee2e7027e0751eb 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedModalContent.jsx
@@ -9,213 +9,283 @@ import Icon from "metabase/components/Icon";
 import SharingPane from "./SharingPane";
 import AdvancedEmbedPane from "./AdvancedEmbedPane";
 
-import { getSignedPreviewUrl, getUnsignedPreviewUrl, getSignedToken } from "metabase/public/lib/embed";
+import {
+  getSignedPreviewUrl,
+  getUnsignedPreviewUrl,
+  getSignedToken,
+} from "metabase/public/lib/embed";
 
-import { getSiteUrl, getEmbeddingSecretKey, getIsPublicSharingEnabled, getIsApplicationEmbeddingEnabled } from "metabase/selectors/settings";
+import {
+  getSiteUrl,
+  getEmbeddingSecretKey,
+  getIsPublicSharingEnabled,
+  getIsApplicationEmbeddingEnabled,
+} from "metabase/selectors/settings";
 import { getUserIsAdmin } from "metabase/selectors/user";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 import type { Parameter, ParameterId } from "metabase/meta/types/Parameter";
-import type { EmbeddableResource, EmbeddingParams } from "metabase/public/lib/types";
+import type {
+  EmbeddableResource,
+  EmbeddingParams,
+} from "metabase/public/lib/types";
 
-export type Pane = "preview"|"code";
-export type EmbedType = null|"simple"|"application";
+export type Pane = "preview" | "code";
+export type EmbedType = null | "simple" | "application";
 
 export type DisplayOptions = {
-    theme: ?string,
-    bordered: boolean,
-    titled: boolean,
-}
+  theme: ?string,
+  bordered: boolean,
+  titled: boolean,
+};
 
 type Props = {
-    className?: string,
-    resource: EmbeddableResource,
-    resourceType: string,
-    resourceParameters: Parameter[],
-
-    isAdmin: boolean,
-    siteUrl: string,
-    secretKey: string,
-
-    // Flow doesn't understand these are provided by @connect?
-    // isPublicSharingEnabled: bool,
-    // isApplicationEmbeddingEnabled: bool,
-
-    getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
-
-    onUpdateEnableEmbedding: (enable_embedding: bool) => Promise<void>,
-    onUpdateEmbeddingParams: (embedding_params: EmbeddingParams) => Promise<void>,
-    onCreatePublicLink: () => Promise<void>,
-    onDisablePublicLink: () => Promise<void>,
-    onClose: () => void
+  className?: string,
+  resource: EmbeddableResource,
+  resourceType: string,
+  resourceParameters: Parameter[],
+
+  isAdmin: boolean,
+  siteUrl: string,
+  secretKey: string,
+
+  // Flow doesn't understand these are provided by @connect?
+  // isPublicSharingEnabled: bool,
+  // isApplicationEmbeddingEnabled: bool,
+
+  getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
+
+  onUpdateEnableEmbedding: (enable_embedding: boolean) => Promise<void>,
+  onUpdateEmbeddingParams: (embedding_params: EmbeddingParams) => Promise<void>,
+  onCreatePublicLink: () => Promise<void>,
+  onDisablePublicLink: () => Promise<void>,
+  onClose: () => void,
 };
 
 type State = {
-    pane: Pane,
-    embedType: EmbedType,
-    embeddingParams: EmbeddingParams,
-    displayOptions: DisplayOptions,
-    parameterValues: { [id: ParameterId]: string },
+  pane: Pane,
+  embedType: EmbedType,
+  embeddingParams: EmbeddingParams,
+  displayOptions: DisplayOptions,
+  parameterValues: { [id: ParameterId]: string },
 };
 
 const mapStateToProps = (state, props) => ({
-    isAdmin:                        getUserIsAdmin(state, props),
-    siteUrl:                        getSiteUrl(state, props),
-    secretKey:                      getEmbeddingSecretKey(state, props),
-    isPublicSharingEnabled:         getIsPublicSharingEnabled(state, props),
-    isApplicationEmbeddingEnabled:  getIsApplicationEmbeddingEnabled(state, props),
-})
+  isAdmin: getUserIsAdmin(state, props),
+  siteUrl: getSiteUrl(state, props),
+  secretKey: getEmbeddingSecretKey(state, props),
+  isPublicSharingEnabled: getIsPublicSharingEnabled(state, props),
+  isApplicationEmbeddingEnabled: getIsApplicationEmbeddingEnabled(state, props),
+});
 
 @connect(mapStateToProps)
 export default class EmbedModalContent extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            pane: "preview",
-            embedType: null,
-            embeddingParams: props.resource.embedding_params || {},
-            displayOptions: {
-                theme: null,
-                bordered: true,
-                titled: true
-            },
-
-            parameterValues: {},
-        };
-    }
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      pane: "preview",
+      embedType: null,
+      embeddingParams: props.resource.embedding_params || {},
+      displayOptions: {
+        theme: null,
+        bordered: true,
+        titled: true,
+      },
+
+      parameterValues: {},
+    };
+  }
 
-    static defaultProps = {};
-
-    handleSave = async () => {
-        try {
-            const { resource } = this.props;
-            const { embeddingParams, embedType } = this.state;
-            if (embedType === "application") {
-                if (!resource.enable_embedding) {
-                    await this.props.onUpdateEnableEmbedding(true);
-                }
-                await this.props.onUpdateEmbeddingParams(embeddingParams);
-            } else {
-                if (!resource.public_uuid) {
-                    await this.props.onCreatePublicLink();
-                }
-            }
-        } catch (e) {
-            console.error(e);
-            throw e;
+  static defaultProps = {};
+
+  handleSave = async () => {
+    try {
+      const { resource } = this.props;
+      const { embeddingParams, embedType } = this.state;
+      if (embedType === "application") {
+        if (!resource.enable_embedding) {
+          await this.props.onUpdateEnableEmbedding(true);
+        }
+        await this.props.onUpdateEmbeddingParams(embeddingParams);
+      } else {
+        if (!resource.public_uuid) {
+          await this.props.onCreatePublicLink();
         }
+      }
+    } catch (e) {
+      console.error(e);
+      throw e;
     }
+  };
 
-    handleUnpublish = async () => {
-        await this.props.onUpdateEnableEmbedding(false);
-    }
+  handleUnpublish = async () => {
+    await this.props.onUpdateEnableEmbedding(false);
+  };
 
-    handleDiscard = () => {
-        const { resource } = this.props;
-        this.setState({ embeddingParams: resource.embedding_params || {} });
-    }
+  handleDiscard = () => {
+    const { resource } = this.props;
+    this.setState({ embeddingParams: resource.embedding_params || {} });
+  };
 
-    getPreviewParams() {
-        const { resourceParameters } = this.props;
-        const { embeddingParams, parameterValues } = this.state;
-        const params = {};
-        for (const parameter of resourceParameters) {
-            if (embeddingParams[parameter.slug] === "locked") {
-                params[parameter.slug] = (parameter.id in parameterValues) ?
-                    parameterValues[parameter.id] :
-                    null;
-            }
-        }
-        return params;
+  getPreviewParams() {
+    const { resourceParameters } = this.props;
+    const { embeddingParams, parameterValues } = this.state;
+    const params = {};
+    for (const parameter of resourceParameters) {
+      if (embeddingParams[parameter.slug] === "locked") {
+        params[parameter.slug] =
+          parameter.id in parameterValues
+            ? parameterValues[parameter.id]
+            : null;
+      }
     }
+    return params;
+  }
 
-    render() {
-        const { siteUrl, secretKey,
-            resource, resourceType, resourceParameters,
-            onClose
-        } = this.props;
-        const { pane, embedType, embeddingParams, parameterValues, displayOptions } = this.state;
-
-        const params = this.getPreviewParams();
-
-        const previewParameters = resourceParameters.filter(p => embeddingParams[p.slug] === "locked");
-
-        return (
-            <div className="flex flex-column full-height">
-                <div className="px2 py1 z1 flex align-center" style={{ boxShadow: embedType === "application" ? "0px 8px 15px -9px rgba(0,0,0,0.2)" : undefined }}>
-                    <h2 className="ml-auto">
-                        <EmbedTitle
-                            onClick={() => this.setState({ embedType: null })}
-                            type={embedType && titleize(embedType)}
-                        />
-                    </h2>
-                    <Icon
-                        className="text-grey-2 text-grey-4-hover cursor-pointer p2 ml-auto"
-                        name="close"
-                        size={24}
-                        onClick={() => {
-                            MetabaseAnalytics.trackEvent("Sharing Modal", "Modal Closed")
-                            onClose()
-                        }}
-                    />
-                </div>
-                { embedType == null ?
-                    <div className="flex-full">
-                        {/* Center only using margins because  */}
-                        <div className="ml-auto mr-auto" style={{maxWidth: 1040}}>
-                            <SharingPane
-                                // $FlowFixMe: Flow doesn't understand these are provided by @connect?
-                                {...this.props}
-                                publicUrl={getUnsignedPreviewUrl(siteUrl, resourceType, resource.public_uuid, displayOptions)}
-                                iframeUrl={getUnsignedPreviewUrl(siteUrl, resourceType, resource.public_uuid, displayOptions)}
-                                onChangeEmbedType={(embedType) => this.setState({embedType})}
-                            />
-                        </div>
-                    </div>
-                    : embedType === "application" ?
-                        <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 }
+  render() {
+    const {
+      siteUrl,
+      secretKey,
+      resource,
+      resourceType,
+      resourceParameters,
+      onClose,
+    } = this.props;
+    const {
+      pane,
+      embedType,
+      embeddingParams,
+      parameterValues,
+      displayOptions,
+    } = this.state;
+
+    const params = this.getPreviewParams();
+
+    const previewParameters = resourceParameters.filter(
+      p => embeddingParams[p.slug] === "locked",
+    );
+
+    return (
+      <div className="flex flex-column full-height">
+        <div
+          className="px2 py1 z1 flex align-center"
+          style={{
+            boxShadow:
+              embedType === "application"
+                ? "0px 8px 15px -9px rgba(0,0,0,0.2)"
+                : undefined,
+          }}
+        >
+          <h2 className="ml-auto">
+            <EmbedTitle
+              onClick={() => this.setState({ embedType: null })}
+              type={embedType && titleize(embedType)}
+            />
+          </h2>
+          <Icon
+            className="text-grey-2 text-grey-4-hover cursor-pointer p2 ml-auto"
+            name="close"
+            size={24}
+            onClick={() => {
+              MetabaseAnalytics.trackEvent("Sharing Modal", "Modal Closed");
+              onClose();
+            }}
+          />
+        </div>
+        {embedType == null ? (
+          <div className="flex-full">
+            {/* Center only using margins because  */}
+            <div className="ml-auto mr-auto" style={{ maxWidth: 1040 }}>
+              <SharingPane
+                // $FlowFixMe: Flow doesn't understand these are provided by @connect?
+                {...this.props}
+                publicUrl={getUnsignedPreviewUrl(
+                  siteUrl,
+                  resourceType,
+                  resource.public_uuid,
+                  displayOptions,
+                )}
+                iframeUrl={getUnsignedPreviewUrl(
+                  siteUrl,
+                  resourceType,
+                  resource.public_uuid,
+                  displayOptions,
+                )}
+                onChangeEmbedType={embedType => this.setState({ embedType })}
+              />
             </div>
-        );
-    }
+          </div>
+        ) : embedType === "application" ? (
+          <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>
+    );
+  }
 }
 
-export const EmbedTitle = ({ type, onClick }: { type: ?string, onClick: () => any}) =>
-    <a className="flex align-center" onClick={onClick}>
-        <span className="text-brand-hover">Sharing</span>
-        { type && <Icon name="chevronright" className="mx1 text-grey-3" /> }
-        {type}
-    </a>;
+export const EmbedTitle = ({
+  type,
+  onClick,
+}: {
+  type: ?string,
+  onClick: () => any,
+}) => (
+  <a className="flex align-center" onClick={onClick}>
+    <span className="text-brand-hover">Sharing</span>
+    {type && <Icon name="chevronright" className="mx1 text-grey-3" />}
+    {type}
+  </a>
+);
diff --git a/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx b/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx
index 5e569c9b2859318c112f50fe8922d316f6149246..400ffe5a802a462da6f43802f72972889132acce 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedSelect.jsx
@@ -7,28 +7,32 @@ import Icon from "metabase/components/Icon";
 import cx from "classnames";
 
 type Props = {
-    className?: string,
-    value: any,
-    onChange: (value: any) => void,
-    options: Array<{ name: string, value: any}>
-}
+  className?: string,
+  value: any,
+  onChange: (value: any) => void,
+  options: Array<{ name: string, value: any }>,
+};
 
-const EmbedSelect = ({ className, value, onChange, options }: Props) =>
-    <div className={cx(className, "flex")}>
-        { options.map(option =>
-            <div
-                className={cx("flex-full flex layout-centered mx1 p1 border-bottom border-med border-dark-hover", {
-                    "border-dark": value === option.value,
-                    "cursor-pointer": value !== option.value
-                })}
-                onClick={() => onChange(option.value)}
-            >
-                { option.icon && <Icon name={option.icon} className="mr1" />}
-                {option.name}
-            </div>
+const EmbedSelect = ({ className, value, onChange, options }: Props) => (
+  <div className={cx(className, "flex")}>
+    {options.map(option => (
+      <div
+        className={cx(
+          "flex-full flex layout-centered mx1 p1 border-bottom border-med border-dark-hover",
+          {
+            "border-dark": value === option.value,
+            "cursor-pointer": value !== option.value,
+          },
         )}
-        {/* hack because border-bottom doesn't add a border to the last element :-/ */}
-        <div className="hide" />
-    </div>
+        onClick={() => onChange(option.value)}
+      >
+        {option.icon && <Icon name={option.icon} className="mr1" />}
+        {option.name}
+      </div>
+    ))}
+    {/* hack because border-bottom doesn't add a border to the last element :-/ */}
+    <div className="hide" />
+  </div>
+);
 
 export default EmbedSelect;
diff --git a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
index a9589adc956b96736c8739925ebf363587ec83a8..71918ae812bdfecff02a782196bc3e43c2392b9c 100644
--- a/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
+++ b/frontend/src/metabase/public/components/widgets/EmbedWidget.jsx
@@ -5,60 +5,74 @@ import React, { Component } from "react";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import Tooltip from "metabase/components/Tooltip";
 import Icon from "metabase/components/Icon";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 import EmbedModalContent from "./EmbedModalContent";
 
 import cx from "classnames";
 
-import type { EmbeddableResource, EmbeddingParams } from "metabase/public/lib/types";
+import type {
+  EmbeddableResource,
+  EmbeddingParams,
+} from "metabase/public/lib/types";
 import type { Parameter } from "metabase/meta/types/Parameter";
 
 type Props = {
-    className?: string,
+  className?: string,
 
-    resource: EmbeddableResource,
-    resourceType: string,
-    resourceParameters:  Parameter[],
+  resource: EmbeddableResource,
+  resourceType: string,
+  resourceParameters: Parameter[],
 
-    siteUrl: string,
-    secretKey: string,
-    isAdmin: boolean,
+  siteUrl: string,
+  secretKey: string,
+  isAdmin: boolean,
 
-    getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
+  getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
 
-    onUpdateEnableEmbedding: (enable_embedding: bool) => Promise<void>,
-    onUpdateEmbeddingParams: (embedding_params: EmbeddingParams) => Promise<void>,
-    onCreatePublicLink: () => Promise<void>,
-    onDisablePublicLink: () => Promise<void>,
+  onUpdateEnableEmbedding: (enable_embedding: boolean) => Promise<void>,
+  onUpdateEmbeddingParams: (embedding_params: EmbeddingParams) => Promise<void>,
+  onCreatePublicLink: () => Promise<void>,
+  onDisablePublicLink: () => Promise<void>,
 };
 
 export default class EmbedWidget extends Component {
-    props: Props;
+  props: Props;
 
-    _modal: ?ModalWithTrigger
+  _modal: ?ModalWithTrigger;
 
-    render() {
-        const { className, resourceType } = this.props;
-        return (
-            <ModalWithTrigger
-                ref={m => this._modal = m}
-                full
-                triggerElement={
-                    <Tooltip tooltip={t`Sharing and embedding`}>
-                        <Icon name="share" onClick={() => MetabaseAnalytics.trackEvent("Sharing / Embedding", resourceType, "Sharing Link Clicked") } />
-                    </Tooltip>
-                }
-                triggerClasses={cx(className, "text-brand-hover")}
-                className="scroll-y"
-            >
-                <EmbedModalContent
-                    {...this.props}
-                    onClose={() => { this._modal && this._modal.close() }}
-                    className="full-height"
-                />
-            </ModalWithTrigger>
-        );
-    }
+  render() {
+    const { className, resourceType } = this.props;
+    return (
+      <ModalWithTrigger
+        ref={m => (this._modal = m)}
+        full
+        triggerElement={
+          <Tooltip tooltip={t`Sharing and embedding`}>
+            <Icon
+              name="share"
+              onClick={() =>
+                MetabaseAnalytics.trackEvent(
+                  "Sharing / Embedding",
+                  resourceType,
+                  "Sharing Link Clicked",
+                )
+              }
+            />
+          </Tooltip>
+        }
+        triggerClasses={cx(className, "text-brand-hover")}
+        className="scroll-y"
+      >
+        <EmbedModalContent
+          {...this.props}
+          onClose={() => {
+            this._modal && this._modal.close();
+          }}
+          className="full-height"
+        />
+      </ModalWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/components/widgets/PreviewPane.jsx b/frontend/src/metabase/public/components/widgets/PreviewPane.jsx
index 28fa3d17766654dbf287d70d37febbf6eff3f4f7..b9205e8db2cf813e437a2789ce5e0d3c47e46110 100644
--- a/frontend/src/metabase/public/components/widgets/PreviewPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/PreviewPane.jsx
@@ -1,35 +1,37 @@
-
 import React, { Component } from "react";
 
 import cx from "classnames";
 
 export default class PreviewPane extends Component {
-    constructor(props) {
-        super(props);
+  constructor(props) {
+    super(props);
 
-        this.state = {
-            loading: true
-        };
-    }
+    this.state = {
+      loading: true,
+    };
+  }
 
-    componentWillReceiveProps(nextProps) {
-        if (nextProps.previewUrl !== this.props.previewUrl) {
-            this.setState({ loading: true })
-        }
+  componentWillReceiveProps(nextProps) {
+    if (nextProps.previewUrl !== this.props.previewUrl) {
+      this.setState({ loading: true });
     }
+  }
 
-    render() {
-        const { className, previewUrl } = this.props;
-        return (
-            <div className={cx(className, "flex relative")} style={{ minHeight: 280 }}>
-                <iframe
-                    className="flex-full"
-                    src={previewUrl}
-                    frameBorder={0}
-                    allowTransparency
-                    onLoad={() => this.setState({ loading: false })}
-                />
-            </div>
-        );
-    }
+  render() {
+    const { className, previewUrl } = this.props;
+    return (
+      <div
+        className={cx(className, "flex relative")}
+        style={{ minHeight: 280 }}
+      >
+        <iframe
+          className="flex-full"
+          src={previewUrl}
+          frameBorder={0}
+          allowTransparency
+          onLoad={() => this.setState({ loading: false })}
+        />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/components/widgets/SharingPane.jsx b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
index 18fc484ccd61fb25125dd6ca56f8e034b6182e4e..a9f19fc3e1dcdac72e82b967445708d48f07724b 100644
--- a/frontend/src/metabase/public/components/widgets/SharingPane.jsx
+++ b/frontend/src/metabase/public/components/widgets/SharingPane.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import RetinaImage from "react-retina-image";
 import Icon from "metabase/components/Icon";
 import Toggle from "metabase/components/Toggle";
@@ -18,129 +18,167 @@ import type { EmbeddableResource } from "metabase/public/lib/types";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 type Props = {
-    resourceType: string,
-    resource: EmbeddableResource,
-    extensions?: string[],
+  resourceType: string,
+  resource: EmbeddableResource,
+  extensions?: string[],
 
-    isAdmin: bool,
+  isAdmin: boolean,
 
-    isPublicSharingEnabled: bool,
-    isApplicationEmbeddingEnabled: bool,
+  isPublicSharingEnabled: boolean,
+  isApplicationEmbeddingEnabled: boolean,
 
-    onCreatePublicLink: () => Promise<void>,
-    onDisablePublicLink: () => Promise<void>,
-    getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
-    onChangeEmbedType: (embedType: EmbedType) => void,
+  onCreatePublicLink: () => Promise<void>,
+  onDisablePublicLink: () => Promise<void>,
+  getPublicUrl: (resource: EmbeddableResource, extension: ?string) => string,
+  onChangeEmbedType: (embedType: EmbedType) => void,
 };
 
 type State = {
-    extension: ?string,
+  extension: ?string,
 };
 
 export default class SharingPane extends Component {
-    props: Props;
-    state: State = {
-        extension: null
-    };
+  props: Props;
+  state: State = {
+    extension: null,
+  };
 
-    static defaultProps = {
-        extensions: []
-    };
+  static defaultProps = {
+    extensions: [],
+  };
 
-    render() {
-        const {
-            resource, resourceType,
-            onCreatePublicLink, onDisablePublicLink,
-            extensions,
-            getPublicUrl,
-            onChangeEmbedType,
-            isAdmin,
-            isPublicSharingEnabled,
-            isApplicationEmbeddingEnabled
-        } = this.props;
+  render() {
+    const {
+      resource,
+      resourceType,
+      onCreatePublicLink,
+      onDisablePublicLink,
+      extensions,
+      getPublicUrl,
+      onChangeEmbedType,
+      isAdmin,
+      isPublicSharingEnabled,
+      isApplicationEmbeddingEnabled,
+    } = this.props;
 
-        const publicLink = getPublicUrl(resource, this.state.extension);
-        const iframeSource = getPublicEmbedHTML(getPublicUrl(resource));
+    const publicLink = getPublicUrl(resource, this.state.extension);
+    const iframeSource = getPublicEmbedHTML(getPublicUrl(resource));
 
-        return (
-            <div className="pt2 ml-auto mr-auto" style={{ maxWidth: 600 }}>
-                { isAdmin && isPublicSharingEnabled &&
-                    <div className="pb2 mb4 border-bottom flex align-center">
-                        <h4>{t`Enable sharing`}</h4>
-                        <div className="ml-auto">
-                            { resource.public_uuid ?
-                                <Confirm
-                                    title={t`Disable this public link?`}
-                                    content={t`This will cause the existing link to stop working. You can re-enable it, but when you do it will be a different link.`}
-                                    action={() => {
-                                        MetabaseAnalytics.trackEvent("Sharing Modal", "Public Link Disabled", resourceType);
-                                        onDisablePublicLink();
-                                    }}
-                                >
-                                    <Toggle value={true} />
-                                </Confirm>
-                            :
-                                <Toggle value={false} onChange={() => {
-                                    MetabaseAnalytics.trackEvent("Sharing Modal", "Public Link Enabled", resourceType);
-                                    onCreatePublicLink();
-                                }}/>
-                            }
-                        </div>
-                    </div>
-                }
-                <div className={cx("mb4 flex align-center", { disabled: !resource.public_uuid })}>
-                    <div style={{ width: 98, height: 63 }} className="bordered rounded shadowed flex layout-centered">
-                        <Icon name="link" size={32} />
-                    </div>
-                    <div className="ml2 flex-full">
-                        <h3 className="text-brand mb1">{t`Public link`}</h3>
-                        <div className="mb1">{t`Share this ${resourceType} with people who don't have a Metabase account using the URL below:`}</div>
-                        <CopyWidget value={publicLink} />
-                        { extensions && extensions.length > 0 &&
-                            <div className="mt1">
-                                {extensions.map(extension =>
-                                    <span
-                                        className={cx("cursor-pointer text-brand-hover text-bold text-uppercase",
-                                            extension === this.state.extension ? "text-brand" : "text-grey-2"
-                                        )}
-                                        onClick={() => this.setState({ extension: extension === this.state.extension ? null : extension })}
-                                    >
-                                        {extension}{" "}
-                                    </span>
-                                )}
-                            </div>
-                        }
-                    </div>
-                </div>
-                <div className={cx("mb4 flex align-center", { disabled: !resource.public_uuid })}>
-                    <RetinaImage
-                        width={98}
-                        src="app/assets/img/simple_embed.png"
-                        forceOriginalDimensions={false}
-                    />
-                    <div className="ml2 flex-full">
-                        <h3 className="text-green mb1">{t`Public embed`}</h3>
-                        <div className="mb1">{t`Embed this ${resourceType} in blog posts or web pages by copying and pasting this snippet:`}</div>
-                        <CopyWidget value={iframeSource} />
-                    </div>
-                </div>
-                { isAdmin &&
-                    <div
-                        className={cx("mb4 flex align-center cursor-pointer", { disabled: !isApplicationEmbeddingEnabled })}
-                        onClick={() => onChangeEmbedType("application")}
+    return (
+      <div className="pt2 ml-auto mr-auto" style={{ maxWidth: 600 }}>
+        {isAdmin &&
+          isPublicSharingEnabled && (
+            <div className="pb2 mb4 border-bottom flex align-center">
+              <h4>{t`Enable sharing`}</h4>
+              <div className="ml-auto">
+                {resource.public_uuid ? (
+                  <Confirm
+                    title={t`Disable this public link?`}
+                    content={t`This will cause the existing link to stop working. You can re-enable it, but when you do it will be a different link.`}
+                    action={() => {
+                      MetabaseAnalytics.trackEvent(
+                        "Sharing Modal",
+                        "Public Link Disabled",
+                        resourceType,
+                      );
+                      onDisablePublicLink();
+                    }}
+                  >
+                    <Toggle value={true} />
+                  </Confirm>
+                ) : (
+                  <Toggle
+                    value={false}
+                    onChange={() => {
+                      MetabaseAnalytics.trackEvent(
+                        "Sharing Modal",
+                        "Public Link Enabled",
+                        resourceType,
+                      );
+                      onCreatePublicLink();
+                    }}
+                  />
+                )}
+              </div>
+            </div>
+          )}
+        <div
+          className={cx("mb4 flex align-center", {
+            disabled: !resource.public_uuid,
+          })}
+        >
+          <div
+            style={{ width: 98, height: 63 }}
+            className="bordered rounded shadowed flex layout-centered"
+          >
+            <Icon name="link" size={32} />
+          </div>
+          <div className="ml2 flex-full">
+            <h3 className="text-brand mb1">{t`Public link`}</h3>
+            <div className="mb1">{t`Share this ${resourceType} with people who don't have a Metabase account using the URL below:`}</div>
+            <CopyWidget value={publicLink} />
+            {extensions &&
+              extensions.length > 0 && (
+                <div className="mt1">
+                  {extensions.map(extension => (
+                    <span
+                      className={cx(
+                        "cursor-pointer text-brand-hover text-bold text-uppercase",
+                        extension === this.state.extension
+                          ? "text-brand"
+                          : "text-grey-2",
+                      )}
+                      onClick={() =>
+                        this.setState({
+                          extension:
+                            extension === this.state.extension
+                              ? null
+                              : extension,
+                        })
+                      }
                     >
-                        <RetinaImage
-                            width={100}
-                            src="app/assets/img/secure_embed.png"
-                            forceOriginalDimensions={false}
-                        />
-                        <div className="ml2 flex-full">
-                            <h3 className="text-purple mb1">{t`Embed this ${resourceType} in an application`}</h3>
-                            <div className="">{t`By integrating with your application server code, you can provide a secure stats ${resourceType} limited to a specific user, customer, organization, etc.`}</div>
-                        </div>
-                    </div>
-                }
+                      {extension}{" "}
+                    </span>
+                  ))}
+                </div>
+              )}
+          </div>
+        </div>
+        <div
+          className={cx("mb4 flex align-center", {
+            disabled: !resource.public_uuid,
+          })}
+        >
+          <RetinaImage
+            width={98}
+            src="app/assets/img/simple_embed.png"
+            forceOriginalDimensions={false}
+          />
+          <div className="ml2 flex-full">
+            <h3 className="text-green mb1">{t`Public embed`}</h3>
+            <div className="mb1">{t`Embed this ${resourceType} in blog posts or web pages by copying and pasting this snippet:`}</div>
+            <CopyWidget value={iframeSource} />
+          </div>
+        </div>
+        {isAdmin && (
+          <div
+            className={cx("mb4 flex align-center cursor-pointer", {
+              disabled: !isApplicationEmbeddingEnabled,
+            })}
+            onClick={() => onChangeEmbedType("application")}
+          >
+            <RetinaImage
+              width={100}
+              src="app/assets/img/secure_embed.png"
+              forceOriginalDimensions={false}
+            />
+            <div className="ml2 flex-full">
+              <h3 className="text-purple mb1">{t`Embed this ${resourceType} in an application`}</h3>
+              <div className="">{t`By integrating with your application server code, you can provide a secure stats ${resourceType} limited to a specific user, customer, organization, etc.`}</div>
             </div>
-        );
-    }
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/containers/PublicApp.jsx b/frontend/src/metabase/public/containers/PublicApp.jsx
index 4d41faef88d229005fe3f320837f62fa3acd78b3..e16e11e656525b75d5245ac3f2175560eb42a3d9 100644
--- a/frontend/src/metabase/public/containers/PublicApp.jsx
+++ b/frontend/src/metabase/public/containers/PublicApp.jsx
@@ -7,28 +7,28 @@ import PublicNotFound from "metabase/public/components/PublicNotFound";
 import PublicError from "metabase/public/components/PublicError";
 
 type Props = {
-    children: any,
-    errorPage?: { status: number }
+  children: any,
+  errorPage?: { status: number },
 };
 
 const mapStateToProps = (state, props) => ({
-    errorPage: state.app.errorPage
+  errorPage: state.app.errorPage,
 });
 
 @connect(mapStateToProps)
 export default class PublicApp extends Component {
-    props: Props;
+  props: Props;
 
-    render() {
-        const { children, errorPage } = this.props;
-        if (errorPage) {
-            if (errorPage.status === 404) {
-                return <PublicNotFound />;
-            } else {
-                return <PublicError />
-            }
-        } else {
-            return children;
-        }
+  render() {
+    const { children, errorPage } = this.props;
+    if (errorPage) {
+      if (errorPage.status === 404) {
+        return <PublicNotFound />;
+      } else {
+        return <PublicError />;
+      }
+    } else {
+      return children;
     }
+  }
 }
diff --git a/frontend/src/metabase/public/containers/PublicDashboard.jsx b/frontend/src/metabase/public/containers/PublicDashboard.jsx
index 05939a7a03a173952150a1b2720f11a9dda26184..88a1940cdc3743a89f8d3f8a3f4ba32a96691153 100644
--- a/frontend/src/metabase/public/containers/PublicDashboard.jsx
+++ b/frontend/src/metabase/public/containers/PublicDashboard.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push } from "react-router-redux";
-import cx from 'classnames';
+import cx from "classnames";
 
 import { IFRAMED } from "metabase/lib/dom";
 
@@ -16,7 +16,13 @@ import EmbedFrame from "../components/EmbedFrame";
 import { fetchDatabaseMetadata } from "metabase/redux/metadata";
 import { setErrorPage } from "metabase/redux/app";
 
-import { getDashboardComplete, getCardData, getSlowCards, getParameters, getParameterValues } from "metabase/dashboard/selectors";
+import {
+  getDashboardComplete,
+  getCardData,
+  getSlowCards,
+  getParameters,
+  getParameterValues,
+} from "metabase/dashboard/selectors";
 
 import * as dashboardActions from "metabase/dashboard/dashboard";
 
@@ -27,94 +33,124 @@ 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),
-      slowCards:            getSlowCards(state, props),
-      parameters:           getParameters(state, props),
-      parameterValues:      getParameterValues(state, props)
-  }
-}
+    dashboardId:
+      props.params.dashboardId || props.params.uuid || props.params.token,
+    dashboard: getDashboardComplete(state, props),
+    dashcardData: getCardData(state, props),
+    slowCards: getSlowCards(state, props),
+    parameters: getParameters(state, props),
+    parameterValues: getParameterValues(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    ...dashboardActions,
-    fetchDatabaseMetadata,
-    setErrorPage,
-    onChangeLocation: push
-}
+  ...dashboardActions,
+  fetchDatabaseMetadata,
+  setErrorPage,
+  onChangeLocation: push,
+};
 
 type Props = {
-    params:                 { uuid?: string, token?: string },
-    location:               { query: { [key:string]: string }},
-    dashboardId:            string,
-
-    dashboard?:             Dashboard,
-    parameters:             Parameter[],
-    parameterValues:        {[key:string]: string},
-
-    initialize:             () => void,
-    isFullscreen:           boolean,
-    isNightMode:            boolean,
-    fetchDashboard:         (dashId: string, query: { [key:string]: string }) => Promise<void>,
-    fetchDashboardCardData: (options: { reload: bool, clear: bool }) => Promise<void>,
-    setParameterValue:      (id: string, value: string) => void,
-    setErrorPage:           (error: { status: number }) => void,
+  params: { uuid?: string, token?: string },
+  location: { query: { [key: string]: string } },
+  dashboardId: string,
+
+  dashboard?: Dashboard,
+  parameters: Parameter[],
+  parameterValues: { [key: string]: string },
+
+  initialize: () => void,
+  isFullscreen: boolean,
+  isNightMode: boolean,
+  fetchDashboard: (
+    dashId: string,
+    query: { [key: string]: string },
+  ) => Promise<void>,
+  fetchDashboardCardData: (options: {
+    reload: boolean,
+    clear: boolean,
+  }) => Promise<void>,
+  setParameterValue: (id: string, value: string) => void,
+  setErrorPage: (error: { status: number }) => void,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 @DashboardControls
 export default class PublicDashboard extends Component {
-    props: Props;
-
-    // $FlowFixMe
-    async componentWillMount() {
-        const { initialize, fetchDashboard, fetchDashboardCardData, setErrorPage, location, params: { uuid, token }}  = this.props;
-        initialize();
-        try {
-            // $FlowFixMe
-            await fetchDashboard(uuid || token, location.query);
-            await fetchDashboardCardData({ reload: false, clear: true });
-        } catch (error) {
-            setErrorPage(error);
-        }
+  props: Props;
+
+  // $FlowFixMe
+  async componentWillMount() {
+    const {
+      initialize,
+      fetchDashboard,
+      fetchDashboardCardData,
+      setErrorPage,
+      location,
+      params: { uuid, token },
+    } = this.props;
+    initialize();
+    try {
+      // $FlowFixMe
+      await fetchDashboard(uuid || token, location.query);
+      await fetchDashboardCardData({ reload: false, clear: true });
+    } catch (error) {
+      setErrorPage(error);
     }
+  }
 
-    componentWillReceiveProps(nextProps: Props) {
-        if (!_.isEqual(this.props.parameterValues, nextProps.parameterValues)) {
-            this.props.fetchDashboardCardData({ reload: false, clear: true });
-        }
+  componentWillReceiveProps(nextProps: Props) {
+    if (!_.isEqual(this.props.parameterValues, nextProps.parameterValues)) {
+      this.props.fetchDashboardCardData({ reload: false, clear: true });
     }
+  }
 
-    render() {
-        const { dashboard, parameters, parameterValues, isFullscreen, isNightMode } = this.props;
-        const buttons = !IFRAMED ? getDashboardActions(this.props) : [];
-
-        return (
-            <EmbedFrame
-                name={dashboard && dashboard.name}
-                description={dashboard && dashboard.description}
-                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={cx("Dashboard p1 flex-full", { "Dashboard--fullscreen": isFullscreen, "Dashboard--night": isNightMode })} loading={!dashboard}>
-                { () =>
-                    <DashboardGrid
-                        {...this.props}
-                        className={"spread"}
-                        // Don't allow clicking titles on public dashboards
-                        navigateToNewCardFromDashboard={null}
-                    />
-                }
-                </LoadingAndErrorWrapper>
-            </EmbedFrame>
-        );
-    }
+  render() {
+    const {
+      dashboard,
+      parameters,
+      parameterValues,
+      isFullscreen,
+      isNightMode,
+    } = this.props;
+    const buttons = !IFRAMED ? getDashboardActions(this.props) : [];
+
+    return (
+      <EmbedFrame
+        name={dashboard && dashboard.name}
+        description={dashboard && dashboard.description}
+        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={cx("Dashboard p1 flex-full", {
+            "Dashboard--fullscreen": isFullscreen,
+            "Dashboard--night": isNightMode,
+          })}
+          loading={!dashboard}
+        >
+          {() => (
+            <DashboardGrid
+              {...this.props}
+              className={"spread"}
+              // Don't allow clicking titles on public dashboards
+              navigateToNewCardFromDashboard={null}
+            />
+          )}
+        </LoadingAndErrorWrapper>
+      </EmbedFrame>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/containers/PublicQuestion.jsx b/frontend/src/metabase/public/containers/PublicQuestion.jsx
index fef4dd0294e8eba7de368bac569a3eb5a0a937f0..248eac3053a09c316e4be71842f38e6ee87dfb4f 100644
--- a/frontend/src/metabase/public/containers/PublicQuestion.jsx
+++ b/frontend/src/metabase/public/containers/PublicQuestion.jsx
@@ -14,7 +14,11 @@ import type { Dataset } from "metabase/meta/types/Dataset";
 import type { ParameterValues } from "metabase/meta/types/Parameter";
 
 import { getParametersBySlug } from "metabase/meta/Parameter";
-import { getParameters, getParametersWithExtras, applyParameters } from "metabase/meta/Card";
+import {
+  getParameters,
+  getParametersWithExtras,
+  applyParameters,
+} from "metabase/meta/Card";
 
 import { PublicApi, EmbedApi } from "metabase/services";
 
@@ -24,155 +28,166 @@ 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,
-    addParamValues: (any) => 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:    ParameterValues
+  card: ?Card,
+  result: ?Dataset,
+  parameterValues: ParameterValues,
 };
 
 const mapDispatchToProps = {
-    setErrorPage,
-    addParamValues
+  setErrorPage,
+  addParamValues,
 };
 
 @connect(null, mapDispatchToProps)
 @ExplicitSize
 export default class PublicQuestion extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            card: null,
-            result: null,
-            parameterValues: {}
-        }
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      card: null,
+      result: null,
+      parameterValues: {},
+    };
+  }
+
+  // $FlowFixMe
+  async componentWillMount() {
+    const {
+      setErrorPage,
+      params: { uuid, token },
+      location: { query },
+    } = this.props;
+    try {
+      let card;
+      if (token) {
+        card = await EmbedApi.card({ token });
+      } else if (uuid) {
+        card = await PublicApi.card({ uuid });
+      } else {
+        throw { status: 404 };
+      }
+
+      if (card.param_values) {
+        this.props.addParamValues(card.param_values);
+      }
+
+      let parameterValues: ParameterValues = {};
+      for (let parameter of getParameters(card)) {
+        parameterValues[String(parameter.id)] = query[parameter.slug];
+      }
+
+      this.setState({ card, parameterValues }, this.run);
+    } catch (error) {
+      console.error("error", error);
+      setErrorPage(error);
     }
-
-    // $FlowFixMe
-    async componentWillMount() {
-        const { setErrorPage, params: { uuid, token }, location: { query }} = this.props;
-        try {
-            let card;
-            if (token) {
-                card = await EmbedApi.card({ token });
-            } else if (uuid) {
-                card = await PublicApi.card({ uuid });
-            } else {
-                throw { status: 404 }
-            }
-
-            if (card.param_values) {
-                this.props.addParamValues(card.param_values);
-            }
-
-            let parameterValues: ParameterValues = {};
-            for (let parameter of getParameters(card)) {
-                parameterValues[String(parameter.id)] = query[parameter.slug];
-            }
-
-            this.setState({ card, parameterValues }, this.run);
-        } catch (error) {
-            console.error("error", error)
-            setErrorPage(error);
-        }
+  }
+
+  setParameterValue = (id: string, value: string) => {
+    this.setState(
+      {
+        parameterValues: {
+          ...this.state.parameterValues,
+          [id]: value,
+        },
+      },
+      this.run,
+    );
+  };
+
+  // $FlowFixMe: setState expects return type void
+  run = async (): void => {
+    const { setErrorPage, params: { uuid, token } } = this.props;
+    const { card, parameterValues } = this.state;
+
+    if (!card) {
+      return;
     }
 
-    setParameterValue = (id: string, value: string) => {
-        this.setState({
-            parameterValues: {
-                ...this.state.parameterValues,
-                [id]: value
-            }
-        }, this.run);
+    const parameters = getParameters(card);
+
+    try {
+      let newResult;
+      if (token) {
+        // embeds apply parameter values server-side
+        newResult = await EmbedApi.cardQuery({
+          token,
+          ...getParametersBySlug(parameters, parameterValues),
+        });
+      } else if (uuid) {
+        // public links currently apply parameters client-side
+        const datasetQuery = applyParameters(card, parameters, parameterValues);
+        newResult = await PublicApi.cardQuery({
+          uuid,
+          parameters: JSON.stringify(datasetQuery.parameters),
+        });
+      } else {
+        throw { status: 404 };
+      }
+
+      this.setState({ result: newResult });
+    } catch (error) {
+      console.error("error", error);
+      setErrorPage(error);
     }
-
-    // $FlowFixMe: setState expects return type void
-    run = async (): void => {
-        const { setErrorPage, params: { uuid, token } } = this.props;
-        const { card, parameterValues } = this.state;
-
-        if (!card) {
-            return;
-        }
-
-        const parameters = getParameters(card);
-
-        try {
-            let newResult;
-            if (token) {
-                // embeds apply parameter values server-side
-                newResult = await EmbedApi.cardQuery({
-                    token,
-                    ...getParametersBySlug(parameters, parameterValues)
-                });
-            } else if (uuid) {
-                // public links currently apply parameters client-side
-                const datasetQuery = applyParameters(card, parameters, parameterValues);
-                newResult = await PublicApi.cardQuery({
-                    uuid,
-                    parameters: JSON.stringify(datasetQuery.parameters)
+  };
+
+  render() {
+    const { params: { uuid, token } } = this.props;
+    const { card, result, parameterValues } = this.state;
+
+    const actionButtons = result && (
+      <QueryDownloadWidget
+        className="m1 text-grey-4-hover"
+        uuid={uuid}
+        token={token}
+        result={result}
+      />
+    );
+
+    return (
+      <EmbedFrame
+        name={card && card.name}
+        description={card && card.description}
+        parameters={card && getParametersWithExtras(card)}
+        actionButtons={actionButtons}
+        parameterValues={parameterValues}
+        setParameterValue={this.setParameterValue}
+      >
+        <LoadingAndErrorWrapper loading={!result}>
+          {() => (
+            <Visualization
+              rawSeries={[{ card: card, data: result && result.data }]}
+              className="full flex-full"
+              onUpdateVisualizationSettings={settings =>
+                this.setState({
+                  // $FlowFixMe
+                  result: updateIn(
+                    result,
+                    ["card", "visualization_settings"],
+                    s => ({ ...s, ...settings }),
+                  ),
                 })
-            } else  {
-                throw { status: 404 };
-            }
-
-            this.setState({ result: newResult });
-        } catch (error) {
-            console.error("error", error)
-            setErrorPage(error);
-        }
-    }
-
-    render() {
-        const { params: { uuid, token } } = this.props;
-        const { card, result, parameterValues } = this.state;
-
-        const actionButtons = result && (
-            <QueryDownloadWidget
-                className="m1 text-grey-4-hover"
-                uuid={uuid}
-                token={token}
-                result={result}
+              }
+              gridUnit={12}
+              showTitle={false}
+              isDashboard
             />
-        )
-
-        return (
-            <EmbedFrame
-                name={card && card.name}
-                description={card && card.description}
-                parameters={card && getParametersWithExtras(card)}
-                actionButtons={actionButtons}
-                parameterValues={parameterValues}
-                setParameterValue={this.setParameterValue}
-            >
-                <LoadingAndErrorWrapper loading={!result}>
-                { () =>
-                    <Visualization
-                        rawSeries={[{ card: card, data: result && result.data }]}
-                        className="full flex-full"
-                        onUpdateVisualizationSettings={(settings) =>
-                            this.setState({
-                                // $FlowFixMe
-                                result: updateIn(result, ["card", "visualization_settings"], (s) => ({ ...s, ...settings }))
-                            })
-                        }
-                        gridUnit={12}
-                        showTitle={false}
-                        isDashboard
-                    />
-                }
-                </LoadingAndErrorWrapper>
-            </EmbedFrame>
-        )
-    }
+          )}
+        </LoadingAndErrorWrapper>
+      </EmbedFrame>
+    );
+  }
 }
diff --git a/frontend/src/metabase/public/lib/code.js b/frontend/src/metabase/public/lib/code.js
index af73eedb5a51168d4b6ab91bd02ba61c6d8159ba..5ab7aa279c4e556b641e653e1fa262216558c8fd 100644
--- a/frontend/src/metabase/public/lib/code.js
+++ b/frontend/src/metabase/public/lib/code.js
@@ -3,61 +3,93 @@
 import { optionsToHashParams } from "./embed";
 
 export type CodeSampleOption = {
-    name: string,
-    source: () => string,
-    mode?: string,
-    embedOption?: string
+  name: string,
+  source: () => string,
+  mode?: string,
+  embedOption?: string,
 };
 
-export const getPublicEmbedOptions = ({ iframeUrl }: { iframeUrl: string }): CodeSampleOption[] => [
-    { name: "HTML",    source: () => html({ iframeUrl: `"${iframeUrl}"` }), mode: "ace/mode/html" }
+export const getPublicEmbedOptions = ({
+  iframeUrl,
+}: {
+  iframeUrl: string,
+}): CodeSampleOption[] => [
+  {
+    name: "HTML",
+    source: () => html({ iframeUrl: `"${iframeUrl}"` }),
+    mode: "ace/mode/html",
+  },
 ];
 
 export const getSignedEmbedOptions = (): CodeSampleOption[] => [
-    { name: "Mustache",   source: () => html({ iframeUrl: `"{{iframeUrl}}"`, mode: "ace/mode/html" })},
-    { name: "Pug / Jade", source: () =>  pug({ iframeUrl: `iframeUrl` })},
-    { name: "ERB",        source: () => html({ iframeUrl: `"<%= @iframe_url %>"` })},
-    { name: "JSX",        source: () =>  jsx({ iframeUrl: `{iframeUrl}`,     mode: "ace/mode/jsx" })},
+  {
+    name: "Mustache",
+    source: () => html({ iframeUrl: `"{{iframeUrl}}"`, mode: "ace/mode/html" }),
+  },
+  { name: "Pug / Jade", source: () => pug({ iframeUrl: `iframeUrl` }) },
+  { name: "ERB", source: () => html({ iframeUrl: `"<%= @iframe_url %>"` }) },
+  {
+    name: "JSX",
+    source: () => jsx({ iframeUrl: `{iframeUrl}`, mode: "ace/mode/jsx" }),
+  },
 ];
 
 export const getSignTokenOptions = (params: any): CodeSampleOption[] => [
-    { name: "Node.js", source: () => node(params),    mode: "ace/mode/javascript", embedOption: "Pug / Jade" },
-    { name: "Ruby",    source: () => ruby(params),    mode: "ace/mode/ruby",       embedOption: "ERB" },
-    { name: "Python",  source: () => python(params),  mode: "ace/mode/python" },
-    { name: "Clojure", source: () => clojure(params), mode: "ace/mode/clojure" },
+  {
+    name: "Node.js",
+    source: () => node(params),
+    mode: "ace/mode/javascript",
+    embedOption: "Pug / Jade",
+  },
+  {
+    name: "Ruby",
+    source: () => ruby(params),
+    mode: "ace/mode/ruby",
+    embedOption: "ERB",
+  },
+  { name: "Python", source: () => python(params), mode: "ace/mode/python" },
+  { name: "Clojure", source: () => clojure(params), mode: "ace/mode/clojure" },
 ];
 
-export const getPublicEmbedHTML = (iframeUrl: string): string => html({ iframeUrl: JSON.stringify(iframeUrl )});
+export const getPublicEmbedHTML = (iframeUrl: string): string =>
+  html({ iframeUrl: JSON.stringify(iframeUrl) });
 
 const html = ({ iframeUrl }) =>
-`<iframe
+  `<iframe
     src=${iframeUrl}
     frameborder="0"
     width="800"
     height="600"
     allowtransparency
-></iframe>`
+></iframe>`;
 
 const jsx = ({ iframeUrl }) =>
-`<iframe
+  `<iframe
     src=${iframeUrl}
     frameBorder={0}
     width={800}
     height={600}
     allowTransparency
-/>`
+/>`;
 
 const pug = ({ iframeUrl }) =>
-`iframe(
+  `iframe(
     src=${iframeUrl}
     frameborder="0"
     width="800"
     height="600"
     allowtransparency
-)`
-
-const node = ({ siteUrl, secretKey, resourceType, resourceId, params, displayOptions }) =>
-`// you will need to install via 'npm install jsonwebtoken' or in your package.json
+)`;
+
+const node = ({
+  siteUrl,
+  secretKey,
+  resourceType,
+  resourceId,
+  params,
+  displayOptions,
+}) =>
+  `// you will need to install via 'npm install jsonwebtoken' or in your package.json
 
 var jwt = require("jsonwebtoken");
 
@@ -66,14 +98,27 @@ var METABASE_SECRET_KEY = ${JSON.stringify(secretKey)};
 
 var payload = {
   resource: { ${resourceType}: ${resourceId} },
-  params: ${JSON.stringify(params, null, 2).split("\n").join("\n  ")}
+  params: ${JSON.stringify(params, null, 2)
+    .split("\n")
+    .join("\n  ")}
 };
 var token = jwt.sign(payload, METABASE_SECRET_KEY);
 
-var iframeUrl = METABASE_SITE_URL + "/embed/${resourceType}/" + token${optionsToHashParams(displayOptions) ? " + " + JSON.stringify(optionsToHashParams(displayOptions)) : "" };`;
-
-const ruby = ({ siteUrl, secretKey, resourceType, resourceId, params, displayOptions }) =>
-`# you will need to install 'jwt' gem first via 'gem install jwt' or in your project Gemfile
+var iframeUrl = METABASE_SITE_URL + "/embed/${resourceType}/" + token${
+    optionsToHashParams(displayOptions)
+      ? " + " + JSON.stringify(optionsToHashParams(displayOptions))
+      : ""
+  };`;
+
+const ruby = ({
+  siteUrl,
+  secretKey,
+  resourceType,
+  resourceId,
+  params,
+  displayOptions,
+}) =>
+  `# you will need to install 'jwt' gem first via 'gem install jwt' or in your project Gemfile
 
 require 'jwt'
 
@@ -83,15 +128,30 @@ METABASE_SECRET_KEY = ${JSON.stringify(secretKey)}
 payload = {
   :resource => {:${resourceType} => ${resourceId}},
   :params => {
-    ${Object.entries(params).map(([key,value]) => JSON.stringify(key) + " => " + JSON.stringify(value)).join(",\n    ")}
+    ${Object.entries(params)
+      .map(
+        ([key, value]) => JSON.stringify(key) + " => " + JSON.stringify(value),
+      )
+      .join(",\n    ")}
   }
 }
 token = JWT.encode payload, METABASE_SECRET_KEY
 
-iframe_url = METABASE_SITE_URL + "/embed/${resourceType}/" + token${optionsToHashParams(displayOptions) ? " + " + JSON.stringify(optionsToHashParams(displayOptions)) : "" }`;
-
-const python = ({ siteUrl, secretKey, resourceType, resourceId, params, displayOptions }) =>
-`# You'll need to install PyJWT via pip 'pip install PyJWT' or your project packages file
+iframe_url = METABASE_SITE_URL + "/embed/${resourceType}/" + token${
+    optionsToHashParams(displayOptions)
+      ? " + " + JSON.stringify(optionsToHashParams(displayOptions))
+      : ""
+  }`;
+
+const python = ({
+  siteUrl,
+  secretKey,
+  resourceType,
+  resourceId,
+  params,
+  displayOptions,
+}) =>
+  `# You'll need to install PyJWT via pip 'pip install PyJWT' or your project packages file
 
 import jwt
 
@@ -101,23 +161,42 @@ METABASE_SECRET_KEY = ${JSON.stringify(secretKey)}
 payload = {
   "resource": {"${resourceType}": ${resourceId}},
   "params": {
-    ${Object.entries(params).map(([key,value]) => JSON.stringify(key) + ": " + JSON.stringify(value)).join(",\n    ")}
+    ${Object.entries(params)
+      .map(([key, value]) => JSON.stringify(key) + ": " + JSON.stringify(value))
+      .join(",\n    ")}
   }
 }
 token = jwt.encode(payload, METABASE_SECRET_KEY, algorithm="HS256")
 
-iframeUrl = METABASE_SITE_URL + "/embed/${resourceType}/" + token${optionsToHashParams(displayOptions) ? " + " + JSON.stringify(optionsToHashParams(displayOptions)) : "" }`;
-
-const clojure = ({ siteUrl, secretKey, resourceType, resourceId, params, displayOptions }) =>
-`(require '[buddy.sign.jwt :as jwt])
+iframeUrl = METABASE_SITE_URL + "/embed/${resourceType}/" + token${
+    optionsToHashParams(displayOptions)
+      ? " + " + JSON.stringify(optionsToHashParams(displayOptions))
+      : ""
+  }`;
+
+const clojure = ({
+  siteUrl,
+  secretKey,
+  resourceType,
+  resourceId,
+  params,
+  displayOptions,
+}) =>
+  `(require '[buddy.sign.jwt :as jwt])
 
 (def metabase-site-url   ${JSON.stringify(siteUrl)})
 (def metabase-secret-key ${JSON.stringify(secretKey)})
 
 (def payload
   {:resource {:${resourceType} ${resourceId}}
-   :params   {${Object.entries(params).map(([key,value]) => JSON.stringify(key) + " " + JSON.stringify(value)).join(",\n              ")}}})
+   :params   {${Object.entries(params)
+     .map(([key, value]) => JSON.stringify(key) + " " + JSON.stringify(value))
+     .join(",\n              ")}}})
 
 (def token (jwt/sign payload metabase-secret-key))
 
-(def iframe-url (str metabase-site-url "/embed/${resourceType}/" token${optionsToHashParams(displayOptions) ? (" " + JSON.stringify(optionsToHashParams(displayOptions))) : ""}))`;
+(def iframe-url (str metabase-site-url "/embed/${resourceType}/" token${
+    optionsToHashParams(displayOptions)
+      ? " " + JSON.stringify(optionsToHashParams(displayOptions))
+      : ""
+  }))`;
diff --git a/frontend/src/metabase/public/lib/embed.js b/frontend/src/metabase/public/lib/embed.js
index 98b88e0222b56e95e1557e08107d69291b81531f..0c812928b975e363a2f9b864acf7ef2d2300bbc6 100644
--- a/frontend/src/metabase/public/lib/embed.js
+++ b/frontend/src/metabase/public/lib/embed.js
@@ -1,39 +1,69 @@
-
 import querystring from "querystring";
 
 // using jsrsasign because jsonwebtoken doesn't work on the web :-/
 import KJUR from "jsrsasign";
 
-export function getSignedToken(resourceType, resourceId, params = {}, secretKey, previewEmbeddingParams) {
-    const unsignedToken = {
-        resource: { [resourceType]: resourceId },
-        params: params,
-        iat: Math.round(new Date().getTime() / 1000)
-    };
-    // include the `embedding_params` settings inline in the token for previews
-    if (previewEmbeddingParams) {
-        unsignedToken._embedding_params = previewEmbeddingParams;
-    }
-    return KJUR.jws.JWS.sign(null, { alg: "HS256", typ: "JWT" }, unsignedToken, { utf8: secretKey });
+export function getSignedToken(
+  resourceType,
+  resourceId,
+  params = {},
+  secretKey,
+  previewEmbeddingParams,
+) {
+  const unsignedToken = {
+    resource: { [resourceType]: resourceId },
+    params: params,
+    iat: Math.round(new Date().getTime() / 1000),
+  };
+  // include the `embedding_params` settings inline in the token for previews
+  if (previewEmbeddingParams) {
+    unsignedToken._embedding_params = previewEmbeddingParams;
+  }
+  return KJUR.jws.JWS.sign(null, { alg: "HS256", typ: "JWT" }, unsignedToken, {
+    utf8: secretKey,
+  });
 }
 
-export function getSignedPreviewUrl(siteUrl, resourceType, resourceId, params = {}, options, secretKey, previewEmbeddingParams) {
-    const token = getSignedToken(resourceType, resourceId, params, secretKey, previewEmbeddingParams);
-    return `${siteUrl}/embed/${resourceType}/${token}${optionsToHashParams(options)}`;
+export function getSignedPreviewUrl(
+  siteUrl,
+  resourceType,
+  resourceId,
+  params = {},
+  options,
+  secretKey,
+  previewEmbeddingParams,
+) {
+  const token = getSignedToken(
+    resourceType,
+    resourceId,
+    params,
+    secretKey,
+    previewEmbeddingParams,
+  );
+  return `${siteUrl}/embed/${resourceType}/${token}${optionsToHashParams(
+    options,
+  )}`;
 }
 
-export function getUnsignedPreviewUrl(siteUrl, resourceType, resourceId, options) {
-    return `${siteUrl}/public/${resourceType}/${resourceId}${optionsToHashParams(options)}`
+export function getUnsignedPreviewUrl(
+  siteUrl,
+  resourceType,
+  resourceId,
+  options,
+) {
+  return `${siteUrl}/public/${resourceType}/${resourceId}${optionsToHashParams(
+    options,
+  )}`;
 }
 
 export function optionsToHashParams(options = {}) {
-    options = { ...options };
-    // filter out null, undefined, ""
-    for (var name in options) {
-        if (options[name] == null || options[name] === "") {
-            delete options[name];
-        }
+  options = { ...options };
+  // filter out null, undefined, ""
+  for (var name in options) {
+    if (options[name] == null || options[name] === "") {
+      delete options[name];
     }
-    const query = querystring.stringify(options);
-    return query ? `#${query}` : ``
+  }
+  const query = querystring.stringify(options);
+  return query ? `#${query}` : ``;
 }
diff --git a/frontend/src/metabase/public/lib/types.js b/frontend/src/metabase/public/lib/types.js
index 7aa0f3415243f9cd65dbc65ec380bb16af549716..c5fe2d2f01fe462320400944e59121e46200e0bb 100644
--- a/frontend/src/metabase/public/lib/types.js
+++ b/frontend/src/metabase/public/lib/types.js
@@ -1,11 +1,11 @@
 /* @flow */
 
 export type EmbeddingParams = {
-    [key: string]: string
-}
+  [key: string]: string,
+};
 
 export type EmbeddableResource = {
-    id: string,
-    public_uuid: string,
-    embedding_params: EmbeddingParams
-}
+  id: string,
+  public_uuid: string,
+  embedding_params: EmbeddingParams,
+};
diff --git a/frontend/src/metabase/pulse/actions.js b/frontend/src/metabase/pulse/actions.js
index f21ef27467714cf5779027ad10cd559e659b4c60..351dac69367919726a5cbaf8e14caf432c95b55a 100644
--- a/frontend/src/metabase/pulse/actions.js
+++ b/frontend/src/metabase/pulse/actions.js
@@ -1,4 +1,3 @@
-
 import { createAction } from "redux-actions";
 import { createThunkAction } from "metabase/lib/redux";
 import { normalize, schema } from "normalizr";
@@ -8,108 +7,119 @@ import { formInputSelector } from "./selectors";
 
 import { getDefaultChannel, createChannel } from "metabase/lib/pulse";
 
-const card = new schema.Entity('card');
-const pulse = new schema.Entity('pulse');
-const user = new schema.Entity('user');
+const card = new schema.Entity("card");
+const pulse = new schema.Entity("pulse");
+const user = new schema.Entity("user");
 
-export const FETCH_PULSES = 'FETCH_PULSES';
-export const SET_EDITING_PULSE = 'SET_EDITING_PULSE';
-export const UPDATE_EDITING_PULSE = 'UPDATE_EDITING_PULSE';
-export const SAVE_PULSE = 'SAVE_PULSE';
-export const SAVE_EDITING_PULSE = 'SAVE_EDITING_PULSE';
-export const DELETE_PULSE = 'DELETE_PULSE';
-export const TEST_PULSE = 'TEST_PULSE';
+export const FETCH_PULSES = "FETCH_PULSES";
+export const SET_EDITING_PULSE = "SET_EDITING_PULSE";
+export const UPDATE_EDITING_PULSE = "UPDATE_EDITING_PULSE";
+export const SAVE_PULSE = "SAVE_PULSE";
+export const SAVE_EDITING_PULSE = "SAVE_EDITING_PULSE";
+export const DELETE_PULSE = "DELETE_PULSE";
+export const TEST_PULSE = "TEST_PULSE";
 
-export const FETCH_CARDS = 'FETCH_CARDS';
-export const FETCH_USERS = 'FETCH_USERS';
-export const FETCH_PULSE_FORM_INPUT = 'FETCH_PULSE_FORM_INPUT';
-export const FETCH_PULSE_CARD_PREVIEW = 'FETCH_PULSE_CARD_PREVIEW';
+export const FETCH_CARDS = "FETCH_CARDS";
+export const FETCH_USERS = "FETCH_USERS";
+export const FETCH_PULSE_FORM_INPUT = "FETCH_PULSE_FORM_INPUT";
+export const FETCH_PULSE_CARD_PREVIEW = "FETCH_PULSE_CARD_PREVIEW";
 
 export const fetchPulses = createThunkAction(FETCH_PULSES, function() {
-    return async function(dispatch, getState) {
-        let pulses = await PulseApi.list();
-        return normalize(pulses, [pulse]);
-    };
+  return async function(dispatch, getState) {
+    let pulses = await PulseApi.list();
+    return normalize(pulses, [pulse]);
+  };
 });
 
-export const setEditingPulse = createThunkAction(SET_EDITING_PULSE, function(id) {
-    return async function(dispatch, getState) {
-        if (id != null) {
-            try {
-                return await PulseApi.get({ pulseId: id });
-            } catch (e) {
-            }
-        }
-        // HACK: need a way to wait for form_input to finish loading
-        const channels = formInputSelector(getState()).channels ||
-            (await PulseApi.form_input()).channels;
-        const defaultChannelSpec = getDefaultChannel(channels);
-        return {
-            name: null,
-            cards: [],
-            channels: defaultChannelSpec ?
-              [createChannel(defaultChannelSpec)] :
-              [],
-            skip_if_empty: false,
-        }
+export const setEditingPulse = createThunkAction(SET_EDITING_PULSE, function(
+  id,
+) {
+  return async function(dispatch, getState) {
+    if (id != null) {
+      try {
+        return await PulseApi.get({ pulseId: id });
+      } catch (e) {}
+    }
+    // HACK: need a way to wait for form_input to finish loading
+    const channels =
+      formInputSelector(getState()).channels ||
+      (await PulseApi.form_input()).channels;
+    const defaultChannelSpec = getDefaultChannel(channels);
+    return {
+      name: null,
+      cards: [],
+      channels: defaultChannelSpec ? [createChannel(defaultChannelSpec)] : [],
+      skip_if_empty: false,
     };
+  };
 });
 
 export const updateEditingPulse = createAction(UPDATE_EDITING_PULSE);
 
 export const savePulse = createThunkAction(SAVE_PULSE, function(pulse) {
-    return async function(dispatch, getState) {
-        return await PulseApi.update(pulse);
-    };
+  return async function(dispatch, getState) {
+    return await PulseApi.update(pulse);
+  };
 });
 
-export const saveEditingPulse = createThunkAction(SAVE_EDITING_PULSE, function() {
+export const saveEditingPulse = createThunkAction(
+  SAVE_EDITING_PULSE,
+  function() {
     return async function(dispatch, getState) {
-        let { pulse: { editingPulse } } = getState();
-        if (editingPulse.id != null) {
-            return await PulseApi.update(editingPulse);
-        } else {
-            return await PulseApi.create(editingPulse);
-        }
+      let { pulse: { editingPulse } } = getState();
+      if (editingPulse.id != null) {
+        return await PulseApi.update(editingPulse);
+      } else {
+        return await PulseApi.create(editingPulse);
+      }
     };
-});
+  },
+);
 
 export const deletePulse = createThunkAction(DELETE_PULSE, function(id) {
-    return async function(dispatch, getState) {
-        return await PulseApi.delete({ pulseId: id });
-    };
+  return async function(dispatch, getState) {
+    return await PulseApi.delete({ pulseId: id });
+  };
 });
 
 export const testPulse = createThunkAction(TEST_PULSE, function(pulse) {
-    return async function(dispatch, getState) {
-        return await PulseApi.test(pulse);
-    };
+  return async function(dispatch, getState) {
+    return await PulseApi.test(pulse);
+  };
 });
 
 // NOTE: duplicated from dashboards/actions.js
-export const fetchCards = createThunkAction(FETCH_CARDS, function(filterMode = "all") {
-    return async function(dispatch, getState) {
-        let cards = await CardApi.list({ f: filterMode });
-        return normalize(cards, [card]);
-    };
+export const fetchCards = createThunkAction(FETCH_CARDS, function(
+  filterMode = "all",
+) {
+  return async function(dispatch, getState) {
+    let cards = await CardApi.list({ f: filterMode });
+    return normalize(cards, [card]);
+  };
 });
 
 // NOTE: duplicated from admin/people/actions.js
 export const fetchUsers = createThunkAction(FETCH_USERS, function() {
-    return async function(dispatch, getState) {
-        let users = await UserApi.list();
-        return normalize(users, [user]);
-    };
+  return async function(dispatch, getState) {
+    let users = await UserApi.list();
+    return normalize(users, [user]);
+  };
 });
 
-export const fetchPulseFormInput = createThunkAction(FETCH_PULSE_FORM_INPUT, function() {
+export const fetchPulseFormInput = createThunkAction(
+  FETCH_PULSE_FORM_INPUT,
+  function() {
     return async function(dispatch, getState) {
-        return await PulseApi.form_input();
+      return await PulseApi.form_input();
     };
-});
+  },
+);
 
-export const fetchPulseCardPreview = createThunkAction(FETCH_PULSE_CARD_PREVIEW, function(id) {
+export const fetchPulseCardPreview = createThunkAction(
+  FETCH_PULSE_CARD_PREVIEW,
+  function(id) {
     return async function(dispatch, getState) {
-        return await PulseApi.preview_card({ id: id });
-    }
-});
+      return await PulseApi.preview_card({ id: id });
+    };
+  },
+);
diff --git a/frontend/src/metabase/pulse/components/CardPicker.jsx b/frontend/src/metabase/pulse/components/CardPicker.jsx
index c044b3ca961a1a3020634ceb6efdf67e9cf9840d..f2b90440acf7f457a60fcf70e11b199c1b02aa82 100644
--- a/frontend/src/metabase/pulse/components/CardPicker.jsx
+++ b/frontend/src/metabase/pulse/components/CardPicker.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import Icon from "metabase/components/Icon.jsx";
 import Popover from "metabase/components/Popover.jsx";
@@ -11,187 +11,221 @@ import Query from "metabase/lib/query";
 import _ from "underscore";
 
 export default class CardPicker extends Component {
-    state = {
-        isOpen: false,
-        inputValue: "",
-        inputWidth: 300,
-        collectionId: undefined,
-    };
-
-
-    static propTypes = {
-        cardList: PropTypes.array.isRequired,
-        onChange: PropTypes.func.isRequired,
-        attachmentsEnabled: PropTypes.bool,
-    };
-
-    componentWillUnmount() {
-        clearTimeout(this._timer);
+  state = {
+    isOpen: false,
+    inputValue: "",
+    inputWidth: 300,
+    collectionId: undefined,
+  };
+
+  static propTypes = {
+    cardList: PropTypes.array.isRequired,
+    onChange: PropTypes.func.isRequired,
+    attachmentsEnabled: PropTypes.bool,
+  };
+
+  componentWillUnmount() {
+    clearTimeout(this._timer);
+  }
+
+  onInputChange = ({ target }) => {
+    this.setState({ inputValue: target.value });
+  };
+
+  onInputFocus = () => {
+    this.setState({ isOpen: true });
+  };
+
+  onInputBlur = () => {
+    // Without a timeout here isOpen gets set to false when an item is clicked
+    // which causes the click handler to not fire. For some reason this even
+    // happens with a 100ms delay, but not 200ms?
+    clearTimeout(this._timer);
+    this._timer = setTimeout(() => {
+      if (!this.state.isClicking) {
+        this.setState({ isOpen: false });
+      } else {
+        this.setState({ isClicking: false });
+      }
+    }, 250);
+  };
+
+  onChange = id => {
+    this.props.onChange(id);
+    ReactDOM.findDOMNode(this.refs.input).blur();
+  };
+
+  renderItem(card) {
+    const { attachmentsEnabled } = this.props;
+    let error;
+    try {
+      if (!attachmentsEnabled && Query.isBareRows(card.dataset_query.query)) {
+        error = t`Raw data cannot be included in pulses`;
+      }
+    } catch (e) {}
+    if (
+      !attachmentsEnabled &&
+      (card.display === "pin_map" ||
+        card.display === "state" ||
+        card.display === "country")
+    ) {
+      error = t`Maps cannot be included in pulses`;
     }
 
-    onInputChange = ({target}) => {
-        this.setState({ inputValue: target.value });
+    if (error) {
+      return (
+        <li key={card.id} className="px2 py1">
+          <h4 className="text-grey-2">{card.name}</h4>
+          <h4 className="text-gold mt1">{error}</h4>
+        </li>
+      );
+    } else {
+      return (
+        <li
+          key={card.id}
+          className="List-item cursor-pointer"
+          onClickCapture={this.onChange.bind(this, card.id)}
+        >
+          <h4 className="List-item-title px2 py1">{card.name}</h4>
+        </li>
+      );
     }
+  }
 
-    onInputFocus = () => {
-        this.setState({ isOpen: true });
+  // keep the modal width in sync with the input width :-/
+  componentDidUpdate() {
+    let { scrollWidth } = ReactDOM.findDOMNode(this.refs.input);
+    if (this.state.inputWidth !== scrollWidth) {
+      this.setState({ inputWidth: scrollWidth });
     }
-
-    onInputBlur = () => {
-        // Without a timeout here isOpen gets set to false when an item is clicked
-        // which causes the click handler to not fire. For some reason this even
-        // happens with a 100ms delay, but not 200ms?
-        clearTimeout(this._timer);
-        this._timer = setTimeout(() => {
-            if (!this.state.isClicking) {
-                this.setState({ isOpen: false })
-            } else {
-                this.setState({ isClicking: false })
-            }
-        }, 250);
-    }
-
-    onChange = (id) => {
-        this.props.onChange(id);
-        ReactDOM.findDOMNode(this.refs.input).blur();
-    }
-
-    renderItem(card) {
-        const { attachmentsEnabled } = this.props;
-        let error;
-        try {
-            if (!attachmentsEnabled && Query.isBareRows(card.dataset_query.query)) {
-                error = t`Raw data cannot be included in pulses`;
-            }
-        } catch (e) {}
-        if (!attachmentsEnabled && (card.display === "pin_map" || card.display === "state" || card.display === "country")) {
-            error = t`Maps cannot be included in pulses`;
-        }
-
-        if (error) {
-            return (
-                <li key={card.id} className="px2 py1">
-                    <h4 className="text-grey-2">{card.name}</h4>
-                    <h4 className="text-gold mt1">{error}</h4>
-                </li>
-            )
-        } else {
-            return (
-                <li key={card.id} className="List-item cursor-pointer" onClickCapture={this.onChange.bind(this, card.id)}>
-                    <h4 className="List-item-title px2 py1">{card.name}</h4>
-                </li>
-            );
-        }
+  }
+
+  render() {
+    let { cardList } = this.props;
+
+    let { isOpen, inputValue, inputWidth, collectionId } = this.state;
+
+    let cardByCollectionId = _.groupBy(cardList, "collection_id");
+    let collectionIds = Object.keys(cardByCollectionId);
+
+    const collections = _.chain(cardList)
+      .map(card => card.collection)
+      .uniq(c => c && c.id)
+      .filter(c => c)
+      .sortBy("name")
+      // add "Everything else" as the last option for cards without a
+      // collection
+      .concat([{ id: null, name: t`Everything else` }])
+      .value();
+
+    let visibleCardList;
+    if (inputValue) {
+      let searchString = inputValue.toLowerCase();
+      visibleCardList = cardList.filter(
+        card =>
+          ~(card.name || "").toLowerCase().indexOf(searchString) ||
+          ~(card.description || "").toLowerCase().indexOf(searchString),
+      );
+    } else {
+      if (collectionId !== undefined) {
+        visibleCardList = cardByCollectionId[collectionId];
+      } else if (collectionIds.length === 1) {
+        visibleCardList = cardByCollectionId[collectionIds[0]];
+      }
     }
 
-    // keep the modal width in sync with the input width :-/
-    componentDidUpdate() {
-        let { scrollWidth } = ReactDOM.findDOMNode(this.refs.input);
-        if (this.state.inputWidth !== scrollWidth) {
-            this.setState({ inputWidth: scrollWidth });
-        }
-    }
-
-    render() {
-        let { cardList } = this.props;
-
-        let { isOpen, inputValue, inputWidth, collectionId } = this.state;
-
-        let cardByCollectionId = _.groupBy(cardList, "collection_id");
-        let collectionIds = Object.keys(cardByCollectionId);
-
-        const collections = _.chain(cardList)
-            .map(card => card.collection)
-            .uniq(c => c && c.id)
-            .filter(c => c)
-            .sortBy("name")
-            // add "Everything else" as the last option for cards without a
-            // collection
-            .concat([{ id: null, name: t`Everything else`}])
-            .value();
-
-        let visibleCardList;
-        if (inputValue) {
-            let searchString = inputValue.toLowerCase();
-            visibleCardList = cardList.filter((card) =>
-                ~(card.name || "").toLowerCase().indexOf(searchString) ||
-                ~(card.description || "").toLowerCase().indexOf(searchString)
-            );
-        } else {
-            if (collectionId !== undefined) {
-                visibleCardList = cardByCollectionId[collectionId];
-            } else if (collectionIds.length === 1) {
-                visibleCardList = cardByCollectionId[collectionIds[0]];
-            }
-        }
-
-        const collection = _.findWhere(collections, { id: collectionId });
-        return (
-            <div className="CardPicker flex-full">
-                <input
-                    ref="input"
-                    className="input no-focus full text-bold"
-                    placeholder={t`Type a question name to filter`}
-                    value={this.inputValue}
-                    onFocus={this.onInputFocus}
-                    onBlur={this.onInputBlur}
-                    onChange={this.onInputChange}
-                />
-                <Popover
-                    isOpen={isOpen && cardList.length > 0}
-                    hasArrow={false}
-                    tetherOptions={{
-                        attachment: "top left",
-                        targetAttachment: "bottom left",
-                        targetOffset: "0 0"
-                    }}
+    const collection = _.findWhere(collections, { id: collectionId });
+    return (
+      <div className="CardPicker flex-full">
+        <input
+          ref="input"
+          className="input no-focus full text-bold"
+          placeholder={t`Type a question name to filter`}
+          value={this.inputValue}
+          onFocus={this.onInputFocus}
+          onBlur={this.onInputBlur}
+          onChange={this.onInputChange}
+        />
+        <Popover
+          isOpen={isOpen && cardList.length > 0}
+          hasArrow={false}
+          tetherOptions={{
+            attachment: "top left",
+            targetAttachment: "bottom left",
+            targetOffset: "0 0",
+          }}
+        >
+          <div
+            className="rounded bordered scroll-y scroll-show"
+            style={{ width: inputWidth + "px", maxHeight: "400px" }}
+          >
+            {visibleCardList &&
+              collectionIds.length > 1 && (
+                <div
+                  className="flex align-center text-slate cursor-pointer border-bottom p2"
+                  onClick={e => {
+                    this.setState({
+                      collectionId: undefined,
+                      isClicking: true,
+                    });
+                  }}
                 >
-                    <div className="rounded bordered scroll-y scroll-show" style={{ width: inputWidth + "px", maxHeight: "400px" }}>
-                    { visibleCardList && collectionIds.length > 1 &&
-                        <div className="flex align-center text-slate cursor-pointer border-bottom p2"  onClick={(e) => {
-                            this.setState({ collectionId: undefined, isClicking: true });
-                        }}>
-                            <Icon name="chevronleft" size={18} />
-                            <h3 className="ml1">{collection && collection.name}</h3>
-                        </div>
-                    }
-                    { visibleCardList ?
-                        <ul className="List text-brand">
-                            {visibleCardList.map((card) => this.renderItem(card))}
-                        </ul>
-                    : collections ?
-                        <CollectionList>
-                            {collections.map(collection =>
-                                <CollectionListItem key={collection.id} collection={collection} onClick={(e) => {
-                                    this.setState({ collectionId: collection.id, isClicking: true });
-                                }}/>
-                            )}
-                        </CollectionList>
-                    : null }
-                    </div>
-                </Popover>
-            </div>
-        );
-    }
+                  <Icon name="chevronleft" size={18} />
+                  <h3 className="ml1">{collection && collection.name}</h3>
+                </div>
+              )}
+            {visibleCardList ? (
+              <ul className="List text-brand">
+                {visibleCardList.map(card => this.renderItem(card))}
+              </ul>
+            ) : collections ? (
+              <CollectionList>
+                {collections.map(collection => (
+                  <CollectionListItem
+                    key={collection.id}
+                    collection={collection}
+                    onClick={e => {
+                      this.setState({
+                        collectionId: collection.id,
+                        isClicking: true,
+                      });
+                    }}
+                  />
+                ))}
+              </CollectionList>
+            ) : null}
+          </div>
+        </Popover>
+      </div>
+    );
+  }
 }
 
-const CollectionListItem = ({ collection, onClick }) =>
-    <li className="List-item cursor-pointer flex align-center py1 px2" onClick={onClick}>
-        <Icon name="collection" style={{ color: collection.color }} className="Icon mr2 text-default" size={18} />
-        <h4 className="List-item-title">{collection.name}</h4>
-        <Icon name="chevronright" className="flex-align-right text-grey-2" />
-    </li>
+const CollectionListItem = ({ collection, onClick }) => (
+  <li
+    className="List-item cursor-pointer flex align-center py1 px2"
+    onClick={onClick}
+  >
+    <Icon
+      name="collection"
+      style={{ color: collection.color }}
+      className="Icon mr2 text-default"
+      size={18}
+    />
+    <h4 className="List-item-title">{collection.name}</h4>
+    <Icon name="chevronright" className="flex-align-right text-grey-2" />
+  </li>
+);
 
 CollectionListItem.propTypes = {
-    collection: PropTypes.object.isRequired,
-    onClick: PropTypes.func.isRequired
+  collection: PropTypes.object.isRequired,
+  onClick: PropTypes.func.isRequired,
 };
 
-const CollectionList = ({ children }) =>
-    <ul className="List text-brand">
-        {children}
-    </ul>
+const CollectionList = ({ children }) => (
+  <ul className="List text-brand">{children}</ul>
+);
 
 CollectionList.propTypes = {
-    children: PropTypes.array.isRequired
+  children: PropTypes.array.isRequired,
 };
diff --git a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
index 59539d79c6ba173741cd61c22f2c04300b7eac9e..3db924022146e883bd9e8233665e394ba8b571a2 100644
--- a/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
+++ b/frontend/src/metabase/pulse/components/PulseCardPreview.jsx
@@ -11,107 +11,140 @@ import { t } from "c-3po";
 import cx from "classnames";
 
 export default class PulseCardPreview extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
+  }
+
+  static propTypes = {
+    card: PropTypes.object.isRequired,
+    cardPreview: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onRemove: PropTypes.func.isRequired,
+    fetchPulseCardPreview: PropTypes.func.isRequired,
+    attachmentsEnabled: PropTypes.bool,
+    trackPulseEvent: PropTypes.func.isRequired,
+  };
+
+  componentWillMount() {
+    this.props.fetchPulseCardPreview(this.props.card.id);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    // if we can't render this card as a pulse, set include_csv = true
+    const unrenderablePulseCard =
+      nextProps.cardPreview && nextProps.cardPreview.pulse_card_type == null;
+    const hasAttachment =
+      nextProps.card.include_csv || nextProps.card.include_xls;
+    if (unrenderablePulseCard && !hasAttachment) {
+      nextProps.onChange({ ...nextProps.card, include_csv: true });
     }
+  }
 
-    static propTypes = {
-        card: PropTypes.object.isRequired,
-        cardPreview: PropTypes.object,
-        onChange: PropTypes.func.isRequired,
-        onRemove: PropTypes.func.isRequired,
-        fetchPulseCardPreview: PropTypes.func.isRequired,
-        attachmentsEnabled: PropTypes.bool,
-        trackPulseEvent: PropTypes.func.isRequired
-    };
-
-    componentWillMount() {
-        this.props.fetchPulseCardPreview(this.props.card.id);
-    }
-
-    componentWillReceiveProps(nextProps) {
-        // if we can't render this card as a pulse, set include_csv = true
-        const unrenderablePulseCard = nextProps.cardPreview && nextProps.cardPreview.pulse_card_type == null;
-        const hasAttachment = nextProps.card.include_csv || nextProps.card.include_xls;
-        if (unrenderablePulseCard && !hasAttachment) {
-            nextProps.onChange({ ...nextProps.card, include_csv: true })
-        }
-    }
+  hasAttachment() {
+    const { card } = this.props;
+    return card.include_csv || card.include_xls;
+  }
 
-    hasAttachment() {
-        const { card } = this.props;
-        return card.include_csv || card.include_xls;
-    }
-
-    toggleAttachment = () => {
-        const { card, onChange } = this.props;
-        if (this.hasAttachment()) {
-            onChange({ ...card, include_csv: false, include_xls: false })
+  toggleAttachment = () => {
+    const { card, onChange } = this.props;
+    if (this.hasAttachment()) {
+      onChange({ ...card, include_csv: false, include_xls: false });
 
-            this.props.trackPulseEvent("RemoveAttachment")
-        } else {
-            onChange({ ...card, include_csv: true })
+      this.props.trackPulseEvent("RemoveAttachment");
+    } else {
+      onChange({ ...card, include_csv: true });
 
-            this.props.trackPulseEvent("AddAttachment", 'csv')
-        }
+      this.props.trackPulseEvent("AddAttachment", "csv");
     }
-
-    render() {
-        let { cardPreview, attachmentsEnabled } = this.props;
-        const hasAttachment = this.hasAttachment();
-        const isAttachmentOnly = attachmentsEnabled && hasAttachment && cardPreview && cardPreview.pulse_card_type == null;
-        return (
-            <div className="flex relative flex-full">
-                <div className="absolute p2 text-grey-2" style={{ top: 2, right: 2, background: 'linear-gradient(to right, rgba(255,255,255,0.2), white, white)', paddingLeft: 100}}>
-                    { attachmentsEnabled && !isAttachmentOnly &&
-                        <Tooltip tooltip={hasAttachment ? t`Remove attachment` : t`Attach file with results`}>
-                            <Icon
-                                name="attachment" size={18}
-                                className={cx("cursor-pointer py1 pr1 text-brand-hover", { "text-brand": this.hasAttachment() })}
-                                onClick={this.toggleAttachment}
-                            />
-                        </Tooltip>
-                    }
-                    <Icon
-                        name="close" size={18}
-                        className="cursor-pointer py1 pr1 text-brand-hover"
-                        onClick={this.props.onRemove}
-                    />
-                </div>
-                <div
-                    className="bordered rounded flex-full scroll-x"
-                    style={{ display: !cardPreview && "none" }}
-                >
-                    {/* Override backend rendering if pulse_card_type == null */}
-                    { cardPreview && cardPreview.pulse_card_type == null ?
-                      <RenderedPulseCardPreview href={cardPreview.pulse_card_url}>
-                        <RenderedPulseCardPreviewHeader>
-                          {cardPreview.pulse_card_name}
-                        </RenderedPulseCardPreviewHeader>
-                        <RenderedPulseCardPreviewMessage>
-                          { isAttachmentOnly ?
-                            t`This question will be added as a file attachment`
-                          :
-                            t`This question won't be included in your Pulse`
-                          }
-                        </RenderedPulseCardPreviewMessage>
-                      </RenderedPulseCardPreview>
-                    :
-                        <div dangerouslySetInnerHTML={{__html: cardPreview && cardPreview.pulse_card_html}} />
-                    }
-                </div>
-                { !cardPreview &&
-                    <div className="flex-full flex align-center layout-centered pt1">
-                        <LoadingSpinner className="inline-block" />
-                    </div>
+  };
+
+  render() {
+    let { cardPreview, attachmentsEnabled } = this.props;
+    const hasAttachment = this.hasAttachment();
+    const isAttachmentOnly =
+      attachmentsEnabled &&
+      hasAttachment &&
+      cardPreview &&
+      cardPreview.pulse_card_type == null;
+    return (
+      <div
+        className="flex relative flex-full"
+        style={{
+          maxWidth: 379,
+        }}
+      >
+        <div
+          className="absolute p2 text-grey-2"
+          style={{
+            top: 2,
+            right: 2,
+            background:
+              "linear-gradient(to right, rgba(255,255,255,0.2), white, white)",
+            paddingLeft: 100,
+          }}
+        >
+          {attachmentsEnabled &&
+            !isAttachmentOnly && (
+              <Tooltip
+                tooltip={
+                  hasAttachment
+                    ? t`Remove attachment`
+                    : t`Attach file with results`
                 }
-            </div>
-        );
-    }
+              >
+                <Icon
+                  name="attachment"
+                  size={18}
+                  className={cx("cursor-pointer py1 pr1 text-brand-hover", {
+                    "text-brand": this.hasAttachment(),
+                  })}
+                  onClick={this.toggleAttachment}
+                />
+              </Tooltip>
+            )}
+          <Icon
+            name="close"
+            size={18}
+            className="cursor-pointer py1 pr1 text-brand-hover"
+            onClick={this.props.onRemove}
+          />
+        </div>
+        <div
+          className="bordered rounded flex-full scroll-x"
+          style={{ display: !cardPreview && "none" }}
+        >
+          {/* Override backend rendering if pulse_card_type == null */}
+          {cardPreview && cardPreview.pulse_card_type == null ? (
+            <RenderedPulseCardPreview href={cardPreview.pulse_card_url}>
+              <RenderedPulseCardPreviewHeader>
+                {cardPreview.pulse_card_name}
+              </RenderedPulseCardPreviewHeader>
+              <RenderedPulseCardPreviewMessage>
+                {isAttachmentOnly
+                  ? t`This question will be added as a file attachment`
+                  : t`This question won't be included in your Pulse`}
+              </RenderedPulseCardPreviewMessage>
+            </RenderedPulseCardPreview>
+          ) : (
+            <div
+              dangerouslySetInnerHTML={{
+                __html: cardPreview && cardPreview.pulse_card_html,
+              }}
+            />
+          )}
+        </div>
+        {!cardPreview && (
+          <div className="flex-full flex align-center layout-centered pt1">
+            <LoadingSpinner className="inline-block" />
+          </div>
+        )}
+      </div>
+    );
+  }
 }
 
 // implements the same layout as in metabase/pulse/render.clj
-const RenderedPulseCardPreview = ({ href, children }) =>
+const RenderedPulseCardPreview = ({ href, children }) => (
   <a
     href={href}
     style={{
@@ -119,48 +152,52 @@ const RenderedPulseCardPreview = ({ href, children }) =>
       margin: 16,
       marginBottom: 16,
       display: "block",
-      textDecoration: "none"
+      textDecoration: "none",
     }}
     target="_blank"
   >
     {children}
   </a>
+);
 
 RenderedPulseCardPreview.propTypes = {
   href: PropTypes.string,
-  children: PropTypes.node
-}
+  children: PropTypes.node,
+};
 
 // implements the same layout as in metabase/pulse/render.clj
-const RenderedPulseCardPreviewHeader = ({ children }) =>
-    <table style={{ marginBottom: 8, width: "100%" }}>
-      <tbody>
-        <tr>
-          <td>
-            <span style={{
-              fontFamily: 'Lato, "Helvetica Neue", Helvetica, Arial, sans-serif',
+const RenderedPulseCardPreviewHeader = ({ children }) => (
+  <table style={{ marginBottom: 8, width: "100%" }}>
+    <tbody>
+      <tr>
+        <td>
+          <span
+            style={{
+              fontFamily:
+                'Lato, "Helvetica Neue", Helvetica, Arial, sans-serif',
               fontSize: 16,
               fontWeight: 700,
               color: "rgb(57,67,64)",
-              textDecoration: "none"
-            }}>
-              {children}
-            </span>
-          </td>
-          <td style={{ textAlign: "right" }}></td>
-        </tr>
-      </tbody>
-    </table>
+              textDecoration: "none",
+            }}
+          >
+            {children}
+          </span>
+        </td>
+        <td style={{ textAlign: "right" }} />
+      </tr>
+    </tbody>
+  </table>
+);
 
 RenderedPulseCardPreviewHeader.propTypes = {
-  children: PropTypes.node
-}
+  children: PropTypes.node,
+};
 
-const RenderedPulseCardPreviewMessage = ({ children }) =>
-  <div className="text-grey-4">
-    {children}
-  </div>
+const RenderedPulseCardPreviewMessage = ({ children }) => (
+  <div className="text-grey-4">{children}</div>
+);
 
 RenderedPulseCardPreviewMessage.propTypes = {
-  children: PropTypes.node
-}
+  children: PropTypes.node,
+};
diff --git a/frontend/src/metabase/pulse/components/PulseEdit.jsx b/frontend/src/metabase/pulse/components/PulseEdit.jsx
index 2ee8d4fc96590b6d8d1b088b68783c1e057fce20..b7c26174a6a28b935652bb9db8acb097c40b27bd 100644
--- a/frontend/src/metabase/pulse/components/PulseEdit.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEdit.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 
 import PulseEditName from "./PulseEditName.jsx";
 import PulseEditCards from "./PulseEditCards.jsx";
@@ -16,7 +16,6 @@ import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
 import DeleteModalWithConfirm from "metabase/components/DeleteModalWithConfirm.jsx";
 
-
 import { pulseIsValid, cleanPulse, emailIsEnabled } from "metabase/lib/pulse";
 
 import _ from "underscore";
@@ -24,138 +23,182 @@ import cx from "classnames";
 import { inflect } from "inflection";
 
 export default class PulseEdit extends Component {
-    constructor(props) {
-        super(props);
-
-        _.bindAll(this, "save", "delete", "setPulse");
-    }
-
-    static propTypes = {
-        pulse: PropTypes.object.isRequired,
-        pulseId: PropTypes.number,
-        formInput: PropTypes.object.isRequired,
-        setEditingPulse: PropTypes.func.isRequired,
-        fetchCards: PropTypes.func.isRequired,
-        fetchUsers: PropTypes.func.isRequired,
-        fetchPulseFormInput: PropTypes.func.isRequired,
-        updateEditingPulse: PropTypes.func.isRequired,
-        saveEditingPulse: PropTypes.func.isRequired,
-        deletePulse: PropTypes.func.isRequired,
-        onChangeLocation: PropTypes.func.isRequired
-    };
-
-    componentDidMount() {
-        this.props.setEditingPulse(this.props.pulseId);
-        this.props.fetchCards();
-        this.props.fetchUsers();
-        this.props.fetchPulseFormInput();
-
-        MetabaseAnalytics.trackEvent((this.props.pulseId) ? "PulseEdit" : "PulseCreate", "Start");
-    }
-
-    async save() {
-        let pulse = cleanPulse(this.props.pulse,  this.props.formInput.channels);
-        await this.props.updateEditingPulse(pulse);
-        await this.props.saveEditingPulse();
-
-        MetabaseAnalytics.trackEvent((this.props.pulseId) ? "PulseEdit" : "PulseCreate", "Complete", this.props.pulse.cards.length);
-
-        this.props.onChangeLocation("/pulse");
-    }
-
-    async delete() {
-        await this.props.deletePulse(this.props.pulse.id);
-
-        MetabaseAnalytics.trackEvent("PulseDelete", "Complete");
-
-        this.props.onChangeLocation("/pulse");
-    }
-
-    setPulse(pulse) {
-        this.props.updateEditingPulse(pulse);
-    }
-
-    getConfirmItems() {
-        return this.props.pulse.channels.map((c, index) =>
-            c.channel_type === "email" ?
-                <span key={index}>{jt`This pulse will no longer be emailed to ${<strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong>} ${<strong>{c.schedule_type}</strong>}`}.</span>
-            : c.channel_type === "slack" ?
-                <span key={index}>{jt`Slack channel ${<strong>{c.details && c.details.channel}</strong>} will no longer get this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
-            :
-                <span key={index}>{jt`Channel ${<strong>{c.channel_type}</strong>} will no longer receive this pulse ${<strong>{c.schedule_type}</strong>}`}.</span>
-        );
-    }
-
-    render() {
-        const { pulse, formInput } = this.props;
-        const isValid = pulseIsValid(pulse, formInput.channels);
-        const attachmentsEnabled = emailIsEnabled(pulse);
-        return (
-            <div className="PulseEdit">
-                <div className="PulseEdit-header flex align-center border-bottom py3">
-                    <h1>{pulse && pulse.id != null ? t`Edit pulse` : t`New pulse`}</h1>
+  constructor(props) {
+    super(props);
+
+    _.bindAll(this, "save", "delete", "setPulse");
+  }
+
+  static propTypes = {
+    pulse: PropTypes.object.isRequired,
+    pulseId: PropTypes.number,
+    formInput: PropTypes.object.isRequired,
+    setEditingPulse: PropTypes.func.isRequired,
+    fetchCards: PropTypes.func.isRequired,
+    fetchUsers: PropTypes.func.isRequired,
+    fetchPulseFormInput: PropTypes.func.isRequired,
+    updateEditingPulse: PropTypes.func.isRequired,
+    saveEditingPulse: PropTypes.func.isRequired,
+    deletePulse: PropTypes.func.isRequired,
+    onChangeLocation: PropTypes.func.isRequired,
+  };
+
+  componentDidMount() {
+    this.props.setEditingPulse(this.props.pulseId);
+    this.props.fetchCards();
+    this.props.fetchUsers();
+    this.props.fetchPulseFormInput();
+
+    MetabaseAnalytics.trackEvent(
+      this.props.pulseId ? "PulseEdit" : "PulseCreate",
+      "Start",
+    );
+  }
+
+  async save() {
+    let pulse = cleanPulse(this.props.pulse, this.props.formInput.channels);
+    await this.props.updateEditingPulse(pulse);
+    await this.props.saveEditingPulse();
+
+    MetabaseAnalytics.trackEvent(
+      this.props.pulseId ? "PulseEdit" : "PulseCreate",
+      "Complete",
+      this.props.pulse.cards.length,
+    );
+
+    this.props.onChangeLocation("/pulse");
+  }
+
+  async delete() {
+    await this.props.deletePulse(this.props.pulse.id);
+
+    MetabaseAnalytics.trackEvent("PulseDelete", "Complete");
+
+    this.props.onChangeLocation("/pulse");
+  }
+
+  setPulse(pulse) {
+    this.props.updateEditingPulse(pulse);
+  }
+
+  getConfirmItems() {
+    return this.props.pulse.channels.map(
+      (c, index) =>
+        c.channel_type === "email" ? (
+          <span key={index}>
+            {jt`This pulse will no longer be emailed to ${(
+              <strong>
+                {c.recipients.length} {inflect("address", c.recipients.length)}
+              </strong>
+            )} ${<strong>{c.schedule_type}</strong>}`}.
+          </span>
+        ) : c.channel_type === "slack" ? (
+          <span key={index}>
+            {jt`Slack channel ${(
+              <strong>{c.details && c.details.channel}</strong>
+            )} will no longer get this pulse ${(
+              <strong>{c.schedule_type}</strong>
+            )}`}.
+          </span>
+        ) : (
+          <span key={index}>
+            {jt`Channel ${(
+              <strong>{c.channel_type}</strong>
+            )} will no longer receive this pulse ${(
+              <strong>{c.schedule_type}</strong>
+            )}`}.
+          </span>
+        ),
+    );
+  }
+
+  render() {
+    const { pulse, formInput } = this.props;
+    const isValid = pulseIsValid(pulse, formInput.channels);
+    const attachmentsEnabled = emailIsEnabled(pulse);
+    return (
+      <div className="PulseEdit">
+        <div className="PulseEdit-header flex align-center border-bottom py3">
+          <h1>{pulse && pulse.id != null ? t`Edit pulse` : t`New pulse`}</h1>
+          <ModalWithTrigger
+            ref="pulseInfo"
+            className="Modal WhatsAPulseModal"
+            triggerElement={t`What's a Pulse?`}
+            triggerClasses="text-brand text-bold flex-align-right"
+          >
+            <ModalContent onClose={() => this.refs.pulseInfo.close()}>
+              <div className="mx4 mb4">
+                <WhatsAPulse
+                  button={
+                    <button
+                      className="Button Button--primary"
+                      onClick={() => this.refs.pulseInfo.close()}
+                    >{t`Got it`}</button>
+                  }
+                />
+              </div>
+            </ModalContent>
+          </ModalWithTrigger>
+        </div>
+        <div className="PulseEdit-content pt2 pb4">
+          <PulseEditName {...this.props} setPulse={this.setPulse} />
+          <PulseEditCards
+            {...this.props}
+            setPulse={this.setPulse}
+            attachmentsEnabled={attachmentsEnabled}
+          />
+          <div className="py1 mb4">
+            <h2 className="mb3">{t`Where should this data go?`}</h2>
+            <PulseEditChannels
+              {...this.props}
+              setPulse={this.setPulse}
+              pulseIsValid={isValid}
+            />
+          </div>
+          <PulseEditSkip {...this.props} setPulse={this.setPulse} />
+          {pulse &&
+            pulse.id != null && (
+              <div className="DangerZone mb2 p3 rounded bordered relative">
+                <h3
+                  className="text-error absolute top bg-white px1"
+                  style={{ marginTop: "-12px" }}
+                >{t`Danger Zone`}</h3>
+                <div className="ml1">
+                  <h4 className="text-bold mb1">{t`Delete this pulse`}</h4>
+                  <div className="flex">
+                    <p className="h4 pr2">{t`Stop delivery and delete this pulse. There's no undo, so be careful.`}</p>
                     <ModalWithTrigger
-                        ref="pulseInfo"
-                        className="Modal WhatsAPulseModal"
-                        triggerElement={t`What's a Pulse?`}
-                        triggerClasses="text-brand text-bold flex-align-right"
+                      ref={"deleteModal" + pulse.id}
+                      triggerClasses="Button Button--danger flex-align-right flex-no-shrink"
+                      triggerElement={t`Delete this Pulse`}
                     >
-                        <ModalContent
-                            onClose={() => this.refs.pulseInfo.close()}
-                        >
-                            <div className="mx4 mb4">
-                                <WhatsAPulse
-                                    button={<button className="Button Button--primary" onClick={() => this.refs.pulseInfo.close()}>{t`Got it`}</button>}
-                                />
-                            </div>
-                        </ModalContent>
+                      <DeleteModalWithConfirm
+                        objectType="pulse"
+                        title={t`Delete` + ' "' + pulse.name + '"?'}
+                        confirmItems={this.getConfirmItems()}
+                        onClose={() =>
+                          this.refs["deleteModal" + pulse.id].close()
+                        }
+                        onDelete={this.delete}
+                      />
                     </ModalWithTrigger>
+                  </div>
                 </div>
-                <div className="PulseEdit-content pt2 pb4">
-                    <PulseEditName {...this.props} setPulse={this.setPulse} />
-                    <PulseEditCards {...this.props} setPulse={this.setPulse} attachmentsEnabled={attachmentsEnabled} />
-                    <div className="py1 mb4">
-                        <h2 className="mb3">{t`Where should this data go?`}</h2>
-                        <PulseEditChannels {...this.props} setPulse={this.setPulse} pulseIsValid={isValid} />
-                    </div>
-                    <PulseEditSkip {...this.props} setPulse={this.setPulse} />
-                    { pulse && pulse.id != null &&
-                        <div className="DangerZone mb2 p3 rounded bordered relative">
-                            <h3 className="text-error absolute top bg-white px1" style={{ marginTop: "-12px" }}>{t`Danger Zone`}</h3>
-                            <div className="ml1">
-                                <h4 className="text-bold mb1">{t`Delete this pulse`}</h4>
-                                <div className="flex">
-                                    <p className="h4 pr2">{t`Stop delivery and delete this pulse. There's no undo, so be careful.`}</p>
-                                    <ModalWithTrigger
-                                        ref={"deleteModal"+pulse.id}
-                                        triggerClasses="Button Button--danger flex-align-right flex-no-shrink"
-                                        triggerElement={t`Delete this Pulse`}
-                                    >
-                                        <DeleteModalWithConfirm
-                                            objectType="pulse"
-                                            title={t`Delete`+" \"" + pulse.name + "\"?"}
-                                            confirmItems={this.getConfirmItems()}
-                                            onClose={() => this.refs["deleteModal"+pulse.id].close()}
-                                            onDelete={this.delete}
-                                        />
-                                    </ModalWithTrigger>
-                                </div>
-                            </div>
-                        </div>
-                    }
-                </div>
-                <div className="PulseEdit-footer flex align-center border-top py3">
-                    <ActionButton
-                        actionFn={this.save}
-                        className={cx("Button Button--primary", { "disabled": !isValid })}
-                        normalText={pulse.id != null ? t`Save changes` : t`Create pulse`}
-                        activeText={t`Saving…`}
-                        failedText={t`Save failed`}
-                        successText={t`Saved`}
-                    />
-                  <Link to="/pulse" className="Button ml2">{t`Cancel`}</Link>
-                </div>
-            </div>
-        );
-    }
+              </div>
+            )}
+        </div>
+        <div className="PulseEdit-footer flex align-center border-top py3">
+          <ActionButton
+            actionFn={this.save}
+            className={cx("Button Button--primary", { disabled: !isValid })}
+            normalText={pulse.id != null ? t`Save changes` : t`Create pulse`}
+            activeText={t`Saving…`}
+            failedText={t`Save failed`}
+            successText={t`Saved`}
+          />
+          <Link to="/pulse" className="Button ml2">{t`Cancel`}</Link>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/PulseEditCards.jsx b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
index dff1db88710a8505b08f4b04561c61e1aad30ed2..df98dfd4a23f3e9cb6d2a2174712d9f3e76a8d8e 100644
--- a/frontend/src/metabase/pulse/components/PulseEditCards.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditCards.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 import CardPicker from "./CardPicker.jsx";
@@ -11,181 +11,230 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 
 const SOFT_LIMIT = 10;
 const HARD_LIMIT = 25;
+const TABLE_MAX_ROWS = 20;
+const TABLE_MAX_COLS = 10;
+
+function isAutoAttached(cardPreview) {
+  return (
+    cardPreview &&
+    cardPreview.pulse_card_type === "table" &&
+    (cardPreview.row_count > TABLE_MAX_ROWS ||
+      cardPreview.col_cound > TABLE_MAX_COLS)
+  );
+}
 
 export default class PulseEditCards extends Component {
-    constructor(props) {
-        super(props);
-        this.state = {};
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
+
+  static propTypes = {
+    pulse: PropTypes.object.isRequired,
+    pulseId: PropTypes.number,
+    cardPreviews: PropTypes.object.isRequired,
+    cards: PropTypes.object.isRequired,
+    cardList: PropTypes.array.isRequired,
+    fetchPulseCardPreview: PropTypes.func.isRequired,
+    setPulse: PropTypes.func.isRequired,
+    attachmentsEnabled: PropTypes.bool,
+  };
+  static defaultProps = {};
+
+  setCard(index, card) {
+    let { pulse } = this.props;
+    this.props.setPulse({
+      ...pulse,
+      cards: [
+        ...pulse.cards.slice(0, index),
+        card,
+        ...pulse.cards.slice(index + 1),
+      ],
+    });
+  }
+
+  trackPulseEvent = (eventName: string, eventValue: string) => {
+    MetabaseAnalytics.trackEvent(
+      this.props.pulseId ? "PulseEdit" : "PulseCreate",
+      eventName,
+      eventValue,
+    );
+  };
+
+  addCard(index, cardId) {
+    this.setCard(index, { id: cardId });
+    this.trackPulseEvent("AddCard", index);
+  }
+
+  removeCard(index) {
+    let { pulse } = this.props;
+    this.props.setPulse({
+      ...pulse,
+      cards: [...pulse.cards.slice(0, index), ...pulse.cards.slice(index + 1)],
+    });
+
+    this.trackPulseEvent("RemoveCard", index);
+  }
+
+  getNotices(card, cardPreview, index) {
+    const showSoftLimitWarning = index === SOFT_LIMIT;
+    let notices = [];
+    const hasAttachment =
+      isAutoAttached(cardPreview) ||
+      (this.props.attachmentsEnabled &&
+        card &&
+        (card.include_csv || card.include_xls));
+    if (hasAttachment) {
+      notices.push({
+        head: t`Attachment`,
+        body: (
+          <AttachmentWidget
+            card={card}
+            onChange={card => this.setCard(index, card)}
+            trackPulseEvent={this.trackPulseEvent}
+          />
+        ),
+      });
     }
-
-    static propTypes = {
-        pulse: PropTypes.object.isRequired,
-        pulseId: PropTypes.number,
-        cardPreviews: PropTypes.object.isRequired,
-        cards: PropTypes.object.isRequired,
-        cardList: PropTypes.array.isRequired,
-        fetchPulseCardPreview: PropTypes.func.isRequired,
-        setPulse: PropTypes.func.isRequired,
-        attachmentsEnabled: PropTypes.bool,
-    };
-    static defaultProps = {};
-
-    setCard(index, card) {
-        let { pulse } = this.props;
-        this.props.setPulse({
-            ...pulse,
-            cards: [...pulse.cards.slice(0, index), card, ...pulse.cards.slice(index + 1)]
+    if (cardPreview) {
+      if (isAutoAttached(cardPreview)) {
+        notices.push({
+          type: "warning",
+          head: t`Heads up`,
+          body: t`We'll show the first 10 columns and 20 rows of this table in your Pulse. If you email this, we'll add a file attachment with all columns and up to 2,000 rows.`,
+        });
+      }
+      if (cardPreview.pulse_card_type == null && !hasAttachment) {
+        notices.push({
+          type: "warning",
+          head: t`Heads up`,
+          body: t`Raw data questions can only be included as email attachments`,
         });
+      }
     }
-
-    trackPulseEvent = (eventName: string, eventValue: string) => {
-        MetabaseAnalytics.trackEvent(
-            (this.props.pulseId) ? "PulseEdit" : "PulseCreate",
-            eventName,
-            eventValue
-        );
+    if (showSoftLimitWarning) {
+      notices.push({
+        type: "warning",
+        head: t`Looks like this pulse is getting big`,
+        body: t`We recommend keeping pulses small and focused to help keep them digestable and useful to the whole team.`,
+      });
     }
-
-    addCard(index, cardId) {
-        this.setCard(index, { id: cardId })
-        this.trackPulseEvent("AddCard", index);
+    return notices;
+  }
+
+  renderCardNotices(card, index) {
+    let cardPreview = card && this.props.cardPreviews[card.id];
+    let notices = this.getNotices(card, cardPreview, index);
+    if (notices.length > 0) {
+      return (
+        <div className="absolute" style={{ width: 400, marginLeft: 420 }}>
+          {notices.map((notice, index) => (
+            <div
+              key={index}
+              className={cx("border-left mt1 mb2 ml3 pl3", {
+                "text-gold border-gold": notice.type === "warning",
+                "border-brand": notice.type !== "warning",
+              })}
+              style={{ borderWidth: 3 }}
+            >
+              <h3 className="mb1">{notice.head}</h3>
+              <div className="h4">{notice.body}</div>
+            </div>
+          ))}
+        </div>
+      );
     }
+  }
 
-    removeCard(index) {
-        let { pulse } = this.props;
-        this.props.setPulse({
-            ...pulse,
-            cards: [...pulse.cards.slice(0, index), ...pulse.cards.slice(index + 1)]
-        });
+  render() {
+    let { pulse, cards, cardList, cardPreviews } = this.props;
 
-        this.trackPulseEvent("RemoveCard", index);
+    let pulseCards = pulse ? pulse.cards.slice() : [];
+    if (pulseCards.length < HARD_LIMIT) {
+      pulseCards.push(null);
     }
 
-    getNotices(card, cardPreview, index) {
-        const showSoftLimitWarning = index === SOFT_LIMIT;
-        let notices = [];
-        const hasAttachment = this.props.attachmentsEnabled && card && (card.include_csv || card.include_xls);
-        if (hasAttachment) {
-            notices.push({
-                head: t`Attachment`,
-                body: <AttachmentWidget card={card} onChange={(card) => this.setCard(index, card)} trackPulseEvent={this.trackPulseEvent} />
-            });
-        }
-        if (cardPreview) {
-            if (cardPreview.pulse_card_type == null && !hasAttachment) {
-                notices.push({
-                    type: "warning",
-                    head: t`Heads up`,
-                    body: t`Raw data questions can only be included as email attachments`
-                });
-            }
-        }
-        if (showSoftLimitWarning) {
-            notices.push({
-                type: "warning",
-                head: t`Looks like this pulse is getting big`,
-                body: t`We recommend keeping pulses small and focused to help keep them digestable and useful to the whole team.`
-            });
-        }
-        return notices;
-    }
-
-    renderCardNotices(card, index) {
-        let cardPreview = card && this.props.cardPreviews[card.id];
-        let notices = this.getNotices(card, cardPreview, index);
-        if (notices.length > 0) {
-            return (
-                <div className="absolute" style={{ width: 400, marginLeft: 420 }}>
-                    {notices.map((notice, index) =>
-                        <div
-                            key={index}
-                            className={cx("border-left mt1 mb2 ml3 pl3", {
-                              "text-gold border-gold": notice.type === "warning",
-                              "border-brand":          notice.type !== "warning"
-                            })}
-                            style={{ borderWidth: 3 }}
-                        >
-                            <h3 className="mb1">{notice.head}</h3>
-                            <div className="h4">{notice.body}</div>
-                        </div>
+    return (
+      <div className="py1">
+        <h2>{t`Pick your data`}</h2>
+        <p className="mt1 h4 text-bold text-grey-3">
+          {t`Choose questions you'd like to send in this pulse`}.
+        </p>
+        <ol className="my3">
+          {cards &&
+            pulseCards.map((card, index) => (
+              <li key={index} className="my1">
+                {index === SOFT_LIMIT && (
+                  <div
+                    className="my4 ml3"
+                    style={{
+                      width: 375,
+                      borderTop: "1px dashed rgb(214,214,214)",
+                    }}
+                  />
+                )}
+                <div className="flex align-top">
+                  <div className="flex align-top" style={{ width: 400 }}>
+                    <span className="h3 text-bold mr1 mt2">{index + 1}.</span>
+                    {card ? (
+                      <PulseCardPreview
+                        card={card}
+                        cardPreview={cardPreviews[card.id]}
+                        onChange={this.setCard.bind(this, index)}
+                        onRemove={this.removeCard.bind(this, index)}
+                        fetchPulseCardPreview={this.props.fetchPulseCardPreview}
+                        attachmentsEnabled={
+                          this.props.attachmentsEnabled &&
+                          !isAutoAttached(cardPreviews[card.id])
+                        }
+                        trackPulseEvent={this.trackPulseEvent}
+                      />
+                    ) : (
+                      <CardPicker
+                        cardList={cardList}
+                        onChange={this.addCard.bind(this, index)}
+                        attachmentsEnabled={this.props.attachmentsEnabled}
+                      />
                     )}
+                  </div>
+                  {this.renderCardNotices(card, index)}
                 </div>
-            )
-        }
-    }
-
-    render() {
-        let { pulse, cards, cardList, cardPreviews } = this.props;
-
-        let pulseCards = pulse ? pulse.cards.slice() : [];
-        if (pulseCards.length < HARD_LIMIT) {
-            pulseCards.push(null);
-        }
-
-        return (
-            <div className="py1">
-                <h2>{t`Pick your data`}</h2>
-                <p className="mt1 h4 text-bold text-grey-3">{t`Choose questions you'd like to send in this pulse`}.</p>
-                <ol className="my3">
-                    {cards && pulseCards.map((card, index) =>
-                        <li key={index} className="my1">
-                            { index === SOFT_LIMIT && <div className="my4 ml3" style={{ width: 375, borderTop: "1px dashed rgb(214,214,214)"}}/> }
-                            <div className="flex align-top">
-                                <div className="flex align-top" style={{ width: 400 }}>
-                                    <span className="h3 text-bold mr1 mt2">{index + 1}.</span>
-                                    { card ?
-                                        <PulseCardPreview
-                                            card={card}
-                                            cardPreview={cardPreviews[card.id]}
-                                            onChange={this.setCard.bind(this, index)}
-                                            onRemove={this.removeCard.bind(this, index)}
-                                            fetchPulseCardPreview={this.props.fetchPulseCardPreview}
-                                            attachmentsEnabled={this.props.attachmentsEnabled}
-                                            trackPulseEvent={this.trackPulseEvent}
-                                        />
-                                    :
-                                        <CardPicker
-                                            cardList={cardList}
-                                            onChange={this.addCard.bind(this, index)}
-                                            attachmentsEnabled={this.props.attachmentsEnabled}
-                                        />
-                                    }
-                                </div>
-                                {this.renderCardNotices(card, index)}
-                            </div>
-                        </li>
-                    )}
-                </ol>
-            </div>
-        );
-    }
+              </li>
+            ))}
+        </ol>
+      </div>
+    );
+  }
 }
 
 const ATTACHMENT_TYPES = ["csv", "xls"];
 
-const AttachmentWidget = ({ card, onChange, trackPulseEvent }) =>
-    <div>
-        { ATTACHMENT_TYPES.map(type =>
-            <span
-                key={type}
-                className={cx("text-brand-hover cursor-pointer mr1", { "text-brand": card["include_"+type] })}
-                onClick={() => {
-                    const newCard = { ...card }
-                    for (const attachmentType of ATTACHMENT_TYPES) {
-                      newCard["include_" + attachmentType] = type === attachmentType;
-                    }
-
-                    trackPulseEvent("AttachmentTypeChanged", type);
-                    onChange(newCard)
-                }}
-            >
-                {"." + type}
-            </span>
-        )}
-    </div>
+const AttachmentWidget = ({ card, onChange, trackPulseEvent }) => (
+  <div>
+    {ATTACHMENT_TYPES.map(type => (
+      <span
+        key={type}
+        className={cx("text-brand-hover cursor-pointer mr1", {
+          "text-brand": card["include_" + type],
+        })}
+        onClick={() => {
+          const newCard = { ...card };
+          for (const attachmentType of ATTACHMENT_TYPES) {
+            newCard["include_" + attachmentType] = type === attachmentType;
+          }
+
+          trackPulseEvent("AttachmentTypeChanged", type);
+          onChange(newCard);
+        }}
+      >
+        {"." + type}
+      </span>
+    ))}
+  </div>
+);
 
 AttachmentWidget.propTypes = {
-    card: PropTypes.object.isRequired,
-    onChange: PropTypes.func.isRequired,
-    trackPulseEvent: PropTypes.func.isRequired
-}
+  card: PropTypes.object.isRequired,
+  onChange: PropTypes.func.isRequired,
+  trackPulseEvent: PropTypes.func.isRequired,
+};
diff --git a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
index 85c4f92b1eac256a5c0c000379473e2c4db206e8..26f64805efdac5e48ae94e133c9707da7e030dbc 100644
--- a/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditChannels.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import _ from "underscore";
 import { assoc, assocIn } from "icepick";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import RecipientPicker from "./RecipientPicker.jsx";
 
@@ -21,226 +21,287 @@ import { channelIsValid, createChannel } from "metabase/lib/pulse";
 import cx from "classnames";
 
 export const CHANNEL_ICONS = {
-    email: "mail",
-    slack: "slack"
+  email: "mail",
+  slack: "slack",
 };
 
 const CHANNEL_NOUN_PLURAL = {
-    "email": t`Emails`,
-    "slack": t`Slack messages`
+  email: t`Emails`,
+  slack: t`Slack messages`,
 };
 
 export default class PulseEditChannels extends Component {
-    constructor(props) {
-        super(props);
-        this.state = {};
-    }
+  constructor(props) {
+    super(props);
+    this.state = {};
+  }
 
-    static propTypes = {
-        pulse: PropTypes.object.isRequired,
-        pulseId: PropTypes.number,
-        pulseIsValid: PropTypes.bool.isRequired,
-        formInput: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        userList: PropTypes.array.isRequired,
-        setPulse: PropTypes.func.isRequired,
-        testPulse: PropTypes.func,
-        cardPreviews: PropTypes.object,
-        hideSchedulePicker: PropTypes.bool,
-        emailRecipientText: PropTypes.string
-    };
-    static defaultProps = {};
+  static propTypes = {
+    pulse: PropTypes.object.isRequired,
+    pulseId: PropTypes.number,
+    pulseIsValid: PropTypes.bool.isRequired,
+    formInput: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    userList: PropTypes.array.isRequired,
+    setPulse: PropTypes.func.isRequired,
+    testPulse: PropTypes.func,
+    cardPreviews: PropTypes.object,
+    hideSchedulePicker: PropTypes.bool,
+    emailRecipientText: PropTypes.string,
+  };
+  static defaultProps = {};
 
-    addChannel(type) {
-        let { pulse, formInput } = this.props;
+  addChannel(type) {
+    let { pulse, formInput } = this.props;
 
-        let channelSpec = formInput.channels[type];
-        if (!channelSpec) {
-            return;
-        }
+    let channelSpec = formInput.channels[type];
+    if (!channelSpec) {
+      return;
+    }
 
-        let channel = createChannel(channelSpec);
+    let channel = createChannel(channelSpec);
 
-        this.props.setPulse({ ...pulse, channels: pulse.channels.concat(channel) });
+    this.props.setPulse({ ...pulse, channels: pulse.channels.concat(channel) });
 
-        MetabaseAnalytics.trackEvent((this.props.pulseId) ? "PulseEdit" : "PulseCreate", "AddChannel", type);
-    }
+    MetabaseAnalytics.trackEvent(
+      this.props.pulseId ? "PulseEdit" : "PulseCreate",
+      "AddChannel",
+      type,
+    );
+  }
 
-    removeChannel(index) {
-        let { pulse } = this.props;
-        this.props.setPulse(assocIn(pulse, ["channels", index, "enabled"], false));
-    }
+  removeChannel(index) {
+    let { pulse } = this.props;
+    this.props.setPulse(assocIn(pulse, ["channels", index, "enabled"], false));
+  }
 
-    onChannelPropertyChange(index, name, value) {
-        let { pulse } = this.props;
-        let channels = [...pulse.channels];
+  onChannelPropertyChange(index, name, value) {
+    let { pulse } = this.props;
+    let channels = [...pulse.channels];
 
-        channels[index] = { ...channels[index], [name]: value };
+    channels[index] = { ...channels[index], [name]: value };
 
-        this.props.setPulse({ ...pulse, channels });
-    }
+    this.props.setPulse({ ...pulse, channels });
+  }
 
-    // changedProp contains the schedule property that user just changed
-    // newSchedule may contain also other changed properties as some property changes reset other properties
-    onChannelScheduleChange(index, newSchedule, changedProp) {
-        let { pulse } = this.props;
-        let channels = [...pulse.channels];
+  // changedProp contains the schedule property that user just changed
+  // newSchedule may contain also other changed properties as some property changes reset other properties
+  onChannelScheduleChange(index, newSchedule, changedProp) {
+    let { pulse } = this.props;
+    let channels = [...pulse.channels];
 
-        MetabaseAnalytics.trackEvent(
-            (this.props.pulseId) ? "PulseEdit" : "PulseCreate",
-            channels[index].channel_type + ":" + changedProp.name,
-            changedProp.value
-        );
+    MetabaseAnalytics.trackEvent(
+      this.props.pulseId ? "PulseEdit" : "PulseCreate",
+      channels[index].channel_type + ":" + changedProp.name,
+      changedProp.value,
+    );
 
-        channels[index] = { ...channels[index], ...newSchedule };
-        this.props.setPulse({ ...pulse, channels });
-    }
+    channels[index] = { ...channels[index], ...newSchedule };
+    this.props.setPulse({ ...pulse, channels });
+  }
 
-    toggleChannel(type, enable) {
-        const { pulse } = this.props;
-        if (enable) {
-            if (pulse.channels.some(c => c.channel_type === type)) {
-                this.props.setPulse(assoc(pulse, "channels", pulse.channels.map(c =>
-                    c.channel_type === type ? assoc(c, "enabled", true) : c
-                )));
-            } else {
-                this.addChannel(type)
-            }
-        } else {
-            this.props.setPulse(assoc(pulse, "channels", pulse.channels.map(c =>
-                c.channel_type === type ? assoc(c, "enabled", false) : c
-            )));
-
-            MetabaseAnalytics.trackEvent((this.props.pulseId) ? "PulseEdit" : "PulseCreate", "RemoveChannel", type);
-        }
-    }
+  toggleChannel(type, enable) {
+    const { pulse } = this.props;
+    if (enable) {
+      if (pulse.channels.some(c => c.channel_type === type)) {
+        this.props.setPulse(
+          assoc(
+            pulse,
+            "channels",
+            pulse.channels.map(
+              c => (c.channel_type === type ? assoc(c, "enabled", true) : c),
+            ),
+          ),
+        );
+      } else {
+        this.addChannel(type);
+      }
+    } else {
+      this.props.setPulse(
+        assoc(
+          pulse,
+          "channels",
+          pulse.channels.map(
+            c => (c.channel_type === type ? assoc(c, "enabled", false) : c),
+          ),
+        ),
+      );
 
-    onTestPulseChannel(channel) {
-        // test a single channel
-        return this.props.testPulse({ ...this.props.pulse, channels: [channel] });
+      MetabaseAnalytics.trackEvent(
+        this.props.pulseId ? "PulseEdit" : "PulseCreate",
+        "RemoveChannel",
+        type,
+      );
     }
+  }
 
-    willPulseSkip = () => {
-        let cards = _.pluck(this.props.pulse.cards, 'id');
-        let cardPreviews = this.props.cardPreviews;
-        let previews = _.map(cards, function (id) { return _.find(cardPreviews, function(card){ return (id == card.id);})});
-        let types = _.pluck(previews, 'pulse_card_type');
-        let empty = _.isEqual( _.uniq(types), ["empty"]);
-        return (empty && this.props.pulse.skip_if_empty);
-    }
+  onTestPulseChannel(channel) {
+    // test a single channel
+    return this.props.testPulse({ ...this.props.pulse, channels: [channel] });
+  }
 
-    renderFields(channel, index, channelSpec) {
-        return (
-            <div>
-                {channelSpec.fields.map(field =>
-                    <div key={field.name} className={field.name}>
-                        <span className="h4 text-bold mr1">{field.displayName}</span>
-                        { field.type === "select" ?
-                            <Select
-                                className="h4 text-bold bg-white"
-                                value={channel.details && channel.details[field.name]}
-                                options={field.options}
-                                optionNameFn={o => o}
-                                optionValueFn={o => o}
-                                // Address #5799 where `details` object is missing for some reason
-                                onChange={(o) => this.onChannelPropertyChange(index, "details", { ...channel.details, [field.name]: o })}
-                            />
-                        : null }
-                    </div>
-                )}
-            </div>
-        )
-    }
+  willPulseSkip = () => {
+    let cards = _.pluck(this.props.pulse.cards, "id");
+    let cardPreviews = this.props.cardPreviews;
+    let previews = _.map(cards, function(id) {
+      return _.find(cardPreviews, function(card) {
+        return id == card.id;
+      });
+    });
+    let types = _.pluck(previews, "pulse_card_type");
+    let empty = _.isEqual(_.uniq(types), ["empty"]);
+    return empty && this.props.pulse.skip_if_empty;
+  };
 
-    renderChannel(channel, index, channelSpec) {
-        let isValid = this.props.pulseIsValid && channelIsValid(channel, channelSpec);
-        return (
-            <li key={index} className="py2">
-                { channelSpec.error &&
-                    <div className="pb2 text-bold text-error">{channelSpec.error}</div>
+  renderFields(channel, index, channelSpec) {
+    return (
+      <div>
+        {channelSpec.fields.map(field => (
+          <div key={field.name} className={field.name}>
+            <span className="h4 text-bold mr1">{field.displayName}</span>
+            {field.type === "select" ? (
+              <Select
+                className="h4 text-bold bg-white"
+                value={channel.details && channel.details[field.name]}
+                options={field.options}
+                optionNameFn={o => o}
+                optionValueFn={o => o}
+                // Address #5799 where `details` object is missing for some reason
+                onChange={o =>
+                  this.onChannelPropertyChange(index, "details", {
+                    ...channel.details,
+                    [field.name]: o,
+                  })
                 }
-                { channelSpec.recipients &&
-                    <div>
-                        <div className="h4 text-bold mb1">{ this.props.emailRecipientText || "To:" }</div>
-                        <RecipientPicker
-                            isNewPulse={this.props.pulseId === undefined}
-                            autoFocus={!!this.props.pulse.name}
-                            recipients={channel.recipients}
-                            recipientTypes={channelSpec.recipients}
-                            users={this.props.userList}
-                            onRecipientsChange={(recipients) => this.onChannelPropertyChange(index, "recipients", recipients)}
-                        />
-                    </div>
-                }
-                { channelSpec.fields &&
-                    this.renderFields(channel, index, channelSpec)
-                }
-                { !this.props.hideSchedulePicker && channelSpec.schedules &&
-                    <SchedulePicker
-                        schedule={_.pick(channel, "schedule_day", "schedule_frame", "schedule_hour", "schedule_type") }
-                        scheduleOptions={channelSpec.schedules}
-                        textBeforeInterval={t`Sent`}
-                        textBeforeSendTime={t`${CHANNEL_NOUN_PLURAL[channelSpec && channelSpec.type] || t`Messages`} will be sent at`}
-                        onScheduleChange={this.onChannelScheduleChange.bind(this, index)}
-                    />
-                }
-                { this.props.testPulse &&
-                    <div className="pt2">
-                        <ActionButton
-                            actionFn={this.onTestPulseChannel.bind(this, channel)}
-                            className={cx("Button", { disabled: !isValid })}
-                            normalText={channelSpec.type === "email" ?
-                                t`Send email now` :
-                                t`Send to ${channelSpec.name} now`}
-                            activeText={t`Sending…`}
-                            failedText={t`Sending failed`}
-                            successText={ this.willPulseSkip() ?  t`Didn’t send because the pulse has no results.` : t`Pulse sent`}
-                            forceActiveStyle={ this.willPulseSkip() }
-                        />
-                    </div>
-                }
-            </li>
-        );
-    }
+              />
+            ) : null}
+          </div>
+        ))}
+      </div>
+    );
+  }
 
-    renderChannelSection(channelSpec) {
-        let { pulse, user } = this.props;
-        let channels = pulse.channels
-            .map((c, i) => [c, i]).filter(([c, i]) => c.enabled && c.channel_type === channelSpec.type)
-            .map(([channel, index]) => this.renderChannel(channel, index, channelSpec));
-        return (
-            <li key={channelSpec.type} className="border-row-divider">
-                <div className="flex align-center p3 border-row-divider">
-                    {CHANNEL_ICONS[channelSpec.type] && <Icon className="mr1 text-grey-2" name={CHANNEL_ICONS[channelSpec.type]} size={28} />}
-                    <h2>{channelSpec.name}</h2>
-                    <Toggle className="flex-align-right" value={channels.length > 0} onChange={this.toggleChannel.bind(this, channelSpec.type)} />
-                </div>
-                {channels.length > 0 && channelSpec.configured ?
-                    <ul className="bg-grey-0 px3">{channels}</ul>
-                : channels.length > 0 && !channelSpec.configured ?
-                    <div className="p4 text-centered">
-                        <h3 className="mb2">{t`${channelSpec.name} needs to be set up by an administrator.`}</h3>
-                        <ChannelSetupMessage user={user} channels={[channelSpec.name]} />
-                    </div>
-                : null
-                }
-            </li>
-        )
-    }
+  renderChannel(channel, index, channelSpec) {
+    let isValid =
+      this.props.pulseIsValid && channelIsValid(channel, channelSpec);
+    return (
+      <li key={index} className="py2">
+        {channelSpec.error && (
+          <div className="pb2 text-bold text-error">{channelSpec.error}</div>
+        )}
+        {channelSpec.recipients && (
+          <div>
+            <div className="h4 text-bold mb1">
+              {this.props.emailRecipientText || "To:"}
+            </div>
+            <RecipientPicker
+              isNewPulse={this.props.pulseId === undefined}
+              autoFocus={!!this.props.pulse.name}
+              recipients={channel.recipients}
+              recipientTypes={channelSpec.recipients}
+              users={this.props.userList}
+              onRecipientsChange={recipients =>
+                this.onChannelPropertyChange(index, "recipients", recipients)
+              }
+            />
+          </div>
+        )}
+        {channelSpec.fields && this.renderFields(channel, index, channelSpec)}
+        {!this.props.hideSchedulePicker &&
+          channelSpec.schedules && (
+            <SchedulePicker
+              schedule={_.pick(
+                channel,
+                "schedule_day",
+                "schedule_frame",
+                "schedule_hour",
+                "schedule_type",
+              )}
+              scheduleOptions={channelSpec.schedules}
+              textBeforeInterval={t`Sent`}
+              textBeforeSendTime={t`${CHANNEL_NOUN_PLURAL[
+                channelSpec && channelSpec.type
+              ] || t`Messages`} will be sent at`}
+              onScheduleChange={this.onChannelScheduleChange.bind(this, index)}
+            />
+          )}
+        {this.props.testPulse && (
+          <div className="pt2">
+            <ActionButton
+              actionFn={this.onTestPulseChannel.bind(this, channel)}
+              className={cx("Button", { disabled: !isValid })}
+              normalText={
+                channelSpec.type === "email"
+                  ? t`Send email now`
+                  : t`Send to ${channelSpec.name} now`
+              }
+              activeText={t`Sending…`}
+              failedText={t`Sending failed`}
+              successText={
+                this.willPulseSkip()
+                  ? t`Didn’t send because the pulse has no results.`
+                  : t`Pulse sent`
+              }
+              forceActiveStyle={this.willPulseSkip()}
+            />
+          </div>
+        )}
+      </li>
+    );
+  }
 
-    render() {
-        let { formInput } = this.props;
-        // Default to show the default channels until full formInput is loaded
-        let channels = formInput.channels || {
-            email: { name: t`Email`, type: "email" },
-            slack: { name: t`Slack`, type: "slack" }
-        };
-        return (
-            <ul className="bordered rounded">
-                {Object.values(channels).map(channelSpec =>
-                    this.renderChannelSection(channelSpec)
-                )}
-            </ul>
-        );
-    }
+  renderChannelSection(channelSpec) {
+    let { pulse, user } = this.props;
+    let channels = pulse.channels
+      .map((c, i) => [c, i])
+      .filter(([c, i]) => c.enabled && c.channel_type === channelSpec.type)
+      .map(([channel, index]) =>
+        this.renderChannel(channel, index, channelSpec),
+      );
+    return (
+      <li key={channelSpec.type} className="border-row-divider">
+        <div className="flex align-center p3 border-row-divider">
+          {CHANNEL_ICONS[channelSpec.type] && (
+            <Icon
+              className="mr1 text-grey-2"
+              name={CHANNEL_ICONS[channelSpec.type]}
+              size={28}
+            />
+          )}
+          <h2>{channelSpec.name}</h2>
+          <Toggle
+            className="flex-align-right"
+            value={channels.length > 0}
+            onChange={this.toggleChannel.bind(this, channelSpec.type)}
+          />
+        </div>
+        {channels.length > 0 && channelSpec.configured ? (
+          <ul className="bg-grey-0 px3">{channels}</ul>
+        ) : channels.length > 0 && !channelSpec.configured ? (
+          <div className="p4 text-centered">
+            <h3 className="mb2">{t`${
+              channelSpec.name
+            } needs to be set up by an administrator.`}</h3>
+            <ChannelSetupMessage user={user} channels={[channelSpec.name]} />
+          </div>
+        ) : null}
+      </li>
+    );
+  }
+
+  render() {
+    let { formInput } = this.props;
+    // Default to show the default channels until full formInput is loaded
+    let channels = formInput.channels || {
+      email: { name: t`Email`, type: "email" },
+      slack: { name: t`Slack`, type: "slack" },
+    };
+    return (
+      <ul className="bordered rounded">
+        {Object.values(channels).map(channelSpec =>
+          this.renderChannelSection(channelSpec),
+        )}
+      </ul>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/PulseEditName.jsx b/frontend/src/metabase/pulse/components/PulseEditName.jsx
index bf6192cff8f2d5f30b4d953d7874a473da1fa17e..3bf984dfd32e2330a1b4b789aae3d41f321a252d 100644
--- a/frontend/src/metabase/pulse/components/PulseEditName.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditName.jsx
@@ -1,52 +1,56 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import _ from "underscore";
 import cx from "classnames";
 
 export default class PulseEditName extends Component {
-    constructor(props) {
-        super(props);
-
-        this.state = {
-            valid: true
-        };
-
-        _.bindAll(this, "setName", "validate");
-    }
-
-    static propTypes = {};
-    static defaultProps = {};
-
-    setName(e) {
-        let { pulse } = this.props;
-        this.props.setPulse({ ...pulse, name: e.target.value });
-    }
-
-    validate() {
-        this.setState({ valid: !!ReactDOM.findDOMNode(this.refs.name).value });
-    }
-
-    render() {
-        let { pulse } = this.props;
-        return (
-            <div className="py1">
-                <h2>{t`Name your pulse`}</h2>
-                <p className="mt1 h4 text-bold text-grey-3">{t`Give your pulse a name to help others understand what it's about`}.</p>
-                <div className="my3">
-                    <input
-                        ref="name"
-                        className={cx("input text-bold", { "border-error": !this.state.valid })}
-                        style={{"width":"400px"}}
-                        value={pulse.name || ""}
-                        onChange={this.setName}
-                        onBlur={this.refs.name && this.validate}
-                        placeholder={t`Important metrics`}
-                        autoFocus
-                    />
-                </div>
-            </div>
-        );
-    }
+  constructor(props) {
+    super(props);
+
+    this.state = {
+      valid: true,
+    };
+
+    _.bindAll(this, "setName", "validate");
+  }
+
+  static propTypes = {};
+  static defaultProps = {};
+
+  setName(e) {
+    let { pulse } = this.props;
+    this.props.setPulse({ ...pulse, name: e.target.value });
+  }
+
+  validate() {
+    this.setState({ valid: !!ReactDOM.findDOMNode(this.refs.name).value });
+  }
+
+  render() {
+    let { pulse } = this.props;
+    return (
+      <div className="py1">
+        <h2>{t`Name your pulse`}</h2>
+        <p className="mt1 h4 text-bold text-grey-3">
+          {t`Give your pulse a name to help others understand what it's about`}.
+        </p>
+        <div className="my3">
+          <input
+            ref="name"
+            className={cx("input text-bold", {
+              "border-error": !this.state.valid,
+            })}
+            style={{ width: "400px" }}
+            value={pulse.name || ""}
+            onChange={this.setName}
+            onBlur={this.refs.name && this.validate}
+            placeholder={t`Important metrics`}
+            autoFocus
+          />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
index a60386d0658c014b085a772b10d8d2011c2b7426..32e29bf58225ad1fd9d0f75e361b6911a87bf6fe 100644
--- a/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
+++ b/frontend/src/metabase/pulse/components/PulseEditSkip.jsx
@@ -1,30 +1,32 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import Toggle from "metabase/components/Toggle.jsx";
 
 export default class PulseEditSkip extends Component {
-    static propTypes = {
-        pulse: PropTypes.object.isRequired,
-        setPulse: PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    pulse: PropTypes.object.isRequired,
+    setPulse: PropTypes.func.isRequired,
+  };
 
-    toggle = () => {
-        const { pulse, setPulse } = this.props;
-        setPulse({ ...pulse, skip_if_empty: !pulse.skip_if_empty });
-    }
+  toggle = () => {
+    const { pulse, setPulse } = this.props;
+    setPulse({ ...pulse, skip_if_empty: !pulse.skip_if_empty });
+  };
 
-    render() {
-        const { pulse } = this.props;
-        return (
-            <div className="py1">
-                <h2>{t`Skip if no results`}</h2>
-                <p className="mt1 h4 text-bold text-grey-3">{t`Skip a scheduled Pulse if none of its questions have any results`}.</p>
-                <div className="my3">
-                    <Toggle value={pulse.skip_if_empty || false} onChange={this.toggle} />
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const { pulse } = this.props;
+    return (
+      <div className="py1">
+        <h2>{t`Skip if no results`}</h2>
+        <p className="mt1 h4 text-bold text-grey-3">
+          {t`Skip a scheduled Pulse if none of its questions have any results`}.
+        </p>
+        <div className="my3">
+          <Toggle value={pulse.skip_if_empty || false} onChange={this.toggle} />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/PulseList.jsx b/frontend/src/metabase/pulse/components/PulseList.jsx
index 41f9f0d2fa397c9f090b53ce8ad47d0f21f558b0..dc96c270241f70470c2b7c892cc9072a2100e54a 100644
--- a/frontend/src/metabase/pulse/components/PulseList.jsx
+++ b/frontend/src/metabase/pulse/components/PulseList.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import PulseListItem from "./PulseListItem.jsx";
 import WhatsAPulse from "./WhatsAPulse.jsx";
@@ -11,74 +11,87 @@ import Modal from "metabase/components/Modal.jsx";
 import _ from "underscore";
 
 export default class PulseList extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            showSetupModal: false
-        };
+    this.state = {
+      showSetupModal: false,
+    };
 
-        _.bindAll(this, "create");
-    }
+    _.bindAll(this, "create");
+  }
 
-    static propTypes = {};
-    static defaultProps = {};
+  static propTypes = {};
+  static defaultProps = {};
 
-    componentDidMount() {
-        this.props.fetchPulses();
-        this.props.fetchPulseFormInput();
-    }
+  componentDidMount() {
+    this.props.fetchPulses();
+    this.props.fetchPulseFormInput();
+  }
 
-    create() {
-        if (this.props.hasConfiguredAnyChannel) {
-            this.props.onChangeLocation("/pulse/create");
-        } else {
-            this.setState({ showSetupModal: true });
-        }
+  create() {
+    if (this.props.hasConfiguredAnyChannel) {
+      this.props.onChangeLocation("/pulse/create");
+    } else {
+      this.setState({ showSetupModal: true });
     }
+  }
 
-    render() {
-        let { pulses, user } = this.props;
-        return (
-            <div className="PulseList pt3">
-                <div className="border-bottom mb2">
-                    <div className="wrapper wrapper--trim flex align-center mb2">
-                        <h1>{t`Pulses`}</h1>
-                        <a onClick={this.create} className="PulseButton Button flex-align-right">{t`Create a pulse`}</a>
-                    </div>
-                </div>
-                <LoadingAndErrorWrapper loading={!pulses}>
-                { () => pulses.length > 0 ?
-                    <ul className="wrapper wrapper--trim">
-                        {pulses.slice().sort((a,b) => b.created_at - a.created_at).map(pulse =>
-                            <li key={pulse.id}>
-                                <PulseListItem
-                                    scrollTo={pulse.id === this.props.pulseId}
-                                    pulse={pulse}
-                                    user={user}
-                                    formInput={this.props.formInput}
-                                    savePulse={this.props.savePulse}
-                                />
-                            </li>
-                        )}
-                    </ul>
-                :
-                    <div className="mt4 ml-auto mr-auto">
-                        <WhatsAPulse
-                            button={<a onClick={this.create} className="Button Button--primary">{t`Create a pulse`}</a>}
-                        />
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-                <Modal isOpen={this.state.showSetupModal}>
-                    <ChannelSetupModal
+  render() {
+    let { pulses, user } = this.props;
+    return (
+      <div className="PulseList pt3">
+        <div className="border-bottom mb2">
+          <div className="wrapper wrapper--trim flex align-center mb2">
+            <h1>{t`Pulses`}</h1>
+            <a
+              onClick={this.create}
+              className="PulseButton Button flex-align-right"
+            >{t`Create a pulse`}</a>
+          </div>
+        </div>
+        <LoadingAndErrorWrapper loading={!pulses}>
+          {() =>
+            pulses.length > 0 ? (
+              <ul className="wrapper wrapper--trim">
+                {pulses
+                  .slice()
+                  .sort((a, b) => b.created_at - a.created_at)
+                  .map(pulse => (
+                    <li key={pulse.id}>
+                      <PulseListItem
+                        scrollTo={pulse.id === this.props.pulseId}
+                        pulse={pulse}
                         user={user}
-                        onClose={() => this.setState({ showSetupModal: false })}
-                        onChangeLocation={this.props.onChangeLocation}
-                        entityNamePlural={t`pulses`}
-                    />
-                </Modal>
-            </div>
-        );
-    }
+                        formInput={this.props.formInput}
+                        savePulse={this.props.savePulse}
+                      />
+                    </li>
+                  ))}
+              </ul>
+            ) : (
+              <div className="mt4 ml-auto mr-auto">
+                <WhatsAPulse
+                  button={
+                    <a
+                      onClick={this.create}
+                      className="Button Button--primary"
+                    >{t`Create a pulse`}</a>
+                  }
+                />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+        <Modal isOpen={this.state.showSetupModal}>
+          <ChannelSetupModal
+            user={user}
+            onClose={() => this.setState({ showSetupModal: false })}
+            onChangeLocation={this.props.onChangeLocation}
+            entityNamePlural={t`pulses`}
+          />
+        </Modal>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/PulseListChannel.jsx b/frontend/src/metabase/pulse/components/PulseListChannel.jsx
index 3bba5509bb2b06664ca61be0d01dc2890bf456fd..d6ac236358b2e71f6082604157257a63b8223c07 100644
--- a/frontend/src/metabase/pulse/components/PulseListChannel.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListChannel.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import Icon from "metabase/components/Icon.jsx";
 
@@ -9,97 +9,121 @@ import { inflect } from "inflection";
 import _ from "underscore";
 
 export default class PulseListChannel extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        _.bindAll(this, "subscribe", "unsubscribe");
-    }
+    _.bindAll(this, "subscribe", "unsubscribe");
+  }
 
-    static propTypes = {
-        pulse: PropTypes.object.isRequired,
-        channel: PropTypes.object.isRequired,
-        channelSpec: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        savePulse: PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    pulse: PropTypes.object.isRequired,
+    channel: PropTypes.object.isRequired,
+    channelSpec: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    savePulse: PropTypes.func.isRequired,
+  };
 
-    subscribe() {
-        let { pulse, channel, user } = this.props;
-        this.props.savePulse({
-            ...pulse,
-            channels: pulse.channels.map(c => c !== channel ? c :
-                { ...c, recipients: [...c.recipients, user]}
-            )
-        });
-    }
+  subscribe() {
+    let { pulse, channel, user } = this.props;
+    this.props.savePulse({
+      ...pulse,
+      channels: pulse.channels.map(
+        c =>
+          c !== channel ? c : { ...c, recipients: [...c.recipients, user] },
+      ),
+    });
+  }
 
-    unsubscribe() {
-        let { pulse, channel, user } = this.props;
-        this.props.savePulse({
-            ...pulse,
-            channels: pulse.channels.map(c => c !== channel ? c :
-                { ...c, recipients: c.recipients.filter(r => r.id !== user.id)}
-            )
-        });
-    }
+  unsubscribe() {
+    let { pulse, channel, user } = this.props;
+    this.props.savePulse({
+      ...pulse,
+      channels: pulse.channels.map(
+        c =>
+          c !== channel
+            ? c
+            : { ...c, recipients: c.recipients.filter(r => r.id !== user.id) },
+      ),
+    });
+  }
 
-    renderChannelSchedule() {
-        let { channel, channelSpec } = this.props;
+  renderChannelSchedule() {
+    let { channel, channelSpec } = this.props;
 
-        let channelIcon = null;
-        let channelVerb = channelSpec && channelSpec.displayName || channel.channel_type;
-        let channelSchedule = channel.schedule_type;
-        let channelTarget = channel.recipients && (channel.recipients.length + " " + inflect("people", channel.recipients.length));
+    let channelIcon = null;
+    let channelVerb =
+      (channelSpec && channelSpec.displayName) || channel.channel_type;
+    let channelSchedule = channel.schedule_type;
+    let channelTarget =
+      channel.recipients &&
+      channel.recipients.length +
+        " " +
+        inflect("people", channel.recipients.length);
 
-        if (channel.channel_type === "email") {
-            channelIcon = "mail";
-            channelVerb = t`Emailed`;
-        } else if (channel.channel_type === "slack") {
-            channelIcon = "slack";
-            channelVerb = t`Slack'd`;
-            // Address #5799 where `details` object is missing for some reason
-            channelTarget = channel.details ? channel.details.channel : t`No channel`;
-        }
-
-        return (
-            <div className="h4 text-grey-4 py2 flex align-center">
-                { channelIcon && <Icon className="mr1" name={channelIcon} size={24}/> }
-                <span>
-                    {channelVerb + " "}
-                    <strong>{channelSchedule}</strong>
-                    {channelTarget && <span>{" " + t`to` + " "}<strong>{channelTarget}</strong></span>}
-                </span>
-            </div>
-        );
+    if (channel.channel_type === "email") {
+      channelIcon = "mail";
+      channelVerb = t`Emailed`;
+    } else if (channel.channel_type === "slack") {
+      channelIcon = "slack";
+      channelVerb = t`Slack'd`;
+      // Address #5799 where `details` object is missing for some reason
+      channelTarget = channel.details ? channel.details.channel : t`No channel`;
     }
 
-    render() {
-        let { pulse, channel, channelSpec, user } = this.props;
+    return (
+      <div className="h4 text-grey-4 py2 flex align-center">
+        {channelIcon && <Icon className="mr1" name={channelIcon} size={24} />}
+        <span>
+          {channelVerb + " "}
+          <strong>{channelSchedule}</strong>
+          {channelTarget && (
+            <span>
+              {" " + t`to` + " "}
+              <strong>{channelTarget}</strong>
+            </span>
+          )}
+        </span>
+      </div>
+    );
+  }
+
+  render() {
+    let { pulse, channel, channelSpec, user } = this.props;
 
-        let subscribable = channelSpec && channelSpec.allows_recipients;
-        let subscribed = false;
-        if (subscribable) {
-            subscribed = _.any(channel.recipients, r => r.id === user.id);
-        }
-        return (
-            <div className="py2 flex align-center">
-                { this.renderChannelSchedule() }
-                { subscribable &&
-                    <div className="flex-align-right">
-                        { subscribed ?
-                            <div className="flex align-center rounded bg-green text-white text-bold">
-                                <div className="pl2">{t`You get this ${channel.channel_type}`}</div>
-                                <Icon className="p2 text-grey-1 text-white-hover cursor-pointer" name="close" size={12} onClick={this.unsubscribe}/>
-                            </div>
-                        : !pulse.read_only ?
-                            <div className="flex align-center rounded bordered bg-white text-default text-bold cursor-pointer" onClick={this.subscribe}>
-                                <Icon className="p2" name="add" size={12}/>
-                                <div className="pr2">{t`Get this ${channel.channel_type}`}</div>
-                            </div>
-                        : null }
-                    </div>
-                }
-            </div>
-        );
+    let subscribable = channelSpec && channelSpec.allows_recipients;
+    let subscribed = false;
+    if (subscribable) {
+      subscribed = _.any(channel.recipients, r => r.id === user.id);
     }
+    return (
+      <div className="py2 flex align-center">
+        {this.renderChannelSchedule()}
+        {subscribable && (
+          <div className="flex-align-right">
+            {subscribed ? (
+              <div className="flex align-center rounded bg-green text-white text-bold">
+                <div className="pl2">{t`You get this ${
+                  channel.channel_type
+                }`}</div>
+                <Icon
+                  className="p2 text-grey-1 text-white-hover cursor-pointer"
+                  name="close"
+                  size={12}
+                  onClick={this.unsubscribe}
+                />
+              </div>
+            ) : !pulse.read_only ? (
+              <div
+                className="flex align-center rounded bordered bg-white text-default text-bold cursor-pointer"
+                onClick={this.subscribe}
+              >
+                <Icon className="p2" name="add" size={12} />
+                <div className="pr2">{t`Get this ${channel.channel_type}`}</div>
+              </div>
+            ) : null}
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/PulseListItem.jsx b/frontend/src/metabase/pulse/components/PulseListItem.jsx
index 26419370574727a9c6beff2c163a377246b245d6..ecfa87c83598188c952ac2bd11361266ae785f4f 100644
--- a/frontend/src/metabase/pulse/components/PulseListItem.jsx
+++ b/frontend/src/metabase/pulse/components/PulseListItem.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { Link } from "react-router";
-import { jt, t } from 'c-3po';
+import { jt, t } from "c-3po";
 
 import cx from "classnames";
 
@@ -11,68 +11,79 @@ import * as Urls from "metabase/lib/urls";
 import PulseListChannel from "./PulseListChannel.jsx";
 
 export default class PulseListItem extends Component {
-    static propTypes = {
-        pulse: PropTypes.object.isRequired,
-        formInput: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        scrollTo: PropTypes.bool.isRequired,
-        savePulse: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    pulse: PropTypes.object.isRequired,
+    formInput: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    scrollTo: PropTypes.bool.isRequired,
+    savePulse: PropTypes.func.isRequired
+  };
 
-    componentDidMount() {
-        if (this.props.scrollTo) {
-            const element = ReactDOM.findDOMNode(this.refs.pulseListItem);
-            element.scrollIntoView(true);
-        }
+  componentDidMount() {
+    if (this.props.scrollTo) {
+      const element = ReactDOM.findDOMNode(this.refs.pulseListItem);
+      element.scrollIntoView(true);
     }
+  }
 
-    render() {
-        let { pulse, formInput, user } = this.props;
+  render() {
+    let { pulse, formInput, user } = this.props;
 
-        const creator = <span className="text-bold">{pulse.creator && pulse.creator.common_name}</span>;
-        return (
-            <div ref="pulseListItem" className={cx("PulseListItem bordered rounded mb2 pt3", {"PulseListItem--focused": this.props.scrollTo})}>
-                <div className="px4 mb2">
-                    <div className="flex align-center mb1">
-                        <h2 className="break-word" style={{ maxWidth: '80%' }}>
-                            {pulse.name}
-                        </h2>
-                        { !pulse.read_only &&
-                            <div className="ml-auto">
-                                <Link
-                                    to={"/pulse/" + pulse.id}
-                                    className="PulseEditButton PulseButton Button no-decoration text-bold"
-                                >
-                                    {t`Edit`}
-                                </Link>
-                            </div>
-                        }
-                    </div>
-                    <span>{jt`Pulse by ${creator}`}</span>
-                </div>
-                <ol className="mb2 px4 flex flex-wrap">
-                    { pulse.cards.map((card, index) =>
-                        <li key={index} className="mr1 mb1">
-                            <Link to={Urls.question(card.id)} className="Button">
-                                {card.name}
-                            </Link>
-                        </li>
-                    )}
-                </ol>
-                <ul className="border-top px4 bg-grey-0">
-                    {pulse.channels.filter(channel => channel.enabled).map(channel =>
-                        <li key={channel.id} className="border-row-divider">
-                            <PulseListChannel
-                                pulse={pulse}
-                                channel={channel}
-                                channelSpec={formInput.channels && formInput.channels[channel.channel_type]}
-                                user={user}
-                                savePulse={this.props.savePulse}
-                            />
-                        </li>
-                    )}
-                </ul>
-            </div>
-        );
-    }
+    const creator = (
+      <span className="text-bold">
+        {pulse.creator && pulse.creator.common_name}
+      </span>
+    );
+    return (
+      <div
+        ref="pulseListItem"
+        className={cx("PulseListItem bordered rounded mb2 pt3", {
+          "PulseListItem--focused": this.props.scrollTo
+        })}
+      >
+        <div className="px4 mb2">
+          <div className="flex align-center mb1">
+            <h2 className="break-word" style={{ maxWidth: "80%" }}>
+              {pulse.name}
+            </h2>
+            {!pulse.read_only && (
+              <div className="ml-auto">
+                <Link
+                  to={"/pulse/" + pulse.id}
+                  className="PulseEditButton PulseButton Button no-decoration text-bold"
+                >
+                  {t`Edit`}
+                </Link>
+              </div>
+            )}
+          </div>
+          <span>{jt`Pulse by ${creator}`}</span>
+        </div>
+        <ol className="mb2 px4 flex flex-wrap">
+          {pulse.cards.map((card, index) => (
+            <li key={index} className="mr1 mb1">
+              <Link to={Urls.question(card.id)} className="Button">
+                {card.name}
+              </Link>
+            </li>
+          ))}
+        </ol>
+        <ul className="border-top px4 bg-grey-0">
+          {pulse.channels.filter(channel => channel.enabled).map(channel => (
+            <li key={channel.id} className="border-row-divider">
+              <PulseListChannel
+                pulse={pulse}
+                channel={channel}
+                channelSpec={
+                  formInput.channels && formInput.channels[channel.channel_type]
+                }
+                user={user}
+                savePulse={this.props.savePulse}
+              />
+            </li>
+          ))}
+        </ul>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/RecipientPicker.jsx b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
index dbe0c34567132be41e20ff4fea5d6828384a9bfb..904cfe789c641ac48c6d48f7e662f9003b0b57a8 100644
--- a/frontend/src/metabase/pulse/components/RecipientPicker.jsx
+++ b/frontend/src/metabase/pulse/components/RecipientPicker.jsx
@@ -1,271 +1,90 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { findDOMNode } from "react-dom";
-import _ from "underscore";
-import cx from "classnames";
 import { t } from "c-3po";
 
-import OnClickOutsideWrapper from 'metabase/components/OnClickOutsideWrapper';
-import Icon from "metabase/components/Icon";
-import Input from "metabase/components/Input";
-import Popover from "metabase/components/Popover";
+import TokenField from "metabase/components/TokenField";
 import UserAvatar from "metabase/components/UserAvatar";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-import {
-    KEYCODE_ESCAPE,
-    KEYCODE_ENTER,
-    KEYCODE_COMMA,
-    KEYCODE_TAB,
-    KEYCODE_UP,
-    KEYCODE_DOWN,
-    KEYCODE_BACKSPACE
-} from "metabase/lib/keyboard";
-
-
 const VALID_EMAIL_REGEX = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
 
 export default class RecipientPicker extends Component {
-    constructor(props) {
-        super(props);
-
-        this.state = {
-            inputValue: "",
-            filteredUsers: [],
-            selectedUserID: null,
-            focused: props.autoFocus && props.recipients.length === 0
-        };
-    }
-
-    // TODO: use recipientTypes to limit the type of recipient that can be added
-
-    static propTypes = {
-        recipients: PropTypes.array,
-        recipientTypes: PropTypes.array.isRequired,
-        users: PropTypes.array,
-        isNewPulse: PropTypes.bool.isRequired,
-        onRecipientsChange: PropTypes.func.isRequired,
-        autoFocus: PropTypes.bool,
-    };
-
-    static defaultProps = {
-        recipientTypes: ["user", "email"],
-        autoFocus: true
-    };
-
-    setInputValue(inputValue) {
-        const { users, recipients } = this.props;
-        const searchString = inputValue.toLowerCase()
-
-        let { selectedUserID } = this.state;
-        let filteredUsers = [];
-
-
-        let recipientsById = {};
-        for (let recipient of recipients) {
-            if (recipient.id != null) {
-                recipientsById[recipient.id] = recipient;
-            }
-        }
-
-
-        if (inputValue) {
-            // case insensitive search of name or email
-            filteredUsers = users.filter(user =>
-                // filter out users who have already been selected
-                !(user.id in recipientsById) &&
-                (
-                    user.common_name.toLowerCase().indexOf(searchString) >= 0 ||
-                    user.email.toLowerCase().indexOf(searchString) >= 0
-                )
-            );
-        }
-
-
-        if (selectedUserID == null || !_.find(filteredUsers, (user) => user.id === selectedUserID)) {
-            // if there are results based on the user's typing...
-            if (filteredUsers.length > 0) {
-                // select the first user in the list and set the ID to that
-                selectedUserID = filteredUsers[0].id;
-            } else {
-                selectedUserID = null;
-            }
-        }
-
-        this.setState({
-            inputValue,
-            filteredUsers,
-            selectedUserID
-        });
-    }
-
-    onInputChange = ({ target }) => {
-        this.setInputValue(target.value);
-    }
-
-    // capture events on the input to allow for convenient keyboard shortcuts
-    onInputKeyDown = (event) => {
-        const keyCode = event.keyCode
-
-        const { filteredUsers, selectedUserID } = this.state
-
-        // enter, tab, comma
-        if (keyCode === KEYCODE_ESCAPE || keyCode === KEYCODE_TAB || keyCode === KEYCODE_COMMA || keyCode === KEYCODE_ENTER) {
-            this.addCurrentRecipient();
-        }
-
-        // up arrow
-        else if (event.keyCode === KEYCODE_UP) {
-            event.preventDefault();
-            let index = _.findIndex(filteredUsers, (u) => u.id === selectedUserID);
-            if (index > 0) {
-                this.setState({ selectedUserID: filteredUsers[index - 1].id });
-            }
-        }
-
-        // down arrow
-        else if (keyCode === KEYCODE_DOWN) {
-            event.preventDefault();
-            let index = _.findIndex(filteredUsers, (u) => u.id === selectedUserID);
-            if (index >= 0 && index < filteredUsers.length - 1) {
-                this.setState({ selectedUserID: filteredUsers[index + 1].id });
-            }
-        }
-
-        // backspace
-        else if (keyCode === KEYCODE_BACKSPACE) {
-            let { recipients } = this.props;
-            if (!this.state.inputValue && recipients.length > 0) {
-                this.removeRecipient(recipients[recipients.length - 1])
-            }
-        }
-    }
-
-    onInputFocus = () => {
-        this.setState({ focused: true });
-    }
-
-    onInputBlur = () => {
-        this.addCurrentRecipient();
-        this.setState({ focused: false });
-    }
-
-    onMouseDownCapture = (e) => {
-        let input = findDOMNode(this.refs.input);
-        input.focus();
-        // prevents clicks from blurring input while still allowing text selection:
-        if (input !== e.target) {
-            e.preventDefault();
+  static propTypes = {
+    recipients: PropTypes.array,
+    recipientTypes: PropTypes.array.isRequired,
+    users: PropTypes.array,
+    isNewPulse: PropTypes.bool.isRequired,
+    onRecipientsChange: PropTypes.func.isRequired,
+    autoFocus: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    recipientTypes: ["user", "email"],
+    autoFocus: true,
+  };
+
+  handleOnChange = newRecipients => {
+    this.props.onRecipientsChange(newRecipients);
+    this._trackChange(newRecipients);
+  };
+
+  _trackChange(newRecipients) {
+    const { recipients, isNewPulse } = this.props;
+
+    // kind of hacky way to find the changed recipient
+    const previous = new Set(recipients.map(r => JSON.stringify(r)));
+    const next = new Set(newRecipients.map(r => JSON.stringify(r)));
+    const recipient =
+      [...next].filter(r => !previous.has(r))[0] ||
+      [...previous].filter(r => !next.has(r))[0];
+
+    MetabaseAnalytics.trackEvent(
+      isNewPulse ? "PulseCreate" : "PulseEdit",
+      newRecipients.length > recipients.length
+        ? "AddRecipient"
+        : "RemoveRecipient",
+      recipient && (recipient.id ? "user" : "email"),
+    );
+  }
+
+  render() {
+    const { recipients, users, autoFocus } = this.props;
+    return (
+      <TokenField
+        value={recipients}
+        options={users.map(user => ({ label: user.common_name, value: user }))}
+        onChange={this.handleOnChange}
+        placeholder={
+          recipients.length === 0
+            ? t`Enter email addresses you'd like this data to go to`
+            : null
         }
-    }
-
-    addCurrentRecipient() {
-        let input = findDOMNode(this.refs.input);
-        let user = _.find(this.state.filteredUsers, (u) => u.id === this.state.selectedUserID);
-        if (user) {
-            this.addRecipient(user);
-        } else if (VALID_EMAIL_REGEX.test(input.value)) {
-            this.addRecipient({ email: input.value });
+        autoFocus={autoFocus && recipients.length === 0}
+        multi
+        valueRenderer={value => value.common_name || value.email}
+        optionRenderer={option => (
+          <div className="flex align-center">
+            <span className="text-white">
+              <UserAvatar user={option.value} />
+            </span>
+            <span className="ml1 h4">{option.value.common_name}</span>
+          </div>
+        )}
+        filterOption={(option, filterString) =>
+          // case insensitive search of name or email
+          ~option.value.common_name
+            .toLowerCase()
+            .indexOf(filterString.toLowerCase()) ||
+          ~option.value.email.toLowerCase().indexOf(filterString.toLowerCase())
         }
-    }
-
-    addRecipient = (recipient) => {
-        const { recipients } = this.props
-
-        // recipient is a user object, or plain object containing "email" key
-        this.props.onRecipientsChange(
-            // return the list of recipients with the new user added
-            recipients.concat(recipient)
-        );
-        // reset the input value
-        this.setInputValue("");
-
-        MetabaseAnalytics.trackEvent(
-            (this.props.isNewPulse) ? "PulseCreate" : "PulseEdit",
-            "AddRecipient",
-            (recipient.id) ? "user" : "email"
-        );
-    }
-
-    removeRecipient(recipient) {
-        const { recipients, onRecipientsChange } = this.props
-        onRecipientsChange(
-            recipients.filter(r =>
-                recipient.id != null
-                    ? recipient.id !== r.id
-                    : recipient.email !== r.email
-            )
-        );
-
-        MetabaseAnalytics.trackEvent(
-            (this.props.isNewPulse) ? "PulseCreate" : "PulseEdit",
-            "RemoveRecipient",
-            (recipient.id) ? "user" : "email"
-        );
-    }
-
-    render() {
-        const { filteredUsers, inputValue, focused, selectedUserID } = this.state;
-        const { recipients } = this.props;
-
-        return (
-            <OnClickOutsideWrapper handleDismissal={() => {
-                this.setState({ focused: false });
-            }}>
-                <ul className={cx("px1 pb1 bordered rounded flex flex-wrap bg-white", { "input--focus": this.state.focused })} onMouseDownCapture={this.onMouseDownCapture}>
-                    {recipients.map((recipient, index) =>
-                        <li key={recipient.id} className="mr1 py1 pl1 mt1 rounded bg-grey-1">
-                            <span className="h4 text-bold">{recipient.common_name || recipient.email}</span>
-                            <a
-                                className="text-grey-2 text-grey-4-hover px1"
-                                onClick={() => this.removeRecipient(recipient)}
-                            >
-                                <Icon name="close" className="" size={12} />
-                            </a>
-                        </li>
-                    )}
-                    <li className="flex-full mr1 py1 pl1 mt1 bg-white" style={{ "minWidth": " 100px" }}>
-                        <Input
-                            ref="input"
-                            className="full h4 text-bold text-default no-focus borderless"
-                            placeholder={recipients.length === 0 ? t`Enter email addresses you'd like this data to go to` : null}
-                            value={inputValue}
-                            autoFocus={focused}
-                            onKeyDown={this.onInputKeyDown}
-                            onChange={this.onInputChange}
-                            onFocus={this.onInputFocus}
-                            onBlurChange={this.onInputBlur}
-                        />
-                        <Popover
-                            isOpen={filteredUsers.length > 0}
-                            hasArrow={false}
-                            tetherOptions={{
-                                attachment: "top left",
-                                targetAttachment: "bottom left",
-                                targetOffset: "10 0"
-                            }}
-                        >
-                            <ul className="py1">
-                                {filteredUsers.map(user =>
-                                    <li
-                                        key={user.id}
-                                        className={cx(
-                                            "py1 px2 flex align-center text-bold bg-brand-hover text-white-hover", {
-                                            "bg-grey-1": user.id === selectedUserID
-                                        })}
-                                        onClick={() => this.addRecipient(user)}
-                                    >
-                                        <span className="text-white"><UserAvatar user={user} /></span>
-                                        <span className="ml1 h4">{user.common_name}</span>
-                                    </li>
-                                )}
-                            </ul>
-                        </Popover>
-                    </li>
-                </ul>
-            </OnClickOutsideWrapper>
-        );
-    }
+        parseFreeformValue={inputValue => {
+          if (VALID_EMAIL_REGEX.test(inputValue)) {
+            return { email: inputValue };
+          }
+        }}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
index d0f934b92624b4022c15432069a242cb23677b50..c8e76f0c8cccd99d2ae58e3f5f102dcda798f659 100644
--- a/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
+++ b/frontend/src/metabase/pulse/components/WhatsAPulse.jsx
@@ -1,32 +1,35 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import RetinaImage from "react-retina-image";
 
 export default class WhatsAPulse extends Component {
-    static propTypes = {
-        button: PropTypes.object
-    };
-    render() {
-        return (
-            <div className="flex flex-column align-center px4">
-                <h2 className="my4 text-brand">
-                    {t`Help everyone on your team stay in sync with your data.`}
-                </h2>
-                <div className="mx4">
-                    <RetinaImage
-                        width={574}
-                        src="app/assets/img/pulse_empty_illustration.png"
-                        forceOriginalDimensions={false}
-                    />
-                </div>
-                <div className="h3 my3 text-centered text-grey-2 text-bold" style={{maxWidth: "500px"}}>
-                    {t`Pulses let you send data from Metabase to email or Slack on the schedule of your choice.`}
-                </div>
-                {this.props.button}
-            </div>
-        );
-    }
+  static propTypes = {
+    button: PropTypes.object,
+  };
+  render() {
+    return (
+      <div className="flex flex-column align-center px4">
+        <h2 className="my4 text-brand">
+          {t`Help everyone on your team stay in sync with your data.`}
+        </h2>
+        <div className="mx4">
+          <RetinaImage
+            width={574}
+            src="app/assets/img/pulse_empty_illustration.png"
+            forceOriginalDimensions={false}
+          />
+        </div>
+        <div
+          className="h3 my3 text-centered text-grey-2 text-bold"
+          style={{ maxWidth: "500px" }}
+        >
+          {t`Pulses let you send data from Metabase to email or Slack on the schedule of your choice.`}
+        </div>
+        {this.props.button}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/pulse/containers/PulseEditApp.jsx b/frontend/src/metabase/pulse/containers/PulseEditApp.jsx
index cc54cf9df4699f98a7e7586a0b3c3623b63fc692..50fb70f53da04fb1ab1399da36fe8f42c682c522 100644
--- a/frontend/src/metabase/pulse/containers/PulseEditApp.jsx
+++ b/frontend/src/metabase/pulse/containers/PulseEditApp.jsx
@@ -9,43 +9,41 @@ import PulseEdit from "../components/PulseEdit.jsx";
 
 import { editPulseSelectors } from "../selectors";
 import {
-    setEditingPulse,
-    updateEditingPulse,
-    saveEditingPulse,
-    deletePulse,
-    fetchCards,
-    fetchUsers,
-    fetchPulseFormInput,
-    fetchPulseCardPreview,
-    testPulse,
+  setEditingPulse,
+  updateEditingPulse,
+  saveEditingPulse,
+  deletePulse,
+  fetchCards,
+  fetchUsers,
+  fetchPulseFormInput,
+  fetchPulseCardPreview,
+  testPulse,
 } from "../actions";
 
 const mapStateToProps = (state, props) => {
-    return {
-        ...editPulseSelectors(state, props),
-        user: state.currentUser
-    }
-}
+  return {
+    ...editPulseSelectors(state, props),
+    user: state.currentUser,
+  };
+};
 
 const mapDispatchToProps = {
-    setEditingPulse,
-    updateEditingPulse,
-    saveEditingPulse,
-    deletePulse,
-    fetchCards,
-    fetchUsers,
-    fetchPulseFormInput,
-    fetchPulseCardPreview,
-    testPulse,
-    onChangeLocation: push
+  setEditingPulse,
+  updateEditingPulse,
+  saveEditingPulse,
+  deletePulse,
+  fetchCards,
+  fetchUsers,
+  fetchPulseFormInput,
+  fetchPulseCardPreview,
+  testPulse,
+  onChangeLocation: push,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 @title(({ pulse }) => pulse && pulse.name)
 export default class PulseEditApp extends Component {
-    render() {
-        return (
-            <PulseEdit { ...this.props } />
-        );
-    }
+  render() {
+    return <PulseEdit {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/pulse/containers/PulseListApp.jsx b/frontend/src/metabase/pulse/containers/PulseListApp.jsx
index 239841391e1608daede3a89807d13a8d14806af7..38f2171a1de0073337623197bd4f5a934327a4e4 100644
--- a/frontend/src/metabase/pulse/containers/PulseListApp.jsx
+++ b/frontend/src/metabase/pulse/containers/PulseListApp.jsx
@@ -6,29 +6,26 @@ import { push } from "react-router-redux";
 import PulseList from "../components/PulseList.jsx";
 import { listPulseSelectors } from "../selectors";
 
-
 import { fetchPulses, fetchPulseFormInput, savePulse } from "../actions";
 
 const mapStateToProps = (state, props) => {
-    return {
-        ...listPulseSelectors(state, props),
-        user: state.currentUser,
-        // onChangeLocation: onChangeLocation
-    }
-}
+  return {
+    ...listPulseSelectors(state, props),
+    user: state.currentUser,
+    // onChangeLocation: onChangeLocation
+  };
+};
 
 const mapDispatchToProps = {
-    fetchPulses,
-    fetchPulseFormInput,
-    savePulse,
-    onChangeLocation: push
+  fetchPulses,
+  fetchPulseFormInput,
+  savePulse,
+  onChangeLocation: push,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class PulseListApp extends Component {
-    render() {
-        return (
-            <PulseList { ...this.props } />
-        );
-    }
+  render() {
+    return <PulseList {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/pulse/reducers.js b/frontend/src/metabase/pulse/reducers.js
index b569b23a136ffea34b1d8e8a374d855247680df7..9aedf0bf0b2cdf0dbcc38cc2dd2c4c07446b04fd 100644
--- a/frontend/src/metabase/pulse/reducers.js
+++ b/frontend/src/metabase/pulse/reducers.js
@@ -1,55 +1,104 @@
+import { handleActions } from "redux-actions";
 
-import { handleActions } from 'redux-actions';
-
-import { momentifyTimestamps, momentifyObjectsTimestamps } from "metabase/lib/redux";
+import {
+  momentifyTimestamps,
+  momentifyObjectsTimestamps,
+} from "metabase/lib/redux";
 
 import {
-    FETCH_PULSES,
-    SET_EDITING_PULSE,
-    UPDATE_EDITING_PULSE,
-    SAVE_EDITING_PULSE,
-    SAVE_PULSE,
-    FETCH_CARDS,
-    FETCH_USERS,
-    FETCH_PULSE_FORM_INPUT,
-    FETCH_PULSE_CARD_PREVIEW
+  FETCH_PULSES,
+  SET_EDITING_PULSE,
+  UPDATE_EDITING_PULSE,
+  SAVE_EDITING_PULSE,
+  SAVE_PULSE,
+  FETCH_CARDS,
+  FETCH_USERS,
+  FETCH_PULSE_FORM_INPUT,
+  FETCH_PULSE_CARD_PREVIEW,
 } from "./actions";
 
-export const pulses = handleActions({
-    [FETCH_PULSES]:       { next: (state, { payload }) => ({ ...momentifyObjectsTimestamps(payload.entities.pulse) }) },
-    [SAVE_PULSE]:         { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) },
-    [SAVE_EDITING_PULSE]: { next: (state, { payload }) => ({ ...state, [payload.id]: momentifyTimestamps(payload) }) }
-}, {});
+export const pulses = handleActions(
+  {
+    [FETCH_PULSES]: {
+      next: (state, { payload }) => ({
+        ...momentifyObjectsTimestamps(payload.entities.pulse),
+      }),
+    },
+    [SAVE_PULSE]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+    [SAVE_EDITING_PULSE]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.id]: momentifyTimestamps(payload),
+      }),
+    },
+  },
+  {},
+);
 
-export const pulseList = handleActions({
+export const pulseList = handleActions(
+  {
     [FETCH_PULSES]: { next: (state, { payload }) => payload.result },
     // [DELETE_PULSE]: { next: (state, { payload }) => state }
-}, null);
+  },
+  null,
+);
 
-export const editingPulse = handleActions({
-    [SET_EDITING_PULSE]:    { next: (state, { payload }) => payload },
+export const editingPulse = handleActions(
+  {
+    [SET_EDITING_PULSE]: { next: (state, { payload }) => payload },
     [UPDATE_EDITING_PULSE]: { next: (state, { payload }) => payload },
-    [SAVE_EDITING_PULSE]:   { next: (state, { payload }) => payload }
-}, { name: null, cards: [], channels: [] });
-
+    [SAVE_EDITING_PULSE]: { next: (state, { payload }) => payload },
+  },
+  { name: null, cards: [], channels: [] },
+);
 
 // NOTE: duplicated from dashboards/reducers.js
-export const cards = handleActions({
-    [FETCH_CARDS]: { next: (state, { payload }) => ({ ...momentifyObjectsTimestamps(payload.entities.card) }) }
-}, {});
-export const cardList = handleActions({
-    [FETCH_CARDS]: { next: (state, { payload }) => payload.result }
-}, []);
+export const cards = handleActions(
+  {
+    [FETCH_CARDS]: {
+      next: (state, { payload }) => ({
+        ...momentifyObjectsTimestamps(payload.entities.card),
+      }),
+    },
+  },
+  {},
+);
+export const cardList = handleActions(
+  {
+    [FETCH_CARDS]: { next: (state, { payload }) => payload.result },
+  },
+  [],
+);
 
 // NOTE: duplicated from admin/people/reducers.js
-export const users = handleActions({
-    [FETCH_USERS]: { next: (state, { payload }) => ({ ...momentifyObjectsTimestamps(payload.entities.user) }) }
-}, []);
+export const users = handleActions(
+  {
+    [FETCH_USERS]: {
+      next: (state, { payload }) => ({
+        ...momentifyObjectsTimestamps(payload.entities.user),
+      }),
+    },
+  },
+  [],
+);
 
-export const formInput = handleActions({
-    [FETCH_PULSE_FORM_INPUT]: { next: (state, { payload }) => payload }
-}, {});
+export const formInput = handleActions(
+  {
+    [FETCH_PULSE_FORM_INPUT]: { next: (state, { payload }) => payload },
+  },
+  {},
+);
 
-export const cardPreviews = handleActions({
-    [FETCH_PULSE_CARD_PREVIEW]: { next: (state, { payload }) => ({ ...state, [payload.id]: payload })}
-}, {});
+export const cardPreviews = handleActions(
+  {
+    [FETCH_PULSE_CARD_PREVIEW]: {
+      next: (state, { payload }) => ({ ...state, [payload.id]: payload }),
+    },
+  },
+  {},
+);
diff --git a/frontend/src/metabase/pulse/selectors.js b/frontend/src/metabase/pulse/selectors.js
index c63968494f600c5d46fa20a52138680d02c27a7a..6034dcbcf0c4cdaeb71fe2319bd1b6166357d3e7 100644
--- a/frontend/src/metabase/pulse/selectors.js
+++ b/frontend/src/metabase/pulse/selectors.js
@@ -1,58 +1,93 @@
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 import _ from "underscore";
 
 const pulsesSelector = state => state.pulse.pulses;
 const pulseIdListSelector = state => state.pulse.pulseList;
 
 const pulseListSelector = createSelector(
-    [pulseIdListSelector, pulsesSelector],
-    (pulseIdList, pulses) => pulseIdList && pulseIdList.map(id => pulses[id])
+  [pulseIdListSelector, pulsesSelector],
+  (pulseIdList, pulses) => pulseIdList && pulseIdList.map(id => pulses[id]),
 );
 
 const editingPulseSelector = state => state.pulse.editingPulse;
 
-const cardsSelector        = state => state.pulse.cards
-const cardIdListSelector   = state => state.pulse.cardList
+const cardsSelector = state => state.pulse.cards;
+const cardIdListSelector = state => state.pulse.cardList;
 
-const usersSelector        = state => state.pulse.users
+const usersSelector = state => state.pulse.users;
 
-export const formInputSelector    = state => state.pulse.formInput
+export const formInputSelector = state => state.pulse.formInput;
 
 export const hasLoadedChannelInfoSelector = createSelector(
-    [formInputSelector],
-    (formInput) => !!formInput.channels
-)
+  [formInputSelector],
+  formInput => !!formInput.channels,
+);
 export const hasConfiguredAnyChannelSelector = createSelector(
-    [formInputSelector],
-    (formInput) => formInput.channels && _.some(Object.values(formInput.channels), (c) => c.configured) || false
-)
+  [formInputSelector],
+  formInput =>
+    (formInput.channels &&
+      _.some(Object.values(formInput.channels), c => c.configured)) ||
+    false,
+);
 export const hasConfiguredEmailChannelSelector = createSelector(
-    [formInputSelector],
-    (formInput) => formInput.channels && _.some(Object.values(formInput.channels), (c) => c.type === "email" && c.configured) || false
-)
+  [formInputSelector],
+  formInput =>
+    (formInput.channels &&
+      _.some(
+        Object.values(formInput.channels),
+        c => c.type === "email" && c.configured,
+      )) ||
+    false,
+);
 
-const cardPreviewsSelector = state => state.pulse.cardPreviews
+const cardPreviewsSelector = state => state.pulse.cardPreviews;
 
 const cardListSelector = createSelector(
-    [cardIdListSelector, cardsSelector],
-    (cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id])
+  [cardIdListSelector, cardsSelector],
+  (cardIdList, cards) => cardIdList && cardIdList.map(id => cards[id]),
 );
 
-export const userListSelector = createSelector(
-    [usersSelector],
-    (users) => Object.values(users)
+export const userListSelector = createSelector([usersSelector], users =>
+  Object.values(users),
 );
 
-const getPulseId = (state, props) => props.params.pulseId ? parseInt(props.params.pulseId) : null;
+const getPulseId = (state, props) =>
+  props.params.pulseId ? parseInt(props.params.pulseId) : null;
 
 // LIST
 export const listPulseSelectors = createSelector(
-    [getPulseId, pulseListSelector, formInputSelector, hasConfiguredAnyChannelSelector],
-    (pulseId, pulses, formInput, hasConfiguredAnyChannel) => ({ pulseId, pulses, formInput, hasConfiguredAnyChannel })
+  [
+    getPulseId,
+    pulseListSelector,
+    formInputSelector,
+    hasConfiguredAnyChannelSelector,
+  ],
+  (pulseId, pulses, formInput, hasConfiguredAnyChannel) => ({
+    pulseId,
+    pulses,
+    formInput,
+    hasConfiguredAnyChannel,
+  }),
 );
 
 // EDIT
 export const editPulseSelectors = createSelector(
-    [getPulseId, editingPulseSelector, cardsSelector, cardListSelector, cardPreviewsSelector, userListSelector, formInputSelector],
-    (pulseId, pulse, cards, cardList, cardPreviews, userList, formInput) => ({ pulseId, pulse, cards, cardList, cardPreviews, userList, formInput})
+  [
+    getPulseId,
+    editingPulseSelector,
+    cardsSelector,
+    cardListSelector,
+    cardPreviewsSelector,
+    userListSelector,
+    formInputSelector,
+  ],
+  (pulseId, pulse, cards, cardList, cardPreviews, userList, formInput) => ({
+    pulseId,
+    pulse,
+    cards,
+    cardList,
+    cardPreviews,
+    userList,
+    formInput,
+  }),
 );
diff --git a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
index 7af40eee6edc6a720149187a58e9667478d8fc74..f26b4b0f2b7b650108c39ad6aa6135d536dafd71 100644
--- a/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
+++ b/frontend/src/metabase/qb/components/TimeseriesFilterWidget.jsx
@@ -2,8 +2,7 @@
 
 import React, { Component } from "react";
 import { t } from "c-3po";
-import DatePicker
-    from "metabase/query_builder/components/filters/pickers/DatePicker";
+import DatePicker from "metabase/query_builder/components/filters/pickers/DatePicker";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import { SelectButton } from "metabase/components/Select";
 import Button from "metabase/components/Button";
@@ -14,159 +13,148 @@ import * as Field from "metabase/lib/query/field";
 import * as Card from "metabase/meta/Card";
 
 import {
-    parseFieldTarget,
-    parseFieldTargetId,
-    generateTimeFilterValuesDescriptions
+  parseFieldTarget,
+  parseFieldTargetId,
+  generateTimeFilterValuesDescriptions,
 } from "metabase/lib/query_time";
 
 import cx from "classnames";
 import _ from "underscore";
 
 import type {
-    Card as CardObject,
-    StructuredDatasetQuery
+  Card as CardObject,
+  StructuredDatasetQuery,
 } from "metabase/meta/types/Card";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
 import type { FieldFilter } from "metabase/meta/types/Query";
 
 type Props = {
-    className?: string,
-    card: CardObject,
-    tableMetadata: TableMetadata,
-    setDatasetQuery: (
-        datasetQuery: StructuredDatasetQuery,
-        options: { run: boolean }
-    ) => void
+  className?: string,
+  card: CardObject,
+  tableMetadata: TableMetadata,
+  setDatasetQuery: (
+    datasetQuery: StructuredDatasetQuery,
+    options: { run: boolean },
+  ) => void,
 };
 
 type State = {
-    filterIndex: number,
-    filter: FieldFilter,
-    currentFilter: any
+  filterIndex: number,
+  filter: FieldFilter,
+  currentFilter: any,
 };
 
 export default class TimeseriesFilterWidget extends Component {
-    props: Props;
-    state: State = {
-        // $FlowFixMe
-        filter: null,
-        filterIndex: -1,
-        currentFilter: null
-    };
-
-    _popover: ?any;
-
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
+  props: Props;
+  state: State = {
+    // $FlowFixMe
+    filter: null,
+    filterIndex: -1,
+    currentFilter: null,
+  };
+
+  _popover: ?any;
+
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
+
+  componentWillReceiveProps(nextProps: Props) {
+    const query = Card.getQuery(nextProps.card);
+    if (query) {
+      const breakouts = Query.getBreakouts(query);
+      const filters = Query.getFilters(query);
+
+      const timeFieldId = parseFieldTargetId(breakouts[0]);
+      const timeField = parseFieldTarget(breakouts[0]);
+
+      const filterIndex = _.findIndex(
+        filters,
+        filter =>
+          Filter.isFieldFilter(filter) &&
+          Field.getFieldTargetId(filter[1]) === timeFieldId,
+      );
+
+      let filter, currentFilter;
+      if (filterIndex >= 0) {
+        filter = currentFilter = filters[filterIndex];
+      } else {
+        filter = ["time-interval", timeField, -30, "day"];
+      }
+
+      // $FlowFixMe
+      this.setState({ filter, filterIndex, currentFilter });
     }
-
-    componentWillReceiveProps(nextProps: Props) {
-        const query = Card.getQuery(nextProps.card);
-        if (query) {
-            const breakouts = Query.getBreakouts(query);
-            const filters = Query.getFilters(query);
-
-            const timeFieldId = parseFieldTargetId(breakouts[0]);
-            const timeField = parseFieldTarget(breakouts[0]);
-
-            const filterIndex = _.findIndex(
-                filters,
-                filter =>
-                    Filter.isFieldFilter(filter) &&
-                    Field.getFieldTargetId(filter[1]) === timeFieldId
-            );
-
-            let filter, currentFilter;
-            if (filterIndex >= 0) {
-                filter = (currentFilter = filters[filterIndex]);
-            } else {
-                filter = ["time-interval", timeField, -30, "day"];
-            }
-
-            // $FlowFixMe
-            this.setState({ filter, filterIndex, currentFilter });
-        }
+  }
+
+  render() {
+    const { className, card, tableMetadata, setDatasetQuery } = this.props;
+    const { filter, filterIndex, currentFilter } = this.state;
+    let currentDescription;
+
+    if (currentFilter) {
+      currentDescription = generateTimeFilterValuesDescriptions(
+        currentFilter,
+      ).join(" - ");
+      if (currentFilter[0] === ">") {
+        currentDescription = t`After ${currentDescription}`;
+      } else if (currentFilter[0] === "<") {
+        currentDescription = t`Before ${currentDescription}`;
+      } else if (currentFilter[0] === "IS_NULL") {
+        currentDescription = t`Is Empty`;
+      } else if (currentFilter[0] === "NOT_NULL") {
+        currentDescription = t`Not Empty`;
+      }
+    } else {
+      currentDescription = t`All Time`;
     }
 
-    render() {
-        const {
-            className,
-            card,
-            tableMetadata,
-            setDatasetQuery
-        } = this.props;
-        const { filter, filterIndex, currentFilter } = this.state;
-        let currentDescription;
-
-        if (currentFilter) {
-            currentDescription = generateTimeFilterValuesDescriptions(
-                currentFilter
-            ).join(" - ");
-            if (currentFilter[0] === ">") {
-                currentDescription = t`After ${currentDescription}`;
-            } else if (currentFilter[0] === "<") {
-                currentDescription = t`Before ${currentDescription}`;
-            } else if (currentFilter[0] === "IS_NULL") {
-                currentDescription = t`Is Empty`;
-            } else if (currentFilter[0] === "NOT_NULL") {
-                currentDescription = t`Not Empty`;
-            }
-        } else {
-            currentDescription = t`All Time`;
+    return (
+      <PopoverWithTrigger
+        triggerElement={
+          <SelectButton hasValue>{currentDescription}</SelectButton>
         }
-
-        return (
-            <PopoverWithTrigger
-                triggerElement={
-                    <SelectButton hasValue>
-                        {currentDescription}
-                    </SelectButton>
+        triggerClasses={cx(className, "my2")}
+        ref={ref => (this._popover = ref)}
+        sizeToFit
+        // accomodate dual calendar size
+        autoWidth={true}
+      >
+        <DatePicker
+          filter={this.state.filter}
+          onFilterChange={newFilter => {
+            this.setState({ filter: newFilter });
+          }}
+          tableMetadata={tableMetadata}
+          includeAllTime
+        />
+        <div className="p1">
+          <Button
+            purple
+            className="full"
+            onClick={() => {
+              let query = Card.getQuery(card);
+              if (query) {
+                if (filterIndex >= 0) {
+                  query = Query.updateFilter(query, filterIndex, filter);
+                } else {
+                  query = Query.addFilter(query, filter);
                 }
-                triggerClasses={cx(className, "my2")}
-                ref={ref => this._popover = ref}
-                sizeToFit
-                // accomodate dual calendar size
-                autoWidth={true}
-            >
-                <DatePicker
-                    filter={this.state.filter}
-                    onFilterChange={newFilter => {
-                        this.setState({ filter: newFilter });
-                    }}
-                    tableMetadata={tableMetadata}
-                    includeAllTime
-                />
-                <div className="p1">
-                    <Button
-                        purple
-                        className="full"
-                        onClick={() => {
-                            let query = Card.getQuery(card);
-                            if (query) {
-                                if (filterIndex >= 0) {
-                                    query = Query.updateFilter(
-                                        query,
-                                        filterIndex,
-                                        filter
-                                    );
-                                } else {
-                                    query = Query.addFilter(query, filter);
-                                }
-                                const datasetQuery: StructuredDatasetQuery = {
-                                    ...card.dataset_query,
-                                    query
-                                };
-                                setDatasetQuery(datasetQuery, { run: true });
-                            }
-                            if (this._popover) {
-                                this._popover.close();
-                            }
-                        }}
-                    >
-                        Apply
-                    </Button>
-                </div>
-            </PopoverWithTrigger>
-        );
-    }
+                const datasetQuery: StructuredDatasetQuery = {
+                  ...card.dataset_query,
+                  query,
+                };
+                setDatasetQuery(datasetQuery, { run: true });
+              }
+              if (this._popover) {
+                this._popover.close();
+              }
+            }}
+          >
+            Apply
+          </Button>
+        </div>
+      </PopoverWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
index a11dc8d67a9ece8b49a3cfa5dfc780f6abe77240..457bd7584d53da6dfa95768d6eb29283245bd486 100644
--- a/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
+++ b/frontend/src/metabase/qb/components/TimeseriesGroupingWidget.jsx
@@ -2,8 +2,7 @@
 
 import React, { Component } from "react";
 
-import TimeGroupingPopover
-    from "metabase/query_builder/components/TimeGroupingPopover";
+import TimeGroupingPopover from "metabase/query_builder/components/TimeGroupingPopover";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger";
 import { SelectButton } from "metabase/components/Select";
 
@@ -13,80 +12,76 @@ import * as Card from "metabase/meta/Card";
 import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
 
 import type {
-    Card as CardObject,
-    StructuredDatasetQuery
+  Card as CardObject,
+  StructuredDatasetQuery,
 } from "metabase/meta/types/Card";
 
 type Props = {
-    card: CardObject,
-    setDatasetQuery: (
-        datasetQuery: StructuredDatasetQuery,
-        options: { run: boolean }
-    ) => void
+  card: CardObject,
+  setDatasetQuery: (
+    datasetQuery: StructuredDatasetQuery,
+    options: { run: boolean },
+  ) => void,
 };
 
 export default class TimeseriesGroupingWidget extends Component {
-    props: Props;
+  props: Props;
 
-    _popover: ?any;
+  _popover: ?any;
 
-    render() {
-        const { card, setDatasetQuery } = this.props;
+  render() {
+    const { card, setDatasetQuery } = this.props;
 
-        if (Card.isStructured(card)) {
-            const query = Card.getQuery(card);
-            const breakouts = query && Query.getBreakouts(query);
+    if (Card.isStructured(card)) {
+      const query = Card.getQuery(card);
+      const breakouts = query && Query.getBreakouts(query);
 
-            if (!breakouts || breakouts.length === 0) {
-                return null;
-            }
+      if (!breakouts || breakouts.length === 0) {
+        return null;
+      }
 
-            return (
-                <PopoverWithTrigger
-                    triggerElement={
-                        <SelectButton hasValue>
-                            {formatBucketing(parseFieldBucketing(breakouts[0]))}
-                        </SelectButton>
-                    }
-                    triggerClasses="my2"
-                    ref={ref => this._popover = ref}
-                >
-                    <TimeGroupingPopover
-                        className="text-brand"
-                        field={breakouts[0]}
-                        onFieldChange={breakout => {
-                            let query = Card.getQuery(card);
-                            if (query) {
-                                query = Query.updateBreakout(
-                                    query,
-                                    0,
-                                    breakout
-                                );
-                                const datasetQuery: StructuredDatasetQuery = {
-                                    ...card.dataset_query,
-                                    query
-                                };
-                                setDatasetQuery(datasetQuery, { run: true });
-                                if (this._popover) {
-                                    this._popover.close();
-                                }
-                            }
-                        }}
-                        title={null}
-                        groupingOptions={[
-                            "minute",
-                            "hour",
-                            "day",
-                            "week",
-                            "month",
-                            "quarter",
-                            "year"
-                        ]}
-                    />
-                </PopoverWithTrigger>
-            );
-        } else {
-            return null;
-        }
+      return (
+        <PopoverWithTrigger
+          triggerElement={
+            <SelectButton hasValue>
+              {formatBucketing(parseFieldBucketing(breakouts[0]))}
+            </SelectButton>
+          }
+          triggerClasses="my2"
+          ref={ref => (this._popover = ref)}
+        >
+          <TimeGroupingPopover
+            className="text-brand"
+            field={breakouts[0]}
+            onFieldChange={breakout => {
+              let query = Card.getQuery(card);
+              if (query) {
+                query = Query.updateBreakout(query, 0, breakout);
+                const datasetQuery: StructuredDatasetQuery = {
+                  ...card.dataset_query,
+                  query,
+                };
+                setDatasetQuery(datasetQuery, { run: true });
+                if (this._popover) {
+                  this._popover.close();
+                }
+              }
+            }}
+            title={null}
+            groupingOptions={[
+              "minute",
+              "hour",
+              "day",
+              "week",
+              "month",
+              "quarter",
+              "year",
+            ]}
+          />
+        </PopoverWithTrigger>
+      );
+    } else {
+      return null;
     }
+  }
 }
diff --git a/frontend/src/metabase/qb/components/__support__/fixtures.js b/frontend/src/metabase/qb/components/__support__/fixtures.js
index d2ce186ac283ba61806ae3499ae71586274806dd..379266079a20c1714ad886ccd9ce7dc81bb8ec53 100644
--- a/frontend/src/metabase/qb/components/__support__/fixtures.js
+++ b/frontend/src/metabase/qb/components/__support__/fixtures.js
@@ -3,128 +3,128 @@
 import { TYPE } from "metabase/lib/types";
 
 const FLOAT_FIELD = {
-    id: 1,
-    display_name: "Mock Float Field",
-    base_type: TYPE.Float
+  id: 1,
+  display_name: "Mock Float Field",
+  base_type: TYPE.Float,
 };
 
 const CATEGORY_FIELD = {
-    id: 2,
-    display_name: "Mock Category Field",
-    base_type: TYPE.Text,
-    special_type: TYPE.Category
+  id: 2,
+  display_name: "Mock Category Field",
+  base_type: TYPE.Text,
+  special_type: TYPE.Category,
 };
 
 const DATE_FIELD = {
-    id: 3,
-    display_name: "Mock Date Field",
-    base_type: TYPE.DateTime
+  id: 3,
+  display_name: "Mock Date Field",
+  base_type: TYPE.DateTime,
 };
 
 const PK_FIELD = {
-    id: 4,
-    display_name: "Mock PK Field",
-    base_type: TYPE.Integer,
-    special_type: TYPE.PK
+  id: 4,
+  display_name: "Mock PK Field",
+  base_type: TYPE.Integer,
+  special_type: TYPE.PK,
 };
 
 const foreignTableMetadata = {
-    id: 20,
-    db_id: 100,
-    display_name: "Mock Foreign Table",
-    fields: []
+  id: 20,
+  db_id: 100,
+  display_name: "Mock Foreign Table",
+  fields: [],
 };
 
 const FK_FIELD = {
-    id: 5,
-    display_name: "Mock FK Field",
-    base_type: TYPE.Integer,
-    special_type: TYPE.FK,
-    target: {
-        id: 25,
-        table_id: foreignTableMetadata.id,
-        table: foreignTableMetadata
-    }
+  id: 5,
+  display_name: "Mock FK Field",
+  base_type: TYPE.Integer,
+  special_type: TYPE.FK,
+  target: {
+    id: 25,
+    table_id: foreignTableMetadata.id,
+    table: foreignTableMetadata,
+  },
 };
 
 export const tableMetadata = {
-    id: 10,
-    db_id: 100,
-    display_name: "Mock Table",
-    fields: [FLOAT_FIELD, CATEGORY_FIELD, DATE_FIELD, PK_FIELD, FK_FIELD]
+  id: 10,
+  db_id: 100,
+  display_name: "Mock Table",
+  fields: [FLOAT_FIELD, CATEGORY_FIELD, DATE_FIELD, PK_FIELD, FK_FIELD],
 };
 
 export const card = {
-    dataset_query: {
-        type: "query",
-        query: {
-            source_table: 10
-        }
-    }
+  dataset_query: {
+    type: "query",
+    query: {
+      source_table: 10,
+    },
+  },
 };
 
 export const nativeCard = {
-    dataset_query: {
-        type: "native",
-        native: {
-            query: "SELECT count(*) from ORDERS"
-        }
-    }
+  dataset_query: {
+    type: "native",
+    native: {
+      query: "SELECT count(*) from ORDERS",
+    },
+  },
 };
 
 export const savedCard = {
-    id: 1,
-    dataset_query: {
-        type: "query",
-        query: {
-            source_table: 10
-        }
-    }
+  id: 1,
+  dataset_query: {
+    type: "query",
+    query: {
+      source_table: 10,
+    },
+  },
 };
 export const savedNativeCard = {
-    id: 2,
-    dataset_query: {
-        type: "native",
-        native: {
-            query: "SELECT count(*) from ORDERS"
-        }
-    }
+  id: 2,
+  dataset_query: {
+    type: "native",
+    native: {
+      query: "SELECT count(*) from ORDERS",
+    },
+  },
 };
 
 export const clickedFloatHeader = {
-    column: {
-        ...FLOAT_FIELD,
-        source: "fields"
-    }
+  column: {
+    ...FLOAT_FIELD,
+    source: "fields",
+  },
 };
 
 export const clickedCategoryHeader = {
-    column: {
-        ...CATEGORY_FIELD,
-        source: "fields"
-    }
+  column: {
+    ...CATEGORY_FIELD,
+    source: "fields",
+  },
 };
 
 export const clickedFloatValue = {
-    column: {
-        ...CATEGORY_FIELD,
-        source: "fields"
-    },
-    value: 1234
+  column: {
+    ...CATEGORY_FIELD,
+    source: "fields",
+  },
+  value: 1234,
 };
 
 export const clickedPKValue = {
-    column: {
-        ...PK_FIELD,
-        source: "fields"
-    },
-    value: 42
+  column: {
+    ...PK_FIELD,
+    source: "fields",
+  },
+  value: 42,
 };
 
 export const clickedFKValue = {
-    column: {
-        ...FK_FIELD,
-        source: "fields"
-    },
-    value: 43
+  column: {
+    ...FK_FIELD,
+    source: "fields",
+  },
+  value: 43,
 };
diff --git a/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx b/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx
index ff70068d6ef28cda14488291b414f26a3961a40b..5d8eff61abe94f905c6aa637b93d49c784f582c7 100644
--- a/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/CommonMetricsAction.jsx
@@ -5,20 +5,20 @@ import { jt } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery)) {
-        return [];
-    }
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-    const activeMetrics = query.table().metrics.filter(m => m.isActive());
-    return activeMetrics.slice(0, 5).map(metric => ({
-        name: "common-metric",
-        title: <span>{jt`View ${<strong>{metric.name}</strong>}`}</span>,
-        question: () => question.summarize(["METRIC", metric.id])
-    }));
+  const activeMetrics = query.table().metrics.filter(m => m.isActive());
+  return activeMetrics.slice(0, 5).map(metric => ({
+    name: "common-metric",
+    title: <span>{jt`View ${<strong>{metric.name}</strong>}`}</span>,
+    question: () => question.summarize(["METRIC", metric.id]),
+  }));
 };
diff --git a/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx b/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx
index 4cbc98cc04f17b0513c012f8e261744e64b0bafe..e8705e1297eefaa5ecc59beb7c6964968d68b52f 100644
--- a/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/CompoundQueryAction.jsx
@@ -1,21 +1,21 @@
 /* @flow */
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 import { t } from "c-3po";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
-    if (question.id()) {
-        return [
-            {
-                name: "nest-query",
-                title: t`Analyze the results of this Query`,
-                icon: "table",
-                question: () => question.composeThisQuery()
-            }
-        ];
-    }
-    return [];
+  if (question.id()) {
+    return [
+      {
+        name: "nest-query",
+        title: t`Analyze the results of this Query`,
+        icon: "table",
+        question: () => question.composeThisQuery(),
+      },
+    ];
+  }
+  return [];
 };
diff --git a/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx b/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx
index 61ef0b22fbb83b38122eb76486153b8e44a2b7b8..02f515460b72a960d30d872c558ce69e3524d986 100644
--- a/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/CountByTimeAction.jsx
@@ -5,38 +5,38 @@ import { t } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 import { isDate } from "metabase/lib/schema_metadata";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery)) {
-        return [];
-    }
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-    const dateField = query.table().fields.filter(isDate)[0];
-    if (!dateField) {
-        return [];
-    }
+  const dateField = query.table().fields.filter(isDate)[0];
+  if (!dateField) {
+    return [];
+  }
 
-    return [
-        {
-            name: "count-by-time",
-            section: "sum",
-            title: <span>{t`Count of rows by time`}</span>,
-            icon: "line",
-            question: () =>
-                question
-                    .summarize(["count"])
-                    .breakout([
-                        "datetime-field",
-                        ["field-id", dateField.id],
-                        "as",
-                        "day"
-                    ])
-        }
-    ];
+  return [
+    {
+      name: "count-by-time",
+      section: "sum",
+      title: <span>{t`Count of rows by time`}</span>,
+      icon: "line",
+      question: () =>
+        question
+          .summarize(["count"])
+          .breakout([
+            "datetime-field",
+            ["field-id", dateField.id],
+            "as",
+            "day",
+          ]),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
index 2fad6e9a44f0e82be638b0382ae6064601e51214..f3d109f238504bcffeae5d2d8100f2633448fb8c 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByAction.jsx
@@ -7,74 +7,72 @@ import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import type { Field } from "metabase/meta/types/Field";
 import type {
-    ClickAction,
-    ClickActionProps,
-    ClickActionPopoverProps
+  ClickAction,
+  ClickActionProps,
+  ClickActionPopoverProps,
 } from "metabase/meta/types/Visualization";
 
 type FieldFilter = (field: Field) => boolean;
 
 // PivotByAction displays a breakout picker, and optionally filters by the
 // clicked dimesion values (and removes corresponding breakouts)
-export default (name: string, icon: string, fieldFilter: FieldFilter) =>
-    ({ question, clicked }: ClickActionProps): ClickAction[] => {
-        const query = question.query();
-        if (!(query instanceof StructuredQuery)) {
-            return [];
-        }
+export default (name: string, icon: string, fieldFilter: FieldFilter) => ({
+  question,
+  clicked,
+}: ClickActionProps): ClickAction[] => {
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-        // $FlowFixMe
-        const tableMetadata: TableMetadata = query.table();
+  // $FlowFixMe
+  const tableMetadata: TableMetadata = query.table();
 
-        // Click target types: metric value
-        if (
-            clicked &&
-            (clicked.value === undefined ||
-                // $FlowFixMe
-                clicked.column.source !== "aggregation")
-        ) {
-            return [];
-        }
+  // Click target types: metric value
+  if (
+    clicked &&
+    (clicked.value === undefined ||
+      // $FlowFixMe
+      clicked.column.source !== "aggregation")
+  ) {
+    return [];
+  }
 
-        let dimensions = (clicked && clicked.dimensions) || [];
+  let dimensions = (clicked && clicked.dimensions) || [];
 
-        const breakoutOptions = query.breakoutOptions(null, fieldFilter);
-        if (breakoutOptions.count === 0) {
-            return [];
-        }
-        return [
-            {
-                name: "pivot-by-" + name.toLowerCase(),
-                section: "breakout",
-                title: clicked
-                    ? name
-                    : <span>
-                          {
-                              jt`Break out by ${<span className="text-dark">
-                                      {name.toLowerCase()}
-                                  </span>}`
-                          }
-                      </span>,
-                icon: icon,
-                // eslint-disable-next-line react/display-name
-                popover: (
-                    { onChangeCardAndRun, onClose }: ClickActionPopoverProps
-                ) => (
-                    <BreakoutPopover
-                        tableMetadata={tableMetadata}
-                        fieldOptions={breakoutOptions}
-                        onCommitBreakout={breakout => {
-                            const nextCard = question
-                                .pivot([breakout], dimensions)
-                                .card();
+  const breakoutOptions = query.breakoutOptions(null, fieldFilter);
+  if (breakoutOptions.count === 0) {
+    return [];
+  }
+  return [
+    {
+      name: "pivot-by-" + name.toLowerCase(),
+      section: "breakout",
+      title: clicked ? (
+        name
+      ) : (
+        <span>
+          {jt`Break out by ${(
+            <span className="text-dark">{name.toLowerCase()}</span>
+          )}`}
+        </span>
+      ),
+      icon: icon,
+      // eslint-disable-next-line react/display-name
+      popover: ({ onChangeCardAndRun, onClose }: ClickActionPopoverProps) => (
+        <BreakoutPopover
+          tableMetadata={tableMetadata}
+          fieldOptions={breakoutOptions}
+          onCommitBreakout={breakout => {
+            const nextCard = question.pivot([breakout], dimensions).card();
 
-                            if (nextCard) {
-                                onChangeCardAndRun({ nextCard });
-                            }
-                        }}
-                        onClose={onClose}
-                    />
-                )
+            if (nextCard) {
+              onChangeCardAndRun({ nextCard });
             }
-        ];
-    };
+          }}
+          onClose={onClose}
+        />
+      ),
+    },
+  ];
+};
diff --git a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
index f2ba6b0a071f91c7b766a070a85c77f04324b5db..dfafc6220ddb2f7d50eb4eca890f608a6d397343 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByCategoryAction.jsx
@@ -6,7 +6,7 @@ import PivotByAction from "./PivotByAction";
 import { t } from "c-3po";
 
 export default PivotByAction(
-    t`Category`,
-    "label",
-    field => isCategory(field) && !isAddress(field)
+  t`Category`,
+  "label",
+  field => isCategory(field) && !isAddress(field),
 );
diff --git a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
index db26fa1cc774bcc755294d0eefe5f4b898875f54..d9ca388621580585ab8f4632de600f52aac81275 100644
--- a/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/PivotByLocationAction.jsx
@@ -6,4 +6,5 @@ import PivotByAction from "./PivotByAction";
 import { t } from "c-3po";
 
 export default PivotByAction(t`Location`, "location", field =>
-    isAddress(field));
+  isAddress(field),
+);
diff --git a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
index 7c4389008172ba1dfe3a3a5cd4b95a097a6e20d2..fafd7e272a1a21f643aba61c36df9f9bb728517a 100644
--- a/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/SummarizeBySegmentMetricAction.jsx
@@ -6,56 +6,50 @@ import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import AggregationPopover from "metabase/qb/components/gui/AggregationPopover";
 
 import type {
-    ClickAction,
-    ClickActionProps,
-    ClickActionPopoverProps
+  ClickAction,
+  ClickActionProps,
+  ClickActionPopoverProps,
 } from "metabase/meta/types/Visualization";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
 
 const omittedAggregations = ["rows", "cum_sum", "cum_count", "stddev"];
 const getAggregationOptionsForSummarize = query => {
-    return query
-        .table()
-        .aggregations()
-        .filter(
-            aggregation => !omittedAggregations.includes(aggregation.short)
-        );
+  return query
+    .table()
+    .aggregations()
+    .filter(aggregation => !omittedAggregations.includes(aggregation.short));
 };
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery)) {
-        return [];
-    }
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-    const tableMetadata: TableMetadata = query.table();
+  const tableMetadata: TableMetadata = query.table();
 
-    return [
-        {
-            name: "summarize",
-            title: t`Summarize this segment`,
-            icon: "sum",
-            // eslint-disable-next-line react/display-name
-            popover: (
-                { onChangeCardAndRun, onClose }: ClickActionPopoverProps
-            ) => (
-                <AggregationPopover
-                    query={query}
-                    tableMetadata={tableMetadata}
-                    customFields={query.expressions()}
-                    availableAggregations={getAggregationOptionsForSummarize(
-                        query
-                    )}
-                    onCommitAggregation={aggregation => {
-                        onChangeCardAndRun({
-                            nextCard: question.summarize(aggregation).card()
-                        });
-                        onClose && onClose();
-                    }}
-                    onClose={onClose}
-                    showOnlyProvidedAggregations
-                />
-            )
-        }
-    ];
+  return [
+    {
+      name: "summarize",
+      title: t`Summarize this segment`,
+      icon: "sum",
+      // eslint-disable-next-line react/display-name
+      popover: ({ onChangeCardAndRun, onClose }: ClickActionPopoverProps) => (
+        <AggregationPopover
+          query={query}
+          tableMetadata={tableMetadata}
+          customFields={query.expressions()}
+          availableAggregations={getAggregationOptionsForSummarize(query)}
+          onCommitAggregation={aggregation => {
+            onChangeCardAndRun({
+              nextCard: question.summarize(aggregation).card(),
+            });
+            onClose && onClose();
+          }}
+          onClose={onClose}
+          showOnlyProvidedAggregations
+        />
+      ),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
index cbe11b750b0ca361e15f1be9d2768ea49166d2a1..d8fab7fc488c9fc6a1ad8de98435a9bd0353b29b 100644
--- a/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/UnderlyingDataAction.jsx
@@ -1,21 +1,21 @@
 /* @flow */
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 import { t } from "c-3po";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
-    if (question.display() !== "table" && question.display() !== "scalar") {
-        return [
-            {
-                name: "underlying-data",
-                title: t`View this as a table`,
-                icon: "table",
-                question: () => question.toUnderlyingData()
-            }
-        ];
-    }
-    return [];
+  if (question.display() !== "table" && question.display() !== "scalar") {
+    return [
+      {
+        name: "underlying-data",
+        title: t`View this as a table`,
+        icon: "table",
+        question: () => question.toUnderlyingData(),
+      },
+    ];
+  }
+  return [];
 };
diff --git a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
index 65d005d9da95932d1f13172db0217dee3abe5b79..ef31d9cbdabe3d77d73dc0d4427f6bc09a7a28b2 100644
--- a/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
+++ b/frontend/src/metabase/qb/components/actions/UnderlyingRecordsAction.jsx
@@ -5,29 +5,27 @@ import React from "react";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import { jt } from "c-3po";
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery) || query.isBareRows()) {
-        return [];
-    }
-    return [
-        {
-            name: "underlying-records",
-            title: (
-                <span>
-                    {
-                        jt`View the underlying ${<span className="text-dark">
-                                {query.table().display_name}
-                            </span>} records`
-                    }
-                </span>
-            ),
-            icon: "table2",
-            question: () => question.toUnderlyingRecords()
-        }
-    ];
+  const query = question.query();
+  if (!(query instanceof StructuredQuery) || query.isBareRows()) {
+    return [];
+  }
+  return [
+    {
+      name: "underlying-records",
+      title: (
+        <span>
+          {jt`View the underlying ${(
+            <span className="text-dark">{query.table().display_name}</span>
+          )} records`}
+        </span>
+      ),
+      icon: "table2",
+      question: () => question.toUnderlyingRecords(),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/actions/XRayCard.jsx b/frontend/src/metabase/qb/components/actions/XRayCard.jsx
index 7876b8e000eab6c23041b55d6882653c30486e8d..58b1006551db7d609df23c64061fe8152a2cde1f 100644
--- a/frontend/src/metabase/qb/components/actions/XRayCard.jsx
+++ b/frontend/src/metabase/qb/components/actions/XRayCard.jsx
@@ -1,27 +1,27 @@
 /* @flow */
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 import { t } from "c-3po";
 
 export default ({ question, settings }: ClickActionProps): ClickAction[] => {
-    // currently time series xrays require the maximum fidelity
-    if (
-        question.card().id &&
-        settings["enable_xrays"] &&
-        settings["xray_max_cost"] === "extended"
-    ) {
-        return [
-            {
-                name: "xray-card",
-                title: t`X-ray this question`,
-                icon: "beaker",
-                url: () => `/xray/card/${question.card().id}/extended`
-            }
-        ];
-    } else {
-        return [];
-    }
+  // currently time series xrays require the maximum fidelity
+  if (
+    question.card().id &&
+    settings["enable_xrays"] &&
+    settings["xray_max_cost"] === "extended"
+  ) {
+    return [
+      {
+        name: "xray-card",
+        title: t`X-ray this question`,
+        icon: "beaker",
+        url: () => `/xray/card/${question.card().id}/extended`,
+      },
+    ];
+  } else {
+    return [];
+  }
 };
diff --git a/frontend/src/metabase/qb/components/actions/XRaySegment.jsx b/frontend/src/metabase/qb/components/actions/XRaySegment.jsx
index f5ee7270455159150831395c6b5e3aa641d624a6..d6eda7151b26974fe36fba8fa2652c0f0cc75f20 100644
--- a/frontend/src/metabase/qb/components/actions/XRaySegment.jsx
+++ b/frontend/src/metabase/qb/components/actions/XRaySegment.jsx
@@ -1,31 +1,31 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 import { isSegmentFilter } from "metabase/lib/query/filter";
 import { t } from "c-3po";
 
 export default ({ question, settings }: ClickActionProps): ClickAction[] => {
-    if (question.card().id && settings["enable_xrays"]) {
-        return question
-            .query()
-            .filters()
-            .filter(filter => isSegmentFilter(filter))
-            .map(filter => {
-                const id = filter[1];
-                const segment = question.metadata().segments[id];
-                const xraysegmentname = segment && segment.name;
-                return {
-                    name: "xray-segment",
-                    title: t`X-ray ${xraysegmentname}`,
-                    icon: "beaker",
-                    url: () => `/xray/segment/${id}/approximate`
-                };
-            });
-    } else {
-        return [];
-    }
+  if (question.card().id && settings["enable_xrays"]) {
+    return question
+      .query()
+      .filters()
+      .filter(filter => isSegmentFilter(filter))
+      .map(filter => {
+        const id = filter[1];
+        const segment = question.metadata().segments[id];
+        const xraysegmentname = segment && segment.name;
+        return {
+          name: "xray-segment",
+          title: t`X-ray ${xraysegmentname}`,
+          icon: "beaker",
+          url: () => `/xray/segment/${id}/approximate`,
+        };
+      });
+  } else {
+    return [];
+  }
 };
diff --git a/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js b/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js
index e9c783c7cc3da8d3111d593b9f248209cc46c1c3..b165d7d8155fc7faf11fb7a28a0ac084cad06752 100644
--- a/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js
+++ b/frontend/src/metabase/qb/components/drill/CountByColumnDrill.js
@@ -6,31 +6,29 @@ import { getFieldRefFromColumn } from "metabase/qb/lib/actions";
 import { isCategory } from "metabase/lib/schema_metadata";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    if (
-        !clicked ||
-        !clicked.column ||
-        clicked.value !== undefined ||
-        clicked.column.source !== "fields" ||
-        !isCategory(clicked.column)
-    ) {
-        return [];
-    }
-    const { column } = clicked;
+  if (
+    !clicked ||
+    !clicked.column ||
+    clicked.value !== undefined ||
+    clicked.column.source !== "fields" ||
+    !isCategory(clicked.column)
+  ) {
+    return [];
+  }
+  const { column } = clicked;
 
-    return [
-        {
-            name: "count-by-column",
-            section: "distribution",
-            title: <span>{t`Distribution`}</span>,
-            question: () =>
-                question
-                    .summarize(["count"])
-                    .pivot([getFieldRefFromColumn(column)])
-        }
-    ];
+  return [
+    {
+      name: "count-by-column",
+      section: "distribution",
+      title: <span>{t`Distribution`}</span>,
+      question: () =>
+        question.summarize(["count"]).pivot([getFieldRefFromColumn(column)]),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
index 9b634982ed08a781337b44e332c5e884cb323f0d..72334b7072fbe9fe74b15114da4025af7c59deae 100644
--- a/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/ObjectDetailDrill.jsx
@@ -3,41 +3,41 @@
 import { isFK, isPK } from "metabase/lib/schema_metadata";
 import { t } from "c-3po";
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    if (
-        !clicked ||
-        !clicked.column ||
-        clicked.value === undefined ||
-        !(isFK(clicked.column) || isPK(clicked.column))
-    ) {
-        return [];
-    }
+  if (
+    !clicked ||
+    !clicked.column ||
+    clicked.value === undefined ||
+    !(isFK(clicked.column) || isPK(clicked.column))
+  ) {
+    return [];
+  }
 
-    // $FlowFixMe
-    let field = question.metadata().fields[clicked.column.id];
-    if (!field) {
-        return [];
-    }
+  // $FlowFixMe
+  let field = question.metadata().fields[clicked.column.id];
+  if (!field) {
+    return [];
+  }
 
-    if (field.target) {
-        field = field.target;
-    }
+  if (field.target) {
+    field = field.target;
+  }
 
-    if (!clicked) {
-        return [];
-    }
+  if (!clicked) {
+    return [];
+  }
 
-    return [
-        {
-            name: "object-detail",
-            section: "details",
-            title: t`View details`,
-            default: true,
-            question: () => question.drillPK(field, clicked && clicked.value)
-        }
-    ];
+  return [
+    {
+      name: "object-detail",
+      section: "details",
+      title: t`View details`,
+      default: true,
+      question: () => question.drillPK(field, clicked && clicked.value),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
index d56747b464c4e456db9ccf9ab5fe57ec3e811f00..613d075403c2c674f8a1621cc2045482ca4f26c7 100644
--- a/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/PivotByCategoryDrill.jsx
@@ -3,10 +3,10 @@
 import PivotByCategoryAction from "../actions/PivotByCategoryAction";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default (props: ClickActionProps): ClickAction[] => {
-    return PivotByCategoryAction(props);
+  return PivotByCategoryAction(props);
 };
diff --git a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
index e92f2e35d72af8cd810d29d6c7e5e2d95c6c1762..955306ab29eb44bc1aa9c99e5cb5cf5e7406c87e 100644
--- a/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/PivotByLocationDrill.jsx
@@ -3,10 +3,10 @@
 import PivotByLocationAction from "../actions/PivotByLocationAction";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default (props: ClickActionProps): ClickAction[] => {
-    return PivotByLocationAction(props);
+  return PivotByLocationAction(props);
 };
diff --git a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
index 420bcab17cb489573ebb5a117856aeca50d03966..54bf9fd27913e13e28e155cf9df346e362933cb7 100644
--- a/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/PivotByTimeDrill.jsx
@@ -3,10 +3,10 @@
 import PivotByTimeAction from "../actions/PivotByTimeAction";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default (props: ClickActionProps): ClickAction[] => {
-    return PivotByTimeAction(props);
+  return PivotByTimeAction(props);
 };
diff --git a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
index 65d4ced0396c348aefd132890ff79c2ae3915cee..082ca1306fc8a71157647ed96a5b950f86dfa8fc 100644
--- a/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/QuickFilterDrill.jsx
@@ -7,65 +7,65 @@ import { singularize, pluralize, stripId } from "metabase/lib/formatting";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 function getFiltersForColumn(column) {
-    if (
-        isa(column.base_type, TYPE.Number) ||
-        isa(column.base_type, TYPE.DateTime)
-    ) {
-        return [
-            { name: "<", operator: "<" },
-            { name: "=", operator: "=" },
-            { name: "≠", operator: "!=" },
-            { name: ">", operator: ">" }
-        ];
-    } else {
-        return [{ name: "=", operator: "=" }, { name: "≠", operator: "!=" }];
-    }
+  if (
+    isa(column.base_type, TYPE.Number) ||
+    isa(column.base_type, TYPE.DateTime)
+  ) {
+    return [
+      { name: "<", operator: "<" },
+      { name: "=", operator: "=" },
+      { name: "≠", operator: "!=" },
+      { name: ">", operator: ">" },
+    ];
+  } else {
+    return [{ name: "=", operator: "=" }, { name: "≠", operator: "!=" }];
+  }
 }
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (
-        !(query instanceof StructuredQuery) ||
-        !clicked ||
-        !clicked.column ||
-        clicked.column.id == null ||
-        clicked.value == undefined
-    ) {
-        return [];
-    }
+  const query = question.query();
+  if (
+    !(query instanceof StructuredQuery) ||
+    !clicked ||
+    !clicked.column ||
+    clicked.column.id == null ||
+    clicked.value == undefined
+  ) {
+    return [];
+  }
 
-    const { value, column } = clicked;
+  const { value, column } = clicked;
 
-    if (isPK(column.special_type)) {
-        return [];
-    }
-    if (isFK(column.special_type)) {
-        return [
-            {
-                name: "view-fks",
-                section: "filter",
-                title: (
-                    <span>
-                        {
-                            jt`View this ${singularize(stripId(column.display_name))}'s ${pluralize(query.table().display_name)}`
-                        }
-                    </span>
-                ),
-                question: () => question.filter("=", column, value)
-            }
-        ];
-    }
-
-    let operators = getFiltersForColumn(column) || [];
-    return operators.map(({ name, operator }) => ({
-        name: operator,
+  if (isPK(column.special_type)) {
+    return [];
+  }
+  if (isFK(column.special_type)) {
+    return [
+      {
+        name: "view-fks",
         section: "filter",
-        title: <span className="h2">{name}</span>,
-        question: () => question.filter(operator, column, value)
-    }));
+        title: (
+          <span>
+            {jt`View this ${singularize(
+              stripId(column.display_name),
+            )}'s ${pluralize(query.table().display_name)}`}
+          </span>
+        ),
+        question: () => question.filter("=", column, value),
+      },
+    ];
+  }
+
+  let operators = getFiltersForColumn(column) || [];
+  return operators.map(({ name, operator }) => ({
+    name: operator,
+    section: "filter",
+    title: <span className="h2">{name}</span>,
+    question: () => question.filter(operator, column, value),
+  }));
 };
diff --git a/frontend/src/metabase/qb/components/drill/SortAction.jsx b/frontend/src/metabase/qb/components/drill/SortAction.jsx
index 17557cdc1d2943740094920dd49ab92a23973b38..5fe8be5433204e072363c6fd07060bef9840b6cf 100644
--- a/frontend/src/metabase/qb/components/drill/SortAction.jsx
+++ b/frontend/src/metabase/qb/components/drill/SortAction.jsx
@@ -4,61 +4,59 @@ import Query from "metabase/lib/query";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import { t } from "c-3po";
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery)) {
-        return [];
-    }
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-    if (
-        !clicked ||
-        !clicked.column ||
-        clicked.value !== undefined ||
-        !clicked.column.source
-    ) {
-        return [];
-    }
-    const { column } = clicked;
+  if (
+    !clicked ||
+    !clicked.column ||
+    clicked.value !== undefined ||
+    !clicked.column.source
+  ) {
+    return [];
+  }
+  const { column } = clicked;
 
-    const fieldRef = query.fieldReferenceForColumn(column);
-    if (!fieldRef) {
-        return [];
-    }
+  const fieldRef = query.fieldReferenceForColumn(column);
+  if (!fieldRef) {
+    return [];
+  }
 
-    const [sortFieldRef, sortDirection] = query.sorts()[0] || [];
-    const isAlreadySorted = sortFieldRef != null &&
-        Query.isSameField(sortFieldRef, fieldRef);
+  const [sortFieldRef, sortDirection] = query.sorts()[0] || [];
+  const isAlreadySorted =
+    sortFieldRef != null && Query.isSameField(sortFieldRef, fieldRef);
 
-    const actions = [];
-    if (
-        !isAlreadySorted ||
-        sortDirection === "descending" ||
-        sortDirection === "desc"
-    ) {
-        actions.push({
-            name: "sort-ascending",
-            section: "sort",
-            title: t`Ascending`,
-            question: () =>
-                query.replaceSort([fieldRef, "ascending"]).question()
-        });
-    }
-    if (
-        !isAlreadySorted ||
-        sortDirection === "ascending" ||
-        sortDirection === "asc"
-    ) {
-        actions.push({
-            name: "sort-descending",
-            section: "sort",
-            title: t`Descending`,
-            question: () =>
-                query.replaceSort([fieldRef, "descending"]).question()
-        });
-    }
-    return actions;
+  const actions = [];
+  if (
+    !isAlreadySorted ||
+    sortDirection === "descending" ||
+    sortDirection === "desc"
+  ) {
+    actions.push({
+      name: "sort-ascending",
+      section: "sort",
+      title: t`Ascending`,
+      question: () => query.replaceSort([fieldRef, "ascending"]).question(),
+    });
+  }
+  if (
+    !isAlreadySorted ||
+    sortDirection === "ascending" ||
+    sortDirection === "asc"
+  ) {
+    actions.push({
+      name: "sort-descending",
+      section: "sort",
+      title: t`Descending`,
+      question: () => query.replaceSort([fieldRef, "descending"]).question(),
+    });
+  }
+  return actions;
 };
diff --git a/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js b/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js
index d667a167c12c6e48507f65b6511a8edc2a53566d..f4c15bf63cddc7aeec8f57b57a033ccda521298f 100644
--- a/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js
+++ b/frontend/src/metabase/qb/components/drill/SummarizeColumnByTimeDrill.js
@@ -5,53 +5,54 @@ import { t } from "c-3po";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import { getFieldRefFromColumn } from "metabase/qb/lib/actions";
 import {
-    isDate,
-    getAggregator,
-    isCompatibleAggregatorForField
+  isDate,
+  getAggregator,
+  isCompatibleAggregatorForField,
 } from "metabase/lib/schema_metadata";
 import { capitalize } from "metabase/lib/formatting";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery)) {
-        return [];
-    }
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-    const dateField = query.table().fields.filter(isDate)[0];
-    if (
-        !dateField || !clicked || !clicked.column || clicked.value !== undefined
-    ) {
-        return [];
-    }
-    const { column } = clicked;
+  const dateField = query.table().fields.filter(isDate)[0];
+  if (
+    !dateField ||
+    !clicked ||
+    !clicked.column ||
+    clicked.value !== undefined
+  ) {
+    return [];
+  }
+  const { column } = clicked;
 
-    return ["sum", "count"]
-        .map(getAggregator)
-        .filter(aggregator =>
-            isCompatibleAggregatorForField(aggregator, column))
-        .map(aggregator => ({
-            name: "summarize-by-time",
-            section: "sum",
-            title: <span>{capitalize(aggregator.short)} {t`by time`}</span>,
-            question: () =>
-                question
-                    .summarize(
-                        aggregator.requiresField
-                            ? [aggregator.short, getFieldRefFromColumn(column)]
-                            : [aggregator.short]
-                    )
-                    .pivot([
-                        [
-                            "datetime-field",
-                            getFieldRefFromColumn(dateField),
-                            "as",
-                            "day"
-                        ]
-                    ])
-        }));
+  return ["sum", "count"]
+    .map(getAggregator)
+    .filter(aggregator => isCompatibleAggregatorForField(aggregator, column))
+    .map(aggregator => ({
+      name: "summarize-by-time",
+      section: "sum",
+      title: (
+        <span>
+          {capitalize(aggregator.short)} {t`by time`}
+        </span>
+      ),
+      question: () =>
+        question
+          .summarize(
+            aggregator.requiresField
+              ? [aggregator.short, getFieldRefFromColumn(column)]
+              : [aggregator.short],
+          )
+          .pivot([
+            ["datetime-field", getFieldRefFromColumn(dateField), "as", "day"],
+          ]),
+    }));
 };
diff --git a/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js b/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js
index 3a6d299f3681cbee637badbc5ffe9d62f182114d..8dbc4ec714411709905ca1e0dfced10edc283fe6 100644
--- a/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js
+++ b/frontend/src/metabase/qb/components/drill/SummarizeColumnDrill.js
@@ -2,71 +2,64 @@
 
 import { getFieldRefFromColumn } from "metabase/qb/lib/actions";
 import {
-    getAggregator,
-    isCompatibleAggregatorForField
+  getAggregator,
+  isCompatibleAggregatorForField,
 } from "metabase/lib/schema_metadata";
 import { t } from "c-3po";
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 const AGGREGATIONS = {
-    sum: {
-        section: "sum",
-        title: t`Sum`
-    },
-    avg: {
-        section: "averages",
-        title: t`Avg`
-    },
-    min: {
-        section: "averages",
-        title: t`Min`
-    },
-    max: {
-        section: "averages",
-        title: t`Max`
-    },
-    distinct: {
-        section: "averages",
-        title: t`Distincts`
-    }
+  sum: {
+    section: "sum",
+    title: t`Sum`,
+  },
+  avg: {
+    section: "averages",
+    title: t`Avg`,
+  },
+  min: {
+    section: "averages",
+    title: t`Min`,
+  },
+  max: {
+    section: "averages",
+    title: t`Max`,
+  },
+  distinct: {
+    section: "averages",
+    title: t`Distincts`,
+  },
 };
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    if (
-        !clicked ||
-        !clicked.column ||
-        clicked.value !== undefined ||
-        clicked.column.source !== "fields"
-    ) {
-        // TODO Atte Keinänen 7/21/17: Does it slow down the drill-through option calculations remarkably
-        // that I removed the `isSummable` condition from here and use `isCompatibleAggregator` method below instead?
-        return [];
-    }
-    const { column } = clicked;
+  if (
+    !clicked ||
+    !clicked.column ||
+    clicked.value !== undefined ||
+    clicked.column.source !== "fields"
+  ) {
+    // TODO Atte Keinänen 7/21/17: Does it slow down the drill-through option calculations remarkably
+    // that I removed the `isSummable` condition from here and use `isCompatibleAggregator` method below instead?
+    return [];
+  }
+  const { column } = clicked;
 
-    return (
-        Object.entries(AGGREGATIONS)
-            .map(([aggregationShort, action]) => [
-                getAggregator(aggregationShort),
-                action
-            ])
-            .filter(([aggregator]) =>
-                isCompatibleAggregatorForField(aggregator, column))
-            // $FlowFixMe
-            .map(([aggregator, action]: [any, {
-                section: string,
-                title: string
-            }]) => ({
-                name: action.title.toLowerCase(),
-                ...action,
-                question: () =>
-                    question.summarize([
-                        aggregator.short,
-                        getFieldRefFromColumn(column)
-                    ])
-            }))
-    );
+  return Object.entries(AGGREGATIONS)
+    .map(([aggregationShort, action]) => [
+      getAggregator(aggregationShort),
+      // $FlowFixMe
+      action,
+    ])
+    .filter(([aggregator]) =>
+      isCompatibleAggregatorForField(aggregator, column),
+    )
+    .map(([aggregator, action]: [any, { section: string, title: string }]) => ({
+      name: action.title.toLowerCase(),
+      ...action,
+      question: () =>
+        question.summarize([aggregator.short, getFieldRefFromColumn(column)]),
+    }));
 };
diff --git a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
index d397757ad326f32f6a7f4b910e2e9022e9864c29..5627b17a11034104af570a8296ca429bc7b381d1 100644
--- a/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/UnderlyingRecordsDrill.jsx
@@ -5,30 +5,33 @@ import { inflect } from "metabase/lib/formatting";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import { t } from "c-3po";
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 
 export default ({ question, clicked }: ClickActionProps): ClickAction[] => {
-    const query = question.query();
-    if (!(query instanceof StructuredQuery)) {
-        return [];
-    }
+  const query = question.query();
+  if (!(query instanceof StructuredQuery)) {
+    return [];
+  }
 
-    const dimensions = (clicked && clicked.dimensions) || [];
-    if (!clicked || dimensions.length === 0) {
-        return [];
-    }
+  const dimensions = (clicked && clicked.dimensions) || [];
+  if (!clicked || dimensions.length === 0) {
+    return [];
+  }
 
-    // the metric value should be the number of rows that will be displayed
-    const count = typeof clicked.value === "number" ? clicked.value : 2;
+  // the metric value should be the number of rows that will be displayed
+  const count = typeof clicked.value === "number" ? clicked.value : 2;
 
-    return [
-        {
-            name: "underlying-records",
-            section: "records",
-            title: t`View ${inflect(t`these`, count, t`this`, t`these`)} ${inflect(query.table().display_name, count)}`,
-            question: () => question.drillUnderlyingRecords(dimensions)
-        }
-    ];
+  return [
+    {
+      name: "underlying-records",
+      section: "records",
+      title: t`View ${inflect(t`these`, count, t`this`, t`these`)} ${inflect(
+        query.table().display_name,
+        count,
+      )}`,
+      question: () => question.drillUnderlyingRecords(dimensions),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx b/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx
index 086baf00b597a7fee0336a08c1f48bfcbd2820f2..4ecd90a06c85b03b913350ecb63def9d29fbd33e 100644
--- a/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx
+++ b/frontend/src/metabase/qb/components/drill/ZoomDrill.jsx
@@ -3,26 +3,28 @@
 import { drillDownForDimensions } from "metabase/qb/lib/actions";
 
 import type {
-    ClickAction,
-    ClickActionProps
+  ClickAction,
+  ClickActionProps,
 } from "metabase/meta/types/Visualization";
 import { t } from "c-3po";
 
-export default (
-    { question, clicked, settings }: ClickActionProps
-): ClickAction[] => {
-    const dimensions = (clicked && clicked.dimensions) || [];
-    const drilldown = drillDownForDimensions(dimensions, question.metadata());
-    if (!drilldown) {
-        return [];
-    }
+export default ({
+  question,
+  clicked,
+  settings,
+}: ClickActionProps): ClickAction[] => {
+  const dimensions = (clicked && clicked.dimensions) || [];
+  const drilldown = drillDownForDimensions(dimensions, question.metadata());
+  if (!drilldown) {
+    return [];
+  }
 
-    return [
-        {
-            name: "timeseries-zoom",
-            section: "zoom",
-            title: t`Zoom in`,
-            question: () => question.pivot(drilldown.breakouts, dimensions)
-        }
-    ];
+  return [
+    {
+      name: "timeseries-zoom",
+      section: "zoom",
+      title: t`Zoom in`,
+      question: () => question.pivot(drilldown.breakouts, dimensions),
+    },
+  ];
 };
diff --git a/frontend/src/metabase/qb/components/drill/index.js b/frontend/src/metabase/qb/components/drill/index.js
index 1a1fb85fb599ef66f732d8f47c12c1299e1e5b0f..3075dc11c130b809c0573229d3302a53845fda7c 100644
--- a/frontend/src/metabase/qb/components/drill/index.js
+++ b/frontend/src/metabase/qb/components/drill/index.js
@@ -7,9 +7,9 @@ import UnderlyingRecordsDrill from "./UnderlyingRecordsDrill";
 import ZoomDrill from "./ZoomDrill";
 
 export const DEFAULT_DRILLS = [
-    ZoomDrill,
-    SortAction,
-    ObjectDetailDrill,
-    QuickFilterDrill,
-    UnderlyingRecordsDrill
+  ZoomDrill,
+  SortAction,
+  ObjectDetailDrill,
+  QuickFilterDrill,
+  UnderlyingRecordsDrill,
 ];
diff --git a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
index 4e9a0bbd1508094d3e5539b807188d25d2ed8c38..9a7479c78c058d4024a086912177ad98a3c5e497 100644
--- a/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
+++ b/frontend/src/metabase/qb/components/gui/AggregationPopover.jsx
@@ -8,17 +8,17 @@ import type { Aggregation, ExpressionName } from "metabase/meta/types/Query";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
 
 type Props = {
-    aggregation?: Aggregation,
-    tableMetadata: TableMetadata,
-    customFields: { [key: ExpressionName]: any },
-    onCommitAggregation: (aggregation: Aggregation) => void,
-    onClose?: () => void,
-    availableAggregations: [Aggregation],
-    showOnlyProvidedAggregations: boolean
+  aggregation?: Aggregation,
+  tableMetadata: TableMetadata,
+  customFields: { [key: ExpressionName]: any },
+  onCommitAggregation: (aggregation: Aggregation) => void,
+  onClose?: () => void,
+  availableAggregations: [Aggregation],
+  showOnlyProvidedAggregations: boolean,
 };
 
 const AggregationPopover = (props: Props) => (
-    <AggPopover {...props} aggregation={props.aggregation || []} />
+  <AggPopover {...props} aggregation={props.aggregation || []} />
 );
 
 export default AggregationPopover;
diff --git a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
index d27aaf5d58ae2a2969d2df3f4300600d5172b9ee..8521b77e95640d3c233da83e3560da681bc8b82c 100644
--- a/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
+++ b/frontend/src/metabase/qb/components/gui/BreakoutPopover.jsx
@@ -8,39 +8,37 @@ import type { Breakout } from "metabase/meta/types/Query";
 import type { TableMetadata, FieldOptions } from "metabase/meta/types/Metadata";
 
 type Props = {
-    maxHeight?: number,
-    breakout?: Breakout,
-    tableMetadata: TableMetadata,
-    fieldOptions: FieldOptions,
-    onCommitBreakout: (breakout: Breakout) => void,
-    onClose?: () => void
+  maxHeight?: number,
+  breakout?: Breakout,
+  tableMetadata: TableMetadata,
+  fieldOptions: FieldOptions,
+  onCommitBreakout: (breakout: Breakout) => void,
+  onClose?: () => void,
 };
 
-const BreakoutPopover = (
-    {
-        breakout,
-        tableMetadata,
-        fieldOptions,
-        onCommitBreakout,
-        onClose,
-        maxHeight
-    }: Props
-) => (
-    <FieldList
-        className="text-green"
-        maxHeight={maxHeight}
-        tableMetadata={tableMetadata}
-        field={breakout}
-        fieldOptions={fieldOptions}
-        onFieldChange={field => {
-            onCommitBreakout(field);
-            if (onClose) {
-                onClose();
-            }
-        }}
-        enableSubDimensions
-        alwaysExpanded
-    />
+const BreakoutPopover = ({
+  breakout,
+  tableMetadata,
+  fieldOptions,
+  onCommitBreakout,
+  onClose,
+  maxHeight,
+}: Props) => (
+  <FieldList
+    className="text-green"
+    maxHeight={maxHeight}
+    tableMetadata={tableMetadata}
+    field={breakout}
+    fieldOptions={fieldOptions}
+    onFieldChange={field => {
+      onCommitBreakout(field);
+      if (onClose) {
+        onClose();
+      }
+    }}
+    enableSubDimensions
+    alwaysExpanded
+  />
 );
 
 export default BreakoutPopover;
diff --git a/frontend/src/metabase/qb/components/modes/DefaultMode.jsx b/frontend/src/metabase/qb/components/modes/DefaultMode.jsx
index 6246528a33aa9192a1c6cf93c662489b76faf7f7..452c8f6f01c7f31275d5ff619bd9bd524c0d0a79 100644
--- a/frontend/src/metabase/qb/components/modes/DefaultMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/DefaultMode.jsx
@@ -6,9 +6,9 @@ import { DEFAULT_DRILLS } from "../drill";
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const DefaultMode: QueryMode = {
-    name: "default",
-    actions: DEFAULT_ACTIONS,
-    drills: DEFAULT_DRILLS
+  name: "default",
+  actions: DEFAULT_ACTIONS,
+  drills: DEFAULT_DRILLS,
 };
 
 export default DefaultMode;
diff --git a/frontend/src/metabase/qb/components/modes/GeoMode.jsx b/frontend/src/metabase/qb/components/modes/GeoMode.jsx
index 1476a911907dca60d3bbcbd37de027e8a7c8a372..69bdfa008e840d8b55d84536190927cd64ec282e 100644
--- a/frontend/src/metabase/qb/components/modes/GeoMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/GeoMode.jsx
@@ -12,9 +12,9 @@ import PivotByTimeDrill from "../drill/PivotByTimeDrill";
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const GeoMode: QueryMode = {
-    name: "geo",
-    actions: [...DEFAULT_ACTIONS, PivotByCategoryAction, PivotByTimeAction],
-    drills: [...DEFAULT_DRILLS, PivotByCategoryDrill, PivotByTimeDrill]
+  name: "geo",
+  actions: [...DEFAULT_ACTIONS, PivotByCategoryAction, PivotByTimeAction],
+  drills: [...DEFAULT_DRILLS, PivotByCategoryDrill, PivotByTimeDrill],
 };
 
 export default GeoMode;
diff --git a/frontend/src/metabase/qb/components/modes/MetricMode.jsx b/frontend/src/metabase/qb/components/modes/MetricMode.jsx
index 9a5109308bdf0dfc46174b9c7905bb96d392609b..bcc7fb0c7d69d3761054dc24e2ccd180b170349d 100644
--- a/frontend/src/metabase/qb/components/modes/MetricMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/MetricMode.jsx
@@ -14,19 +14,19 @@ import PivotByTimeDrill from "../drill/PivotByTimeDrill";
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const MetricMode: QueryMode = {
-    name: "metric",
-    actions: [
-        ...DEFAULT_ACTIONS,
-        PivotByCategoryAction,
-        PivotByLocationAction,
-        PivotByTimeAction
-    ],
-    drills: [
-        ...DEFAULT_DRILLS,
-        PivotByCategoryDrill,
-        PivotByLocationDrill,
-        PivotByTimeDrill
-    ]
+  name: "metric",
+  actions: [
+    ...DEFAULT_ACTIONS,
+    PivotByCategoryAction,
+    PivotByLocationAction,
+    PivotByTimeAction,
+  ],
+  drills: [
+    ...DEFAULT_DRILLS,
+    PivotByCategoryDrill,
+    PivotByLocationDrill,
+    PivotByTimeDrill,
+  ],
 };
 
 export default MetricMode;
diff --git a/frontend/src/metabase/qb/components/modes/NativeMode.jsx b/frontend/src/metabase/qb/components/modes/NativeMode.jsx
index af121db1fef86ce0059d61eecccfb7ba1f46f323..686c8549a8a90b7092e6bf0bf13686e6ff96bb0e 100644
--- a/frontend/src/metabase/qb/components/modes/NativeMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/NativeMode.jsx
@@ -4,9 +4,9 @@ import type { QueryMode } from "metabase/meta/types/Visualization";
 import CompoundQueryAction from "../actions/CompoundQueryAction";
 
 const NativeMode: QueryMode = {
-    name: "native",
-    actions: [CompoundQueryAction],
-    drills: []
+  name: "native",
+  actions: [CompoundQueryAction],
+  drills: [],
 };
 
 export default NativeMode;
diff --git a/frontend/src/metabase/qb/components/modes/ObjectMode.jsx b/frontend/src/metabase/qb/components/modes/ObjectMode.jsx
index 13f551d8faee2fafead49aaa89e1bcf88c86f254..cc8a04cd673e3edb35e5142372cea2b5d7026eb2 100644
--- a/frontend/src/metabase/qb/components/modes/ObjectMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/ObjectMode.jsx
@@ -5,9 +5,9 @@ import ObjectDetailDrill from "../drill/ObjectDetailDrill";
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const ObjectMode: QueryMode = {
-    name: "object",
-    actions: [],
-    drills: [ObjectDetailDrill]
+  name: "object",
+  actions: [],
+  drills: [ObjectDetailDrill],
 };
 
 export default ObjectMode;
diff --git a/frontend/src/metabase/qb/components/modes/PivotMode.jsx b/frontend/src/metabase/qb/components/modes/PivotMode.jsx
index 2751893db3d9a16bfc6bc093406cc926106d269b..4609c13acb30e96103c376c028183e3b8cae4091 100644
--- a/frontend/src/metabase/qb/components/modes/PivotMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/PivotMode.jsx
@@ -14,19 +14,19 @@ import PivotByTimeDrill from "../drill/PivotByTimeDrill";
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const PivotMode: QueryMode = {
-    name: "pivot",
-    actions: [
-        ...DEFAULT_ACTIONS,
-        PivotByCategoryAction,
-        PivotByLocationAction,
-        PivotByTimeAction
-    ],
-    drills: [
-        ...DEFAULT_DRILLS,
-        PivotByCategoryDrill,
-        PivotByLocationDrill,
-        PivotByTimeDrill
-    ]
+  name: "pivot",
+  actions: [
+    ...DEFAULT_ACTIONS,
+    PivotByCategoryAction,
+    PivotByLocationAction,
+    PivotByTimeAction,
+  ],
+  drills: [
+    ...DEFAULT_DRILLS,
+    PivotByCategoryDrill,
+    PivotByLocationDrill,
+    PivotByTimeDrill,
+  ],
 };
 
 export default PivotMode;
diff --git a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
index 35cac1ba624cb2871839bd47e85d1e1c41e2d850..81f4e7908919e2f4acae158fdde9e0ae2452616a 100644
--- a/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/SegmentMode.jsx
@@ -3,8 +3,7 @@
 import { DEFAULT_ACTIONS } from "../actions";
 import { DEFAULT_DRILLS } from "../drill";
 
-import SummarizeBySegmentMetricAction
-    from "../actions/SummarizeBySegmentMetricAction";
+import SummarizeBySegmentMetricAction from "../actions/SummarizeBySegmentMetricAction";
 import CommonMetricsAction from "../actions/CommonMetricsAction";
 import CountByTimeAction from "../actions/CountByTimeAction";
 import XRaySegment from "../actions/XRaySegment";
@@ -16,22 +15,22 @@ import CountByColumnDrill from "../drill/CountByColumnDrill";
 import type { QueryMode } from "metabase/meta/types/Visualization";
 
 const SegmentMode: QueryMode = {
-    name: "segment",
-    actions: [
-        ...DEFAULT_ACTIONS,
-        CommonMetricsAction,
-        CountByTimeAction,
-        XRaySegment,
-        SummarizeBySegmentMetricAction
-        // commenting this out until we sort out viz settings in QB2
-        // PlotSegmentField
-    ],
-    drills: [
-        ...DEFAULT_DRILLS,
-        SummarizeColumnDrill,
-        SummarizeColumnByTimeDrill,
-        CountByColumnDrill
-    ]
+  name: "segment",
+  actions: [
+    ...DEFAULT_ACTIONS,
+    CommonMetricsAction,
+    CountByTimeAction,
+    XRaySegment,
+    SummarizeBySegmentMetricAction,
+    // commenting this out until we sort out viz settings in QB2
+    // PlotSegmentField
+  ],
+  drills: [
+    ...DEFAULT_DRILLS,
+    SummarizeColumnDrill,
+    SummarizeColumnByTimeDrill,
+    CountByColumnDrill,
+  ],
 };
 
 export default SegmentMode;
diff --git a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
index e1ff31247d8aeaf577a9663a2e898c870a93d8b2..075516aba1034cff38c574ade26a553299b27dae 100644
--- a/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
+++ b/frontend/src/metabase/qb/components/modes/TimeseriesMode.jsx
@@ -4,8 +4,7 @@ import React from "react";
 
 // import TimeseriesGroupingWidget
 //     from "metabase/qb/components/TimeseriesGroupingWidget";
-import TimeseriesFilterWidget
-    from "metabase/qb/components/TimeseriesFilterWidget";
+import TimeseriesFilterWidget from "metabase/qb/components/TimeseriesFilterWidget";
 
 import { DEFAULT_ACTIONS } from "../actions";
 import { DEFAULT_DRILLS } from "../drill";
@@ -19,41 +18,40 @@ import PivotByLocationDrill from "../drill/PivotByLocationDrill";
 
 import type { QueryMode } from "metabase/meta/types/Visualization";
 import type {
-    Card as CardObject,
-    DatasetQuery
+  Card as CardObject,
+  DatasetQuery,
 } from "metabase/meta/types/Card";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
-import TimeseriesGroupingWidget
-    from "metabase/qb/components/TimeseriesGroupingWidget";
+import TimeseriesGroupingWidget from "metabase/qb/components/TimeseriesGroupingWidget";
 
 type Props = {
-    lastRunCard: CardObject,
-    tableMetadata: TableMetadata,
-    setDatasetQuery: (datasetQuery: DatasetQuery) => void,
-    runQuestionQuery: () => void
+  lastRunCard: CardObject,
+  tableMetadata: TableMetadata,
+  setDatasetQuery: (datasetQuery: DatasetQuery) => void,
+  runQuestionQuery: () => void,
 };
 
 export const TimeseriesModeFooter = (props: Props) => {
-    return (
-        <div className="flex layout-centered">
-            <span className="mr1">View</span>
-            <TimeseriesFilterWidget {...props} card={props.lastRunCard} />
-            <span className="mx1">by</span>
-            <TimeseriesGroupingWidget {...props} card={props.lastRunCard} />
-        </div>
-    );
+  return (
+    <div className="flex layout-centered">
+      <span className="mr1">View</span>
+      <TimeseriesFilterWidget {...props} card={props.lastRunCard} />
+      <span className="mx1">by</span>
+      <TimeseriesGroupingWidget {...props} card={props.lastRunCard} />
+    </div>
+  );
 };
 
 const TimeseriesMode: QueryMode = {
-    name: "timeseries",
-    actions: [
-        PivotByCategoryAction,
-        PivotByLocationAction,
-        XRayCard,
-        ...DEFAULT_ACTIONS
-    ],
-    drills: [PivotByCategoryDrill, PivotByLocationDrill, ...DEFAULT_DRILLS],
-    ModeFooter: TimeseriesModeFooter
+  name: "timeseries",
+  actions: [
+    PivotByCategoryAction,
+    PivotByLocationAction,
+    XRayCard,
+    ...DEFAULT_ACTIONS,
+  ],
+  drills: [PivotByCategoryDrill, PivotByLocationDrill, ...DEFAULT_DRILLS],
+  ModeFooter: TimeseriesModeFooter,
 };
 
 export default TimeseriesMode;
diff --git a/frontend/src/metabase/qb/lib/actions.js b/frontend/src/metabase/qb/lib/actions.js
index 2bd1c05b0e01663c33754a9b3611c751abbf75d5..58df463ea6903c28b101eeb448b76ed2c68e7e71 100644
--- a/frontend/src/metabase/qb/lib/actions.js
+++ b/frontend/src/metabase/qb/lib/actions.js
@@ -12,19 +12,19 @@ import * as Filter from "metabase/lib/query/filter";
 import { startNewCard } from "metabase/lib/card";
 import { rangeForValue } from "metabase/lib/dataset";
 import {
-    isDate,
-    isState,
-    isCountry,
-    isCoordinate
+  isDate,
+  isState,
+  isCountry,
+  isCoordinate,
 } from "metabase/lib/schema_metadata";
 import Utils from "metabase/lib/utils";
 
 import type Table from "metabase-lib/lib/metadata/Table";
 import type { Card as CardObject } from "metabase/meta/types/Card";
 import type {
-    StructuredQuery,
-    FieldFilter,
-    Breakout
+  StructuredQuery,
+  FieldFilter,
+  Breakout,
 } from "metabase/meta/types/Query";
 import type { DimensionValue } from "metabase/meta/types/Visualization";
 import { parseTimestamp } from "metabase/lib/time";
@@ -32,332 +32,317 @@ import { parseTimestamp } from "metabase/lib/time";
 // TODO: use icepick instead of mutation, make they handle frozen cards
 
 export const toUnderlyingData = (card: CardObject): ?CardObject => {
-    const newCard = startNewCard("query");
-    newCard.dataset_query = Utils.copy(card.dataset_query);
-    newCard.display = "table";
-    newCard.original_card_id = card.id;
-    return newCard;
+  const newCard = startNewCard("query");
+  newCard.dataset_query = Utils.copy(card.dataset_query);
+  newCard.display = "table";
+  newCard.original_card_id = card.id;
+  return newCard;
 };
 
 export const toUnderlyingRecords = (card: CardObject): ?CardObject => {
-    if (card.dataset_query.type === "query") {
-        const query: StructuredQuery = Utils.copy(card.dataset_query).query;
-        const newCard = startNewCard(
-            "query",
-            card.dataset_query.database,
-            query.source_table
-        );
-        newCard.dataset_query.query.filter = query.filter;
-        return newCard;
-    }
+  if (card.dataset_query.type === "query") {
+    const query: StructuredQuery = Utils.copy(card.dataset_query).query;
+    const newCard = startNewCard(
+      "query",
+      card.dataset_query.database,
+      query.source_table,
+    );
+    newCard.dataset_query.query.filter = query.filter;
+    return newCard;
+  }
 };
 
 export const getFieldRefFromColumn = (col, fieldId = col.id) => {
-    if (col.fk_field_id != null) {
-        return ["fk->", col.fk_field_id, fieldId];
-    } else {
-        return ["field-id", fieldId];
-    }
+  if (col.fk_field_id != null) {
+    return ["fk->", col.fk_field_id, fieldId];
+  } else {
+    return ["field-id", fieldId];
+  }
 };
 
 const clone = card => {
-    const newCard = startNewCard("query");
+  const newCard = startNewCard("query");
 
-    newCard.display = card.display;
-    newCard.dataset_query = Utils.copy(card.dataset_query);
+  newCard.display = card.display;
+  newCard.dataset_query = Utils.copy(card.dataset_query);
 
-    // The Question lib doesn't always set a viz setting. Placing a check here, but we should probably refactor this
-    // into a separate test + clean up the question lib.
-    if (card.visualization_settings) {
-        newCard.visualization_settings = Utils.copy(
-            card.visualization_settings
-        );
-    }
+  // The Question lib doesn't always set a viz setting. Placing a check here, but we should probably refactor this
+  // into a separate test + clean up the question lib.
+  if (card.visualization_settings) {
+    newCard.visualization_settings = Utils.copy(card.visualization_settings);
+  }
 
-    return newCard;
+  return newCard;
 };
 
 // Adds a new filter with the specified operator, column, and value
 export const filter = (card, operator, column, value) => {
-    const newCard = clone(card);
-
-    // $FlowFixMe:
-    const filter: FieldFilter = [
-        operator,
-        getFieldRefFromColumn(column),
-        value
-    ];
-    newCard.dataset_query.query = Query.addFilter(
-        newCard.dataset_query.query,
-        filter
-    );
-    return newCard;
+  const newCard = clone(card);
+
+  // $FlowFixMe:
+  const filter: FieldFilter = [operator, getFieldRefFromColumn(column), value];
+  newCard.dataset_query.query = Query.addFilter(
+    newCard.dataset_query.query,
+    filter,
+  );
+  return newCard;
 };
 
 const drillFilter = (card, value, column) => {
-    let filter;
-    if (isDate(column)) {
-        filter = [
-            "=",
-            [
-                "datetime-field",
-                getFieldRefFromColumn(column),
-                "as",
-                column.unit
-            ],
-            parseTimestamp(value, column.unit).toISOString()
-        ];
+  let filter;
+  if (isDate(column)) {
+    filter = [
+      "=",
+      ["datetime-field", getFieldRefFromColumn(column), "as", column.unit],
+      parseTimestamp(value, column.unit).toISOString(),
+    ];
+  } else {
+    const range = rangeForValue(value, column);
+    if (range) {
+      filter = ["BETWEEN", getFieldRefFromColumn(column), range[0], range[1]];
     } else {
-        const range = rangeForValue(value, column);
-        if (range) {
-            filter = [
-                "BETWEEN",
-                getFieldRefFromColumn(column),
-                range[0],
-                range[1]
-            ];
-        } else {
-            filter = ["=", getFieldRefFromColumn(column), value];
-        }
+      filter = ["=", getFieldRefFromColumn(column), value];
     }
+  }
 
-    return addOrUpdateFilter(card, filter);
+  return addOrUpdateFilter(card, filter);
 };
 
 export const addOrUpdateFilter = (card, filter) => {
-    let newCard = clone(card);
-    // replace existing filter, if it exists
-    let filters = Query.getFilters(newCard.dataset_query.query);
-    for (let index = 0; index < filters.length; index++) {
-        if (
-            Filter.isFieldFilter(filters[index]) &&
-            Field.getFieldTargetId(filters[index][1]) ===
-                Field.getFieldTargetId(filter[1])
-        ) {
-            newCard.dataset_query.query = Query.updateFilter(
-                newCard.dataset_query.query,
-                index,
-                filter
-            );
-            return newCard;
-        }
-    }
-
-    // otherwise add a new filter
-    newCard.dataset_query.query = Query.addFilter(
+  let newCard = clone(card);
+  // replace existing filter, if it exists
+  let filters = Query.getFilters(newCard.dataset_query.query);
+  for (let index = 0; index < filters.length; index++) {
+    if (
+      Filter.isFieldFilter(filters[index]) &&
+      Field.getFieldTargetId(filters[index][1]) ===
+        Field.getFieldTargetId(filter[1])
+    ) {
+      newCard.dataset_query.query = Query.updateFilter(
         newCard.dataset_query.query,
-        filter
-    );
-    return newCard;
+        index,
+        filter,
+      );
+      return newCard;
+    }
+  }
+
+  // otherwise add a new filter
+  newCard.dataset_query.query = Query.addFilter(
+    newCard.dataset_query.query,
+    filter,
+  );
+  return newCard;
 };
 
 export const addOrUpdateBreakout = (card, breakout) => {
-    let newCard = clone(card);
-    // replace existing breakout, if it exists
-    let breakouts = Query.getBreakouts(newCard.dataset_query.query);
-    for (let index = 0; index < breakouts.length; index++) {
-        if (
-            fieldIdsEq(
-                Field.getFieldTargetId(breakouts[index]),
-                Field.getFieldTargetId(breakout)
-            )
-        ) {
-            newCard.dataset_query.query = Query.updateBreakout(
-                newCard.dataset_query.query,
-                index,
-                breakout
-            );
-            return newCard;
-        }
-    }
-
-    // otherwise add a new breakout
-    newCard.dataset_query.query = Query.addBreakout(
+  let newCard = clone(card);
+  // replace existing breakout, if it exists
+  let breakouts = Query.getBreakouts(newCard.dataset_query.query);
+  for (let index = 0; index < breakouts.length; index++) {
+    if (
+      fieldIdsEq(
+        Field.getFieldTargetId(breakouts[index]),
+        Field.getFieldTargetId(breakout),
+      )
+    ) {
+      newCard.dataset_query.query = Query.updateBreakout(
         newCard.dataset_query.query,
-        breakout
-    );
-    return newCard;
+        index,
+        breakout,
+      );
+      return newCard;
+    }
+  }
+
+  // otherwise add a new breakout
+  newCard.dataset_query.query = Query.addBreakout(
+    newCard.dataset_query.query,
+    breakout,
+  );
+  return newCard;
 };
 
 const UNITS = ["minute", "hour", "day", "week", "month", "quarter", "year"];
 const getNextUnit = unit => {
-    return UNITS[Math.max(0, UNITS.indexOf(unit) - 1)];
+  return UNITS[Math.max(0, UNITS.indexOf(unit) - 1)];
 };
 
 export { drillDownForDimensions } from "./drilldown";
 
 export const drillUnderlyingRecords = (card, dimensions) => {
-    for (const dimension of dimensions) {
-        card = drillFilter(card, dimension.value, dimension.column);
-    }
-    return toUnderlyingRecords(card);
+  for (const dimension of dimensions) {
+    card = drillFilter(card, dimension.value, dimension.column);
+  }
+  return toUnderlyingRecords(card);
 };
 
 export const drillRecord = (databaseId, tableId, fieldId, value) => {
-    const newCard = startNewCard("query", databaseId, tableId);
-    newCard.dataset_query.query = Query.addFilter(newCard.dataset_query.query, [
-        "=",
-        ["field-id", fieldId],
-        value
-    ]);
-    return newCard;
+  const newCard = startNewCard("query", databaseId, tableId);
+  newCard.dataset_query.query = Query.addFilter(newCard.dataset_query.query, [
+    "=",
+    ["field-id", fieldId],
+    value,
+  ]);
+  return newCard;
 };
 
 export const plotSegmentField = card => {
-    const newCard = startNewCard("query");
-    newCard.display = "scatter";
-    newCard.dataset_query = Utils.copy(card.dataset_query);
-    return newCard;
+  const newCard = startNewCard("query");
+  newCard.display = "scatter";
+  newCard.dataset_query = Utils.copy(card.dataset_query);
+  return newCard;
 };
 
 export const summarize = (card, aggregation, tableMetadata) => {
-    const newCard = startNewCard("query");
-    newCard.dataset_query = Utils.copy(card.dataset_query);
-    newCard.dataset_query.query = Query.addAggregation(
-        newCard.dataset_query.query,
-        aggregation
-    );
-    guessVisualization(newCard, tableMetadata);
-    return newCard;
+  const newCard = startNewCard("query");
+  newCard.dataset_query = Utils.copy(card.dataset_query);
+  newCard.dataset_query.query = Query.addAggregation(
+    newCard.dataset_query.query,
+    aggregation,
+  );
+  guessVisualization(newCard, tableMetadata);
+  return newCard;
 };
 
 export const breakout = (card, breakout, tableMetadata) => {
-    const newCard = startNewCard("query");
-    newCard.dataset_query = Utils.copy(card.dataset_query);
-    newCard.dataset_query.query = Query.addBreakout(
-        newCard.dataset_query.query,
-        breakout
-    );
-    guessVisualization(newCard, tableMetadata);
-    return newCard;
+  const newCard = startNewCard("query");
+  newCard.dataset_query = Utils.copy(card.dataset_query);
+  newCard.dataset_query.query = Query.addBreakout(
+    newCard.dataset_query.query,
+    breakout,
+  );
+  guessVisualization(newCard, tableMetadata);
+  return newCard;
 };
 
 // min number of points when switching units
 const MIN_INTERVALS = 4;
 
 export const updateDateTimeFilter = (card, column, start, end): CardObject => {
-    let newCard = clone(card);
-
-    let fieldRef = getFieldRefFromColumn(column);
-    start = moment(start);
-    end = moment(end);
-    if (column.unit) {
-        // start with the existing breakout unit
-        let unit = column.unit;
-
-        // clamp range to unit to ensure we select exactly what's represented by the dots/bars
-        start = start.add(1, unit).startOf(unit);
-        end = end.endOf(unit);
-
-        // find the largest unit with at least MIN_INTERVALS
-        while (
-            unit !== getNextUnit(unit) && end.diff(start, unit) < MIN_INTERVALS
-        ) {
-            unit = getNextUnit(unit);
-        }
-
-        // update the breakout
-        newCard = addOrUpdateBreakout(newCard, [
-            "datetime-field",
-            fieldRef,
-            "as",
-            unit
-        ]);
-
-        // round to start of the original unit
-        start = start.startOf(column.unit);
-        end = end.startOf(column.unit);
-
-        if (start.isAfter(end)) {
-            return card;
-        }
-        if (start.isSame(end, column.unit)) {
-            // is the start and end are the same (in whatever the original unit was) then just do an "="
-            return addOrUpdateFilter(newCard, [
-                "=",
-                ["datetime-field", fieldRef, "as", column.unit],
-                start.format()
-            ]);
-        } else {
-            // otherwise do a BETWEEN
-            return addOrUpdateFilter(newCard, [
-                "BETWEEN",
-                ["datetime-field", fieldRef, "as", column.unit],
-                start.format(),
-                end.format()
-            ]);
-        }
+  let newCard = clone(card);
+
+  let fieldRef = getFieldRefFromColumn(column);
+  start = moment(start);
+  end = moment(end);
+  if (column.unit) {
+    // start with the existing breakout unit
+    let unit = column.unit;
+
+    // clamp range to unit to ensure we select exactly what's represented by the dots/bars
+    start = start.add(1, unit).startOf(unit);
+    end = end.endOf(unit);
+
+    // find the largest unit with at least MIN_INTERVALS
+    while (
+      unit !== getNextUnit(unit) &&
+      end.diff(start, unit) < MIN_INTERVALS
+    ) {
+      unit = getNextUnit(unit);
+    }
+
+    // update the breakout
+    newCard = addOrUpdateBreakout(newCard, [
+      "datetime-field",
+      fieldRef,
+      "as",
+      unit,
+    ]);
+
+    // round to start of the original unit
+    start = start.startOf(column.unit);
+    end = end.startOf(column.unit);
+
+    if (start.isAfter(end)) {
+      return card;
+    }
+    if (start.isSame(end, column.unit)) {
+      // is the start and end are the same (in whatever the original unit was) then just do an "="
+      return addOrUpdateFilter(newCard, [
+        "=",
+        ["datetime-field", fieldRef, "as", column.unit],
+        start.format(),
+      ]);
     } else {
-        return addOrUpdateFilter(newCard, [
-            "BETWEEN",
-            fieldRef,
-            start.format(),
-            end.format()
-        ]);
+      // otherwise do a BETWEEN
+      return addOrUpdateFilter(newCard, [
+        "BETWEEN",
+        ["datetime-field", fieldRef, "as", column.unit],
+        start.format(),
+        end.format(),
+      ]);
     }
+  } else {
+    return addOrUpdateFilter(newCard, [
+      "BETWEEN",
+      fieldRef,
+      start.format(),
+      end.format(),
+    ]);
+  }
 };
 
 export function updateLatLonFilter(
-    card,
-    latitudeColumn,
-    longitudeColumn,
-    bounds
+  card,
+  latitudeColumn,
+  longitudeColumn,
+  bounds,
 ) {
-    return addOrUpdateFilter(card, [
-        "INSIDE",
-        latitudeColumn.id,
-        longitudeColumn.id,
-        bounds.getNorth(),
-        bounds.getWest(),
-        bounds.getSouth(),
-        bounds.getEast()
-    ]);
+  return addOrUpdateFilter(card, [
+    "INSIDE",
+    latitudeColumn.id,
+    longitudeColumn.id,
+    bounds.getNorth(),
+    bounds.getWest(),
+    bounds.getSouth(),
+    bounds.getEast(),
+  ]);
 }
 
 export function updateNumericFilter(card, column, start, end) {
-    const fieldRef = getFieldRefFromColumn(column);
-    return addOrUpdateFilter(card, ["BETWEEN", fieldRef, start, end]);
+  const fieldRef = getFieldRefFromColumn(column);
+  return addOrUpdateFilter(card, ["BETWEEN", fieldRef, start, end]);
 }
 
 export const pivot = (
-    card: CardObject,
-    tableMetadata: Table,
-    breakouts: Breakout[] = [],
-    dimensions: DimensionValue[] = []
+  card: CardObject,
+  tableMetadata: Table,
+  breakouts: Breakout[] = [],
+  dimensions: DimensionValue[] = [],
 ): ?CardObject => {
-    if (card.dataset_query.type !== "query") {
-        return null;
-    }
-
-    let newCard = startNewCard("query");
-    newCard.dataset_query = Utils.copy(card.dataset_query);
-
-    for (const dimension of dimensions) {
-        newCard = drillFilter(newCard, dimension.value, dimension.column);
-        const breakoutFields = Query.getBreakoutFields(
-            newCard.dataset_query.query,
-            tableMetadata
+  if (card.dataset_query.type !== "query") {
+    return null;
+  }
+
+  let newCard = startNewCard("query");
+  newCard.dataset_query = Utils.copy(card.dataset_query);
+
+  for (const dimension of dimensions) {
+    newCard = drillFilter(newCard, dimension.value, dimension.column);
+    const breakoutFields = Query.getBreakoutFields(
+      newCard.dataset_query.query,
+      tableMetadata,
+    );
+    for (const [index, field] of breakoutFields.entries()) {
+      if (field && fieldIdsEq(field.id, dimension.column.id)) {
+        newCard.dataset_query.query = Query.removeBreakout(
+          newCard.dataset_query.query,
+          index,
         );
-        for (const [index, field] of breakoutFields.entries()) {
-            if (field && fieldIdsEq(field.id, dimension.column.id)) {
-                newCard.dataset_query.query = Query.removeBreakout(
-                    newCard.dataset_query.query,
-                    index
-                );
-            }
-        }
+      }
     }
+  }
 
-    for (const breakout of breakouts) {
-        newCard.dataset_query.query = Query.addBreakout(
-            newCard.dataset_query.query,
-            breakout
-        );
-    }
+  for (const breakout of breakouts) {
+    newCard.dataset_query.query = Query.addBreakout(
+      newCard.dataset_query.query,
+      breakout,
+    );
+  }
 
-    guessVisualization(newCard, tableMetadata);
+  guessVisualization(newCard, tableMetadata);
 
-    return newCard;
+  return newCard;
 };
 
 // const VISUALIZATIONS_ONE_BREAKOUTS = new Set([
@@ -371,49 +356,49 @@ export const pivot = (
 const VISUALIZATIONS_TWO_BREAKOUTS = new Set(["bar", "line", "area"]);
 
 const guessVisualization = (card: CardObject, tableMetadata: Table) => {
-    const query = Card.getQuery(card);
-    if (!query) {
-        return;
-    }
-    const aggregations = Query.getAggregations(query);
-    const breakoutFields = Query.getBreakouts(query).map(
-        breakout => (Q.getFieldTarget(breakout, tableMetadata) || {}).field
-    );
-    if (aggregations.length === 0 && breakoutFields.length === 0) {
-        card.display = "table";
-    } else if (aggregations.length === 1 && breakoutFields.length === 0) {
-        card.display = "scalar";
-    } else if (aggregations.length === 1 && breakoutFields.length === 1) {
-        if (isState(breakoutFields[0])) {
-            card.display = "map";
-            card.visualization_settings["map.type"] = "region";
-            card.visualization_settings["map.region"] = "us_states";
-        } else if (isCountry(breakoutFields[0])) {
-            card.display = "map";
-            card.visualization_settings["map.type"] = "region";
-            card.visualization_settings["map.region"] = "world_countries";
-        } else if (isDate(breakoutFields[0])) {
-            card.display = "line";
-        } else {
-            card.display = "bar";
-        }
-    } else if (aggregations.length === 1 && breakoutFields.length === 2) {
-        if (!VISUALIZATIONS_TWO_BREAKOUTS.has(card.display)) {
-            if (isDate(breakoutFields[0])) {
-                card.display = "line";
-            } else if (_.all(breakoutFields, isCoordinate)) {
-                card.display = "map";
-                // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
-                // Currently show a pin map instead of heat map for double coordinate breakout
-                // This way the binning drill-through works in a somewhat acceptable way (although it is designed for heat maps)
-                card.visualization_settings["map.type"] = "pin";
-                // card.visualization_settings["map.type"] = "grid";
-            } else {
-                card.display = "bar";
-            }
-        }
+  const query = Card.getQuery(card);
+  if (!query) {
+    return;
+  }
+  const aggregations = Query.getAggregations(query);
+  const breakoutFields = Query.getBreakouts(query).map(
+    breakout => (Q.getFieldTarget(breakout, tableMetadata) || {}).field,
+  );
+  if (aggregations.length === 0 && breakoutFields.length === 0) {
+    card.display = "table";
+  } else if (aggregations.length === 1 && breakoutFields.length === 0) {
+    card.display = "scalar";
+  } else if (aggregations.length === 1 && breakoutFields.length === 1) {
+    if (isState(breakoutFields[0])) {
+      card.display = "map";
+      card.visualization_settings["map.type"] = "region";
+      card.visualization_settings["map.region"] = "us_states";
+    } else if (isCountry(breakoutFields[0])) {
+      card.display = "map";
+      card.visualization_settings["map.type"] = "region";
+      card.visualization_settings["map.region"] = "world_countries";
+    } else if (isDate(breakoutFields[0])) {
+      card.display = "line";
     } else {
-        console.warn("Couldn't guess visualization", card);
-        card.display = "table";
+      card.display = "bar";
+    }
+  } else if (aggregations.length === 1 && breakoutFields.length === 2) {
+    if (!VISUALIZATIONS_TWO_BREAKOUTS.has(card.display)) {
+      if (isDate(breakoutFields[0])) {
+        card.display = "line";
+      } else if (_.all(breakoutFields, isCoordinate)) {
+        card.display = "map";
+        // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
+        // Currently show a pin map instead of heat map for double coordinate breakout
+        // This way the binning drill-through works in a somewhat acceptable way (although it is designed for heat maps)
+        card.visualization_settings["map.type"] = "pin";
+        // card.visualization_settings["map.type"] = "grid";
+      } else {
+        card.display = "bar";
+      }
     }
+  } else {
+    console.warn("Couldn't guess visualization", card);
+    card.display = "table";
+  }
 };
diff --git a/frontend/src/metabase/qb/lib/drilldown.js b/frontend/src/metabase/qb/lib/drilldown.js
index a20612ef85547e8fb05161195fd4c21bf4c4a579..a12e60da5acf103be8c4cb2e1c7810d29ab663a7 100644
--- a/frontend/src/metabase/qb/lib/drilldown.js
+++ b/frontend/src/metabase/qb/lib/drilldown.js
@@ -2,10 +2,10 @@
 
 import { isa, TYPE } from "metabase/lib/types";
 import {
-    isLatitude,
-    isLongitude,
-    isDate,
-    isAny
+  isLatitude,
+  isLongitude,
+  isDate,
+  isAny,
 } from "metabase/lib/schema_metadata";
 import { getFieldRefFromColumn } from "./actions";
 
@@ -16,229 +16,231 @@ import { getIn } from "icepick";
 const CategoryDrillDown = type => [field => isa(field.special_type, type)];
 const DateTimeDrillDown = unit => [["datetime-field", isDate, unit]];
 const LatLonDrillDown = binWidth => [
-    ["binning-strategy", isLatitude, "bin-width", binWidth],
-    ["binning-strategy", isLongitude, "bin-width", binWidth]
+  ["binning-strategy", isLatitude, "bin-width", binWidth],
+  ["binning-strategy", isLongitude, "bin-width", binWidth],
 ];
 
 /**
  * Defines the built-in drill-down progressions
  */
 const DEFAULT_DRILL_DOWN_PROGRESSIONS = [
-    // DateTime drill downs
+  // DateTime drill downs
+  [
+    DateTimeDrillDown("year"),
+    DateTimeDrillDown("quarter"),
+    DateTimeDrillDown("month"),
+    DateTimeDrillDown("week"),
+    DateTimeDrillDown("day"),
+    DateTimeDrillDown("hour"),
+    DateTimeDrillDown("minute"),
+  ],
+  // Country => State => City
+  [
+    CategoryDrillDown(TYPE.Country),
+    CategoryDrillDown(TYPE.State),
+    // CategoryDrillDown(TYPE.City)
+  ],
+  // Country, State, or City => LatLon
+  [CategoryDrillDown(TYPE.Country), LatLonDrillDown(10)],
+  [CategoryDrillDown(TYPE.State), LatLonDrillDown(1)],
+  [CategoryDrillDown(TYPE.City), LatLonDrillDown(0.1)],
+  // LatLon drill downs
+  [
+    LatLonDrillDown(30),
+    LatLonDrillDown(10),
+    LatLonDrillDown(1),
+    LatLonDrillDown(0.1),
+    LatLonDrillDown(0.01),
+  ],
+  [
     [
-        DateTimeDrillDown("year"),
-        DateTimeDrillDown("quarter"),
-        DateTimeDrillDown("month"),
-        DateTimeDrillDown("week"),
-        DateTimeDrillDown("day"),
-        DateTimeDrillDown("hour"),
-        DateTimeDrillDown("minute")
+      ["binning-strategy", isLatitude, "num-bins", () => true],
+      ["binning-strategy", isLongitude, "num-bins", () => true],
     ],
-    // Country => State => City
+    LatLonDrillDown(1),
+  ],
+  // generic num-bins drill down
+  [
+    [["binning-strategy", isAny, "num-bins", () => true]],
+    [["binning-strategy", isAny, "default"]],
+  ],
+  // generic bin-width drill down
+  [
+    [["binning-strategy", isAny, "bin-width", () => true]],
     [
-        CategoryDrillDown(TYPE.Country),
-        CategoryDrillDown(TYPE.State)
-        // CategoryDrillDown(TYPE.City)
+      [
+        "binning-strategy",
+        isAny,
+        "bin-width",
+        (previous: number) => previous / 10,
+      ],
     ],
-    // Country, State, or City => LatLon
-    [CategoryDrillDown(TYPE.Country), LatLonDrillDown(10)],
-    [CategoryDrillDown(TYPE.State), LatLonDrillDown(1)],
-    [CategoryDrillDown(TYPE.City), LatLonDrillDown(0.1)],
-    // LatLon drill downs
-    [
-        LatLonDrillDown(30),
-        LatLonDrillDown(10),
-        LatLonDrillDown(1),
-        LatLonDrillDown(0.1),
-        LatLonDrillDown(0.01)
-    ],
-    [
-        [
-            ["binning-strategy", isLatitude, "num-bins", () => true],
-            ["binning-strategy", isLongitude, "num-bins", () => true]
-        ],
-        LatLonDrillDown(1)
-    ],
-    // generic num-bins drill down
-    [
-        [["binning-strategy", isAny, "num-bins", () => true]],
-        [["binning-strategy", isAny, "default"]]
-    ],
-    // generic bin-width drill down
-    [
-        [["binning-strategy", isAny, "bin-width", () => true]],
-        [
-            [
-                "binning-strategy",
-                isAny,
-                "bin-width",
-                (previous: number) => previous / 10
-            ]
-        ]
-    ]
+  ],
 ];
 
 /**
  * Returns the next drill down for the current dimension objects
  */
 export function drillDownForDimensions(dimensions: any, metadata: any) {
-    const table = metadata && tableForDimensions(dimensions, metadata);
+  const table = metadata && tableForDimensions(dimensions, metadata);
 
-    for (const drillProgression of DEFAULT_DRILL_DOWN_PROGRESSIONS) {
-        for (let index = 0; index < drillProgression.length - 1; index++) {
-            const currentDrillBreakoutTemplates = drillProgression[index];
-            const nextDrillBreakoutTemplates = drillProgression[index + 1];
-            if (
-                breakoutTemplatesMatchDimensions(
-                    currentDrillBreakoutTemplates,
-                    dimensions
-                )
-            ) {
-                const breakouts = breakoutsForBreakoutTemplates(
-                    nextDrillBreakoutTemplates,
-                    dimensions,
-                    table
-                );
-                if (breakouts) {
-                    return {
-                        breakouts: breakouts
-                    };
-                }
-            }
+  for (const drillProgression of DEFAULT_DRILL_DOWN_PROGRESSIONS) {
+    for (let index = 0; index < drillProgression.length - 1; index++) {
+      const currentDrillBreakoutTemplates = drillProgression[index];
+      const nextDrillBreakoutTemplates = drillProgression[index + 1];
+      if (
+        breakoutTemplatesMatchDimensions(
+          currentDrillBreakoutTemplates,
+          dimensions,
+        )
+      ) {
+        const breakouts = breakoutsForBreakoutTemplates(
+          nextDrillBreakoutTemplates,
+          dimensions,
+          table,
+        );
+        if (breakouts) {
+          return {
+            breakouts: breakouts,
+          };
         }
+      }
     }
-    return null;
+  }
+  return null;
 }
 
 // Returns true if the supplied dimension object matches the supplied breakout template.
 function breakoutTemplateMatchesDimension(breakoutTemplate, dimension) {
-    const breakout = columnToBreakout(dimension.column);
-    if (Array.isArray(breakoutTemplate) !== Array.isArray(breakout)) {
-        return false;
+  const breakout = columnToBreakout(dimension.column);
+  if (Array.isArray(breakoutTemplate) !== Array.isArray(breakout)) {
+    return false;
+  }
+  if (Array.isArray(breakoutTemplate)) {
+    if (!breakoutTemplate[1](dimension.column)) {
+      return false;
     }
-    if (Array.isArray(breakoutTemplate)) {
-        if (!breakoutTemplate[1](dimension.column)) {
-            return false;
+    for (let i = 2; i < breakoutTemplate.length; i++) {
+      if (typeof breakoutTemplate[i] === "function") {
+        // $FlowFixMe
+        if (!breakoutTemplate[i](breakout[i])) {
+          return false;
         }
-        for (let i = 2; i < breakoutTemplate.length; i++) {
-            if (typeof breakoutTemplate[i] === "function") {
-                // $FlowFixMe
-                if (!breakoutTemplate[i](breakout[i])) {
-                    return false;
-                }
-            } else {
-                // $FlowFixMe
-                if (breakoutTemplate[i] !== breakout[i]) {
-                    return false;
-                }
-            }
+      } else {
+        // $FlowFixMe
+        if (breakoutTemplate[i] !== breakout[i]) {
+          return false;
         }
-        return true;
-    } else {
-        return breakoutTemplate(dimension.column);
+      }
     }
+    return true;
+  } else {
+    return breakoutTemplate(dimension.column);
+  }
 }
 
 // Returns true if all breakout templates having a matching dimension object, but disregarding order
 function breakoutTemplatesMatchDimensions(breakoutTemplates, dimensions) {
-    dimensions = [...dimensions];
-    return _.all(breakoutTemplates, breakoutTemplate => {
-        const index = _.findIndex(dimensions, dimension =>
-            breakoutTemplateMatchesDimension(breakoutTemplate, dimension));
-        if (index >= 0) {
-            dimensions.splice(index, 1);
-            return true;
-        } else {
-            return false;
-        }
-    });
+  dimensions = [...dimensions];
+  return _.all(breakoutTemplates, breakoutTemplate => {
+    const index = _.findIndex(dimensions, dimension =>
+      breakoutTemplateMatchesDimension(breakoutTemplate, dimension),
+    );
+    if (index >= 0) {
+      dimensions.splice(index, 1);
+      return true;
+    } else {
+      return false;
+    }
+  });
 }
 
 // Evaluates a breakout template, returning a completed breakout clause
 function breakoutForBreakoutTemplate(breakoutTemplate, dimensions, table) {
-    let fieldFilter = Array.isArray(breakoutTemplate)
-        ? breakoutTemplate[1]
-        : breakoutTemplate;
-    let dimensionColumns = dimensions.map(d => d.column);
-    let field = _.find(dimensionColumns, fieldFilter) ||
-        _.find(table.fields, fieldFilter);
-    if (!field) {
-        return null;
-    }
-    const fieldRef = getFieldRefFromColumn(dimensions[0].column, field.id);
-    if (Array.isArray(breakoutTemplate)) {
-        const prevDimension = _.find(dimensions, dimension =>
-            breakoutTemplateMatchesDimension(breakoutTemplate, dimension));
-        const breakout = [breakoutTemplate[0], fieldRef];
-        for (let i = 2; i < breakoutTemplate.length; i++) {
-            const arg = breakoutTemplate[i];
-            if (typeof arg === "function") {
-                if (!prevDimension) {
-                    return null;
-                }
-                const prevBreakout = columnToBreakout(prevDimension.column);
-                // $FlowFixMe
-                breakout.push(arg(prevBreakout[i]));
-            } else {
-                breakout.push(arg);
-            }
+  let fieldFilter = Array.isArray(breakoutTemplate)
+    ? breakoutTemplate[1]
+    : breakoutTemplate;
+  let dimensionColumns = dimensions.map(d => d.column);
+  let field =
+    _.find(dimensionColumns, fieldFilter) || _.find(table.fields, fieldFilter);
+  if (!field) {
+    return null;
+  }
+  const fieldRef = getFieldRefFromColumn(dimensions[0].column, field.id);
+  if (Array.isArray(breakoutTemplate)) {
+    const prevDimension = _.find(dimensions, dimension =>
+      breakoutTemplateMatchesDimension(breakoutTemplate, dimension),
+    );
+    const breakout = [breakoutTemplate[0], fieldRef];
+    for (let i = 2; i < breakoutTemplate.length; i++) {
+      const arg = breakoutTemplate[i];
+      if (typeof arg === "function") {
+        if (!prevDimension) {
+          return null;
         }
-        return breakout;
-    } else {
-        return fieldRef;
+        const prevBreakout = columnToBreakout(prevDimension.column);
+        // $FlowFixMe
+        breakout.push(arg(prevBreakout[i]));
+      } else {
+        breakout.push(arg);
+      }
     }
+    return breakout;
+  } else {
+    return fieldRef;
+  }
 }
 
 // Evaluates all the breakout templates of a drill
 function breakoutsForBreakoutTemplates(breakoutTemplates, dimensions, table) {
-    const breakouts = [];
-    for (const breakoutTemplate of breakoutTemplates) {
-        const breakout = breakoutForBreakoutTemplate(
-            breakoutTemplate,
-            dimensions,
-            table
-        );
-        if (!breakout) {
-            return null;
-        }
-        breakouts.push(breakout);
+  const breakouts = [];
+  for (const breakoutTemplate of breakoutTemplates) {
+    const breakout = breakoutForBreakoutTemplate(
+      breakoutTemplate,
+      dimensions,
+      table,
+    );
+    if (!breakout) {
+      return null;
     }
-    return breakouts;
+    breakouts.push(breakout);
+  }
+  return breakouts;
 }
 
 // Guesses the breakout corresponding to the provided columm object
 function columnToBreakout(column) {
-    if (column.unit) {
-        return ["datetime-field", column.id, column.unit];
-    } else if (column.binning_info) {
-        let binningStrategy = column.binning_info.binning_strategy;
+  if (column.unit) {
+    return ["datetime-field", column.id, column.unit];
+  } else if (column.binning_info) {
+    let binningStrategy = column.binning_info.binning_strategy;
 
-        switch (binningStrategy) {
-            case "bin-width":
-                return [
-                    "binning-strategy",
-                    column.id,
-                    "bin-width",
-                    column.binning_info.bin_width
-                ];
-            case "num-bins":
-                return [
-                    "binning-strategy",
-                    column.id,
-                    "num-bins",
-                    column.binning_info.num_bins
-                ];
-            default:
-                return null;
-        }
-    } else {
-        return column.id;
+    switch (binningStrategy) {
+      case "bin-width":
+        return [
+          "binning-strategy",
+          column.id,
+          "bin-width",
+          column.binning_info.bin_width,
+        ];
+      case "num-bins":
+        return [
+          "binning-strategy",
+          column.id,
+          "num-bins",
+          column.binning_info.num_bins,
+        ];
+      default:
+        return null;
     }
+  } else {
+    return column.id;
+  }
 }
 
 // returns the table metadata for a dimension
 function tableForDimensions(dimensions, metadata) {
-    const fieldId = getIn(dimensions, [0, "column", "id"]);
-    const field = metadata.fields[fieldId];
-    return field && field.table;
+  const fieldId = getIn(dimensions, [0, "column", "id"]);
+  const field = metadata.fields[fieldId];
+  return field && field.table;
 }
diff --git a/frontend/src/metabase/qb/lib/modes.js b/frontend/src/metabase/qb/lib/modes.js
index 93eb6295d97b708cb767299c12b147c38e7c9339..f90b3c9823ef3d54e25ea5ad955c339af4c114f7 100644
--- a/frontend/src/metabase/qb/lib/modes.js
+++ b/frontend/src/metabase/qb/lib/modes.js
@@ -2,10 +2,10 @@
 
 import Q_DEPRECATED from "metabase/lib/query"; // legacy query lib
 import {
-    isDate,
-    isAddress,
-    isCategory,
-    isPK
+  isDate,
+  isAddress,
+  isCategory,
+  isPK,
 } from "metabase/lib/schema_metadata";
 import * as Query from "metabase/lib/query/query";
 import * as Card from "metabase/meta/Card";
@@ -26,81 +26,73 @@ import type { QueryMode } from "metabase/meta/types/Visualization";
 import _ from "underscore";
 
 export function getMode(
-    card: CardObject,
-    tableMetadata: ?TableMetadata
+  card: CardObject,
+  tableMetadata: ?TableMetadata,
 ): ?QueryMode {
-    if (!card) {
-        return null;
-    }
+  if (!card) {
+    return null;
+  }
 
-    if (Card.isNative(card)) {
-        return NativeMode;
-    }
+  if (Card.isNative(card)) {
+    return NativeMode;
+  }
 
-    const query = Card.getQuery(card);
-    if (Card.isStructured(card) && query) {
-        if (!tableMetadata) {
-            return null;
-        }
+  const query = Card.getQuery(card);
+  if (Card.isStructured(card) && query) {
+    if (!tableMetadata) {
+      return null;
+    }
 
-        const aggregations = Query.getAggregations(query);
-        const breakouts = Query.getBreakouts(query);
-        const filters = Query.getFilters(query);
+    const aggregations = Query.getAggregations(query);
+    const breakouts = Query.getBreakouts(query);
+    const filters = Query.getFilters(query);
 
-        if (aggregations.length === 0 && breakouts.length === 0) {
-            const isPKFilter = filter => {
-                if (
-                    tableMetadata && Array.isArray(filter) && filter[0] === "="
-                ) {
-                    const fieldId = Q_DEPRECATED.getFieldTargetId(filter[1]);
-                    const field = tableMetadata.fields_lookup[fieldId];
-                    if (
-                        field &&
-                        field.table.id === query.source_table &&
-                        isPK(field)
-                    ) {
-                        return true;
-                    }
-                }
-                return false;
-            };
-            if (_.any(filters, isPKFilter)) {
-                return ObjectMode;
-            } else {
-                return SegmentMode;
-            }
-        }
-        if (aggregations.length > 0 && breakouts.length === 0) {
-            return MetricMode;
-        }
-        if (aggregations.length > 0 && breakouts.length > 0) {
-            let breakoutFields = breakouts.map(
-                breakout =>
-                    (Q_DEPRECATED.getFieldTarget(breakout, tableMetadata) || {
-                    }).field
-            );
-            if (
-                (breakoutFields.length === 1 && isDate(breakoutFields[0])) ||
-                (breakoutFields.length === 2 &&
-                    isDate(breakoutFields[0]) &&
-                    isCategory(breakoutFields[1]))
-            ) {
-                return TimeseriesMode;
-            }
-            if (breakoutFields.length === 1 && isAddress(breakoutFields[0])) {
-                return GeoMode;
-            }
-            if (
-                (breakoutFields.length === 1 &&
-                    isCategory(breakoutFields[0])) ||
-                (breakoutFields.length === 2 &&
-                    isCategory(breakoutFields[0]) &&
-                    isCategory(breakoutFields[1]))
-            ) {
-                return PivotMode;
-            }
+    if (aggregations.length === 0 && breakouts.length === 0) {
+      const isPKFilter = filter => {
+        if (tableMetadata && Array.isArray(filter) && filter[0] === "=") {
+          const fieldId = Q_DEPRECATED.getFieldTargetId(filter[1]);
+          const field = tableMetadata.fields_lookup[fieldId];
+          if (field && field.table.id === query.source_table && isPK(field)) {
+            return true;
+          }
         }
+        return false;
+      };
+      if (_.any(filters, isPKFilter)) {
+        return ObjectMode;
+      } else {
+        return SegmentMode;
+      }
+    }
+    if (aggregations.length > 0 && breakouts.length === 0) {
+      return MetricMode;
+    }
+    if (aggregations.length > 0 && breakouts.length > 0) {
+      let breakoutFields = breakouts.map(
+        breakout =>
+          (Q_DEPRECATED.getFieldTarget(breakout, tableMetadata) || {}).field,
+      );
+      if (
+        (breakoutFields.length === 1 && isDate(breakoutFields[0])) ||
+        (breakoutFields.length === 2 &&
+          isDate(breakoutFields[0]) &&
+          isCategory(breakoutFields[1]))
+      ) {
+        return TimeseriesMode;
+      }
+      if (breakoutFields.length === 1 && isAddress(breakoutFields[0])) {
+        return GeoMode;
+      }
+      if (
+        (breakoutFields.length === 1 && isCategory(breakoutFields[0])) ||
+        (breakoutFields.length === 2 &&
+          isCategory(breakoutFields[0]) &&
+          isCategory(breakoutFields[1]))
+      ) {
+        return PivotMode;
+      }
     }
+  }
 
-    return DefaultMode;
+  return DefaultMode;
 }
diff --git a/frontend/src/metabase/query_builder/actions.js b/frontend/src/metabase/query_builder/actions.js
index f7c24e3e648b0522c16b945b9a79ee893480010e..2eec18284676a11f86329e66b92fc1cc09ee2b69 100644
--- a/frontend/src/metabase/query_builder/actions.js
+++ b/frontend/src/metabase/query_builder/actions.js
@@ -3,7 +3,7 @@ import { fetchAlertsForQuestion } from "metabase/alert/alert";
 
 declare var ace: any;
 
-import React from 'react'
+import React from "react";
 import { createAction } from "redux-actions";
 import _ from "underscore";
 import { assocIn } from "icepick";
@@ -13,7 +13,14 @@ import { push, replace } from "react-router-redux";
 import { setErrorPage } from "metabase/redux/app";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
-import { loadCard, startNewCard, deserializeCardFromUrl, serializeCardForUrl, cleanCopyCard, urlForCardState } from "metabase/lib/card";
+import {
+  loadCard,
+  startNewCard,
+  deserializeCardFromUrl,
+  serializeCardForUrl,
+  cleanCopyCard,
+  urlForCardState,
+} from "metabase/lib/card";
 import { formatSQL } from "metabase/lib/formatting";
 import Query, { createQuery } from "metabase/lib/query";
 import { isPK } from "metabase/lib/types";
@@ -25,18 +32,23 @@ import Question from "metabase-lib/lib/Question";
 import { cardIsEquivalent } from "metabase/meta/Card";
 
 import {
-    getTableMetadata,
-    getNativeDatabases,
-    getQuestion,
-    getOriginalQuestion,
-    getOriginalCard,
-    getIsEditing,
-    getIsShowingDataReference,
-    getTransformedSeries,
-    getResultsMetadata,
+  getTableMetadata,
+  getNativeDatabases,
+  getQuestion,
+  getOriginalQuestion,
+  getOriginalCard,
+  getIsEditing,
+  getIsShowingDataReference,
+  getTransformedSeries,
+  getResultsMetadata,
 } from "./selectors";
 
-import { getDatabases, getTables, getDatabasesList, getMetadata } from "metabase/selectors/metadata";
+import {
+  getDatabases,
+  getTables,
+  getDatabasesList,
+  getMetadata,
+} from "metabase/selectors/metadata";
 
 import { fetchDatabases, fetchTableMetadata } from "metabase/redux/metadata";
 
@@ -44,7 +56,7 @@ import { MetabaseApi, CardApi, UserApi } from "metabase/services";
 
 import { parse as urlParse } from "url";
 import querystring from "querystring";
-import {getCardAfterVisualizationClick} from "metabase/visualizations/lib/utils";
+import { getCardAfterVisualizationClick } from "metabase/visualizations/lib/utils";
 
 import type { Card } from "metabase/meta/types/Card";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
@@ -53,497 +65,599 @@ import { getPersistableDefaultSettings } from "metabase/visualizations/lib/setti
 import { clearRequestState } from "metabase/redux/requests";
 
 type UiControls = {
-    isEditing?: boolean,
-    isShowingTemplateTagsEditor?: boolean,
-    isShowingNewbModal?: boolean,
-    isShowingTutorial?: boolean,
-}
+  isEditing?: boolean,
+  isShowingTemplateTagsEditor?: boolean,
+  isShowingNewbModal?: boolean,
+  isShowingTutorial?: boolean,
+};
 
 const getTemplateTagCount = (question: Question) => {
-    const query = question.query();
-    return query instanceof NativeQuery ?
-        query.templateTags().length :
-        0;
-}
+  const query = question.query();
+  return query instanceof NativeQuery ? query.templateTags().length : 0;
+};
 
-export const SET_CURRENT_STATE = "metabase/qb/SET_CURRENT_STATE"; const setCurrentState = createAction(SET_CURRENT_STATE);
+export const SET_CURRENT_STATE = "metabase/qb/SET_CURRENT_STATE";
+const setCurrentState = createAction(SET_CURRENT_STATE);
 
 export const POP_STATE = "metabase/qb/POP_STATE";
-export const popState = createThunkAction(POP_STATE, (location) =>
-    async (dispatch, getState) => {
-        const { card } = getState().qb;
-        if (location.state && location.state.card) {
-            if (!Utils.equals(card, location.state.card)) {
-                dispatch(setCardAndRun(location.state.card, false));
-                dispatch(setCurrentState(location.state));
-            }
-        }
+export const popState = createThunkAction(
+  POP_STATE,
+  location => async (dispatch, getState) => {
+    const { card } = getState().qb;
+    if (location.state && location.state.card) {
+      if (!Utils.equals(card, location.state.card)) {
+        dispatch(setCardAndRun(location.state.card, false));
+        dispatch(setCurrentState(location.state));
+      }
     }
+  },
 );
 
 export const CREATE_PUBLIC_LINK = "metabase/card/CREATE_PUBLIC_LINK";
-export const createPublicLink = createAction(CREATE_PUBLIC_LINK, ({ id }) => CardApi.createPublicLink({ id }));
+export const createPublicLink = createAction(CREATE_PUBLIC_LINK, ({ id }) =>
+  CardApi.createPublicLink({ id }),
+);
 
 export const DELETE_PUBLIC_LINK = "metabase/card/DELETE_PUBLIC_LINK";
-export const deletePublicLink = createAction(DELETE_PUBLIC_LINK, ({ id }) => CardApi.deletePublicLink({ id }));
+export const deletePublicLink = createAction(DELETE_PUBLIC_LINK, ({ id }) =>
+  CardApi.deletePublicLink({ id }),
+);
 
 export const UPDATE_ENABLE_EMBEDDING = "metabase/card/UPDATE_ENABLE_EMBEDDING";
-export const updateEnableEmbedding = createAction(UPDATE_ENABLE_EMBEDDING, ({ id }, enable_embedding) =>
-    CardApi.update({ id, enable_embedding })
+export const updateEnableEmbedding = createAction(
+  UPDATE_ENABLE_EMBEDDING,
+  ({ id }, enable_embedding) => CardApi.update({ id, enable_embedding }),
 );
 
 export const UPDATE_EMBEDDING_PARAMS = "metabase/card/UPDATE_EMBEDDING_PARAMS";
-export const updateEmbeddingParams = createAction(UPDATE_EMBEDDING_PARAMS, ({ id }, embedding_params) =>
-    CardApi.update({ id, embedding_params })
+export const updateEmbeddingParams = createAction(
+  UPDATE_EMBEDDING_PARAMS,
+  ({ id }, embedding_params) => CardApi.update({ id, embedding_params }),
 );
 
 // TODO Atte Keinänen 6/8/17: Should use the stored question by default instead of requiring an explicit `card` parameter
 export const UPDATE_URL = "metabase/qb/UPDATE_URL";
-export const updateUrl = createThunkAction(UPDATE_URL, (card, { dirty = false, replaceState = false, preserveParameters = true }) =>
-    (dispatch, getState) => {
-        if (!card) {
-            return;
-        }
-        var copy = cleanCopyCard(card);
-        var newState = {
-            card: copy,
-            cardId: copy.id,
-            serializedCard: serializeCardForUrl(copy)
-        };
+export const updateUrl = createThunkAction(
+  UPDATE_URL,
+  (
+    card,
+    { dirty = false, replaceState = false, preserveParameters = true },
+  ) => (dispatch, getState) => {
+    if (!card) {
+      return;
+    }
+    var copy = cleanCopyCard(card);
+    var newState = {
+      card: copy,
+      cardId: copy.id,
+      serializedCard: serializeCardForUrl(copy),
+    };
 
-        const { currentState } = getState().qb;
+    const { currentState } = getState().qb;
 
-        if (Utils.equals(currentState, newState)) {
-            return;
-        }
+    if (Utils.equals(currentState, newState)) {
+      return;
+    }
 
-        var url = urlForCardState(newState, dirty);
+    var url = urlForCardState(newState, dirty);
 
-        // if the serialized card is identical replace the previous state instead of adding a new one
-        // e.x. when saving a new card we want to replace the state and URL with one with the new card ID
-        replaceState = replaceState || (currentState && currentState.serializedCard === newState.serializedCard);
+    // if the serialized card is identical replace the previous state instead of adding a new one
+    // e.x. when saving a new card we want to replace the state and URL with one with the new card ID
+    replaceState =
+      replaceState ||
+      (currentState && currentState.serializedCard === newState.serializedCard);
 
-        const urlParsed = urlParse(url);
-        const locationDescriptor = {
-            pathname: urlParsed.pathname,
-            search: preserveParameters ? window.location.search : "",
-            hash: urlParsed.hash,
-            state: newState
-        };
+    const urlParsed = urlParse(url);
+    const locationDescriptor = {
+      pathname: urlParsed.pathname,
+      search: preserveParameters ? window.location.search : "",
+      hash: urlParsed.hash,
+      state: newState,
+    };
 
-        if (locationDescriptor.pathname === window.location.pathname &&
-            (locationDescriptor.search || "") === (window.location.search || "") &&
-            (locationDescriptor.hash || "") === (window.location.hash || "")
-        ) {
-            replaceState = true;
-        }
+    if (
+      locationDescriptor.pathname === window.location.pathname &&
+      (locationDescriptor.search || "") === (window.location.search || "") &&
+      (locationDescriptor.hash || "") === (window.location.hash || "")
+    ) {
+      replaceState = true;
+    }
 
-        // this is necessary because we can't get the state from history.state
-        dispatch(setCurrentState(newState));
-        if (replaceState) {
-            dispatch(replace(locationDescriptor));
-        } else {
-            dispatch(push(locationDescriptor));
-        }
+    // this is necessary because we can't get the state from history.state
+    dispatch(setCurrentState(newState));
+    if (replaceState) {
+      dispatch(replace(locationDescriptor));
+    } else {
+      dispatch(push(locationDescriptor));
     }
+  },
 );
 
-export const REDIRECT_TO_NEW_QUESTION_FLOW = "metabase/qb/REDIRECT_TO_NEW_QUESTION_FLOW";
-export const redirectToNewQuestionFlow = createThunkAction(REDIRECT_TO_NEW_QUESTION_FLOW, () =>
-    (dispatch, getState) => dispatch(replace("/question/new"))
-)
+export const REDIRECT_TO_NEW_QUESTION_FLOW =
+  "metabase/qb/REDIRECT_TO_NEW_QUESTION_FLOW";
+export const redirectToNewQuestionFlow = createThunkAction(
+  REDIRECT_TO_NEW_QUESTION_FLOW,
+  () => (dispatch, getState) => dispatch(replace("/question/new")),
+);
 
 export const RESET_QB = "metabase/qb/RESET_QB";
 export const resetQB = createAction(RESET_QB);
 
 export const INITIALIZE_QB = "metabase/qb/INITIALIZE_QB";
 export const initializeQB = (location, params) => {
-    return async (dispatch, getState) => {
+  return async (dispatch, getState) => {
+    // do this immediately to ensure old state is cleared before the user sees it
+    dispatch(resetQB());
+    dispatch(cancelQuery());
 
-        // do this immediately to ensure old state is cleared before the user sees it
-        dispatch(resetQB());
-        dispatch(cancelQuery());
+    const { currentUser } = getState();
 
-        const { currentUser } = getState();
+    let card, databasesList, originalCard;
+    let uiControls: UiControls = {
+      isEditing: false,
+      isShowingTemplateTagsEditor: false,
+    };
 
-        let card, databasesList, originalCard;
-        let uiControls: UiControls = {
-            isEditing: false,
-            isShowingTemplateTagsEditor: false
-        };
+    // always start the QB by loading up the databases for the application
+    try {
+      await dispatch(fetchDatabases());
+      databasesList = getDatabasesList(getState());
+    } catch (error) {
+      console.error("error fetching dbs", error);
 
-        // always start the QB by loading up the databases for the application
-        try {
-            await dispatch(fetchDatabases());
-            databasesList = getDatabasesList(getState());
-        } catch(error) {
-            console.error("error fetching dbs", error);
+      // if we can't actually get the databases list then bail now
+      dispatch(setErrorPage(error));
 
-            // if we can't actually get the databases list then bail now
-            dispatch(setErrorPage(error));
+      return { uiControls };
+    }
 
-            return { uiControls };
+    // load up or initialize the card we'll be working on
+    let options = {};
+    let serializedCard;
+    // hash can contain either query params starting with ? or a base64 serialized card
+    if (location.hash) {
+      let hash = location.hash.replace(/^#/, "");
+      if (hash.charAt(0) === "?") {
+        options = querystring.parse(hash.substring(1));
+      } else {
+        serializedCard = hash;
+      }
+    }
+    const sampleDataset = _.findWhere(databasesList, { is_sample: true });
+
+    let preserveParameters = false;
+    if (params.cardId || serializedCard) {
+      // existing card being loaded
+      try {
+        // if we have a serialized card then unpack it and use it
+        card = serializedCard ? deserializeCardFromUrl(serializedCard) : {};
+
+        // load the card either from `cardId` parameter or the serialized card
+        if (params.cardId) {
+          card = await loadCard(params.cardId);
+          // when we are loading from a card id we want an explicit clone of the card we loaded which is unmodified
+          originalCard = Utils.copy(card);
+          // for showing the "started from" lineage correctly when adding filters/breakouts and when going back and forth
+          // in browser history, the original_card_id has to be set for the current card (simply the id of card itself for now)
+          card.original_card_id = card.id;
+        } else if (card.original_card_id) {
+          // deserialized card contains the card id, so just populate originalCard
+          originalCard = await loadCard(card.original_card_id);
+          // if the cards are equal then show the original
+          if (cardIsEquivalent(card, originalCard)) {
+            card = Utils.copy(originalCard);
+          }
         }
 
-        // load up or initialize the card we'll be working on
-        let options = {};
-        let serializedCard;
-        // hash can contain either query params starting with ? or a base64 serialized card
-        if (location.hash) {
-            let hash = location.hash.replace(/^#/, "");
-            if (hash.charAt(0) === "?") {
-                options = querystring.parse(hash.substring(1));
-            } else {
-                serializedCard = hash;
-            }
-        }
-        const sampleDataset = _.findWhere(databasesList, { is_sample: true });
-
-        let preserveParameters = false;
-        if (params.cardId || serializedCard) {
-            // existing card being loaded
-            try {
-                // if we have a serialized card then unpack it and use it
-                card = serializedCard ? deserializeCardFromUrl(serializedCard) : {};
-
-                // load the card either from `cardId` parameter or the serialized card
-                if (params.cardId) {
-                    card = await loadCard(params.cardId);
-                    // when we are loading from a card id we want an explicit clone of the card we loaded which is unmodified
-                    originalCard = Utils.copy(card);
-                    // for showing the "started from" lineage correctly when adding filters/breakouts and when going back and forth
-                    // in browser history, the original_card_id has to be set for the current card (simply the id of card itself for now)
-                    card.original_card_id = card.id;
-                } else if (card.original_card_id) {
-                    // deserialized card contains the card id, so just populate originalCard
-                    originalCard = await loadCard(card.original_card_id);
-                    // if the cards are equal then show the original
-                    if (cardIsEquivalent(card, originalCard)) {
-                        card = Utils.copy(originalCard);
-                    }
-                }
-
-                MetabaseAnalytics.trackEvent("QueryBuilder", "Query Loaded", card.dataset_query.type);
-
-                // if we have deserialized card from the url AND loaded a card by id then the user should be dropped into edit mode
-                uiControls.isEditing = !!options.edit;
-
-                // if this is the users first time loading a saved card on the QB then show them the newb modal
-                if (params.cardId && currentUser.is_qbnewb) {
-                    uiControls.isShowingNewbModal = true;
-                    MetabaseAnalytics.trackEvent("QueryBuilder", "Show Newb Modal");
-                }
-
-                if (card.archived) {
-                    // use the error handler in App.jsx for showing "This question has been archived" message
-                    dispatch(setErrorPage({
-                        data: {
-                            error_code: "archived"
-                        },
-                        context: "query-builder"
-                    }));
-                    card = null;
-                }
-
-                preserveParameters = true;
-            } catch(error) {
-                console.warn('initializeQb failed because of an error:', error);
-                card = null;
-                dispatch(setErrorPage(error));
-            }
-
-        } else if (options.tutorial !== undefined && sampleDataset) {
-            // we are launching the QB tutorial
-            card = startNewCard("query", sampleDataset.id);
-
-            uiControls.isShowingTutorial = true;
-            MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Start", true);
+        MetabaseAnalytics.trackEvent(
+          "QueryBuilder",
+          "Query Loaded",
+          card.dataset_query.type,
+        );
 
-        } else {
-            // we are starting a new/empty card
-            // if no options provided in the hash, redirect to the new question flow
-            if (!options.db && !options.table && !options.segment && !options.metric) {
-                await dispatch(redirectToNewQuestionFlow())
-                return;
-            }
-
-            const databaseId = (options.db) ? parseInt(options.db) : undefined;
-            card = startNewCard("query", databaseId);
-
-            // initialize parts of the query based on optional parameters supplied
-            if (options.table != undefined && card.dataset_query.query) {
-                card.dataset_query.query.source_table = parseInt(options.table);
-            }
-
-            if (options.segment != undefined && card.dataset_query.query) {
-                card.dataset_query.query.filter = ["AND", ["SEGMENT", parseInt(options.segment)]];
-            }
-
-            if (options.metric != undefined && card.dataset_query.query) {
-                card.dataset_query.query.aggregation = ["METRIC", parseInt(options.metric)];
-            }
-
-            MetabaseAnalytics.trackEvent("QueryBuilder", "Query Started", card.dataset_query.type);
+        // if we have deserialized card from the url AND loaded a card by id then the user should be dropped into edit mode
+        uiControls.isEditing = !!options.edit;
+
+        // if this is the users first time loading a saved card on the QB then show them the newb modal
+        if (params.cardId && currentUser.is_qbnewb) {
+          uiControls.isShowingNewbModal = true;
+          MetabaseAnalytics.trackEvent("QueryBuilder", "Show Newb Modal");
         }
 
-        /**** All actions are dispatched here ****/
-
-        // Update the question to Redux state together with the initial state of UI controls
-        dispatch.action(INITIALIZE_QB, {
-            card,
-            originalCard,
-            uiControls
-        });
-
-        // Fetch alerts for the current question if the question is saved
-        card && card.id && dispatch(fetchAlertsForQuestion(card.id))
-
-        // Fetch the question metadata
-        card && dispatch(loadMetadataForCard(card));
-
-        const question = card && new Question(getMetadata(getState()), card);
-
-        // if we have loaded up a card that we can run then lets kick that off as well
-        if (question) {
-            if (question.canRun()) {
-                // NOTE: timeout to allow Parameters widget to set parameterValues
-                setTimeout(() =>
-                    // TODO Atte Keinänen 5/31/17: Check if it is dangerous to create a question object without metadata
-                    dispatch(runQuestionQuery({ shouldUpdateUrl: false }))
-                , 0);
-            }
-
-            // clean up the url and make sure it reflects our card state
-            const originalQuestion = originalCard && new Question(getMetadata(getState()), originalCard);
-            dispatch(updateUrl(card, {
-                dirty: !originalQuestion || originalQuestion && question.isDirtyComparedTo(originalQuestion),
-                replaceState: true,
-                preserveParameters
-            }));
+        if (card.archived) {
+          // use the error handler in App.jsx for showing "This question has been archived" message
+          dispatch(
+            setErrorPage({
+              data: {
+                error_code: "archived",
+              },
+              context: "query-builder",
+            }),
+          );
+          card = null;
         }
-    };
-};
 
+        preserveParameters = true;
+      } catch (error) {
+        console.warn("initializeQb failed because of an error:", error);
+        card = null;
+        dispatch(setErrorPage(error));
+      }
+    } else if (options.tutorial !== undefined && sampleDataset) {
+      // we are launching the QB tutorial
+      card = startNewCard("query", sampleDataset.id);
+
+      uiControls.isShowingTutorial = true;
+      MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Start", true);
+    } else {
+      // we are starting a new/empty card
+      // if no options provided in the hash, redirect to the new question flow
+      if (
+        !options.db &&
+        !options.table &&
+        !options.segment &&
+        !options.metric
+      ) {
+        await dispatch(redirectToNewQuestionFlow());
+        return;
+      }
+
+      const databaseId = options.db ? parseInt(options.db) : undefined;
+      card = startNewCard("query", databaseId);
+
+      // initialize parts of the query based on optional parameters supplied
+      if (options.table != undefined && card.dataset_query.query) {
+        card.dataset_query.query.source_table = parseInt(options.table);
+      }
+
+      if (options.segment != undefined && card.dataset_query.query) {
+        card.dataset_query.query.filter = [
+          "AND",
+          ["SEGMENT", parseInt(options.segment)],
+        ];
+      }
+
+      if (options.metric != undefined && card.dataset_query.query) {
+        card.dataset_query.query.aggregation = [
+          "METRIC",
+          parseInt(options.metric),
+        ];
+      }
+
+      MetabaseAnalytics.trackEvent(
+        "QueryBuilder",
+        "Query Started",
+        card.dataset_query.type,
+      );
+    }
+
+    /**** All actions are dispatched here ****/
+
+    // Update the question to Redux state together with the initial state of UI controls
+    dispatch.action(INITIALIZE_QB, {
+      card,
+      originalCard,
+      uiControls,
+    });
+
+    // Fetch alerts for the current question if the question is saved
+    card && card.id && dispatch(fetchAlertsForQuestion(card.id));
+
+    // Fetch the question metadata
+    card && dispatch(loadMetadataForCard(card));
+
+    const question = card && new Question(getMetadata(getState()), card);
+
+    // if we have loaded up a card that we can run then lets kick that off as well
+    if (question) {
+      if (question.canRun()) {
+        // NOTE: timeout to allow Parameters widget to set parameterValues
+        setTimeout(
+          () =>
+            // TODO Atte Keinänen 5/31/17: Check if it is dangerous to create a question object without metadata
+            dispatch(runQuestionQuery({ shouldUpdateUrl: false })),
+          0,
+        );
+      }
+
+      // clean up the url and make sure it reflects our card state
+      const originalQuestion =
+        originalCard && new Question(getMetadata(getState()), originalCard);
+      dispatch(
+        updateUrl(card, {
+          dirty:
+            !originalQuestion ||
+            (originalQuestion && question.isDirtyComparedTo(originalQuestion)),
+          replaceState: true,
+          preserveParameters,
+        }),
+      );
+    }
+  };
+};
 
 export const TOGGLE_DATA_REFERENCE = "metabase/qb/TOGGLE_DATA_REFERENCE";
 export const toggleDataReference = createAction(TOGGLE_DATA_REFERENCE, () => {
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Data Reference");
+  MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Data Reference");
 });
 
-export const TOGGLE_TEMPLATE_TAGS_EDITOR = "metabase/qb/TOGGLE_TEMPLATE_TAGS_EDITOR";
-export const toggleTemplateTagsEditor = createAction(TOGGLE_TEMPLATE_TAGS_EDITOR, () => {
+export const TOGGLE_TEMPLATE_TAGS_EDITOR =
+  "metabase/qb/TOGGLE_TEMPLATE_TAGS_EDITOR";
+export const toggleTemplateTagsEditor = createAction(
+  TOGGLE_TEMPLATE_TAGS_EDITOR,
+  () => {
     MetabaseAnalytics.trackEvent("QueryBuilder", "Toggle Template Tags Editor");
-});
+  },
+);
 
-export const SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR = "metabase/qb/SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR";
-export const setIsShowingTemplateTagsEditor = (isShowingTemplateTagsEditor) => ({
-        type: SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
-        isShowingTemplateTagsEditor
+export const SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR =
+  "metabase/qb/SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR";
+export const setIsShowingTemplateTagsEditor = isShowingTemplateTagsEditor => ({
+  type: SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
+  isShowingTemplateTagsEditor,
 });
 
 export const CLOSE_QB_TUTORIAL = "metabase/qb/CLOSE_QB_TUTORIAL";
 export const closeQbTutorial = createAction(CLOSE_QB_TUTORIAL, () => {
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Close");
+  MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Close");
 });
 
 export const CLOSE_QB_NEWB_MODAL = "metabase/qb/CLOSE_QB_NEWB_MODAL";
 export const closeQbNewbModal = createThunkAction(CLOSE_QB_NEWB_MODAL, () => {
-    return async (dispatch, getState) => {
-        // persist the fact that this user has seen the NewbModal
-        const { currentUser } = getState();
-        await UserApi.update_qbnewb({id: currentUser.id});
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Close Newb Modal');
-    };
+  return async (dispatch, getState) => {
+    // persist the fact that this user has seen the NewbModal
+    const { currentUser } = getState();
+    await UserApi.update_qbnewb({ id: currentUser.id });
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Close Newb Modal");
+  };
 });
 
-
 export const BEGIN_EDITING = "metabase/qb/BEGIN_EDITING";
 export const beginEditing = createAction(BEGIN_EDITING, () => {
-    MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Begin");
+  MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Begin");
 });
 
 export const CANCEL_EDITING = "metabase/qb/CANCEL_EDITING";
 export const cancelEditing = createThunkAction(CANCEL_EDITING, () => {
-    return (dispatch, getState) => {
-        // clone
-        let card = Utils.copy(getOriginalCard(getState()));
+  return (dispatch, getState) => {
+    // clone
+    let card = Utils.copy(getOriginalCard(getState()));
 
-        dispatch(loadMetadataForCard(card));
+    dispatch(loadMetadataForCard(card));
 
-        // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
-        dispatch(runQuestionQuery({ overrideWithCard: card, shouldUpdateUrl: false }));
-        dispatch(updateUrl(card, { dirty: false }));
+    // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
+    dispatch(
+      runQuestionQuery({ overrideWithCard: card, shouldUpdateUrl: false }),
+    );
+    dispatch(updateUrl(card, { dirty: false }));
 
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Cancel");
-        return card;
-    };
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Edit Cancel");
+    return card;
+  };
 });
 
 // TODO Atte Keinänen 6/8/17: Could (should?) use the stored question by default instead of always requiring the explicit `card` parameter
 export const LOAD_METADATA_FOR_CARD = "metabase/qb/LOAD_METADATA_FOR_CARD";
-export const loadMetadataForCard = createThunkAction(LOAD_METADATA_FOR_CARD, (card) => {
+export const loadMetadataForCard = createThunkAction(
+  LOAD_METADATA_FOR_CARD,
+  card => {
     return async (dispatch, getState) => {
-        // Short-circuit if we're in a weird state where the card isn't completely loaded
-        if (!card && !card.dataset_query) return;
-
-        const query = card && new Question(getMetadata(getState()), card).query();
+      // Short-circuit if we're in a weird state where the card isn't completely loaded
+      if (!card && !card.dataset_query) return;
 
-        async function loadMetadataForAtomicQuery(singleQuery) {
-            if (singleQuery instanceof StructuredQuery && singleQuery.tableId() != null) {
-                await dispatch(loadTableMetadata(singleQuery.tableId()));
-            }
+      const query = card && new Question(getMetadata(getState()), card).query();
 
-            // NOTE Atte Keinänen 1/29/18:
-            // For native queries we don't normally know which table(s) we are working on.
-            // We could load all tables of the current database but historically that has caused
-            // major performance problems with users having large databases.
-            // Now components needing table metadata fetch it on-demand.
+      async function loadMetadataForAtomicQuery(singleQuery) {
+        if (
+          singleQuery instanceof StructuredQuery &&
+          singleQuery.tableId() != null
+        ) {
+          await dispatch(loadTableMetadata(singleQuery.tableId()));
         }
 
-        if (query) {
-            await loadMetadataForAtomicQuery(query);
-        }
-    }
-});
+        // NOTE Atte Keinänen 1/29/18:
+        // For native queries we don't normally know which table(s) we are working on.
+        // We could load all tables of the current database but historically that has caused
+        // major performance problems with users having large databases.
+        // Now components needing table metadata fetch it on-demand.
+      }
+
+      if (query) {
+        await loadMetadataForAtomicQuery(query);
+      }
+    };
+  },
+);
 
 export const LOAD_TABLE_METADATA = "metabase/qb/LOAD_TABLE_METADATA";
-export const loadTableMetadata = createThunkAction(LOAD_TABLE_METADATA, (tableId) => {
+export const loadTableMetadata = createThunkAction(
+  LOAD_TABLE_METADATA,
+  tableId => {
     return async (dispatch, getState) => {
-        try {
-            await dispatch(fetchTableMetadata(tableId));
-            // TODO: finish moving this to metadata duck:
-            const foreignKeys = await MetabaseApi.table_fks({ tableId });
-            return { foreignKeys }
-        } catch(error) {
-            console.error('error getting table metadata', error);
-            return {};
-        }
+      try {
+        await dispatch(fetchTableMetadata(tableId));
+        // TODO: finish moving this to metadata duck:
+        const foreignKeys = await MetabaseApi.table_fks({ tableId });
+        return { foreignKeys };
+      } catch (error) {
+        console.error("error getting table metadata", error);
+        return {};
+      }
     };
-});
+  },
+);
 
 // TODO Atte Keinänen 7/5/17: Move the API call to redux/metadata for being able to see the db fields in the new metadata object
 export const LOAD_DATABASE_FIELDS = "metabase/qb/LOAD_DATABASE_FIELDS";
-export const loadDatabaseFields = createThunkAction(LOAD_DATABASE_FIELDS, (dbId) => {
+export const loadDatabaseFields = createThunkAction(
+  LOAD_DATABASE_FIELDS,
+  dbId => {
     return async (dispatch, getState) => {
-        // if we already have the metadata loaded for the given table then we are done
-        const { qb: { databaseFields } } = getState();
-        try {
-            let fields;
-            if (databaseFields[dbId]) {
-                fields = databaseFields[dbId];
-            } else {
-                fields = await MetabaseApi.db_fields({ dbId: dbId });
-            }
-
-            return {
-                id: dbId,
-                fields: fields
-            };
-        } catch(error) {
-            console.error('error getting database fields', error);
-            return {};
+      // if we already have the metadata loaded for the given table then we are done
+      const { qb: { databaseFields } } = getState();
+      try {
+        let fields;
+        if (databaseFields[dbId]) {
+          fields = databaseFields[dbId];
+        } else {
+          fields = await MetabaseApi.db_fields({ dbId: dbId });
         }
+
+        return {
+          id: dbId,
+          fields: fields,
+        };
+      } catch (error) {
+        console.error("error getting database fields", error);
+        return {};
+      }
     };
-});
+  },
+);
 
 function updateVisualizationSettings(card, isEditing, display, vizSettings) {
-    // don't need to store undefined
-    vizSettings = Utils.copy(vizSettings)
-    for (const name in vizSettings) {
-        if (vizSettings[name] === undefined) {
-            delete vizSettings[name];
-        }
+  // don't need to store undefined
+  vizSettings = Utils.copy(vizSettings);
+  for (const name in vizSettings) {
+    if (vizSettings[name] === undefined) {
+      delete vizSettings[name];
     }
+  }
 
-    // make sure that something actually changed
-    if (card.display === display && _.isEqual(card.visualization_settings, vizSettings)) return card;
+  // make sure that something actually changed
+  if (
+    card.display === display &&
+    _.isEqual(card.visualization_settings, vizSettings)
+  )
+    return card;
 
-    let updatedCard = Utils.copy(card);
+  let updatedCard = Utils.copy(card);
 
-    // when the visualization changes on saved card we change this into a new card w/ a known starting point
-    if (!isEditing && updatedCard.id) {
-        delete updatedCard.id;
-        delete updatedCard.name;
-        delete updatedCard.description;
-    }
+  // when the visualization changes on saved card we change this into a new card w/ a known starting point
+  if (!isEditing && updatedCard.id) {
+    delete updatedCard.id;
+    delete updatedCard.name;
+    delete updatedCard.description;
+  }
 
-    updatedCard.display = display;
-    updatedCard.visualization_settings = vizSettings;
+  updatedCard.display = display;
+  updatedCard.visualization_settings = vizSettings;
 
-    return updatedCard;
+  return updatedCard;
 }
 
 export const SET_CARD_ATTRIBUTE = "metabase/qb/SET_CARD_ATTRIBUTE";
-export const setCardAttribute = createAction(SET_CARD_ATTRIBUTE, (attr, value) => ({attr, value}));
+export const setCardAttribute = createAction(
+  SET_CARD_ATTRIBUTE,
+  (attr, value) => ({ attr, value }),
+);
 
 export const SET_CARD_VISUALIZATION = "metabase/qb/SET_CARD_VISUALIZATION";
-export const setCardVisualization = createThunkAction(SET_CARD_VISUALIZATION, (display) => {
+export const setCardVisualization = createThunkAction(
+  SET_CARD_VISUALIZATION,
+  display => {
     return (dispatch, getState) => {
-        const { qb: { card, uiControls } } = getState();
-        let updatedCard = updateVisualizationSettings(card, uiControls.isEditing, display, card.visualization_settings);
-        dispatch(updateUrl(updatedCard, { dirty: true }));
-        return updatedCard;
-    }
-});
+      const { qb: { card, uiControls } } = getState();
+      let updatedCard = updateVisualizationSettings(
+        card,
+        uiControls.isEditing,
+        display,
+        card.visualization_settings,
+      );
+      dispatch(updateUrl(updatedCard, { dirty: true }));
+      return updatedCard;
+    };
+  },
+);
 
-export const UPDATE_CARD_VISUALIZATION_SETTINGS = "metabase/qb/UPDATE_CARD_VISUALIZATION_SETTINGS";
-export const updateCardVisualizationSettings = createThunkAction(UPDATE_CARD_VISUALIZATION_SETTINGS, (settings) => {
+export const UPDATE_CARD_VISUALIZATION_SETTINGS =
+  "metabase/qb/UPDATE_CARD_VISUALIZATION_SETTINGS";
+export const updateCardVisualizationSettings = createThunkAction(
+  UPDATE_CARD_VISUALIZATION_SETTINGS,
+  settings => {
     return (dispatch, getState) => {
-        const { qb: { card, uiControls } } = getState();
-        let updatedCard = updateVisualizationSettings(card, uiControls.isEditing, card.display, { ...card.visualization_settings, ...settings });
-        dispatch(updateUrl(updatedCard, { dirty: true }));
-        return updatedCard;
+      const { qb: { card, uiControls } } = getState();
+      let updatedCard = updateVisualizationSettings(
+        card,
+        uiControls.isEditing,
+        card.display,
+        { ...card.visualization_settings, ...settings },
+      );
+      dispatch(updateUrl(updatedCard, { dirty: true }));
+      return updatedCard;
     };
-});
+  },
+);
 
-export const REPLACE_ALL_CARD_VISUALIZATION_SETTINGS = "metabase/qb/REPLACE_ALL_CARD_VISUALIZATION_SETTINGS";
-export const replaceAllCardVisualizationSettings = createThunkAction(REPLACE_ALL_CARD_VISUALIZATION_SETTINGS, (settings) => {
+export const REPLACE_ALL_CARD_VISUALIZATION_SETTINGS =
+  "metabase/qb/REPLACE_ALL_CARD_VISUALIZATION_SETTINGS";
+export const replaceAllCardVisualizationSettings = createThunkAction(
+  REPLACE_ALL_CARD_VISUALIZATION_SETTINGS,
+  settings => {
     return (dispatch, getState) => {
-        const { qb: { card, uiControls } } = getState();
-        let updatedCard = updateVisualizationSettings(card, uiControls.isEditing, card.display, settings);
-        dispatch(updateUrl(updatedCard, { dirty: true }));
-        return updatedCard;
+      const { qb: { card, uiControls } } = getState();
+      let updatedCard = updateVisualizationSettings(
+        card,
+        uiControls.isEditing,
+        card.display,
+        settings,
+      );
+      dispatch(updateUrl(updatedCard, { dirty: true }));
+      return updatedCard;
     };
-});
+  },
+);
 
 export const UPDATE_TEMPLATE_TAG = "metabase/qb/UPDATE_TEMPLATE_TAG";
-export const updateTemplateTag = createThunkAction(UPDATE_TEMPLATE_TAG, (templateTag) => {
+export const updateTemplateTag = createThunkAction(
+  UPDATE_TEMPLATE_TAG,
+  templateTag => {
     return (dispatch, getState) => {
-        const { qb: { card, uiControls } } = getState();
+      const { qb: { card, uiControls } } = getState();
 
-        let updatedCard = Utils.copy(card);
+      let updatedCard = Utils.copy(card);
 
-        // when the query changes on saved card we change this into a new query w/ a known starting point
-        if (!uiControls.isEditing && updatedCard.id) {
-            delete updatedCard.id;
-            delete updatedCard.name;
-            delete updatedCard.description;
-        }
+      // when the query changes on saved card we change this into a new query w/ a known starting point
+      if (!uiControls.isEditing && updatedCard.id) {
+        delete updatedCard.id;
+        delete updatedCard.name;
+        delete updatedCard.description;
+      }
 
-        return assocIn(updatedCard, ["dataset_query", "native", "template_tags", templateTag.name], templateTag);
+      return assocIn(
+        updatedCard,
+        ["dataset_query", "native", "template_tags", templateTag.name],
+        templateTag,
+      );
     };
-});
+  },
+);
 
 export const SET_PARAMETER_VALUE = "metabase/qb/SET_PARAMETER_VALUE";
-export const setParameterValue = createAction(SET_PARAMETER_VALUE, (parameterId, value) => {
+export const setParameterValue = createAction(
+  SET_PARAMETER_VALUE,
+  (parameterId, value) => {
     return { id: parameterId, value };
-});
+  },
+);
 
 // reloadCard
 export const RELOAD_CARD = "metabase/qb/RELOAD_CARD";
 export const reloadCard = createThunkAction(RELOAD_CARD, () => {
-    return async (dispatch, getState) => {
-        // clone
-        let card = Utils.copy(getOriginalCard(getState()));
+  return async (dispatch, getState) => {
+    // clone
+    let card = Utils.copy(getOriginalCard(getState()));
 
-        dispatch(loadMetadataForCard(card));
+    dispatch(loadMetadataForCard(card));
 
-        // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
-        dispatch(runQuestionQuery({ overrideWithCard: card, shouldUpdateUrl: false }));
-        dispatch(updateUrl(card, { dirty: false }));
+    // we do this to force the indication of the fact that the card should not be considered dirty when the url is updated
+    dispatch(
+      runQuestionQuery({ overrideWithCard: card, shouldUpdateUrl: false }),
+    );
+    dispatch(updateUrl(card, { dirty: false }));
 
-        return card;
-    };
+    return card;
+  };
 });
 
 /**
@@ -554,24 +668,24 @@ export const reloadCard = createThunkAction(RELOAD_CARD, () => {
  */
 export const SET_CARD_AND_RUN = "metabase/qb/SET_CARD_AND_RUN";
 export const setCardAndRun = (nextCard, shouldUpdateUrl = true) => {
-    return async (dispatch, getState) => {
-        // clone
-        const card = Utils.copy(nextCard);
-
-        const originalCard = card.original_card_id ?
-            // If the original card id is present, dynamically load its information for showing lineage
-            await loadCard(card.original_card_id)
-            // Otherwise, use a current card as the original card if the card has been saved
-            // This is needed for checking whether the card is in dirty state or not
-            : (card.id ? card : null);
-
-        // Update the card and originalCard before running the actual query
-        dispatch.action(SET_CARD_AND_RUN, { card, originalCard })
-        dispatch(runQuestionQuery({ shouldUpdateUrl }));
-
-        // Load table & database metadata for the current question
-        dispatch(loadMetadataForCard(card));
-    };
+  return async (dispatch, getState) => {
+    // clone
+    const card = Utils.copy(nextCard);
+
+    const originalCard = card.original_card_id
+      ? // If the original card id is present, dynamically load its information for showing lineage
+        await loadCard(card.original_card_id)
+      : // Otherwise, use a current card as the original card if the card has been saved
+        // This is needed for checking whether the card is in dirty state or not
+        card.id ? card : null;
+
+    // Update the card and originalCard before running the actual query
+    dispatch.action(SET_CARD_AND_RUN, { card, originalCard });
+    dispatch(runQuestionQuery({ shouldUpdateUrl }));
+
+    // Load table & database metadata for the current question
+    dispatch(loadMetadataForCard(card));
+  };
 };
 
 /**
@@ -586,18 +700,25 @@ export const setCardAndRun = (nextCard, shouldUpdateUrl = true) => {
  * All these events can be applied either for an unsaved question or a saved question.
  */
 export const NAVIGATE_TO_NEW_CARD = "metabase/qb/NAVIGATE_TO_NEW_CARD";
-export const navigateToNewCardInsideQB = createThunkAction(NAVIGATE_TO_NEW_CARD, ({ nextCard, previousCard }) => {
+export const navigateToNewCardInsideQB = createThunkAction(
+  NAVIGATE_TO_NEW_CARD,
+  ({ nextCard, previousCard }) => {
     return async (dispatch, getState) => {
-        const nextCardIsClean = _.isEqual(previousCard.dataset_query, nextCard.dataset_query) && previousCard.display === nextCard.display;
-
-        if (nextCardIsClean) {
-            // This is mainly a fallback for scenarios where a visualization legend is clicked inside QB
-            dispatch(setCardAndRun(await loadCard(nextCard.id)));
-        } else {
-            dispatch(setCardAndRun(getCardAfterVisualizationClick(nextCard, previousCard)));
-        }
-    }
-});
+      const nextCardIsClean =
+        _.isEqual(previousCard.dataset_query, nextCard.dataset_query) &&
+        previousCard.display === nextCard.display;
+
+      if (nextCardIsClean) {
+        // This is mainly a fallback for scenarios where a visualization legend is clicked inside QB
+        dispatch(setCardAndRun(await loadCard(nextCard.id)));
+      } else {
+        dispatch(
+          setCardAndRun(getCardAfterVisualizationClick(nextCard, previousCard)),
+        );
+      }
+    };
+  },
+);
 
 // TODO Atte Keinänen 6/2/2017 See if we should stick to `updateX` naming convention instead of `setX` in all Redux actions
 // We talked with Tom that `setX` method names could be reserved to metabase-lib classes
@@ -608,193 +729,220 @@ export const navigateToNewCardInsideQB = createThunkAction(NAVIGATE_TO_NEW_CARD,
  */
 export const UPDATE_QUESTION = "metabase/qb/UPDATE_QUESTION";
 export const updateQuestion = (newQuestion, { doNotClearNameAndId } = {}) => {
-    return (dispatch, getState) => {
-        // TODO Atte Keinänen 6/2/2017 Ways to have this happen automatically when modifying a question?
-        // Maybe the Question class or a QB-specific question wrapper class should know whether it's being edited or not?
-        if (!doNotClearNameAndId && !getIsEditing(getState()) && newQuestion.isSaved()) {
-            newQuestion = newQuestion.withoutNameAndId();
-        }
-
-        // Replace the current question with a new one
-        dispatch.action(UPDATE_QUESTION, { card: newQuestion.card() });
+  return (dispatch, getState) => {
+    // TODO Atte Keinänen 6/2/2017 Ways to have this happen automatically when modifying a question?
+    // Maybe the Question class or a QB-specific question wrapper class should know whether it's being edited or not?
+    if (
+      !doNotClearNameAndId &&
+      !getIsEditing(getState()) &&
+      newQuestion.isSaved()
+    ) {
+      newQuestion = newQuestion.withoutNameAndId();
+    }
 
-        // See if the template tags editor should be shown/hidden
-        const oldQuestion = getQuestion(getState());
-        const oldTagCount = getTemplateTagCount(oldQuestion);
-        const newTagCount = getTemplateTagCount(newQuestion);
+    // Replace the current question with a new one
+    dispatch.action(UPDATE_QUESTION, { card: newQuestion.card() });
 
-        if (newTagCount > oldTagCount) {
-            dispatch(setIsShowingTemplateTagsEditor(true));
-        } else if (newTagCount === 0 && !getIsShowingDataReference(getState())) {
-            dispatch(setIsShowingTemplateTagsEditor(false));
-        }
+    // See if the template tags editor should be shown/hidden
+    const oldQuestion = getQuestion(getState());
+    const oldTagCount = getTemplateTagCount(oldQuestion);
+    const newTagCount = getTemplateTagCount(newQuestion);
 
-    };
+    if (newTagCount > oldTagCount) {
+      dispatch(setIsShowingTemplateTagsEditor(true));
+    } else if (newTagCount === 0 && !getIsShowingDataReference(getState())) {
+      dispatch(setIsShowingTemplateTagsEditor(false));
+    }
+  };
 };
 
 export const API_CREATE_QUESTION = "metabase/qb/API_CREATE_QUESTION";
-export const apiCreateQuestion = (question) => {
-    return async (dispatch, getState) => {
-        // Needed for persisting visualization columns for pulses/alerts, see #6749
-        const series = getTransformedSeries(getState())
-        const questionWithVizSettings = series ? getQuestionWithDefaultVisualizationSettings(question, series) : question
-
-        let resultsMetadata = getResultsMetadata(getState())
-        const createdQuestion = await (
-            questionWithVizSettings
-                .setQuery(question.query().clean())
-                .setResultsMetadata(resultsMetadata)
-                .apiCreate()
-        )
-
-        // remove the databases in the store that are used to populate the QB databases list.
-        // This is done when saving a Card because the newly saved card will be eligible for use as a source query
-        // so we want the databases list to be re-fetched next time we hit "New Question" so it shows up
-        dispatch(clearRequestState({ statePath: ["metadata", "databases"] }));
-
-        dispatch(updateUrl(createdQuestion.card(), { dirty: false }));
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Create Card", createdQuestion.query().datasetQuery().type);
-
-        dispatch.action(API_CREATE_QUESTION, createdQuestion.card())
-    }
-}
-
-export const API_UPDATE_QUESTION = "metabase/qb/API_UPDATE_QUESTION";
-export const apiUpdateQuestion = (question) => {
-    return async (dispatch, getState) => {
-        question = question || getQuestion(getState())
-
-        // Needed for persisting visualization columns for pulses/alerts, see #6749
-        const series = getTransformedSeries(getState())
-        const questionWithVizSettings = series ? getQuestionWithDefaultVisualizationSettings(question, series) : question
-
-        let resultsMetadata = getResultsMetadata(getState())
-        const updatedQuestion = await (
-            questionWithVizSettings
-                .setQuery(question.query().clean())
-                .setResultsMetadata(resultsMetadata)
-                .apiUpdate()
-        )
-
-        // reload the question alerts for the current question
-        // (some of the old alerts might be removed during update)
-        await dispatch(fetchAlertsForQuestion(updatedQuestion.id()))
+export const apiCreateQuestion = question => {
+  return async (dispatch, getState) => {
+    // Needed for persisting visualization columns for pulses/alerts, see #6749
+    const series = getTransformedSeries(getState());
+    const questionWithVizSettings = series
+      ? getQuestionWithDefaultVisualizationSettings(question, series)
+      : question;
+
+    let resultsMetadata = getResultsMetadata(getState());
+    const createdQuestion = await questionWithVizSettings
+      .setQuery(question.query().clean())
+      .setResultsMetadata(resultsMetadata)
+      .apiCreate();
+
+    // remove the databases in the store that are used to populate the QB databases list.
+    // This is done when saving a Card because the newly saved card will be eligible for use as a source query
+    // so we want the databases list to be re-fetched next time we hit "New Question" so it shows up
+    dispatch(clearRequestState({ statePath: ["metadata", "databases"] }));
+
+    dispatch(updateUrl(createdQuestion.card(), { dirty: false }));
+    MetabaseAnalytics.trackEvent(
+      "QueryBuilder",
+      "Create Card",
+      createdQuestion.query().datasetQuery().type,
+    );
 
-        // remove the databases in the store that are used to populate the QB databases list.
-        // This is done when saving a Card because the newly saved card will be eligible for use as a source query
-        // so we want the databases list to be re-fetched next time we hit "New Question" so it shows up
-        dispatch(clearRequestState({ statePath: ["metadata", "databases"] }));
+    dispatch.action(API_CREATE_QUESTION, createdQuestion.card());
+  };
+};
 
-        dispatch(updateUrl(updatedQuestion.card(), { dirty: false }));
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Update Card", updatedQuestion.query().datasetQuery().type);
+export const API_UPDATE_QUESTION = "metabase/qb/API_UPDATE_QUESTION";
+export const apiUpdateQuestion = question => {
+  return async (dispatch, getState) => {
+    question = question || getQuestion(getState());
+
+    // Needed for persisting visualization columns for pulses/alerts, see #6749
+    const series = getTransformedSeries(getState());
+    const questionWithVizSettings = series
+      ? getQuestionWithDefaultVisualizationSettings(question, series)
+      : question;
+
+    let resultsMetadata = getResultsMetadata(getState());
+    const updatedQuestion = await questionWithVizSettings
+      .setQuery(question.query().clean())
+      .setResultsMetadata(resultsMetadata)
+      .apiUpdate();
+
+    // reload the question alerts for the current question
+    // (some of the old alerts might be removed during update)
+    await dispatch(fetchAlertsForQuestion(updatedQuestion.id()));
+
+    // remove the databases in the store that are used to populate the QB databases list.
+    // This is done when saving a Card because the newly saved card will be eligible for use as a source query
+    // so we want the databases list to be re-fetched next time we hit "New Question" so it shows up
+    dispatch(clearRequestState({ statePath: ["metadata", "databases"] }));
+
+    dispatch(updateUrl(updatedQuestion.card(), { dirty: false }));
+    MetabaseAnalytics.trackEvent(
+      "QueryBuilder",
+      "Update Card",
+      updatedQuestion.query().datasetQuery().type,
+    );
 
-        dispatch.action(API_UPDATE_QUESTION, updatedQuestion.card())
-    }
-}
+    dispatch.action(API_UPDATE_QUESTION, updatedQuestion.card());
+  };
+};
 
 // setDatasetQuery
 // TODO Atte Keinänen 6/1/17: Deprecated, superseded by updateQuestion
 export const SET_DATASET_QUERY = "metabase/qb/SET_DATASET_QUERY";
-export const setDatasetQuery = createThunkAction(SET_DATASET_QUERY, (dataset_query, run = false) => {
+export const setDatasetQuery = createThunkAction(
+  SET_DATASET_QUERY,
+  (dataset_query, run = false) => {
     return (dispatch, getState) => {
-        const { qb: { uiControls }} = getState();
-        const question = getQuestion(getState());
+      const { qb: { uiControls } } = getState();
+      const question = getQuestion(getState());
 
-        let newQuestion = question;
+      let newQuestion = question;
 
-        // when the query changes on saved card we change this into a new query w/ a known starting point
-        if (!uiControls.isEditing && question.isSaved()) {
-            newQuestion = newQuestion.withoutNameAndId();
-        }
+      // when the query changes on saved card we change this into a new query w/ a known starting point
+      if (!uiControls.isEditing && question.isSaved()) {
+        newQuestion = newQuestion.withoutNameAndId();
+      }
 
-        newQuestion = newQuestion.setDatasetQuery(dataset_query);
+      newQuestion = newQuestion.setDatasetQuery(dataset_query);
 
-        const oldTagCount = getTemplateTagCount(question);
-        const newTagCount = getTemplateTagCount(newQuestion);
+      const oldTagCount = getTemplateTagCount(question);
+      const newTagCount = getTemplateTagCount(newQuestion);
 
-        let openTemplateTagsEditor = uiControls.isShowingTemplateTagsEditor;
-        if (newTagCount > oldTagCount) {
-            openTemplateTagsEditor = true;
-        } else if (newTagCount === 0) {
-            openTemplateTagsEditor = false;
-        }
+      let openTemplateTagsEditor = uiControls.isShowingTemplateTagsEditor;
+      if (newTagCount > oldTagCount) {
+        openTemplateTagsEditor = true;
+      } else if (newTagCount === 0) {
+        openTemplateTagsEditor = false;
+      }
 
-        // run updated query
-        if (run) {
-            dispatch(runQuestionQuery({ overrideWithCard: newQuestion.card() }));
-        }
+      // run updated query
+      if (run) {
+        dispatch(runQuestionQuery({ overrideWithCard: newQuestion.card() }));
+      }
 
-        return {
-            card: newQuestion.card(),
-            openTemplateTagsEditor
-        };
+      return {
+        card: newQuestion.card(),
+        openTemplateTagsEditor,
+      };
     };
-});
+  },
+);
 
 // setQueryMode
 export const SET_QUERY_MODE = "metabase/qb/SET_QUERY_MODE";
-export const setQueryMode = createThunkAction(SET_QUERY_MODE, (type) => {
-    return (dispatch, getState) => {
-        // TODO Atte Keinänen 6/1/17: Should use `queryResults` instead
-        const { qb: { card, queryResult, uiControls } } = getState();
-        const tableMetadata = getTableMetadata(getState());
-
-        // if the type didn't actually change then nothing has been modified
-        if (type === card.dataset_query.type) {
-            return card;
-        }
-
-        // if we are going from MBQL -> Native then attempt to carry over the query
-        if (type === "native" && queryResult && queryResult.data && queryResult.data.native_form) {
-            let updatedCard = Utils.copy(card);
-            let datasetQuery = updatedCard.dataset_query;
-            let nativeQuery = _.pick(queryResult.data.native_form, "query", "collection");
-
-            // when the driver requires JSON we need to stringify it because it's been parsed already
-            if (getEngineNativeType(tableMetadata.db.engine) === "json") {
-                nativeQuery.query = formatJsonQuery(queryResult.data.native_form.query, tableMetadata.db.engine);
-            } else {
-                nativeQuery.query = formatSQL(nativeQuery.query);
-            }
-
-            datasetQuery.type = "native";
-            datasetQuery.native = nativeQuery;
-            delete datasetQuery.query;
-
-            // when the query changes on saved card we change this into a new query w/ a known starting point
-            if (!uiControls.isEditing && updatedCard.id) {
-                delete updatedCard.id;
-                delete updatedCard.name;
-                delete updatedCard.description;
-            }
+export const setQueryMode = createThunkAction(SET_QUERY_MODE, type => {
+  return (dispatch, getState) => {
+    // TODO Atte Keinänen 6/1/17: Should use `queryResults` instead
+    const { qb: { card, queryResult, uiControls } } = getState();
+    const tableMetadata = getTableMetadata(getState());
+
+    // if the type didn't actually change then nothing has been modified
+    if (type === card.dataset_query.type) {
+      return card;
+    }
 
-            updatedCard.dataset_query = datasetQuery;
+    // if we are going from MBQL -> Native then attempt to carry over the query
+    if (
+      type === "native" &&
+      queryResult &&
+      queryResult.data &&
+      queryResult.data.native_form
+    ) {
+      let updatedCard = Utils.copy(card);
+      let datasetQuery = updatedCard.dataset_query;
+      let nativeQuery = _.pick(
+        queryResult.data.native_form,
+        "query",
+        "collection",
+      );
+
+      // when the driver requires JSON we need to stringify it because it's been parsed already
+      if (getEngineNativeType(tableMetadata.db.engine) === "json") {
+        nativeQuery.query = formatJsonQuery(
+          queryResult.data.native_form.query,
+          tableMetadata.db.engine,
+        );
+      } else {
+        nativeQuery.query = formatSQL(nativeQuery.query);
+      }
+
+      datasetQuery.type = "native";
+      datasetQuery.native = nativeQuery;
+      delete datasetQuery.query;
+
+      // when the query changes on saved card we change this into a new query w/ a known starting point
+      if (!uiControls.isEditing && updatedCard.id) {
+        delete updatedCard.id;
+        delete updatedCard.name;
+        delete updatedCard.description;
+      }
 
-            dispatch(loadMetadataForCard(updatedCard));
+      updatedCard.dataset_query = datasetQuery;
 
-            MetabaseAnalytics.trackEvent("QueryBuilder", "MBQL->Native");
+      dispatch(loadMetadataForCard(updatedCard));
 
-            return updatedCard;
+      MetabaseAnalytics.trackEvent("QueryBuilder", "MBQL->Native");
 
-        // we are translating an empty query
-        } else {
-            let databaseId = card.dataset_query.database;
+      return updatedCard;
 
-            // only carry over the database id if the user can write native queries
-            if (type === "native") {
-                let nativeDatabases = getNativeDatabases(getState());
-                if (!_.findWhere(nativeDatabases, { id: databaseId })) {
-                    databaseId = nativeDatabases.length > 0 ? nativeDatabases[0].id : null
-                }
-            }
+      // we are translating an empty query
+    } else {
+      let databaseId = card.dataset_query.database;
+
+      // only carry over the database id if the user can write native queries
+      if (type === "native") {
+        let nativeDatabases = getNativeDatabases(getState());
+        if (!_.findWhere(nativeDatabases, { id: databaseId })) {
+          databaseId =
+            nativeDatabases.length > 0 ? nativeDatabases[0].id : null;
+        }
+      }
 
-            let newCard = startNewCard(type, databaseId);
+      let newCard = startNewCard(type, databaseId);
 
-            dispatch(loadMetadataForCard(newCard));
+      dispatch(loadMetadataForCard(newCard));
 
-            return newCard;
-        }
-    };
+      return newCard;
+    }
+  };
 });
 
 // TODO Atte Keinänen: The heavy lifting should be moved to StructuredQuery and NativeQuery
@@ -802,196 +950,209 @@ export const setQueryMode = createThunkAction(SET_QUERY_MODE, (type) => {
 
 // setQueryDatabase
 export const SET_QUERY_DATABASE = "metabase/qb/SET_QUERY_DATABASE";
-export const setQueryDatabase = createThunkAction(SET_QUERY_DATABASE, (databaseId) => {
+export const setQueryDatabase = createThunkAction(
+  SET_QUERY_DATABASE,
+  databaseId => {
     return async (dispatch, getState) => {
-        const { qb: { card, uiControls } } = getState();
-        const databases = getDatabases(getState());
+      const { qb: { card, uiControls } } = getState();
+      const databases = getDatabases(getState());
 
-        // picking the same database doesn't change anything
-        if (databaseId === card.dataset_query.database) {
-            return card;
+      // picking the same database doesn't change anything
+      if (databaseId === card.dataset_query.database) {
+        return card;
+      }
+
+      let existingQuery = card.dataset_query.native
+        ? card.dataset_query.native.query
+        : undefined;
+      if (!uiControls.isEditing) {
+        let updatedCard = startNewCard(card.dataset_query.type, databaseId);
+        if (existingQuery) {
+          updatedCard.dataset_query.native.query = existingQuery;
+          updatedCard.dataset_query.native.template_tags =
+            card.dataset_query.native.template_tags;
         }
 
-        let existingQuery = (card.dataset_query.native) ? card.dataset_query.native.query : undefined;
-        if (!uiControls.isEditing) {
-            let updatedCard = startNewCard(card.dataset_query.type, databaseId);
-            if (existingQuery) {
-                updatedCard.dataset_query.native.query = existingQuery;
-                updatedCard.dataset_query.native.template_tags = card.dataset_query.native.template_tags;
-            }
-
-            // set the initial collection for the query if this is a native query
-            // this is only used for Mongo queries which need to be ran against a specific collection
-            if (updatedCard.dataset_query.type === 'native') {
-                let database = databases[databaseId],
-                    tables   = database ? database.tables : [],
-                    table    = tables.length > 0 ? tables[0] : null;
-                if (table) updatedCard.dataset_query.native.collection = table.name;
-            }
-
-            dispatch(loadMetadataForCard(updatedCard));
-
-            return updatedCard;
-        } else {
-            // if we are editing a saved query we don't want to replace the card, so just start a fresh query only
-            // TODO: should this clear the visualization as well?
-            let updatedCard = Utils.copy(card);
-            updatedCard.dataset_query = createQuery(card.dataset_query.type, databaseId);
-            if (existingQuery) {
-                updatedCard.dataset_query.native.query = existingQuery;
-                updatedCard.dataset_query.native.template_tags = card.dataset_query.native.template_tags;
-            }
-
-            dispatch(loadMetadataForCard(updatedCard));
-
-            return updatedCard;
+        // set the initial collection for the query if this is a native query
+        // this is only used for Mongo queries which need to be ran against a specific collection
+        if (updatedCard.dataset_query.type === "native") {
+          let database = databases[databaseId],
+            tables = database ? database.tables : [],
+            table = tables.length > 0 ? tables[0] : null;
+          if (table) updatedCard.dataset_query.native.collection = table.name;
         }
+
+        dispatch(loadMetadataForCard(updatedCard));
+
+        return updatedCard;
+      } else {
+        // if we are editing a saved query we don't want to replace the card, so just start a fresh query only
+        // TODO: should this clear the visualization as well?
+        let updatedCard = Utils.copy(card);
+        updatedCard.dataset_query = createQuery(
+          card.dataset_query.type,
+          databaseId,
+        );
+        if (existingQuery) {
+          updatedCard.dataset_query.native.query = existingQuery;
+          updatedCard.dataset_query.native.template_tags =
+            card.dataset_query.native.template_tags;
+        }
+
+        dispatch(loadMetadataForCard(updatedCard));
+
+        return updatedCard;
+      }
     };
-});
+  },
+);
 
 // TODO Atte Keinänen: The heavy lifting should be moved to StructuredQuery and NativeQuery
 // Question.js could possibly provide a helper method like `Question.setSourceTable` that delegates it to respective query classes
 
 // setQuerySourceTable
 export const SET_QUERY_SOURCE_TABLE = "metabase/qb/SET_QUERY_SOURCE_TABLE";
-export const setQuerySourceTable = createThunkAction(SET_QUERY_SOURCE_TABLE, (sourceTable) => {
+export const setQuerySourceTable = createThunkAction(
+  SET_QUERY_SOURCE_TABLE,
+  sourceTable => {
     return async (dispatch, getState) => {
-        const { qb: { card, uiControls } } = getState();
-
-        // this will either be the id or an object with an id
-        const tableId = sourceTable.id || sourceTable;
+      const { qb: { card, uiControls } } = getState();
 
-        // if the table didn't actually change then nothing is modified
-        if (tableId === card.dataset_query.query.source_table) {
-            return card;
-        }
-
-        // load up all the table metadata via the api
-        dispatch(loadTableMetadata(tableId));
+      // this will either be the id or an object with an id
+      const tableId = sourceTable.id || sourceTable;
 
-        // find the database associated with this table
-        let databaseId;
-        if (_.isObject(sourceTable)) {
-            databaseId = sourceTable.db_id;
-        } else {
-            const table = getTables(getState())[tableId];
-            if (table) {
-                databaseId = table.db_id;
-            }
+      // if the table didn't actually change then nothing is modified
+      if (tableId === card.dataset_query.query.source_table) {
+        return card;
+      }
+
+      // load up all the table metadata via the api
+      dispatch(loadTableMetadata(tableId));
+
+      // find the database associated with this table
+      let databaseId;
+      if (_.isObject(sourceTable)) {
+        databaseId = sourceTable.db_id;
+      } else {
+        const table = getTables(getState())[tableId];
+        if (table) {
+          databaseId = table.db_id;
         }
+      }
 
-        if (!uiControls.isEditing) {
-            return startNewCard(card.dataset_query.type, databaseId, tableId);
-        } else {
-            // if we are editing a saved query we don't want to replace the card, so just start a fresh query only
-            // TODO: should this clear the visualization as well?
-            let query = createQuery(card.dataset_query.type, databaseId, tableId);
+      if (!uiControls.isEditing) {
+        return startNewCard(card.dataset_query.type, databaseId, tableId);
+      } else {
+        // if we are editing a saved query we don't want to replace the card, so just start a fresh query only
+        // TODO: should this clear the visualization as well?
+        let query = createQuery(card.dataset_query.type, databaseId, tableId);
 
-            let updatedCard = Utils.copy(card);
-            updatedCard.dataset_query = query;
-            return updatedCard;
-        }
+        let updatedCard = Utils.copy(card);
+        updatedCard.dataset_query = query;
+        return updatedCard;
+      }
     };
-});
+  },
+);
 
 function createQueryAction(action, updaterFunction, event) {
-    return createThunkAction(action, (...args) =>
-        (dispatch, getState) => {
-            const { qb: { card } } = getState();
-            if (card.dataset_query.type === "query") {
-                const datasetQuery = Utils.copy(card.dataset_query);
-                updaterFunction(datasetQuery.query, ...args);
-                dispatch(setDatasetQuery(datasetQuery));
-                MetabaseAnalytics.trackEvent(...(typeof event === "function" ? event(...args) : event));
-            }
-            return null;
-        }
-    );
+  return createThunkAction(action, (...args) => (dispatch, getState) => {
+    const { qb: { card } } = getState();
+    if (card.dataset_query.type === "query") {
+      const datasetQuery = Utils.copy(card.dataset_query);
+      updaterFunction(datasetQuery.query, ...args);
+      dispatch(setDatasetQuery(datasetQuery));
+      MetabaseAnalytics.trackEvent(
+        ...(typeof event === "function" ? event(...args) : event),
+      );
+    }
+    return null;
+  });
 }
 
 export const addQueryBreakout = createQueryAction(
-    "metabase/qb/ADD_QUERY_BREAKOUT",
-    Query.addBreakout,
-    ["QueryBuilder", "Add GroupBy"]
+  "metabase/qb/ADD_QUERY_BREAKOUT",
+  Query.addBreakout,
+  ["QueryBuilder", "Add GroupBy"],
 );
 export const updateQueryBreakout = createQueryAction(
-    "metabase/qb/UPDATE_QUERY_BREAKOUT",
-    Query.updateBreakout,
-    ["QueryBuilder", "Modify GroupBy"]
+  "metabase/qb/UPDATE_QUERY_BREAKOUT",
+  Query.updateBreakout,
+  ["QueryBuilder", "Modify GroupBy"],
 );
 export const removeQueryBreakout = createQueryAction(
-    "metabase/qb/REMOVE_QUERY_BREAKOUT",
-    Query.removeBreakout,
-    ["QueryBuilder", "Remove GroupBy"]
+  "metabase/qb/REMOVE_QUERY_BREAKOUT",
+  Query.removeBreakout,
+  ["QueryBuilder", "Remove GroupBy"],
 );
 // Exported for integration tests
-export const ADD_QUERY_FILTER = "metabase/qb/ADD_QUERY_FILTER"
+export const ADD_QUERY_FILTER = "metabase/qb/ADD_QUERY_FILTER";
 export const addQueryFilter = createQueryAction(
-    ADD_QUERY_FILTER,
-    Query.addFilter,
-    ["QueryBuilder", "Add Filter"]
+  ADD_QUERY_FILTER,
+  Query.addFilter,
+  ["QueryBuilder", "Add Filter"],
 );
 export const UPDATE_QUERY_FILTER = "metabase/qb/UPDATE_QUERY_FILTER";
 export const updateQueryFilter = createQueryAction(
-    UPDATE_QUERY_FILTER,
-    Query.updateFilter,
-    ["QueryBuilder", "Modify Filter"]
+  UPDATE_QUERY_FILTER,
+  Query.updateFilter,
+  ["QueryBuilder", "Modify Filter"],
 );
 export const REMOVE_QUERY_FILTER = "metabase/qb/REMOVE_QUERY_FILTER";
 export const removeQueryFilter = createQueryAction(
-    REMOVE_QUERY_FILTER,
-    Query.removeFilter,
-    ["QueryBuilder", "Remove Filter"]
+  REMOVE_QUERY_FILTER,
+  Query.removeFilter,
+  ["QueryBuilder", "Remove Filter"],
 );
 export const addQueryAggregation = createQueryAction(
-    "metabase/qb/ADD_QUERY_AGGREGATION",
-    Query.addAggregation,
-    ["QueryBuilder", "Add Aggregation"]
+  "metabase/qb/ADD_QUERY_AGGREGATION",
+  Query.addAggregation,
+  ["QueryBuilder", "Add Aggregation"],
 );
 export const updateQueryAggregation = createQueryAction(
-    "metabase/qb/UPDATE_QUERY_AGGREGATION",
-    Query.updateAggregation,
-    ["QueryBuilder", "Set Aggregation"]
+  "metabase/qb/UPDATE_QUERY_AGGREGATION",
+  Query.updateAggregation,
+  ["QueryBuilder", "Set Aggregation"],
 );
 export const removeQueryAggregation = createQueryAction(
-    "metabase/qb/REMOVE_QUERY_AGGREGATION",
-    Query.removeAggregation,
-    ["QueryBuilder", "Remove Aggregation"]
+  "metabase/qb/REMOVE_QUERY_AGGREGATION",
+  Query.removeAggregation,
+  ["QueryBuilder", "Remove Aggregation"],
 );
 export const addQueryOrderBy = createQueryAction(
-    "metabase/qb/ADD_QUERY_ORDER_BY",
-    Query.addOrderBy,
-    ["QueryBuilder", "Add OrderBy"]
+  "metabase/qb/ADD_QUERY_ORDER_BY",
+  Query.addOrderBy,
+  ["QueryBuilder", "Add OrderBy"],
 );
 export const updateQueryOrderBy = createQueryAction(
-    "metabase/qb/UPDATE_QUERY_ORDER_BY",
-    Query.updateOrderBy,
-    ["QueryBuilder", "Set OrderBy"]
+  "metabase/qb/UPDATE_QUERY_ORDER_BY",
+  Query.updateOrderBy,
+  ["QueryBuilder", "Set OrderBy"],
 );
 export const removeQueryOrderBy = createQueryAction(
-    "metabase/qb/REMOVE_QUERY_ORDER_BY",
-    Query.removeOrderBy,
-    ["QueryBuilder", "Remove OrderBy"]
+  "metabase/qb/REMOVE_QUERY_ORDER_BY",
+  Query.removeOrderBy,
+  ["QueryBuilder", "Remove OrderBy"],
 );
 export const updateQueryLimit = createQueryAction(
-    "metabase/qb/UPDATE_QUERY_LIMIT",
-    Query.updateLimit,
-    ["QueryBuilder", "Update Limit"]
+  "metabase/qb/UPDATE_QUERY_LIMIT",
+  Query.updateLimit,
+  ["QueryBuilder", "Update Limit"],
 );
 export const addQueryExpression = createQueryAction(
-    "metabase/qb/ADD_QUERY_EXPRESSION",
-    Query.addExpression,
-    ["QueryBuilder", "Add Expression"]
+  "metabase/qb/ADD_QUERY_EXPRESSION",
+  Query.addExpression,
+  ["QueryBuilder", "Add Expression"],
 );
 export const updateQueryExpression = createQueryAction(
-    "metabase/qb/UPDATE_QUERY_EXPRESSION",
-    Query.updateExpression,
-    ["QueryBuilder", "Set Expression"]
+  "metabase/qb/UPDATE_QUERY_EXPRESSION",
+  Query.updateExpression,
+  ["QueryBuilder", "Set Expression"],
 );
 export const removeQueryExpression = createQueryAction(
-    "metabase/qb/REMOVE_QUERY_EXPRESSION",
-    Query.removeExpression,
-    ["QueryBuilder", "Remove Expression"]
+  "metabase/qb/REMOVE_QUERY_EXPRESSION",
+  Query.removeExpression,
+  ["QueryBuilder", "Remove Expression"],
 );
 
 /**
@@ -999,85 +1160,105 @@ export const removeQueryExpression = createQueryAction(
  * The API queries triggered by this action creator can be cancelled using the deferred provided in RUN_QUERY action.
  */
 export type RunQueryParams = {
-    shouldUpdateUrl?: boolean,
-    ignoreCache?: boolean, // currently only implemented for saved cards
-    overrideWithCard?: Card // override the current question with the provided card
-}
+  shouldUpdateUrl?: boolean,
+  ignoreCache?: boolean, // currently only implemented for saved cards
+  overrideWithCard?: Card, // override the current question with the provided card
+};
 export const RUN_QUERY = "metabase/qb/RUN_QUERY";
 export const runQuestionQuery = ({
-    shouldUpdateUrl = true,
-    ignoreCache = false,
-    overrideWithCard
-} : RunQueryParams = {}) => {
-    return async (dispatch, getState) => {
-        const questionFromCard = (c: Card): Question => c && new Question(getMetadata(getState()), c);
-
-        const question: Question = overrideWithCard ? questionFromCard(overrideWithCard) : getQuestion(getState());
-        const originalQuestion: ?Question = getOriginalQuestion(getState());
-
-        const cardIsDirty = originalQuestion ? question.isDirtyComparedTo(originalQuestion) : true;
-
-        if (shouldUpdateUrl) {
-            dispatch(updateUrl(question.card(), { dirty: cardIsDirty }));
-        }
-
-        const startTime = new Date();
-        const cancelQueryDeferred = defer();
-
-        question.apiGetResults({ cancelDeferred: cancelQueryDeferred, isDirty: cardIsDirty })
-            .then((queryResults) => dispatch(queryCompleted(question.card(), queryResults)))
-            .catch((error) => dispatch(queryErrored(startTime, error)));
+  shouldUpdateUrl = true,
+  ignoreCache = false,
+  overrideWithCard,
+}: RunQueryParams = {}) => {
+  return async (dispatch, getState) => {
+    const questionFromCard = (c: Card): Question =>
+      c && new Question(getMetadata(getState()), c);
+
+    const question: Question = overrideWithCard
+      ? questionFromCard(overrideWithCard)
+      : getQuestion(getState());
+    const originalQuestion: ?Question = getOriginalQuestion(getState());
+
+    const cardIsDirty = originalQuestion
+      ? question.isDirtyComparedTo(originalQuestion)
+      : true;
+
+    if (shouldUpdateUrl) {
+      dispatch(updateUrl(question.card(), { dirty: cardIsDirty }));
+    }
 
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Run Query", question.query().datasetQuery().type);
+    const startTime = new Date();
+    const cancelQueryDeferred = defer();
+
+    question
+      .apiGetResults({
+        cancelDeferred: cancelQueryDeferred,
+        isDirty: cardIsDirty,
+      })
+      .then(queryResults =>
+        dispatch(queryCompleted(question.card(), queryResults)),
+      )
+      .catch(error => dispatch(queryErrored(startTime, error)));
+
+    MetabaseAnalytics.trackEvent(
+      "QueryBuilder",
+      "Run Query",
+      question.query().datasetQuery().type,
+    );
 
-        // TODO Move this out from Redux action asap
-        // HACK: prevent SQL editor from losing focus
-        try { ace.edit("id_sql").focus() } catch (e) {}
+    // TODO Move this out from Redux action asap
+    // HACK: prevent SQL editor from losing focus
+    try {
+      ace.edit("id_sql").focus();
+    } catch (e) {}
 
-        dispatch.action(RUN_QUERY, { cancelQueryDeferred });
-    };
+    dispatch.action(RUN_QUERY, { cancelQueryDeferred });
+  };
 };
 
 export const getDisplayTypeForCard = (card, queryResults) => {
-    // TODO Atte Keinänen 6/1/17: Make a holistic decision based on all queryResults, not just one
-    // This method seems to has been a candidate for a rewrite anyway
-    const queryResult = queryResults[0];
-
-    let cardDisplay = card.display;
-
-    // try a little logic to pick a smart display for the data
-    // TODO: less hard-coded rules for picking chart type
-    const isScalarVisualization = card.display === "scalar" || card.display === "progress";
-    if (!isScalarVisualization &&
-        queryResult.data.rows &&
-        queryResult.data.rows.length === 1 &&
-        queryResult.data.cols.length === 1) {
-        // if we have a 1x1 data result then this should always be viewed as a scalar
-        cardDisplay = "scalar";
-
-    } else if (isScalarVisualization &&
-        queryResult.data.rows &&
-        (queryResult.data.rows.length > 1 || queryResult.data.cols.length > 1)) {
-        // any time we were a scalar and now have more than 1x1 data switch to table view
-        cardDisplay = "table";
-
-    } else if (!card.display) {
-        // if our query aggregation is "rows" then ALWAYS set the display to "table"
-        cardDisplay = "table";
-    }
-
-    return cardDisplay;
+  // TODO Atte Keinänen 6/1/17: Make a holistic decision based on all queryResults, not just one
+  // This method seems to has been a candidate for a rewrite anyway
+  const queryResult = queryResults[0];
+
+  let cardDisplay = card.display;
+
+  // try a little logic to pick a smart display for the data
+  // TODO: less hard-coded rules for picking chart type
+  const isScalarVisualization =
+    card.display === "scalar" || card.display === "progress";
+  if (
+    !isScalarVisualization &&
+    queryResult.data.rows &&
+    queryResult.data.rows.length === 1 &&
+    queryResult.data.cols.length === 1
+  ) {
+    // if we have a 1x1 data result then this should always be viewed as a scalar
+    cardDisplay = "scalar";
+  } else if (
+    isScalarVisualization &&
+    queryResult.data.rows &&
+    (queryResult.data.rows.length > 1 || queryResult.data.cols.length > 1)
+  ) {
+    // any time we were a scalar and now have more than 1x1 data switch to table view
+    cardDisplay = "table";
+  } else if (!card.display) {
+    // if our query aggregation is "rows" then ALWAYS set the display to "table"
+    cardDisplay = "table";
+  }
+
+  return cardDisplay;
 };
 
 export const QUERY_COMPLETED = "metabase/qb/QUERY_COMPLETED";
 export const queryCompleted = (card, queryResults) => {
-    return async (dispatch, getState) => {
-        dispatch.action(QUERY_COMPLETED, {
-            card,
-            cardDisplay: getDisplayTypeForCard(card, queryResults),
-            queryResults
-        })
-    };
+  return async (dispatch, getState) => {
+    dispatch.action(QUERY_COMPLETED, {
+      card,
+      cardDisplay: getDisplayTypeForCard(card, queryResults),
+      queryResults,
+    });
+  };
 };
 
 /**
@@ -1087,191 +1268,225 @@ export const queryCompleted = (card, queryResults) => {
  * Needed for persisting visualization columns for pulses/alerts, see #6749.
  */
 const getQuestionWithDefaultVisualizationSettings = (question, series) => {
-    const oldVizSettings = question.visualizationSettings()
-    const newVizSettings = { ...getPersistableDefaultSettings(series), ...oldVizSettings }
-
-    // Don't update the question unnecessarily
-    // (even if fields values haven't changed, updating the settings will make the question appear dirty)
-    if (!_.isEqual(oldVizSettings, newVizSettings)) {
-        return question.setVisualizationSettings(newVizSettings)
-    } else {
-        return question
-    }
-}
+  const oldVizSettings = question.visualizationSettings();
+  const newVizSettings = {
+    ...getPersistableDefaultSettings(series),
+    ...oldVizSettings,
+  };
+
+  // Don't update the question unnecessarily
+  // (even if fields values haven't changed, updating the settings will make the question appear dirty)
+  if (!_.isEqual(oldVizSettings, newVizSettings)) {
+    return question.setVisualizationSettings(newVizSettings);
+  } else {
+    return question;
+  }
+};
 
 export const QUERY_ERRORED = "metabase/qb/QUERY_ERRORED";
-export const queryErrored = createThunkAction(QUERY_ERRORED, (startTime, error) => {
+export const queryErrored = createThunkAction(
+  QUERY_ERRORED,
+  (startTime, error) => {
     return async (dispatch, getState) => {
-        if (error && error.isCancelled) {
-            // cancelled, do nothing
-            return null;
-        } else {
-            return { error: error, duration: new Date() - startTime };
-        }
-    }
-})
+      if (error && error.isCancelled) {
+        // cancelled, do nothing
+        return null;
+      } else {
+        return { error: error, duration: new Date() - startTime };
+      }
+    };
+  },
+);
 
 // cancelQuery
 export const CANCEL_QUERY = "metabase/qb/CANCEL_QUERY";
 export const cancelQuery = createThunkAction(CANCEL_QUERY, () => {
-    return async (dispatch, getState) => {
-        const { qb: { uiControls, cancelQueryDeferred } } = getState();
+  return async (dispatch, getState) => {
+    const { qb: { uiControls, cancelQueryDeferred } } = getState();
 
-        if (uiControls.isRunning && cancelQueryDeferred) {
-            cancelQueryDeferred.resolve();
-        }
-    };
+    if (uiControls.isRunning && cancelQueryDeferred) {
+      cancelQueryDeferred.resolve();
+    }
+  };
 });
 
 export const FOLLOW_FOREIGN_KEY = "metabase/qb/FOLLOW_FOREIGN_KEY";
-export const followForeignKey = createThunkAction(FOLLOW_FOREIGN_KEY, (fk) => {
-    return async (dispatch, getState) => {
-        // TODO Atte Keinänen 6/1/17: Should use `queryResults` instead
-        const { qb: { card, queryResult } } = getState();
-
-        if (!queryResult || !fk) return false;
-
-        // extract the value we will use to filter our new query
-        var originValue;
-        for (var i=0; i < queryResult.data.cols.length; i++) {
-            if (isPK(queryResult.data.cols[i].special_type)) {
-                originValue = queryResult.data.rows[0][i];
-            }
-        }
+export const followForeignKey = createThunkAction(FOLLOW_FOREIGN_KEY, fk => {
+  return async (dispatch, getState) => {
+    // TODO Atte Keinänen 6/1/17: Should use `queryResults` instead
+    const { qb: { card, queryResult } } = getState();
+
+    if (!queryResult || !fk) return false;
+
+    // extract the value we will use to filter our new query
+    var originValue;
+    for (var i = 0; i < queryResult.data.cols.length; i++) {
+      if (isPK(queryResult.data.cols[i].special_type)) {
+        originValue = queryResult.data.rows[0][i];
+      }
+    }
 
-        // action is on an FK column
-        let newCard = startNewCard("query", card.dataset_query.database);
+    // action is on an FK column
+    let newCard = startNewCard("query", card.dataset_query.database);
 
-        newCard.dataset_query.query.source_table = fk.origin.table.id;
-        newCard.dataset_query.query.aggregation = ["rows"];
-        newCard.dataset_query.query.filter = ["AND", ["=", fk.origin.id, originValue]];
+    newCard.dataset_query.query.source_table = fk.origin.table.id;
+    newCard.dataset_query.query.aggregation = ["rows"];
+    newCard.dataset_query.query.filter = [
+      "AND",
+      ["=", fk.origin.id, originValue],
+    ];
 
-        // run it
-        dispatch(setCardAndRun(newCard));
-    };
+    // run it
+    dispatch(setCardAndRun(newCard));
+  };
 });
 
-
-export const LOAD_OBJECT_DETAIL_FK_REFERENCES = "metabase/qb/LOAD_OBJECT_DETAIL_FK_REFERENCES";
-export const loadObjectDetailFKReferences = createThunkAction(LOAD_OBJECT_DETAIL_FK_REFERENCES, () => {
+export const LOAD_OBJECT_DETAIL_FK_REFERENCES =
+  "metabase/qb/LOAD_OBJECT_DETAIL_FK_REFERENCES";
+export const loadObjectDetailFKReferences = createThunkAction(
+  LOAD_OBJECT_DETAIL_FK_REFERENCES,
+  () => {
     return async (dispatch, getState) => {
-        // TODO Atte Keinänen 6/1/17: Should use `queryResults` instead
-        const { qb: { card, queryResult, tableForeignKeys } } = getState();
-
-        function getObjectDetailIdValue(data) {
-            for (var i=0; i < data.cols.length; i++) {
-                var coldef = data.cols[i];
-                if (isPK(coldef.special_type)) {
-                    return data.rows[0][i];
-                }
-            }
+      // TODO Atte Keinänen 6/1/17: Should use `queryResults` instead
+      const { qb: { card, queryResult, tableForeignKeys } } = getState();
+
+      function getObjectDetailIdValue(data) {
+        for (var i = 0; i < data.cols.length; i++) {
+          var coldef = data.cols[i];
+          if (isPK(coldef.special_type)) {
+            return data.rows[0][i];
+          }
         }
+      }
 
-        async function getFKCount(card, queryResult, fk) {
-            let fkQuery = createQuery("query");
-            fkQuery.database = card.dataset_query.database;
-            fkQuery.query.source_table = fk.origin.table_id;
-            fkQuery.query.aggregation = ["count"];
-            fkQuery.query.filter = ["AND", ["=", fk.origin.id, getObjectDetailIdValue(queryResult.data)]];
-
-            let info = {"status": 0, "value": null};
-
-            try {
-                let result = await MetabaseApi.dataset(fkQuery);
-                if (result && result.status === "completed" && result.data.rows.length > 0) {
-                    info["value"] = result.data.rows[0][0];
-                } else {
-                    // $FlowFixMe
-                    info["value"] = "Unknown";
-                }
-            } catch (error) {
-                console.error("error getting fk count", error, fkQuery);
-            } finally {
-                info["status"] = 1;
-            }
-
-            return info;
-        }
+      async function getFKCount(card, queryResult, fk) {
+        let fkQuery = createQuery("query");
+        fkQuery.database = card.dataset_query.database;
+        fkQuery.query.source_table = fk.origin.table_id;
+        fkQuery.query.aggregation = ["count"];
+        fkQuery.query.filter = [
+          "AND",
+          ["=", fk.origin.id, getObjectDetailIdValue(queryResult.data)],
+        ];
 
-        // TODO: there are possible cases where running a query would not require refreshing this data, but
-        // skipping that for now because it's easier to just run this each time
+        let info = { status: 0, value: null };
 
-        // run a query on FK origin table where FK origin field = objectDetailIdValue
-        let fkReferences = {};
-        for (let i=0; i < tableForeignKeys.length; i++) {
-            let fk = tableForeignKeys[i],
-                info = await getFKCount(card, queryResult, fk);
-            fkReferences[fk.origin.id] = info;
+        try {
+          let result = await MetabaseApi.dataset(fkQuery);
+          if (
+            result &&
+            result.status === "completed" &&
+            result.data.rows.length > 0
+          ) {
+            info["value"] = result.data.rows[0][0];
+          } else {
+            // $FlowFixMe
+            info["value"] = "Unknown";
+          }
+        } catch (error) {
+          console.error("error getting fk count", error, fkQuery);
+        } finally {
+          info["status"] = 1;
         }
 
-        return fkReferences;
-    };
-});
+        return info;
+      }
 
-export const ARCHIVE_QUESTION = 'metabase/qb/ARCHIVE_QUESTION';
-export const archiveQuestion = createThunkAction(ARCHIVE_QUESTION, (questionId, archived = true) =>
-    async (dispatch, getState) => {
-        let card = {
-            ...getState().qb.card, // grab the current card
-            archived
-        }
-        let response = await CardApi.update(card)
+      // TODO: there are possible cases where running a query would not require refreshing this data, but
+      // skipping that for now because it's easier to just run this each time
 
-        const type = archived ? "archived" : "unarchived"
+      // run a query on FK origin table where FK origin field = objectDetailIdValue
+      let fkReferences = {};
+      for (let i = 0; i < tableForeignKeys.length; i++) {
+        let fk = tableForeignKeys[i],
+          info = await getFKCount(card, queryResult, fk);
+        fkReferences[fk.origin.id] = info;
+      }
 
-        dispatch(addUndo(createUndo({
-            type,
-            // eslint-disable-next-line react/display-name
-            message: () => <div> { "Question  was " + type + "."} </div>,
-            action: archiveQuestion(card.id, !archived)
-        })));
+      return fkReferences;
+    };
+  },
+);
 
-        dispatch(push('/questions'))
-        return response
-    }
-)
+export const ARCHIVE_QUESTION = "metabase/qb/ARCHIVE_QUESTION";
+export const archiveQuestion = createThunkAction(
+  ARCHIVE_QUESTION,
+  (questionId, archived = true) => async (dispatch, getState) => {
+    let card = {
+      ...getState().qb.card, // grab the current card
+      archived,
+    };
+    let response = await CardApi.update(card);
+
+    const type = archived ? "archived" : "unarchived";
+
+    dispatch(
+      addUndo(
+        createUndo({
+          type,
+          // eslint-disable-next-line react/display-name
+          message: () => <div> {"Question  was " + type + "."} </div>,
+          action: archiveQuestion(card.id, !archived),
+        }),
+      ),
+    );
 
-export const VIEW_NEXT_OBJECT_DETAIL = 'metabase/qb/VIEW_NEXT_OBJECT_DETAIL'
-export const viewNextObjectDetail = () => {
-    return (dispatch, getState) => {
-        const question = getQuestion(getState());
-        let filter = question.query().filters()[0]
+    dispatch(push("/questions"));
+    return response;
+  },
+);
 
+export const VIEW_NEXT_OBJECT_DETAIL = "metabase/qb/VIEW_NEXT_OBJECT_DETAIL";
+export const viewNextObjectDetail = () => {
+  return (dispatch, getState) => {
+    const question = getQuestion(getState());
+    let filter = question.query().filters()[0];
 
-        let newFilter = ["=", filter[1], filter[2] + 1]
+    let newFilter = ["=", filter[1], filter[2] + 1];
 
-        dispatch.action(VIEW_NEXT_OBJECT_DETAIL)
+    dispatch.action(VIEW_NEXT_OBJECT_DETAIL);
 
-        dispatch(updateQuestion(
-            question.query().updateFilter(0, newFilter).question()
-        ))
+    dispatch(
+      updateQuestion(
+        question
+          .query()
+          .updateFilter(0, newFilter)
+          .question(),
+      ),
+    );
 
-        dispatch(runQuestionQuery());
-    }
-}
+    dispatch(runQuestionQuery());
+  };
+};
 
-export const VIEW_PREVIOUS_OBJECT_DETAIL = 'metabase/qb/VIEW_PREVIOUS_OBJECT_DETAIL'
+export const VIEW_PREVIOUS_OBJECT_DETAIL =
+  "metabase/qb/VIEW_PREVIOUS_OBJECT_DETAIL";
 
 export const viewPreviousObjectDetail = () => {
-    return (dispatch, getState) => {
-        const question = getQuestion(getState());
-        let filter = question.query().filters()[0]
+  return (dispatch, getState) => {
+    const question = getQuestion(getState());
+    let filter = question.query().filters()[0];
 
-        if(filter[2] === 1) {
-            return false
-        }
+    if (filter[2] === 1) {
+      return false;
+    }
 
-        let newFilter = ["=", filter[1], filter[2] - 1]
+    let newFilter = ["=", filter[1], filter[2] - 1];
 
-        dispatch.action(VIEW_PREVIOUS_OBJECT_DETAIL)
+    dispatch.action(VIEW_PREVIOUS_OBJECT_DETAIL);
 
-        dispatch(updateQuestion(
-            question.query().updateFilter(0, newFilter).question()
-        ))
+    dispatch(
+      updateQuestion(
+        question
+          .query()
+          .updateFilter(0, newFilter)
+          .question(),
+      ),
+    );
 
-        dispatch(runQuestionQuery());
-    }
-}
+    dispatch(runQuestionQuery());
+  };
+};
 
 // these are just temporary mappings to appease the existing QB code and it's naming prefs
 export const toggleDataReferenceFn = toggleDataReference;
diff --git a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
index a8d76411c612e88d2201a92ece3c2a4eee530c4d..12b08ce2a64eed5dc2b21f285158132dcd725718 100644
--- a/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/ActionsWidget.jsx
@@ -15,22 +15,22 @@ import type { ClickAction } from "metabase/meta/types/Visualization";
 import Question from "metabase-lib/lib/Question";
 
 type Props = {
-    className?: string,
-    card: Card,
-    question: Question,
-    setCardAndRun: (card: Card) => void,
-    navigateToNewCardInsideQB: (any) => void,
-    router: {
-        push: (string) => void
-    },
-    instanceSettings: {}
+  className?: string,
+  card: Card,
+  question: Question,
+  setCardAndRun: (card: Card) => void,
+  navigateToNewCardInsideQB: any => void,
+  router: {
+    push: string => void,
+  },
+  instanceSettings: {},
 };
 
 type State = {
-    iconIsVisible: boolean,
-    popoverIsOpen: boolean,
-    isClosing: boolean,
-    selectedActionIndex: ?number,
+  iconIsVisible: boolean,
+  popoverIsOpen: boolean,
+  isClosing: boolean,
+  selectedActionIndex: ?number,
 };
 
 const CIRCLE_SIZE = 48;
@@ -38,194 +38,206 @@ const NEEDLE_SIZE = 20;
 const POPOVER_WIDTH = 350;
 
 export default class ActionsWidget extends Component {
-    props: Props;
-    state: State = {
-        iconIsVisible: false,
-        popoverIsOpen: false,
-        isClosing: false,
-        selectedActionIndex: null
-    };
-
-    componentWillMount() {
-        window.addEventListener("mousemove", this.handleMouseMoved, false);
+  props: Props;
+  state: State = {
+    iconIsVisible: false,
+    popoverIsOpen: false,
+    isClosing: false,
+    selectedActionIndex: null,
+  };
+
+  componentWillMount() {
+    window.addEventListener("mousemove", this.handleMouseMoved, false);
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener("mousemove", this.handleMouseMoved, false);
+  }
+
+  handleMouseMoved = () => {
+    // Don't auto-show or auto-hide the icon if popover is open
+    if (this.state.popoverIsOpen) return;
+
+    if (!this.state.iconIsVisible) {
+      this.setState({ iconIsVisible: true });
     }
+    this.handleMouseStoppedMoving();
+  };
 
-    componentWillUnmount() {
-        window.removeEventListener("mousemove", this.handleMouseMoved, false);
+  handleMouseStoppedMoving = _.debounce(() => {
+    if (this.state.iconIsVisible) {
+      this.setState({ iconIsVisible: false });
     }
-
-    handleMouseMoved = () => {
-        // Don't auto-show or auto-hide the icon if popover is open
-        if (this.state.popoverIsOpen) return;
-
-        if (!this.state.iconIsVisible) {
-            this.setState({ iconIsVisible: true });
-        }
-        this.handleMouseStoppedMoving();
-    };
-
-    handleMouseStoppedMoving = _.debounce(
-        () => {
-            if (this.state.iconIsVisible) {
-                this.setState({ iconIsVisible: false });
-            }
-        },
-        1000
-    );
-
-    close = () => {
-        this.setState({ isClosing: true, popoverIsOpen: false, selectedActionIndex: null });
-        // Needed because when closing the action widget by clicking compass, this is triggered first
-        // on mousedown (by OnClickOutsideWrapper) and toggle is triggered on mouseup
-        setTimeout(() => this.setState({ isClosing: false }), 500);
-    };
-
-    toggle = () => {
-        if (this.state.isClosing) return;
-
-        if (!this.state.popoverIsOpen) {
-            MetabaseAnalytics.trackEvent("Actions", "Opened Action Menu");
-        }
-        this.setState({
-            popoverIsOpen: !this.state.popoverIsOpen,
-            selectedActionIndex: null
-        });
-    };
-
-    handleOnChangeCardAndRun = ({ nextCard }: { nextCard: Card|UnsavedCard}) => {
-        // TODO: move lineage logic to Question?
-        const { card: previousCard } = this.props;
-        this.props.navigateToNewCardInsideQB({ nextCard, previousCard });
+  }, 1000);
+
+  close = () => {
+    this.setState({
+      isClosing: true,
+      popoverIsOpen: false,
+      selectedActionIndex: null,
+    });
+    // Needed because when closing the action widget by clicking compass, this is triggered first
+    // on mousedown (by OnClickOutsideWrapper) and toggle is triggered on mouseup
+    setTimeout(() => this.setState({ isClosing: false }), 500);
+  };
+
+  toggle = () => {
+    if (this.state.isClosing) return;
+
+    if (!this.state.popoverIsOpen) {
+      MetabaseAnalytics.trackEvent("Actions", "Opened Action Menu");
     }
-
-    handleActionClick = (index: number) => {
-        const { question, router, instanceSettings } = this.props;
-        const mode = question.mode()
-        if (mode) {
-            const action = mode.actions(instanceSettings)[index];
-            if (action && action.popover) {
-                this.setState({ selectedActionIndex: index });
-            } else if (action && action.question) {
-                const nextQuestion = action.question();
-                if (nextQuestion) {
-                    MetabaseAnalytics.trackEvent("Actions", "Executed Action", `${action.section||""}:${action.name||""}`);
-                    this.handleOnChangeCardAndRun({ nextCard: nextQuestion.card() });
-                }
-                this.close();
-            } else if (action && action.url) {
-                router.push(action.url())
-            }
-        } else {
-            console.warn("handleActionClick: Question mode is missing")
+    this.setState({
+      popoverIsOpen: !this.state.popoverIsOpen,
+      selectedActionIndex: null,
+    });
+  };
+
+  handleOnChangeCardAndRun = ({
+    nextCard,
+  }: {
+    nextCard: Card | UnsavedCard,
+  }) => {
+    // TODO: move lineage logic to Question?
+    const { card: previousCard } = this.props;
+    this.props.navigateToNewCardInsideQB({ nextCard, previousCard });
+  };
+
+  handleActionClick = (index: number) => {
+    const { question, router, instanceSettings } = this.props;
+    const mode = question.mode();
+    if (mode) {
+      const action = mode.actions(instanceSettings)[index];
+      if (action && action.popover) {
+        this.setState({ selectedActionIndex: index });
+      } else if (action && action.question) {
+        const nextQuestion = action.question();
+        if (nextQuestion) {
+          MetabaseAnalytics.trackEvent(
+            "Actions",
+            "Executed Action",
+            `${action.section || ""}:${action.name || ""}`,
+          );
+          this.handleOnChangeCardAndRun({ nextCard: nextQuestion.card() });
         }
-    };
-    render() {
-        const { className, question, instanceSettings } = this.props;
-        const { popoverIsOpen, iconIsVisible, selectedActionIndex } = this.state;
-
-        const mode = question.mode();
-        const actions = mode ? mode.actions(instanceSettings) : [];
-        if (actions.length === 0) {
-            return null;
-        }
-
+        this.close();
+      } else if (action && action.url) {
+        router.push(action.url());
+      }
+    } else {
+      console.warn("handleActionClick: Question mode is missing");
+    }
+  };
+  render() {
+    const { className, question, instanceSettings } = this.props;
+    const { popoverIsOpen, iconIsVisible, selectedActionIndex } = this.state;
+
+    const mode = question.mode();
+    const actions = mode ? mode.actions(instanceSettings) : [];
+    if (actions.length === 0) {
+      return null;
+    }
 
-        const selectedAction: ?ClickAction = selectedActionIndex == null ? null :
-            actions[selectedActionIndex];
-        let PopoverComponent = selectedAction && selectedAction.popover;
-
-        return (
-            <div className={cx(className, "relative")}>
-                <div
-                    className="circular bg-brand flex layout-centered m3 cursor-pointer"
-                    style={{
-                        width: CIRCLE_SIZE,
-                        height: CIRCLE_SIZE,
-                        transition: "opacity 300ms ease-in-out",
-                        opacity: popoverIsOpen || iconIsVisible ? 1 : 0,
-                        boxShadow: "2px 2px 4px rgba(0, 0, 0, 0.2)"
-                    }}
-                    onClick={this.toggle}
-                >
+    const selectedAction: ?ClickAction =
+      selectedActionIndex == null ? null : actions[selectedActionIndex];
+    let PopoverComponent = selectedAction && selectedAction.popover;
+
+    return (
+      <div className={cx(className, "relative")}>
+        <div
+          className="circular bg-brand flex layout-centered m3 cursor-pointer"
+          style={{
+            width: CIRCLE_SIZE,
+            height: CIRCLE_SIZE,
+            transition: "opacity 300ms ease-in-out",
+            opacity: popoverIsOpen || iconIsVisible ? 1 : 0,
+            boxShadow: "2px 2px 4px rgba(0, 0, 0, 0.2)",
+          }}
+          onClick={this.toggle}
+        >
+          <Icon
+            name="compass_needle"
+            className="text-white"
+            style={{
+              transition: "transform 500ms ease-in-out",
+              transform: popoverIsOpen ? "rotate(0deg)" : "rotate(720deg)",
+            }}
+            size={NEEDLE_SIZE}
+          />
+        </div>
+        {popoverIsOpen && (
+          <OnClickOutsideWrapper
+            handleDismissal={() => {
+              MetabaseAnalytics.trackEvent("Actions", "Dismissed Action Menu");
+              this.close();
+            }}
+          >
+            <div
+              className="absolute bg-white rounded bordered shadowed py1"
+              style={{
+                width: POPOVER_WIDTH,
+                bottom: "50%",
+                right: "50%",
+                zIndex: -1,
+                maxHeight: "600px",
+                overflow: "scroll",
+              }}
+            >
+              {PopoverComponent ? (
+                <div>
+                  <div className="flex align-center text-grey-4 p1 px2">
                     <Icon
-                        name="compass_needle"
-                        className="text-white"
-                        style={{
-                            transition: "transform 500ms ease-in-out",
-                            transform: popoverIsOpen
-                                ? "rotate(0deg)"
-                                : "rotate(720deg)"
-                        }}
-                        size={NEEDLE_SIZE}
+                      name="chevronleft"
+                      className="cursor-pointer"
+                      onClick={() =>
+                        this.setState({
+                          selectedActionIndex: null,
+                        })
+                      }
                     />
+                    <div className="text-centered flex-full">
+                      {selectedAction && selectedAction.title}
+                    </div>
+                  </div>
+                  <PopoverComponent
+                    onChangeCardAndRun={({ nextCard }) => {
+                      if (nextCard) {
+                        if (selectedAction) {
+                          MetabaseAnalytics.trackEvent(
+                            "Actions",
+                            "Executed Action",
+                            `${selectedAction.section ||
+                              ""}:${selectedAction.name || ""}`,
+                          );
+                        }
+                        this.handleOnChangeCardAndRun({ nextCard });
+                      }
+                    }}
+                    onClose={this.close}
+                  />
                 </div>
-                {popoverIsOpen &&
-                    <OnClickOutsideWrapper handleDismissal={() => {
-                        MetabaseAnalytics.trackEvent("Actions", "Dismissed Action Menu");
-                        this.close();
-                    }}>
-                        <div
-                            className="absolute bg-white rounded bordered shadowed py1"
-                            style={{
-                                width: POPOVER_WIDTH,
-                                bottom: "50%",
-                                right: "50%",
-                                zIndex: -1,
-                                maxHeight: "600px",
-                                overflow: "scroll"
-                            }}
-                        >
-                            {PopoverComponent
-                                ? <div>
-                                      <div
-                                          className="flex align-center text-grey-4 p1 px2"
-                                      >
-                                          <Icon
-                                              name="chevronleft"
-                                              className="cursor-pointer"
-                                              onClick={() => this.setState({
-                                                  selectedActionIndex: null
-                                              })}
-                                          />
-                                          <div
-                                              className="text-centered flex-full"
-                                          >
-                                              {selectedAction && selectedAction.title}
-                                          </div>
-                                      </div>
-                                      <PopoverComponent
-                                          onChangeCardAndRun={({ nextCard }) => {
-                                              if (nextCard) {
-                                                  if (selectedAction) {
-                                                      MetabaseAnalytics.trackEvent("Actions", "Executed Action", `${selectedAction.section||""}:${selectedAction.name||""}`);
-                                                  }
-                                                  this.handleOnChangeCardAndRun({ nextCard })
-                                              }
-                                          }}
-                                          onClose={this.close}
-                                      />
-                                  </div>
-                                : actions.map((action, index) => (
-                                      <div
-                                          key={index}
-                                          className="p2 flex align-center text-grey-4 brand-hover cursor-pointer"
-                                          onClick={() =>
-                                              this.handleActionClick(index)}
-                                      >
-                                          {action.icon &&
-                                              <Icon
-                                                  name={action.icon}
-                                                  className="mr1 flex-no-shrink"
-                                                  size={16}
-                                              />}
-                                          <div>
-                                              {action.title}
-                                          </div>
-                                      </div>
-                                  ))}
-                        </div>
-                    </OnClickOutsideWrapper>}
+              ) : (
+                actions.map((action, index) => (
+                  <div
+                    key={index}
+                    className="p2 flex align-center text-grey-4 brand-hover cursor-pointer"
+                    onClick={() => this.handleActionClick(index)}
+                  >
+                    {action.icon && (
+                      <Icon
+                        name={action.icon}
+                        className="mr1 flex-no-shrink"
+                        size={16}
+                      />
+                    )}
+                    <div>{action.title}</div>
+                  </div>
+                ))
+              )}
             </div>
-        );
-    }
+          </OnClickOutsideWrapper>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx
index fe49f92665908f8f6dcb51ded6dd25725b2aca70..866a2e4e2fc6b750af70f4a84125a8579ca27380 100644
--- a/frontend/src/metabase/query_builder/components/AddClauseButton.jsx
+++ b/frontend/src/metabase/query_builder/components/AddClauseButton.jsx
@@ -4,40 +4,39 @@ import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon.jsx";
 import IconBorder from "metabase/components/IconBorder.jsx";
 
-
 export default class AddClauseButton extends Component {
+  static propTypes = {
+    text: PropTypes.string,
+    onClick: PropTypes.func,
+  };
 
-    static propTypes = {
-        text: PropTypes.string,
-        onClick: PropTypes.func
-    };
-
-    renderAddIcon() {
-        return (
-            <IconBorder borderRadius="3px">
-                <Icon name="add" size={14} />
-            </IconBorder>
-        )
-    }
+  renderAddIcon() {
+    return (
+      <IconBorder borderRadius="3px">
+        <Icon name="add" size={14} />
+      </IconBorder>
+    );
+  }
 
-    render() {
-        const { text, onClick } = this.props;
+  render() {
+    const { text, onClick } = this.props;
 
-        const className = "text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color";
-        if (onClick) {
-            return (
-                <a className={className} onClick={onClick}>
-                    {this.renderAddIcon()}
-                    { text && <span className="ml1">{text}</span> }
-                </a>
-            );
-        } else {
-            return (
-                <span className={className}>
-                    {this.renderAddIcon()}
-                    { text && <span className="ml1">{text}</span> }
-                </span>
-            );
-        }
+    const className =
+      "text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color";
+    if (onClick) {
+      return (
+        <a className={className} onClick={onClick}>
+          {this.renderAddIcon()}
+          {text && <span className="ml1">{text}</span>}
+        </a>
+      );
+    } else {
+      return (
+        <span className={className}>
+          {this.renderAddIcon()}
+          {text && <span className="ml1">{text}</span>}
+        </span>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
index 93f91c9c5eb503aff046c4598029c0309de52d4c..b50d3bc3a8afa6b29b4a3ec9441dd4e1485e10f3 100644
--- a/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationPopover.jsx
@@ -1,9 +1,9 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AccordianList from "metabase/components/AccordianList.jsx";
-import FieldList from './FieldList.jsx';
+import FieldList from "./FieldList.jsx";
 import QueryDefinitionTooltip from "./QueryDefinitionTooltip.jsx";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -14,272 +14,346 @@ import Query, { AggregationClause, NamedClause } from "metabase/lib/query";
 
 import _ from "underscore";
 
-import ExpressionEditorTextfield from "./expressions/ExpressionEditorTextfield.jsx"
+import ExpressionEditorTextfield from "./expressions/ExpressionEditorTextfield.jsx";
 
 const CUSTOM_SECTION_NAME = t`Custom Expression`;
 const METRICS_SECTION_NAME = t`Common Metrics`;
 
 export default class AggregationPopover extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            aggregation: (props.isNew ? [] : props.aggregation),
-            choosingField: (props.aggregation && props.aggregation.length > 1 && AggregationClause.isStandard(props.aggregation)),
-            editingAggregation: (props.aggregation && props.aggregation.length > 1 && AggregationClause.isCustom(props.aggregation))
-        };
+    this.state = {
+      aggregation: props.isNew ? [] : props.aggregation,
+      choosingField:
+        props.aggregation &&
+        props.aggregation.length > 1 &&
+        AggregationClause.isStandard(props.aggregation),
+      editingAggregation:
+        props.aggregation &&
+        props.aggregation.length > 1 &&
+        AggregationClause.isCustom(props.aggregation),
+    };
 
-        _.bindAll(this, "commitAggregation", "onPickAggregation", "onPickField", "onClearAggregation");
-    }
+    _.bindAll(
+      this,
+      "commitAggregation",
+      "onPickAggregation",
+      "onPickField",
+      "onClearAggregation",
+    );
+  }
 
-    static propTypes = {
-        isNew: PropTypes.bool,
-        aggregation: PropTypes.array,
-        onCommitAggregation: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired,
-        tableMetadata: PropTypes.object.isRequired,
-        datasetQuery: PropTypes.object,
-        customFields: PropTypes.object,
-        availableAggregations: PropTypes.array,
-        // Restricts the shown options to contents of `availableActions` only
-        showOnlyProvidedAggregations: PropTypes.boolean
-    };
+  static propTypes = {
+    isNew: PropTypes.bool,
+    aggregation: PropTypes.array,
+    onCommitAggregation: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+    datasetQuery: PropTypes.object,
+    customFields: PropTypes.object,
+    availableAggregations: PropTypes.array,
+    // Restricts the shown options to contents of `availableActions` only
+    showOnlyProvidedAggregations: PropTypes.boolean,
+  };
 
-    componentDidUpdate() {
-        if (this._header) {
-            const { height } = ReactDOM.findDOMNode(this._header).getBoundingClientRect();
-            if (height !== this.state.headerHeight) {
-                this.setState({ headerHeight: height })
-            }
-        }
+  componentDidUpdate() {
+    if (this._header) {
+      const { height } = ReactDOM.findDOMNode(
+        this._header,
+      ).getBoundingClientRect();
+      if (height !== this.state.headerHeight) {
+        this.setState({ headerHeight: height });
+      }
     }
+  }
 
-    commitAggregation(aggregation) {
-        this.props.onCommitAggregation(aggregation);
-        this.props.onClose();
-    }
+  commitAggregation(aggregation) {
+    this.props.onCommitAggregation(aggregation);
+    this.props.onClose();
+  }
 
-    onPickAggregation(agg) {
-        // check if this aggregation requires a field, if so then force user to pick that now, otherwise we are done
-        if (agg.custom) {
-            this.setState({
-                aggregation: agg.value,
-                editingAggregation: true
-            });
-        } else if (agg.aggregation && agg.aggregation.requiresField) {
-            this.setState({
-                aggregation: agg.value,
-                choosingField: true
-            });
-        } else {
-            // this includse picking a METRIC or picking an aggregation which doesn't require a field
-            this.commitAggregation(agg.value);
-        }
+  onPickAggregation(agg) {
+    // check if this aggregation requires a field, if so then force user to pick that now, otherwise we are done
+    if (agg.custom) {
+      this.setState({
+        aggregation: agg.value,
+        editingAggregation: true,
+      });
+    } else if (agg.aggregation && agg.aggregation.requiresField) {
+      this.setState({
+        aggregation: agg.value,
+        choosingField: true,
+      });
+    } else {
+      // this includse picking a METRIC or picking an aggregation which doesn't require a field
+      this.commitAggregation(agg.value);
     }
+  }
 
-    onPickField(fieldId) {
-        this.commitAggregation(AggregationClause.setField(this.state.aggregation, fieldId));
-    }
+  onPickField(fieldId) {
+    this.commitAggregation(
+      AggregationClause.setField(this.state.aggregation, fieldId),
+    );
+  }
 
-    onClearAggregation() {
-        this.setState({
-            choosingField: false,
-            editingAggregation: false
-        });
-    }
+  onClearAggregation() {
+    this.setState({
+      choosingField: false,
+      editingAggregation: false,
+    });
+  }
 
-    getAvailableAggregations() {
-        const { availableAggregations, query } = this.props;
-        return availableAggregations || query.table().aggregations();
-    }
+  getAvailableAggregations() {
+    const { availableAggregations, query } = this.props;
+    return availableAggregations || query.table().aggregations();
+  }
 
-    getCustomFields() {
-        const { customFields, datasetQuery } = this.props;
-        return customFields || (datasetQuery && Query.getExpressions(datasetQuery.query));
-    }
+  getCustomFields() {
+    const { customFields, datasetQuery } = this.props;
+    return (
+      customFields || (datasetQuery && Query.getExpressions(datasetQuery.query))
+    );
+  }
 
-    itemIsSelected(item) {
-        const { aggregation } = this.props;
-        return item.isSelected(NamedClause.getContent(aggregation));
-    }
+  itemIsSelected(item) {
+    const { aggregation } = this.props;
+    return item.isSelected(NamedClause.getContent(aggregation));
+  }
 
-    renderItemExtra(item, itemIndex) {
-        if (item.aggregation && item.aggregation.description) {
-            return (
-                <div className="p1">
-                    <Tooltip tooltip={item.aggregation.description}>
-                        <span className="QuestionTooltipTarget" />
-                    </Tooltip>
-                </div>
-            );
-        } else if (item.metric) {
-            return this.renderMetricTooltip(item.metric);
-        }
+  renderItemExtra(item, itemIndex) {
+    if (item.aggregation && item.aggregation.description) {
+      return (
+        <div className="p1">
+          <Tooltip tooltip={item.aggregation.description}>
+            <span className="QuestionTooltipTarget" />
+          </Tooltip>
+        </div>
+      );
+    } else if (item.metric) {
+      return this.renderMetricTooltip(item.metric);
     }
+  }
 
-    renderMetricTooltip(metric) {
-        let { tableMetadata } = this.props;
-        return (
-            <div className="p1">
-                <Tooltip tooltip={<QueryDefinitionTooltip type="metric" object={metric} tableMetadata={tableMetadata} />}>
-                    <span className="QuestionTooltipTarget" />
-                </Tooltip>
-            </div>
-        );
-    }
+  renderMetricTooltip(metric) {
+    let { tableMetadata } = this.props;
+    return (
+      <div className="p1">
+        <Tooltip
+          tooltip={
+            <QueryDefinitionTooltip
+              type="metric"
+              object={metric}
+              tableMetadata={tableMetadata}
+            />
+          }
+        >
+          <span className="QuestionTooltipTarget" />
+        </Tooltip>
+      </div>
+    );
+  }
 
-    render() {
-        const { query, tableMetadata, showOnlyProvidedAggregations } = this.props;
+  render() {
+    const { query, tableMetadata, showOnlyProvidedAggregations } = this.props;
 
-        const customFields = this.getCustomFields();
-        const availableAggregations = this.getAvailableAggregations();
+    const customFields = this.getCustomFields();
+    const availableAggregations = this.getAvailableAggregations();
 
-        const { choosingField, editingAggregation } = this.state;
-        const aggregation = NamedClause.getContent(this.state.aggregation);
+    const { choosingField, editingAggregation } = this.state;
+    const aggregation = NamedClause.getContent(this.state.aggregation);
 
-        let selectedAggregation;
-        if (AggregationClause.isMetric(aggregation)) {
-            selectedAggregation = _.findWhere(tableMetadata.metrics, { id: AggregationClause.getMetric(aggregation) });
-        } else if (AggregationClause.getOperator(aggregation)) {
-            selectedAggregation = _.findWhere(availableAggregations, { short: AggregationClause.getOperator(aggregation) });
-        }
+    let selectedAggregation;
+    if (AggregationClause.isMetric(aggregation)) {
+      selectedAggregation = _.findWhere(tableMetadata.metrics, {
+        id: AggregationClause.getMetric(aggregation),
+      });
+    } else if (AggregationClause.getOperator(aggregation)) {
+      selectedAggregation = _.findWhere(availableAggregations, {
+        short: AggregationClause.getOperator(aggregation),
+      });
+    }
 
-        let sections = [];
-        let customExpressionIndex = null;
+    let sections = [];
+    let customExpressionIndex = null;
 
-        if (availableAggregations.length > 0) {
-            sections.push({
-                name: showOnlyProvidedAggregations ? null : t`Metabasics`,
-                items: availableAggregations.map(aggregation => ({
-                    name: aggregation.name,
-                    value: [aggregation.short].concat(aggregation.fields.map(field => null)),
-                    isSelected: (agg) => !AggregationClause.isCustom(agg) && AggregationClause.getAggregation(agg) === aggregation.short,
-                    aggregation: aggregation
-                })),
-                icon: showOnlyProvidedAggregations ? null : "table2"
-            });
-        }
+    if (availableAggregations.length > 0) {
+      sections.push({
+        name: showOnlyProvidedAggregations ? null : t`Metabasics`,
+        items: availableAggregations.map(aggregation => ({
+          name: aggregation.name,
+          value: [aggregation.short].concat(
+            aggregation.fields.map(field => null),
+          ),
+          isSelected: agg =>
+            !AggregationClause.isCustom(agg) &&
+            AggregationClause.getAggregation(agg) === aggregation.short,
+          aggregation: aggregation,
+        })),
+        icon: showOnlyProvidedAggregations ? null : "table2",
+      });
+    }
 
-        if (!showOnlyProvidedAggregations) {
-            // we only want to consider active metrics, with the ONE exception that if the currently selected aggregation is a
-            // retired metric then we include it in the list to maintain continuity
-            let metrics = tableMetadata.metrics && tableMetadata.metrics.filter((mtrc) => mtrc.is_active === true || (selectedAggregation && selectedAggregation.id === mtrc.id));
-            if (metrics && metrics.length > 0) {
-                sections.push({
-                    name: METRICS_SECTION_NAME,
-                    items: metrics.map(metric => ({
-                        name: metric.name,
-                        value: ["METRIC", metric.id],
-                        isSelected: (aggregation) => AggregationClause.getMetric(aggregation) === metric.id,
-                        metric: metric
-                    })),
-                    icon: "staroutline"
-                });
-            }
+    if (!showOnlyProvidedAggregations) {
+      // we only want to consider active metrics, with the ONE exception that if the currently selected aggregation is a
+      // retired metric then we include it in the list to maintain continuity
+      let metrics =
+        tableMetadata.metrics &&
+        tableMetadata.metrics.filter(
+          mtrc =>
+            mtrc.is_active === true ||
+            (selectedAggregation && selectedAggregation.id === mtrc.id),
+        );
+      if (metrics && metrics.length > 0) {
+        sections.push({
+          name: METRICS_SECTION_NAME,
+          items: metrics.map(metric => ({
+            name: metric.name,
+            value: ["METRIC", metric.id],
+            isSelected: aggregation =>
+              AggregationClause.getMetric(aggregation) === metric.id,
+            metric: metric,
+          })),
+          icon: "staroutline",
+        });
+      }
 
-            customExpressionIndex = sections.length;
-            if (tableMetadata.db.features.indexOf("expression-aggregations") >= 0) {
-                sections.push({
-                    name: CUSTOM_SECTION_NAME,
-                    icon: "sum"
-                });
-            }
-        }
+      customExpressionIndex = sections.length;
+      if (tableMetadata.db.features.indexOf("expression-aggregations") >= 0) {
+        sections.push({
+          name: CUSTOM_SECTION_NAME,
+          icon: "sum",
+        });
+      }
+    }
 
-        if (sections.length === 1) {
-            sections[0].name = null
-        }
+    if (sections.length === 1) {
+      sections[0].name = null;
+    }
 
-        if (editingAggregation) {
-            return (
-                <div style={{width: editingAggregation ? 500 : 300}}>
-                    <div className="text-grey-3 p1 py2 border-bottom flex align-center">
-                        <a className="cursor-pointer flex align-center" onClick={this.onClearAggregation}>
-                            <Icon name="chevronleft" size={18}/>
-                            <h3 className="inline-block pl1">{CUSTOM_SECTION_NAME}</h3>
-                        </a>
-                    </div>
-                    <div className="p1">
-                        <ExpressionEditorTextfield
-                            startRule="aggregation"
-                            expression={aggregation}
-                            tableMetadata={tableMetadata}
-                            customFields={customFields}
-                            onChange={(parsedExpression) => this.setState({
-                                aggregation: NamedClause.setContent(this.state.aggregation, parsedExpression),
-                                error: null
-                            })}
-                            onError={(errorMessage) => this.setState({
-                                error: errorMessage
-                            })}
-                        />
-                        { this.state.error != null && (
-                            Array.isArray(this.state.error) ?
-                                this.state.error.map(error =>
-                                    <div className="text-error mb1" style={{ whiteSpace: "pre-wrap" }}>{error.message}</div>
-                                )
-                                :
-                                <div className="text-error mb1">{this.state.error.message}</div>
-                        )}
-                        <input
-                            className="input block full my1"
-                            value={NamedClause.getName(this.state.aggregation)}
-                            onChange={(e) => this.setState({
-                                aggregation: e.target.value ?
-                                    NamedClause.setName(aggregation, e.target.value) :
-                                    aggregation
-                            })}
-                            placeholder={t`Name (optional)`}
-                        />
-                        <Button className="full" primary disabled={this.state.error} onClick={() => this.commitAggregation(this.state.aggregation)}>
-                            {t`Done`}
-                        </Button>
-                    </div>
-                </div>
-            );
-        } else if (choosingField) {
-            const [agg, fieldId] = aggregation;
-            return (
-                <div style={{minWidth: 300}}>
-                    <div ref={_ => this._header = _} className="text-grey-3 p1 py2 border-bottom flex align-center">
-                        <a className="cursor-pointer flex align-center" onClick={this.onClearAggregation}>
-                            <Icon name="chevronleft" size={18}/>
-                            <h3 className="inline-block pl1">{selectedAggregation.name}</h3>
-                        </a>
-                    </div>
-                    <FieldList
-                        className={"text-green"}
-                        maxHeight={this.props.maxHeight - (this.state.headerHeight || 0)}
-                        tableMetadata={tableMetadata}
-                        field={fieldId}
-                        fieldOptions={query.aggregationFieldOptions(agg)}
-                        customFieldOptions={customFields}
-                        onFieldChange={this.onPickField}
-                        enableSubDimensions={false}
-                    />
-                </div>
-            );
-        } else {
-            return (
-                <AccordianList
-                    className="text-green"
-                    maxHeight={this.props.maxHeight}
-                    sections={sections}
-                    onChange={this.onPickAggregation}
-                    itemIsSelected={this.itemIsSelected.bind(this)}
-                    renderSectionIcon={(s) => <Icon name={s.icon} size={18} />}
-                    renderItemExtra={this.renderItemExtra.bind(this)}
-                    getItemClasses={(item) => item.metric && !item.metric.is_active ? "text-grey-3" : null }
-                    onChangeSection={(index) => {
-                        if (index === customExpressionIndex) {
-                            this.onPickAggregation({
-                                custom: true,
-                                value: aggregation !== "rows" && !_.isEqual(aggregation, ["rows"]) ? aggregation : null
-                            })
-                        }
-                    }}
-                />
-            );
-        }
+    if (editingAggregation) {
+      return (
+        <div style={{ width: editingAggregation ? 500 : 300 }}>
+          <div className="text-grey-3 p1 py2 border-bottom flex align-center">
+            <a
+              className="cursor-pointer flex align-center"
+              onClick={this.onClearAggregation}
+            >
+              <Icon name="chevronleft" size={18} />
+              <h3 className="inline-block pl1">{CUSTOM_SECTION_NAME}</h3>
+            </a>
+          </div>
+          <div className="p1">
+            <ExpressionEditorTextfield
+              startRule="aggregation"
+              expression={aggregation}
+              tableMetadata={tableMetadata}
+              customFields={customFields}
+              onChange={parsedExpression =>
+                this.setState({
+                  aggregation: NamedClause.setContent(
+                    this.state.aggregation,
+                    parsedExpression,
+                  ),
+                  error: null,
+                })
+              }
+              onError={errorMessage =>
+                this.setState({
+                  error: errorMessage,
+                })
+              }
+            />
+            {this.state.error != null &&
+              (Array.isArray(this.state.error) ? (
+                this.state.error.map(error => (
+                  <div
+                    className="text-error mb1"
+                    style={{ whiteSpace: "pre-wrap" }}
+                  >
+                    {error.message}
+                  </div>
+                ))
+              ) : (
+                <div className="text-error mb1">{this.state.error.message}</div>
+              ))}
+            <input
+              className="input block full my1"
+              value={NamedClause.getName(this.state.aggregation)}
+              onChange={e =>
+                this.setState({
+                  aggregation: e.target.value
+                    ? NamedClause.setName(aggregation, e.target.value)
+                    : aggregation,
+                })
+              }
+              placeholder={t`Name (optional)`}
+            />
+            <Button
+              className="full"
+              primary
+              disabled={this.state.error}
+              onClick={() => this.commitAggregation(this.state.aggregation)}
+            >
+              {t`Done`}
+            </Button>
+          </div>
+        </div>
+      );
+    } else if (choosingField) {
+      const [agg, fieldId] = aggregation;
+      return (
+        <div style={{ minWidth: 300 }}>
+          <div
+            ref={_ => (this._header = _)}
+            className="text-grey-3 p1 py2 border-bottom flex align-center"
+          >
+            <a
+              className="cursor-pointer flex align-center"
+              onClick={this.onClearAggregation}
+            >
+              <Icon name="chevronleft" size={18} />
+              <h3 className="inline-block pl1">{selectedAggregation.name}</h3>
+            </a>
+          </div>
+          <FieldList
+            className={"text-green"}
+            maxHeight={this.props.maxHeight - (this.state.headerHeight || 0)}
+            tableMetadata={tableMetadata}
+            field={fieldId}
+            fieldOptions={query.aggregationFieldOptions(agg)}
+            customFieldOptions={customFields}
+            onFieldChange={this.onPickField}
+            enableSubDimensions={false}
+          />
+        </div>
+      );
+    } else {
+      return (
+        <AccordianList
+          className="text-green"
+          maxHeight={this.props.maxHeight}
+          sections={sections}
+          onChange={this.onPickAggregation}
+          itemIsSelected={this.itemIsSelected.bind(this)}
+          renderSectionIcon={s => <Icon name={s.icon} size={18} />}
+          renderItemExtra={this.renderItemExtra.bind(this)}
+          getItemClasses={item =>
+            item.metric && !item.metric.is_active ? "text-grey-3" : null
+          }
+          onChangeSection={index => {
+            if (index === customExpressionIndex) {
+              this.onPickAggregation({
+                custom: true,
+                value:
+                  aggregation !== "rows" && !_.isEqual(aggregation, ["rows"])
+                    ? aggregation
+                    : null,
+              });
+            }
+          }}
+        />
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
index 4d0b69a4bc24b856abdef9003a9f9d27058d351c..8b7231c726bac854f410595c4183275cf76fb597 100644
--- a/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/AggregationWidget.jsx
@@ -1,9 +1,9 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AggregationPopover from "./AggregationPopover.jsx";
-import FieldName from './FieldName.jsx';
-import Clearable from './Clearable.jsx';
+import FieldName from "./FieldName.jsx";
+import Clearable from "./Clearable.jsx";
 
 import Popover from "metabase/components/Popover.jsx";
 
@@ -15,146 +15,162 @@ import cx from "classnames";
 import _ from "underscore";
 
 export default class AggregationWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            isOpen: false
-        };
-
-        _.bindAll(this, "open", "close", "setAggregation");
-    }
-
-    static propTypes = {
-        aggregation: PropTypes.array,
-        tableMetadata: PropTypes.object.isRequired,
-        customFields: PropTypes.object,
-        updateAggregation: PropTypes.func.isRequired,
-        removeAggregation: PropTypes.func,
+    this.state = {
+      isOpen: false,
     };
 
-    setAggregation(aggregation) {
-        this.props.updateAggregation(aggregation);
+    _.bindAll(this, "open", "close", "setAggregation");
+  }
+
+  static propTypes = {
+    aggregation: PropTypes.array,
+    tableMetadata: PropTypes.object.isRequired,
+    customFields: PropTypes.object,
+    updateAggregation: PropTypes.func.isRequired,
+    removeAggregation: PropTypes.func,
+  };
+
+  setAggregation(aggregation) {
+    this.props.updateAggregation(aggregation);
+  }
+
+  open() {
+    this.setState({ isOpen: true });
+  }
+
+  close() {
+    this.setState({ isOpen: false });
+  }
+
+  renderStandardAggregation() {
+    const { aggregation, tableMetadata } = this.props;
+    const fieldId = AggregationClause.getField(aggregation);
+
+    let selectedAggregation = getAggregator(
+      AggregationClause.getOperator(aggregation),
+    );
+    // if this table doesn't support the selected aggregation, prompt the user to select a different one
+    if (
+      selectedAggregation &&
+      _.findWhere(tableMetadata.aggregation_options, {
+        short: selectedAggregation.short,
+      })
+    ) {
+      return (
+        <span className="flex align-center">
+          {selectedAggregation.name.replace(" of ...", "")}
+          {fieldId && (
+            <span
+              style={{ paddingRight: "4px", paddingLeft: "4px" }}
+              className="text-bold"
+            >{t`of`}</span>
+          )}
+          {fieldId && (
+            <FieldName
+              className="View-section-aggregation-target SelectionModule py1"
+              tableMetadata={tableMetadata}
+              field={fieldId}
+              fieldOptions={Query.getFieldOptions(tableMetadata.fields, true)}
+              customFieldOptions={this.props.customFields}
+            />
+          )}
+        </span>
+      );
     }
+  }
 
-    open() {
-        this.setState({ isOpen: true });
-    }
+  renderMetricAggregation() {
+    const { aggregation, tableMetadata } = this.props;
+    const metricId = AggregationClause.getMetric(aggregation);
 
-    close() {
-        this.setState({ isOpen: false });
+    let selectedMetric = _.findWhere(tableMetadata.metrics, { id: metricId });
+    if (selectedMetric) {
+      return selectedMetric.name.replace(" of ...", "");
     }
-
-    renderStandardAggregation() {
-        const { aggregation, tableMetadata } = this.props;
-        const fieldId = AggregationClause.getField(aggregation);
-
-        let selectedAggregation = getAggregator(AggregationClause.getOperator(aggregation));
-        // if this table doesn't support the selected aggregation, prompt the user to select a different one
-        if (selectedAggregation && _.findWhere(tableMetadata.aggregation_options, { short: selectedAggregation.short })) {
-            return (
-                <span className="flex align-center">
-                    { selectedAggregation.name.replace(" of ...", "") }
-                    { fieldId &&
-                        <span style={{paddingRight: "4px", paddingLeft: "4px"}} className="text-bold">{t`of`}</span>
-                    }
-                    { fieldId &&
-                        <FieldName
-                            className="View-section-aggregation-target SelectionModule py1"
-                            tableMetadata={tableMetadata}
-                            field={fieldId}
-                            fieldOptions={Query.getFieldOptions(tableMetadata.fields, true)}
-                            customFieldOptions={this.props.customFields}
-                        />
-                    }
-                </span>
-            );
-        }
+  }
+
+  renderCustomAggregation() {
+    const { aggregation, tableMetadata, customFields } = this.props;
+    return format(aggregation, { tableMetadata, customFields });
+  }
+
+  renderPopover() {
+    const { query, aggregation, tableMetadata } = this.props;
+
+    if (this.state.isOpen) {
+      return (
+        <Popover
+          id="AggregationPopover"
+          ref="aggregationPopover"
+          className="FilterPopover"
+          isInitiallyOpen={true}
+          onClose={this.close}
+          dismissOnEscape={false} // disable for expression editor
+        >
+          <AggregationPopover
+            query={query}
+            aggregation={aggregation}
+            availableAggregations={tableMetadata.aggregation_options}
+            tableMetadata={tableMetadata}
+            customFields={this.props.customFields}
+            onCommitAggregation={this.setAggregation}
+            onClose={this.close}
+          />
+        </Popover>
+      );
     }
-
-    renderMetricAggregation() {
-        const { aggregation, tableMetadata } = this.props;
-        const metricId = AggregationClause.getMetric(aggregation);
-
-        let selectedMetric = _.findWhere(tableMetadata.metrics, { id: metricId });
-        if (selectedMetric) {
-            return selectedMetric.name.replace(" of ...", "")
-        }
-    }
-
-    renderCustomAggregation() {
-        const { aggregation, tableMetadata, customFields } = this.props;
-        return format(aggregation, { tableMetadata, customFields });
-    }
-
-    renderPopover() {
-        const { query, aggregation, tableMetadata } = this.props;
-
-        if (this.state.isOpen) {
-            return (
-                <Popover
-                    id="AggregationPopover"
-                    ref="aggregationPopover"
-                    className="FilterPopover"
-                    isInitiallyOpen={true}
-                    onClose={this.close}
-                    dismissOnEscape={false} // disable for expression editor
-                >
-                    <AggregationPopover
-                        query={query}
-                        aggregation={aggregation}
-                        availableAggregations={tableMetadata.aggregation_options}
-                        tableMetadata={tableMetadata}
-                        customFields={this.props.customFields}
-                        onCommitAggregation={this.setAggregation}
-                        onClose={this.close}
-                    />
-                </Popover>
-            );
-        }
-    }
-
-    render() {
-        const { aggregation, addButton, name } = this.props;
-        if (aggregation && aggregation.length > 0) {
-            let aggregationName = NamedClause.isNamed(aggregation) ?
-                NamedClause.getName(aggregation)
-            : AggregationClause.isCustom(aggregation) ?
-                this.renderCustomAggregation()
-            : AggregationClause.isMetric(aggregation) ?
-                this.renderMetricAggregation()
-            :
-                this.renderStandardAggregation()
-
-            return (
-                <div className={cx("Query-section Query-section-aggregation", { "selected": this.state.isOpen })}>
-                    <div>
-                        <Clearable onClear={this.props.removeAggregation}>
-                            <div id="Query-section-aggregation" onClick={this.open} className="Query-section Query-section-aggregation cursor-pointer">
-                                <span className="View-section-aggregation QueryOption py1 mx1">
-                                    { aggregationName == null ?
-                                        t`Choose an aggregation`
-                                    : name ?
-                                        name
-                                    :
-                                        aggregationName
-                                    }
-                                </span>
-                            </div>
-                        </Clearable>
-                        {this.renderPopover()}
-                    </div>
-                </div>
-            );
-        } else if (addButton) {
-            return (
-                <div className={cx("Query-section Query-section-aggregation")} onClick={this.open}>
-                    {addButton}
-                    {this.renderPopover()}
-                </div>
-            );
-        } else {
-            return null;
-        }
+  }
+
+  render() {
+    const { aggregation, addButton, name } = this.props;
+    if (aggregation && aggregation.length > 0) {
+      let aggregationName = NamedClause.isNamed(aggregation)
+        ? NamedClause.getName(aggregation)
+        : AggregationClause.isCustom(aggregation)
+          ? this.renderCustomAggregation()
+          : AggregationClause.isMetric(aggregation)
+            ? this.renderMetricAggregation()
+            : this.renderStandardAggregation();
+
+      return (
+        <div
+          className={cx("Query-section Query-section-aggregation", {
+            selected: this.state.isOpen,
+          })}
+        >
+          <div>
+            <Clearable onClear={this.props.removeAggregation}>
+              <div
+                id="Query-section-aggregation"
+                onClick={this.open}
+                className="Query-section Query-section-aggregation cursor-pointer"
+              >
+                <span className="View-section-aggregation QueryOption py1 mx1">
+                  {aggregationName == null
+                    ? t`Choose an aggregation`
+                    : name ? name : aggregationName}
+                </span>
+              </div>
+            </Clearable>
+            {this.renderPopover()}
+          </div>
+        </div>
+      );
+    } else if (addButton) {
+      return (
+        <div
+          className={cx("Query-section Query-section-aggregation")}
+          onClick={this.open}
+        >
+          {addButton}
+          {this.renderPopover()}
+        </div>
+      );
+    } else {
+      return null;
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx
index 82591e32ea14c5b3cea41ba3245904166351d071..4f6cf88b1cbea8914cc0a1aa7d92c83344305573 100644
--- a/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx
+++ b/frontend/src/metabase/query_builder/components/AlertListPopoverContent.jsx
@@ -1,271 +1,347 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
+import { t, jt } from "c-3po";
+import _ from "underscore";
+import cx from "classnames";
+import cxs from "cxs";
+
 import { getQuestionAlerts } from "metabase/query_builder/selectors";
 import { getUser } from "metabase/selectors/user";
 import { deleteAlert, unsubscribeFromAlert } from "metabase/alert/alert";
-import { AM_PM_OPTIONS, DAY_OF_WEEK_OPTIONS, HOUR_OPTIONS } from "metabase/components/SchedulePicker"
+import {
+  AM_PM_OPTIONS,
+  DAY_OF_WEEK_OPTIONS,
+  HOUR_OPTIONS,
+} from "metabase/components/SchedulePicker";
 import Icon from "metabase/components/Icon";
 import Modal from "metabase/components/Modal";
-import { CreateAlertModalContent, UpdateAlertModalContent } from "metabase/query_builder/components/AlertModals";
-import _ from "underscore";
-import cx from "classnames";
-import cxs from 'cxs';
-
-const unsubscribedClasses = cxs ({
-    marginLeft: '10px'
-})
-const ownAlertClasses = cxs ({
-    marginLeft: '9px',
-    marginRight: '17px'
-})
-const unsubscribeButtonClasses = cxs ({
-    transform: `translateY(4px)`
-})
-const popoverClasses = cxs ({
-    minWidth: '410px'
-})
-
-@connect((state) => ({ questionAlerts: getQuestionAlerts(state), user: getUser(state) }), null)
+import {
+  CreateAlertModalContent,
+  UpdateAlertModalContent,
+} from "metabase/query_builder/components/AlertModals";
+
+const unsubscribedClasses = cxs({
+  marginLeft: "10px",
+});
+const ownAlertClasses = cxs({
+  marginLeft: "9px",
+  marginRight: "17px",
+});
+const unsubscribeButtonClasses = cxs({
+  transform: `translateY(4px)`,
+});
+const popoverClasses = cxs({
+  minWidth: "410px",
+});
+
+@connect(
+  state => ({ questionAlerts: getQuestionAlerts(state), user: getUser(state) }),
+  null,
+)
 export class AlertListPopoverContent extends Component {
-    props: {
-        questionAlerts: any[],
-        setMenuFreeze: (boolean) => void,
-        closeMenu: () => void
-    }
-
-    state = {
-        adding: false,
-        hasJustUnsubscribedFromOwnAlert: false
-    }
-
-    onAdd = () => {
-        this.props.setMenuFreeze(true)
-        this.setState({ adding: true })
-    }
-
-    onEndAdding = (closeMenu = false) => {
-        this.props.setMenuFreeze(false)
-        this.setState({ adding: false })
-        if (closeMenu) this.props.closeMenu()
-    }
-
-    isCreatedByCurrentUser = (alert) => {
-        const { user } = this.props;
-        return alert.creator.id === user.id
-    }
-
-    onUnsubscribe = (alert) => {
-        if (this.isCreatedByCurrentUser(alert)) {
-            this.setState({ hasJustUnsubscribedFromOwnAlert: true })
-        }
-    }
-
-    render() {
-        const { questionAlerts, setMenuFreeze, user, closeMenu } = this.props;
-        const { adding, hasJustUnsubscribedFromOwnAlert } = this.state
-
-        const isNonAdmin = !user.is_superuser
-        const [ownAlerts, othersAlerts] = _.partition(questionAlerts, this.isCreatedByCurrentUser)
-        // user's own alert should be shown first if it exists
-        const sortedQuestionAlerts = [...ownAlerts, ...othersAlerts]
-        const hasOwnAlerts = ownAlerts.length > 0
-        const hasOwnAndOthers = hasOwnAlerts && othersAlerts.length > 0
-
-        return (
-            <div className={popoverClasses}>
-                <ul>
-                    { Object.values(sortedQuestionAlerts).map((alert) =>
-                        <AlertListItem
-                            alert={alert}
-                            setMenuFreeze={setMenuFreeze}
-                            closeMenu={closeMenu}
-                            highlight={isNonAdmin && hasOwnAndOthers && this.isCreatedByCurrentUser(alert)}
-                            onUnsubscribe={this.onUnsubscribe}
-                        />)
-                    }
-                </ul>
-                { (!hasOwnAlerts || hasJustUnsubscribedFromOwnAlert) &&
-                    <div className="border-top p2 bg-light-blue">
-                        <a className="link flex align-center text-bold text-small" onClick={this.onAdd}>
-                            <Icon name="add" className={ownAlertClasses} /> {t`Set up your own alert`}
-                        </a>
-                    </div>
-                }
-                { adding && <Modal full onClose={this.onEndAdding}>
-                    <CreateAlertModalContent onCancel={this.onEndAdding} onAlertCreated={() => this.onEndAdding(true) } />
-                </Modal> }
-            </div>
-        )
+  props: {
+    questionAlerts: any[],
+    setMenuFreeze: boolean => void,
+    closeMenu: () => void,
+  };
+
+  state = {
+    adding: false,
+    hasJustUnsubscribedFromOwnAlert: false,
+  };
+
+  onAdd = () => {
+    this.props.setMenuFreeze(true);
+    this.setState({ adding: true });
+  };
+
+  onEndAdding = (closeMenu = false) => {
+    this.props.setMenuFreeze(false);
+    this.setState({ adding: false });
+    if (closeMenu) this.props.closeMenu();
+  };
+
+  isCreatedByCurrentUser = alert => {
+    const { user } = this.props;
+    return alert.creator.id === user.id;
+  };
+
+  onUnsubscribe = alert => {
+    if (this.isCreatedByCurrentUser(alert)) {
+      this.setState({ hasJustUnsubscribedFromOwnAlert: true });
     }
+  };
+
+  render() {
+    const { questionAlerts, setMenuFreeze, user, closeMenu } = this.props;
+    const { adding, hasJustUnsubscribedFromOwnAlert } = this.state;
+
+    const isNonAdmin = !user.is_superuser;
+    const [ownAlerts, othersAlerts] = _.partition(
+      questionAlerts,
+      this.isCreatedByCurrentUser,
+    );
+    // user's own alert should be shown first if it exists
+    const sortedQuestionAlerts = [...ownAlerts, ...othersAlerts];
+    const hasOwnAlerts = ownAlerts.length > 0;
+    const hasOwnAndOthers = hasOwnAlerts && othersAlerts.length > 0;
+
+    return (
+      <div className={popoverClasses}>
+        <ul>
+          {Object.values(sortedQuestionAlerts).map(alert => (
+            <AlertListItem
+              alert={alert}
+              setMenuFreeze={setMenuFreeze}
+              closeMenu={closeMenu}
+              highlight={
+                isNonAdmin &&
+                hasOwnAndOthers &&
+                this.isCreatedByCurrentUser(alert)
+              }
+              onUnsubscribe={this.onUnsubscribe}
+            />
+          ))}
+        </ul>
+        {(!hasOwnAlerts || hasJustUnsubscribedFromOwnAlert) && (
+          <div className="border-top p2 bg-light-blue">
+            <a
+              className="link flex align-center text-bold text-small"
+              onClick={this.onAdd}
+            >
+              <Icon name="add" className={ownAlertClasses} />{" "}
+              {t`Set up your own alert`}
+            </a>
+          </div>
+        )}
+        {adding && (
+          <Modal full onClose={this.onEndAdding}>
+            <CreateAlertModalContent
+              onCancel={this.onEndAdding}
+              onAlertCreated={() => this.onEndAdding(true)}
+            />
+          </Modal>
+        )}
+      </div>
+    );
+  }
 }
 
-@connect((state) => ({ user: getUser(state) }), { unsubscribeFromAlert, deleteAlert })
+@connect(state => ({ user: getUser(state) }), {
+  unsubscribeFromAlert,
+  deleteAlert,
+})
 export class AlertListItem extends Component {
-    props: {
-        alert: any,
-        user: any,
-        setMenuFreeze: (boolean) => void,
-        closeMenu: () => void,
-        onUnsubscribe: () => void
+  props: {
+    alert: any,
+    user: any,
+    setMenuFreeze: boolean => void,
+    closeMenu: () => void,
+    onUnsubscribe: () => void,
+  };
+
+  state = {
+    unsubscribingProgress: null,
+    hasJustUnsubscribed: false,
+    editing: false,
+  };
+
+  onUnsubscribe = async () => {
+    const { alert } = this.props;
+
+    try {
+      this.setState({ unsubscribingProgress: t`Unsubscribing...` });
+      await this.props.unsubscribeFromAlert(alert);
+      this.setState({ hasJustUnsubscribed: true });
+      this.props.onUnsubscribe(alert);
+    } catch (e) {
+      this.setState({ unsubscribingProgress: t`Failed to unsubscribe` });
     }
+  };
 
-    state = {
-        unsubscribingProgress: null,
-        hasJustUnsubscribed: false,
-        editing: false
-    }
+  onEdit = () => {
+    this.props.setMenuFreeze(true);
+    this.setState({ editing: true });
+  };
 
-    onUnsubscribe = async () => {
-        const { alert } = this.props
-
-        try {
-            this.setState({ unsubscribingProgress: t`Unsubscribing...` })
-            await this.props.unsubscribeFromAlert(alert)
-            this.setState({ hasJustUnsubscribed: true })
-            this.props.onUnsubscribe(alert)
-        } catch(e) {
-            this.setState({ unsubscribingProgress: t`Failed to unsubscribe` })
-        }
-    }
+  onEndEditing = (shouldCloseMenu = false) => {
+    this.props.setMenuFreeze(false);
+    this.setState({ editing: false });
+    if (shouldCloseMenu) this.props.closeMenu();
+  };
 
-    onEdit = () => {
-        this.props.setMenuFreeze(true)
-        this.setState({ editing: true })
-    }
+  render() {
+    const { user, alert, highlight } = this.props;
+    const { editing, hasJustUnsubscribed, unsubscribingProgress } = this.state;
+
+    const isAdmin = user.is_superuser;
+    const isCurrentUser = alert.creator.id === user.id;
+
+    const emailChannel = alert.channels.find(c => c.channel_type === "email");
+    const emailEnabled = emailChannel && emailChannel.enabled;
+    const slackChannel = alert.channels.find(c => c.channel_type === "slack");
+    const slackEnabled = slackChannel && slackChannel.enabled;
 
-    onEndEditing = (shouldCloseMenu = false) => {
-        this.props.setMenuFreeze(false)
-        this.setState({ editing: false })
-        if (shouldCloseMenu) this.props.closeMenu()
+    if (hasJustUnsubscribed) {
+      return <UnsubscribedListItem />;
     }
 
-    render() {
-        const { user, alert, highlight } = this.props
-        const { editing, hasJustUnsubscribed, unsubscribingProgress } = this.state
-
-        const isAdmin = user.is_superuser
-        const isCurrentUser = alert.creator.id === user.id
-
-        const emailChannel = alert.channels.find((c) => c.channel_type === "email")
-        const emailEnabled = emailChannel && emailChannel.enabled
-        const slackChannel = alert.channels.find((c) => c.channel_type === "slack")
-        const slackEnabled = slackChannel && slackChannel.enabled
-
-        if (hasJustUnsubscribed) {
-            return <UnsubscribedListItem />
-        }
-
-        return (
-            <li className={cx("flex p3 text-grey-4 border-bottom", { "bg-light-blue": highlight })}>
-                <Icon name="alert" size="20" />
-                <div className="full ml2">
-                    <div className="flex align-top">
-                        <div>
-                            <AlertCreatorTitle alert={alert} user={user} />
-                        </div>
-                        <div className={`${unsubscribeButtonClasses} ml-auto text-bold text-small`}>
-                            { (isAdmin || isCurrentUser) && <a className="link" onClick={this.onEdit}>{jt`Edit`}</a> }
-                            { !isAdmin && !unsubscribingProgress && <a className="link ml2" onClick={this.onUnsubscribe}>{jt`Unsubscribe`}</a> }
-                            { !isAdmin && unsubscribingProgress && <span> {unsubscribingProgress}</span>}
-                        </div>
-                    </div>
-
-                    {
-                        // To-do: @kdoh wants to look into overall alignment
-                    }
-                    <ul className="flex mt2 text-small">
-                        <li className="flex align-center">
-                            <Icon name="clock" size="12" className="mr1" /> <AlertScheduleText schedule={alert.channels[0]} verbose={!isAdmin} />
-                        </li>
-                        { isAdmin && emailEnabled &&
-                            <li className="ml3 flex align-center">
-                                <Icon name="mail" className="mr1" />
-                                { emailChannel.recipients.length }
-                            </li>
-                        }
-                        { isAdmin && slackEnabled &&
-                            <li className="ml3 flex align-center">
-                                <Icon name="slack" size={16} className="mr1" />
-                                { slackChannel.details && slackChannel.details.channel.replace("#","") || t`No channel` }
-                            </li>
-                        }
-                    </ul>
-                </div>
-
-                { editing && <Modal full onClose={this.onEndEditing}>
-                    <UpdateAlertModalContent
-                        alert={alert}
-                        onCancel={this.onEndEditing}
-                        onAlertUpdated={() => this.onEndEditing(true)}
-                    />
-                </Modal> }
+    return (
+      <li
+        className={cx("flex p3 text-grey-4 border-bottom", {
+          "bg-light-blue": highlight,
+        })}
+      >
+        <Icon name="alert" size="20" />
+        <div className="full ml2">
+          <div className="flex align-top">
+            <div>
+              <AlertCreatorTitle alert={alert} user={user} />
+            </div>
+            <div
+              className={`${unsubscribeButtonClasses} ml-auto text-bold text-small`}
+            >
+              {(isAdmin || isCurrentUser) && (
+                <a className="link" onClick={this.onEdit}>{jt`Edit`}</a>
+              )}
+              {!isAdmin &&
+                !unsubscribingProgress && (
+                  <a
+                    className="link ml2"
+                    onClick={this.onUnsubscribe}
+                  >{jt`Unsubscribe`}</a>
+                )}
+              {!isAdmin &&
+                unsubscribingProgress && <span> {unsubscribingProgress}</span>}
+            </div>
+          </div>
+
+          {
+            // To-do: @kdoh wants to look into overall alignment
+          }
+          <ul className="flex mt2 text-small">
+            <li className="flex align-center">
+              <Icon name="clock" size="12" className="mr1" />{" "}
+              <AlertScheduleText
+                schedule={alert.channels[0]}
+                verbose={!isAdmin}
+              />
             </li>
-        )
-    }
+            {isAdmin &&
+              emailEnabled && (
+                <li className="ml3 flex align-center">
+                  <Icon name="mail" className="mr1" />
+                  {emailChannel.recipients.length}
+                </li>
+              )}
+            {isAdmin &&
+              slackEnabled && (
+                <li className="ml3 flex align-center">
+                  <Icon name="slack" size={16} className="mr1" />
+                  {(slackChannel.details &&
+                    slackChannel.details.channel.replace("#", "")) ||
+                    t`No channel`}
+                </li>
+              )}
+          </ul>
+        </div>
+
+        {editing && (
+          <Modal full onClose={this.onEndEditing}>
+            <UpdateAlertModalContent
+              alert={alert}
+              onCancel={this.onEndEditing}
+              onAlertUpdated={() => this.onEndEditing(true)}
+            />
+          </Modal>
+        )}
+      </li>
+    );
+  }
 }
 
-export const UnsubscribedListItem = () =>
-    <li className="border-bottom flex align-center py4 text-bold">
-        <div className="circle flex align-center justify-center p1 bg-grey-0 ml2">
-            <Icon name="check" className="text-success" />
-        </div>
-        <h3 className={`${unsubscribedClasses} text-dark`} >{jt`Okay, you're unsubscribed`}</h3>
-    </li>
+export const UnsubscribedListItem = () => (
+  <li className="border-bottom flex align-center py4 text-bold">
+    <div className="circle flex align-center justify-center p1 bg-grey-0 ml2">
+      <Icon name="check" className="text-success" />
+    </div>
+    <h3
+      className={`${unsubscribedClasses} text-dark`}
+    >{jt`Okay, you're unsubscribed`}</h3>
+  </li>
+);
 
 export class AlertScheduleText extends Component {
-    getScheduleText = () => {
-        const { schedule, verbose } = this.props
-        const scheduleType = schedule.schedule_type
-
-        // these are pretty much copy-pasted from SchedulePicker
-        if (scheduleType === "hourly") {
-            return verbose ? "hourly" : "Hourly";
-        } else if (scheduleType === "daily") {
-            const hourOfDay = schedule.schedule_hour;
-            const hour = _.find(HOUR_OPTIONS, (opt) => opt.value === hourOfDay % 12).name;
-            const amPm = _.find(AM_PM_OPTIONS, (opt) => opt.value === (hourOfDay >= 12 ? 1 : 0)).name;
-
-            return `${verbose ? "daily at " : "Daily, "} ${hour} ${amPm}`
-        } else if (scheduleType === "weekly") {
-            console.log(schedule)
-            const hourOfDay = schedule.schedule_hour;
-            const day = _.find(DAY_OF_WEEK_OPTIONS, (o) => o.value === schedule.schedule_day).name
-            const hour = _.find(HOUR_OPTIONS, (opt) => opt.value === (hourOfDay % 12)).name;
-            const amPm = _.find(AM_PM_OPTIONS, (opt) => opt.value === (hourOfDay >= 12 ? 1 : 0)).name;
-
-            if (verbose) {
-                return `weekly on ${day}s at ${hour} ${amPm}`
-            } else {
-                // omit the minute part of time
-                return `${day}s, ${hour.substr(0, hour.indexOf(':'))} ${amPm}`
-            }
-        }
+  getScheduleText = () => {
+    const { schedule, verbose } = this.props;
+    const scheduleType = schedule.schedule_type;
+
+    // these are pretty much copy-pasted from SchedulePicker
+    if (scheduleType === "hourly") {
+      return verbose ? "hourly" : "Hourly";
+    } else if (scheduleType === "daily") {
+      const hourOfDay = schedule.schedule_hour;
+      const hour = _.find(HOUR_OPTIONS, opt => opt.value === hourOfDay % 12)
+        .name;
+      const amPm = _.find(
+        AM_PM_OPTIONS,
+        opt => opt.value === (hourOfDay >= 12 ? 1 : 0),
+      ).name;
+
+      return `${verbose ? "daily at " : "Daily, "} ${hour} ${amPm}`;
+    } else if (scheduleType === "weekly") {
+      console.log(schedule);
+      const hourOfDay = schedule.schedule_hour;
+      const day = _.find(
+        DAY_OF_WEEK_OPTIONS,
+        o => o.value === schedule.schedule_day,
+      ).name;
+      const hour = _.find(HOUR_OPTIONS, opt => opt.value === hourOfDay % 12)
+        .name;
+      const amPm = _.find(
+        AM_PM_OPTIONS,
+        opt => opt.value === (hourOfDay >= 12 ? 1 : 0),
+      ).name;
+
+      if (verbose) {
+        return `weekly on ${day}s at ${hour} ${amPm}`;
+      } else {
+        // omit the minute part of time
+        return `${day}s, ${hour.substr(0, hour.indexOf(":"))} ${amPm}`;
+      }
     }
+  };
 
-    render() {
-        const { verbose } = this.props
+  render() {
+    const { verbose } = this.props;
 
-        const scheduleText = this.getScheduleText()
+    const scheduleText = this.getScheduleText();
 
-        if (verbose) {
-            return <span>Checking <b>{ scheduleText }</b></span>
-        } else {
-            return <span>{ scheduleText }</span>
-        }
+    if (verbose) {
+      return (
+        <span>
+          Checking <b>{scheduleText}</b>
+        </span>
+      );
+    } else {
+      return <span>{scheduleText}</span>;
     }
+  }
 }
 
 export class AlertCreatorTitle extends Component {
-    render () {
-        const { alert, user } = this.props
-
-        const isAdmin = user.is_superuser
-        const isCurrentUser = alert.creator.id === user.id
-        const creator = alert.creator.id === user.id ? "You" : alert.creator.first_name
-        const text = (!isCurrentUser && !isAdmin)
-            ? t`You're receiving ${creator}'s alerts`
-            : t`${creator} set up an alert`
-
-        return <h3 className="text-dark">{text}</h3>
-    }
+  render() {
+    const { alert, user } = this.props;
+
+    const isAdmin = user.is_superuser;
+    const isCurrentUser = alert.creator.id === user.id;
+    const creator =
+      alert.creator.id === user.id ? "You" : alert.creator.first_name;
+    const text =
+      !isCurrentUser && !isAdmin
+        ? t`You're receiving ${creator}'s alerts`
+        : t`${creator} set up an alert`;
+
+    return <h3 className="text-dark">{text}</h3>;
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/AlertModals.jsx b/frontend/src/metabase/query_builder/components/AlertModals.jsx
index de4efa3c33be4cb6f607ae35beeffde8814d4f3b..24d955b8ba7c28c6f1b8d289dac659d5b23985e8 100644
--- a/frontend/src/metabase/query_builder/components/AlertModals.jsx
+++ b/frontend/src/metabase/query_builder/components/AlertModals.jsx
@@ -1,532 +1,660 @@
 import React, { Component } from "react";
+import { connect } from "react-redux";
+import { t, jt } from "c-3po";
+
 import Button from "metabase/components/Button";
 import SchedulePicker from "metabase/components/SchedulePicker";
-import { connect } from "react-redux";
 import { createAlert, deleteAlert, updateAlert } from "metabase/alert/alert";
 import ModalContent from "metabase/components/ModalContent";
 import { getUser, getUserIsAdmin } from "metabase/selectors/user";
-import { getQuestion, getVisualizationSettings } from "metabase/query_builder/selectors";
+import {
+  getQuestion,
+  getVisualizationSettings,
+} from "metabase/query_builder/selectors";
 import _ from "underscore";
 import PulseEditChannels from "metabase/pulse/components/PulseEditChannels";
 import { fetchPulseFormInput, fetchUsers } from "metabase/pulse/actions";
 import {
-    formInputSelector, hasConfiguredAnyChannelSelector, hasConfiguredEmailChannelSelector, hasLoadedChannelInfoSelector,
-    userListSelector
+  formInputSelector,
+  hasConfiguredAnyChannelSelector,
+  hasConfiguredEmailChannelSelector,
+  hasLoadedChannelInfoSelector,
+  userListSelector,
 } from "metabase/pulse/selectors";
 import DeleteModalWithConfirm from "metabase/components/DeleteModalWithConfirm";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import { inflect } from "metabase/lib/formatting";
 import {
-    ALERT_TYPE_PROGRESS_BAR_GOAL, ALERT_TYPE_ROWS, ALERT_TYPE_TIMESERIES_GOAL,
-    getDefaultAlert
+  ALERT_TYPE_PROGRESS_BAR_GOAL,
+  ALERT_TYPE_ROWS,
+  ALERT_TYPE_TIMESERIES_GOAL,
+  getDefaultAlert,
 } from "metabase-lib/lib/Alert";
 import type { AlertType } from "metabase-lib/lib/Alert";
 import Radio from "metabase/components/Radio";
 import RetinaImage from "react-retina-image";
 import Icon from "metabase/components/Icon";
 import MetabaseCookies from "metabase/lib/cookies";
-import cxs from 'cxs';
+import cxs from "cxs";
 import ChannelSetupModal from "metabase/components/ChannelSetupModal";
 import ButtonWithStatus from "metabase/components/ButtonWithStatus";
 import { apiUpdateQuestion } from "metabase/query_builder/actions";
 
-const getScheduleFromChannel = (channel) =>
-    _.pick(channel, "schedule_day", "schedule_frame", "schedule_hour", "schedule_type")
-const classes = cxs ({
-    width: '162px',
-})
+const getScheduleFromChannel = channel =>
+  _.pick(
+    channel,
+    "schedule_day",
+    "schedule_frame",
+    "schedule_hour",
+    "schedule_type",
+  );
+const classes = cxs({
+  width: "162px",
+});
 
-@connect((state) => ({
+@connect(
+  state => ({
     question: getQuestion(state),
     visualizationSettings: getVisualizationSettings(state),
     isAdmin: getUserIsAdmin(state),
     user: getUser(state),
     hasLoadedChannelInfo: hasLoadedChannelInfoSelector(state),
     hasConfiguredAnyChannel: hasConfiguredAnyChannelSelector(state),
-    hasConfiguredEmailChannel: hasConfiguredEmailChannelSelector(state)
-}), { createAlert, fetchPulseFormInput, apiUpdateQuestion })
+    hasConfiguredEmailChannel: hasConfiguredEmailChannelSelector(state),
+  }),
+  { createAlert, fetchPulseFormInput, apiUpdateQuestion },
+)
 export class CreateAlertModalContent extends Component {
-    props: {
-        onCancel: () => void,
-        onAlertCreated: () => void
-    }
-
-    constructor(props) {
-        super()
-
-        const { question, user, visualizationSettings } = props
-
-        this.state = {
-            hasSeenEducationalScreen: MetabaseCookies.getHasSeenAlertSplash(),
-            alert: getDefaultAlert(question, user, visualizationSettings)
-        }
-    }
-
-    componentWillReceiveProps(newProps) {
-        // NOTE Atte Keinänen 11/6/17: Don't fill in the card information yet
-        // Because `onCreate` and `onSave` of QueryHeader mix Redux action dispatches and `setState` calls,
-        // we don't have up-to-date card information in the constructor yet
-        // TODO: Refactor QueryHeader so that `onCreate` and `onSave` only call Redux actions and don't modify the local state
-        if (this.props.question !== newProps.question) {
-            this.setState({
-                alert: {
-                    ...this.state.alert,
-                    card: { id: newProps.question.id() }
-                }
-            })
-        }
+  props: {
+    onCancel: () => void,
+    onAlertCreated: () => void,
+  };
+
+  constructor(props) {
+    super();
+
+    const { question, user, visualizationSettings } = props;
+
+    this.state = {
+      hasSeenEducationalScreen: MetabaseCookies.getHasSeenAlertSplash(),
+      alert: getDefaultAlert(question, user, visualizationSettings),
+    };
+  }
+
+  componentWillReceiveProps(newProps) {
+    // NOTE Atte Keinänen 11/6/17: Don't fill in the card information yet
+    // Because `onCreate` and `onSave` of QueryHeader mix Redux action dispatches and `setState` calls,
+    // we don't have up-to-date card information in the constructor yet
+    // TODO: Refactor QueryHeader so that `onCreate` and `onSave` only call Redux actions and don't modify the local state
+    if (this.props.question !== newProps.question) {
+      this.setState({
+        alert: {
+          ...this.state.alert,
+          card: { id: newProps.question.id() },
+        },
+      });
     }
-
-    componentWillMount() {
-        // loads the channel information
-        this.props.fetchPulseFormInput();
-    }
-
-    onAlertChange = (alert) => this.setState({ alert })
-
-    onCreateAlert = async () => {
-        const { createAlert, apiUpdateQuestion, onAlertCreated } = this.props
-        const { alert } = this.state
-
-        // Resave the question here (for persisting the x/y axes; see #6749)
-        await apiUpdateQuestion()
-
-        await createAlert(alert)
-
-        // should close be triggered manually like this
-        // but the creation notification would appear automatically ...?
-        // OR should the modal visibility be part of QB redux state
-        // (maybe check how other modals are implemented)
-        onAlertCreated()
+  }
+
+  componentWillMount() {
+    // loads the channel information
+    this.props.fetchPulseFormInput();
+  }
+
+  onAlertChange = alert => this.setState({ alert });
+
+  onCreateAlert = async () => {
+    const { createAlert, apiUpdateQuestion, onAlertCreated } = this.props;
+    const { alert } = this.state;
+
+    // Resave the question here (for persisting the x/y axes; see #6749)
+    await apiUpdateQuestion();
+
+    await createAlert(alert);
+
+    // should close be triggered manually like this
+    // but the creation notification would appear automatically ...?
+    // OR should the modal visibility be part of QB redux state
+    // (maybe check how other modals are implemented)
+    onAlertCreated();
+  };
+
+  proceedFromEducationalScreen = () => {
+    MetabaseCookies.setHasSeenAlertSplash(true);
+    this.setState({ hasSeenEducationalScreen: true });
+  };
+
+  render() {
+    const {
+      question,
+      visualizationSettings,
+      onCancel,
+      hasConfiguredAnyChannel,
+      hasConfiguredEmailChannel,
+      isAdmin,
+      user,
+      hasLoadedChannelInfo,
+    } = this.props;
+    const { alert, hasSeenEducationalScreen } = this.state;
+
+    const channelRequirementsMet = isAdmin
+      ? hasConfiguredAnyChannel
+      : hasConfiguredEmailChannel;
+
+    if (hasLoadedChannelInfo && !channelRequirementsMet) {
+      return (
+        <ChannelSetupModal
+          user={user}
+          onClose={onCancel}
+          entityNamePlural={t`alerts`}
+          channels={isAdmin ? ["email", "Slack"] : ["email"]}
+          fullPageModal
+        />
+      );
     }
-
-    proceedFromEducationalScreen = () => {
-        MetabaseCookies.setHasSeenAlertSplash(true)
-        this.setState({ hasSeenEducationalScreen: true })
+    if (!hasSeenEducationalScreen) {
+      return (
+        <ModalContent onClose={onCancel}>
+          <AlertEducationalScreen
+            onProceed={this.proceedFromEducationalScreen}
+          />
+        </ModalContent>
+      );
     }
 
-    render() {
-        const {
-            question,
-            visualizationSettings,
-            onCancel,
-            hasConfiguredAnyChannel,
-            hasConfiguredEmailChannel,
-            isAdmin,
-            user,
-            hasLoadedChannelInfo
-        } = this.props
-        const { alert, hasSeenEducationalScreen } = this.state
-
-        const channelRequirementsMet = isAdmin ? hasConfiguredAnyChannel : hasConfiguredEmailChannel
-
-        if (hasLoadedChannelInfo && !channelRequirementsMet) {
-            return (
-                <ChannelSetupModal
-                    user={user}
-                    onClose={onCancel}
-                    entityNamePlural={t`alerts`}
-                    channels={isAdmin ? ["email", "Slack"] : ["email"]}
-                    fullPageModal
-                />
-            )
-        }
-        if (!hasSeenEducationalScreen) {
-            return (
-                <ModalContent onClose={onCancel}>
-                    <AlertEducationalScreen onProceed={this.proceedFromEducationalScreen} />
-                </ModalContent>
-            )
-        }
-
-        // TODO: Remove PulseEdit css hack
-        return (
-            <ModalContent
-                onClose={onCancel}
-            >
-                <div className="PulseEdit ml-auto mr-auto mb4" style={{maxWidth: "550px"}}>
-                    <AlertModalTitle text={t`Let's set up your alert`} />
-                    <AlertEditForm
-                        alertType={question.alertType(visualizationSettings)}
-                        alert={alert}
-                        onAlertChange={this.onAlertChange}
-                    />
-                    <div className="flex align-center mt4">
-                        <div className="flex-full" />
-                        <Button onClick={onCancel} className="mr2">{t`Cancel`}</Button>
-                        <ButtonWithStatus
-                            titleForState={{default: t`Done`}}
-                            onClickOperation={this.onCreateAlert}
-                        />
-                    </div>
-                </div>
-            </ModalContent>
-        )
-    }
+    // TODO: Remove PulseEdit css hack
+    return (
+      <ModalContent onClose={onCancel}>
+        <div
+          className="PulseEdit ml-auto mr-auto mb4"
+          style={{ maxWidth: "550px" }}
+        >
+          <AlertModalTitle text={t`Let's set up your alert`} />
+          <AlertEditForm
+            alertType={question.alertType(visualizationSettings)}
+            alert={alert}
+            onAlertChange={this.onAlertChange}
+          />
+          <div className="flex align-center mt4">
+            <div className="flex-full" />
+            <Button onClick={onCancel} className="mr2">{t`Cancel`}</Button>
+            <ButtonWithStatus
+              titleForState={{ default: t`Done` }}
+              onClickOperation={this.onCreateAlert}
+            />
+          </div>
+        </div>
+      </ModalContent>
+    );
+  }
 }
 
 export class AlertEducationalScreen extends Component {
-    props: {
-        onProceed: () => void
-    }
+  props: {
+    onProceed: () => void,
+  };
 
-    render() {
-        const { onProceed } = this.props;
-
-        return (
-            <div className="pt2 pb4 ml-auto mr-auto text-centered">
-                <div className="pt4">
-                    <h1 className="mb1 text-dark">{t`The wide world of alerts`}</h1>
-                    <h3 className="mb4 text-normal text-dark">{t`There are a few different kinds of alerts you can get`}</h3>
-                </div>
-                {
-                    // @mazameli: needed to do some negative margin spacing to match the designs
-                }
-                <div className="text-normal pt3">
-                    <div className="relative flex align-center pr4" style={{marginLeft: -80}}>
-                        <RetinaImage src="app/assets/img/alerts/education-illustration-01-raw-data.png" />
-                        <p className={`${classes} ml2 text-left`}>{jt`When a raw data question ${<strong>returns any results</strong>}`}</p>
-                    </div>
-                    <div className="relative flex align-center flex-reverse pl4" style={{marginTop: -50, marginRight: -80}}>
-                        <RetinaImage src="app/assets/img/alerts/education-illustration-02-goal.png" />
-                        <p className={`${classes} mr2 text-right`}>{jt`When a line or bar ${<strong>crosses a goal line</strong>}`}</p>
-                    </div>
-                    <div className="relative flex align-center" style={{marginTop: -60, marginLeft: -55}}>
-                        <RetinaImage src="app/assets/img/alerts/education-illustration-03-progress.png" />
-                        <p className={`${classes} ml2 text-left`}>{jt`When a progress bar ${<strong>reaches its goal</strong>}`}</p>
-                    </div>
-                </div>
-                <Button primary className="mt4" onClick={onProceed}>{t`Set up an alert`}</Button>
-            </div>
-        )
-    }
+  render() {
+    const { onProceed } = this.props;
+
+    return (
+      <div className="pt2 pb4 ml-auto mr-auto text-centered">
+        <div className="pt4">
+          <h1 className="mb1 text-dark">{t`The wide world of alerts`}</h1>
+          <h3 className="mb4 text-normal text-dark">{t`There are a few different kinds of alerts you can get`}</h3>
+        </div>
+        {
+          // @mazameli: needed to do some negative margin spacing to match the designs
+        }
+        <div className="text-normal pt3">
+          <div
+            className="relative flex align-center pr4"
+            style={{ marginLeft: -80 }}
+          >
+            <RetinaImage src="app/assets/img/alerts/education-illustration-01-raw-data.png" />
+            <p
+              className={`${classes} ml2 text-left`}
+            >{jt`When a raw data question ${(
+              <strong>returns any results</strong>
+            )}`}</p>
+          </div>
+          <div
+            className="relative flex align-center flex-reverse pl4"
+            style={{ marginTop: -50, marginRight: -80 }}
+          >
+            <RetinaImage src="app/assets/img/alerts/education-illustration-02-goal.png" />
+            <p
+              className={`${classes} mr2 text-right`}
+            >{jt`When a line or bar ${(
+              <strong>crosses a goal line</strong>
+            )}`}</p>
+          </div>
+          <div
+            className="relative flex align-center"
+            style={{ marginTop: -60, marginLeft: -55 }}
+          >
+            <RetinaImage src="app/assets/img/alerts/education-illustration-03-progress.png" />
+            <p
+              className={`${classes} ml2 text-left`}
+            >{jt`When a progress bar ${<strong>reaches its goal</strong>}`}</p>
+          </div>
+        </div>
+        <Button
+          primary
+          className="mt4"
+          onClick={onProceed}
+        >{t`Set up an alert`}</Button>
+      </div>
+    );
+  }
 }
 
-@connect((state) => ({
+@connect(
+  state => ({
     user: getUser(state),
     isAdmin: getUserIsAdmin(state),
     question: getQuestion(state),
-    visualizationSettings: getVisualizationSettings(state)
-}), { apiUpdateQuestion, updateAlert, deleteAlert })
+    visualizationSettings: getVisualizationSettings(state),
+  }),
+  { apiUpdateQuestion, updateAlert, deleteAlert },
+)
 export class UpdateAlertModalContent extends Component {
-    props: {
-        alert: any,
-        onCancel: boolean,
-        onAlertUpdated: (any) => void,
-        updateAlert: (any) => void,
-        deleteAlert: (any) => void,
-        isAdmin: boolean
-    }
-
-    constructor(props) {
-        super()
-        this.state = {
-            modifiedAlert: props.alert
-        }
-    }
-
-    onAlertChange = (modifiedAlert) => this.setState({ modifiedAlert })
-
-    onUpdateAlert = async () => {
-        const { apiUpdateQuestion, updateAlert, onAlertUpdated } = this.props
-        const { modifiedAlert } = this.state
-
-        // Resave the question here (for persisting the x/y axes; see #6749)
-        await apiUpdateQuestion()
-
-        await updateAlert(modifiedAlert)
-        onAlertUpdated()
-    }
-
-    onDeleteAlert = async () => {
-        const { alert, deleteAlert, onAlertUpdated } = this.props
-        await deleteAlert(alert.id)
-        onAlertUpdated()
-    }
-
-    render() {
-        const { onCancel, question, visualizationSettings, alert, user, isAdmin } = this.props
-        const { modifiedAlert } = this.state
-
-        const isCurrentUser = alert.creator.id === user.id
-        const title = isCurrentUser ? t`Edit your alert` : t`Edit alert`
-        // TODO: Remove PulseEdit css hack
-        return (
-            <ModalContent
-                onClose={onCancel}
-            >
-                <div className="PulseEdit ml-auto mr-auto mb4" style={{maxWidth: "550px"}}>
-                    <AlertModalTitle text={title} />
-                    <AlertEditForm
-                        alertType={question.alertType(visualizationSettings)}
-                        alert={modifiedAlert}
-                        onAlertChange={this.onAlertChange}
-                    />
-                    { isAdmin && <DeleteAlertSection alert={alert} onDeleteAlert={this.onDeleteAlert} /> }
-
-                    <div className="flex align-center mt4">
-                        <div className="flex-full" />
-                        <Button onClick={onCancel} className="mr2">{t`Cancel`}</Button>
-                        <ButtonWithStatus
-                            titleForState={{default: t`Save changes`}}
-                            onClickOperation={this.onUpdateAlert}
-                        />
-                    </div>
-                </div>
-            </ModalContent>
-        )
-    }
+  props: {
+    alert: any,
+    onCancel: boolean,
+    onAlertUpdated: any => void,
+    updateAlert: any => void,
+    deleteAlert: any => void,
+    isAdmin: boolean,
+  };
+
+  constructor(props) {
+    super();
+    this.state = {
+      modifiedAlert: props.alert,
+    };
+  }
+
+  onAlertChange = modifiedAlert => this.setState({ modifiedAlert });
+
+  onUpdateAlert = async () => {
+    const { apiUpdateQuestion, updateAlert, onAlertUpdated } = this.props;
+    const { modifiedAlert } = this.state;
+
+    // Resave the question here (for persisting the x/y axes; see #6749)
+    await apiUpdateQuestion();
+
+    await updateAlert(modifiedAlert);
+    onAlertUpdated();
+  };
+
+  onDeleteAlert = async () => {
+    const { alert, deleteAlert, onAlertUpdated } = this.props;
+    await deleteAlert(alert.id);
+    onAlertUpdated();
+  };
+
+  render() {
+    const {
+      onCancel,
+      question,
+      visualizationSettings,
+      alert,
+      user,
+      isAdmin,
+    } = this.props;
+    const { modifiedAlert } = this.state;
+
+    const isCurrentUser = alert.creator.id === user.id;
+    const title = isCurrentUser ? t`Edit your alert` : t`Edit alert`;
+    // TODO: Remove PulseEdit css hack
+    return (
+      <ModalContent onClose={onCancel}>
+        <div
+          className="PulseEdit ml-auto mr-auto mb4"
+          style={{ maxWidth: "550px" }}
+        >
+          <AlertModalTitle text={title} />
+          <AlertEditForm
+            alertType={question.alertType(visualizationSettings)}
+            alert={modifiedAlert}
+            onAlertChange={this.onAlertChange}
+          />
+          {isAdmin && (
+            <DeleteAlertSection
+              alert={alert}
+              onDeleteAlert={this.onDeleteAlert}
+            />
+          )}
+
+          <div className="flex align-center mt4">
+            <div className="flex-full" />
+            <Button onClick={onCancel} className="mr2">{t`Cancel`}</Button>
+            <ButtonWithStatus
+              titleForState={{ default: t`Save changes` }}
+              onClickOperation={this.onUpdateAlert}
+            />
+          </div>
+        </div>
+      </ModalContent>
+    );
+  }
 }
 
 export class DeleteAlertSection extends Component {
-    deleteModal: any
-
-    getConfirmItems() {
-        // same as in PulseEdit but with some changes to copy
-        return this.props.alert.channels.map(c =>
-            c.channel_type === "email" ?
-                <span>{jt`This alert will no longer be emailed to ${<strong>{c.recipients.length} {inflect("address", c.recipients.length)}</strong>}.`}</span>
-                : c.channel_type === "slack" ?
-                <span>{jt`Slack channel ${<strong>{c.details && c.details.channel}</strong>} will no longer get this alert.`}</span>
-                :
-                <span>{jt`Channel ${<strong>{c.channel_type}</strong>} will no longer receive this alert.`}</span>
-        );
-    }
+  deleteModal: any;
+
+  getConfirmItems() {
+    // same as in PulseEdit but with some changes to copy
+    return this.props.alert.channels.map(
+      c =>
+        c.channel_type === "email" ? (
+          <span>{jt`This alert will no longer be emailed to ${(
+            <strong>
+              {c.recipients.length} {inflect("address", c.recipients.length)}
+            </strong>
+          )}.`}</span>
+        ) : c.channel_type === "slack" ? (
+          <span>{jt`Slack channel ${(
+            <strong>{c.details && c.details.channel}</strong>
+          )} will no longer get this alert.`}</span>
+        ) : (
+          <span>{jt`Channel ${(
+            <strong>{c.channel_type}</strong>
+          )} will no longer receive this alert.`}</span>
+        ),
+    );
+  }
+
+  render() {
+    const { onDeleteAlert } = this.props;
 
-    render() {
-        const { onDeleteAlert } = this.props
-
-        return (
-            <div className="DangerZone mt4 pt4 mb2 p3 rounded bordered relative">
-                <h3 className="text-error absolute top bg-white px1" style={{ marginTop: "-12px" }}>{jt`Danger Zone`}</h3>
-                <div className="ml1">
-                    <h4 className="text-bold mb1">{jt`Delete this alert`}</h4>
-                    <div className="flex">
-                        <p className="h4 pr2">{jt`Stop delivery and delete this alert. There's no undo, so be careful.`}</p>
-                        <ModalWithTrigger
-                            ref={(ref) => this.deleteModal = ref}
-                            triggerClasses="Button Button--danger flex-align-right flex-no-shrink"
-                            triggerElement="Delete this Alert"
-                        >
-                            <DeleteModalWithConfirm
-                                objectType="alert"
-                                title={t`Delete this alert?`}
-                                confirmItems={this.getConfirmItems()}
-                                onClose={() => this.deleteModal.close()}
-                                onDelete={onDeleteAlert}
-                            />
-                        </ModalWithTrigger>
-                    </div>
-                </div>
-            </div>
-        )
-    }
+    return (
+      <div className="DangerZone mt4 pt4 mb2 p3 rounded bordered relative">
+        <h3
+          className="text-error absolute top bg-white px1"
+          style={{ marginTop: "-12px" }}
+        >{jt`Danger Zone`}</h3>
+        <div className="ml1">
+          <h4 className="text-bold mb1">{jt`Delete this alert`}</h4>
+          <div className="flex">
+            <p className="h4 pr2">{jt`Stop delivery and delete this alert. There's no undo, so be careful.`}</p>
+            <ModalWithTrigger
+              ref={ref => (this.deleteModal = ref)}
+              triggerClasses="Button Button--danger flex-align-right flex-no-shrink"
+              triggerElement="Delete this Alert"
+            >
+              <DeleteModalWithConfirm
+                objectType="alert"
+                title={t`Delete this alert?`}
+                confirmItems={this.getConfirmItems()}
+                onClose={() => this.deleteModal.close()}
+                onDelete={onDeleteAlert}
+              />
+            </ModalWithTrigger>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
 
-const AlertModalTitle = ({ text }) =>
-    <div className="ml-auto mr-auto my4 pb2 text-centered">
-        <RetinaImage className="mb3" src="app/assets/img/alerts/alert-bell-confetti-illustration.png" />
-        <h1 className="text-dark">{ text }</h1>
-    </div>
-
-@connect((state) => ({ isAdmin: getUserIsAdmin(state) }), null)
+const AlertModalTitle = ({ text }) => (
+  <div className="ml-auto mr-auto my4 pb2 text-centered">
+    <RetinaImage
+      className="mb3"
+      src="app/assets/img/alerts/alert-bell-confetti-illustration.png"
+    />
+    <h1 className="text-dark">{text}</h1>
+  </div>
+);
+
+@connect(state => ({ isAdmin: getUserIsAdmin(state) }), null)
 export class AlertEditForm extends Component {
-    props: {
-        alertType: AlertType,
-        alert: any,
-        onAlertChange: (any) => void,
-        isAdmin: boolean
-    }
+  props: {
+    alertType: AlertType,
+    alert: any,
+    onAlertChange: any => void,
+    isAdmin: boolean,
+  };
 
-    onScheduleChange = (schedule) => {
-        const { alert, onAlertChange } = this.props;
+  onScheduleChange = schedule => {
+    const { alert, onAlertChange } = this.props;
 
-        // update the same schedule to all channels at once
-        onAlertChange({
-            ...alert,
-            channels: alert.channels.map((channel) => ({ ...channel, ...schedule }))
-        })
-    }
+    // update the same schedule to all channels at once
+    onAlertChange({
+      ...alert,
+      channels: alert.channels.map(channel => ({ ...channel, ...schedule })),
+    });
+  };
 
-    render() {
-        const { alertType, alert, isAdmin, onAlertChange } = this.props
-
-        // the schedule should be same for all channels so we can use the first one
-        const schedule = getScheduleFromChannel(alert.channels[0])
-
-        return (
-            <div>
-                <AlertGoalToggles
-                    alertType={alertType}
-                    alert={alert}
-                    onAlertChange={onAlertChange}
-                />
-                <AlertEditSchedule
-                    alertType={alertType}
-                    schedule={schedule}
-                    onScheduleChange={this.onScheduleChange}
-                />
-                { isAdmin &&
-                    <AlertEditChannels
-                        alert={alert}
-                        onAlertChange={onAlertChange}
-                    />
-                }
-            </div>
-        )
-    }
-}
+  render() {
+    const { alertType, alert, isAdmin, onAlertChange } = this.props;
 
-export const AlertGoalToggles = ({ alertType, alert, onAlertChange }) => {
-    const isTimeseries = alertType === ALERT_TYPE_TIMESERIES_GOAL
-    const isProgress = alertType === ALERT_TYPE_PROGRESS_BAR_GOAL
-
-    if (!isTimeseries && !isProgress) {
-        // not a goal alert
-        return null
-    }
+    // the schedule should be same for all channels so we can use the first one
+    const schedule = getScheduleFromChannel(alert.channels[0]);
 
     return (
-        <div>
-            <AlertAboveGoalToggle
-                alert={alert}
-                onAlertChange={onAlertChange}
-                title={isTimeseries ? t`Alert me when the line…` : t`Alert me when the progress bar…`}
-                trueText={isTimeseries ? t`Goes above the goal line` : t`Reaches the goal`}
-                falseText={isTimeseries ? t`Goes below the goal line` : t`Goes below the goal`}
-            />
-            <AlertFirstOnlyToggle
-                alert={alert}
-                onAlertChange={onAlertChange}
-                title={isTimeseries
-                    ? t`The first time it crosses, or every time?`
-                    : t`The first time it reaches the goal, or every time?`
-                }
-                trueText={t`The first time`}
-                falseText={t`Every time` }
-            />
-        </div>
-    )
+      <div>
+        <AlertGoalToggles
+          alertType={alertType}
+          alert={alert}
+          onAlertChange={onAlertChange}
+        />
+        <AlertEditSchedule
+          alertType={alertType}
+          schedule={schedule}
+          onScheduleChange={this.onScheduleChange}
+        />
+        {isAdmin && (
+          <AlertEditChannels alert={alert} onAlertChange={onAlertChange} />
+        )}
+      </div>
+    );
+  }
 }
 
-export const AlertAboveGoalToggle = (props) =>
-    <AlertSettingToggle {...props} setting="alert_above_goal" />
-
-export const AlertFirstOnlyToggle = (props) =>
-    <AlertSettingToggle {...props} setting="alert_first_only" />
-
-export const AlertSettingToggle = ({ alert, onAlertChange, title, trueText, falseText, setting }) =>
-    <div className="mb4 pb2">
-        <h3 className="text-dark mb1">{title}</h3>
-        <Radio
-            value={alert[setting]}
-            onChange={(value) => onAlertChange({ ...alert, [setting]: value })}
-            options={[{ name: trueText, value: true }, { name: falseText, value: false }]}
-        />
+export const AlertGoalToggles = ({ alertType, alert, onAlertChange }) => {
+  const isTimeseries = alertType === ALERT_TYPE_TIMESERIES_GOAL;
+  const isProgress = alertType === ALERT_TYPE_PROGRESS_BAR_GOAL;
+
+  if (!isTimeseries && !isProgress) {
+    // not a goal alert
+    return null;
+  }
+
+  return (
+    <div>
+      <AlertAboveGoalToggle
+        alert={alert}
+        onAlertChange={onAlertChange}
+        title={
+          isTimeseries
+            ? t`Alert me when the line…`
+            : t`Alert me when the progress bar…`
+        }
+        trueText={
+          isTimeseries ? t`Goes above the goal line` : t`Reaches the goal`
+        }
+        falseText={
+          isTimeseries ? t`Goes below the goal line` : t`Goes below the goal`
+        }
+      />
+      <AlertFirstOnlyToggle
+        alert={alert}
+        onAlertChange={onAlertChange}
+        title={
+          isTimeseries
+            ? t`The first time it crosses, or every time?`
+            : t`The first time it reaches the goal, or every time?`
+        }
+        trueText={t`The first time`}
+        falseText={t`Every time`}
+      />
     </div>
-
+  );
+};
+
+export const AlertAboveGoalToggle = props => (
+  <AlertSettingToggle {...props} setting="alert_above_goal" />
+);
+
+export const AlertFirstOnlyToggle = props => (
+  <AlertSettingToggle {...props} setting="alert_first_only" />
+);
+
+export const AlertSettingToggle = ({
+  alert,
+  onAlertChange,
+  title,
+  trueText,
+  falseText,
+  setting,
+}) => (
+  <div className="mb4 pb2">
+    <h3 className="text-dark mb1">{title}</h3>
+    <Radio
+      value={alert[setting]}
+      onChange={value => onAlertChange({ ...alert, [setting]: value })}
+      options={[
+        { name: trueText, value: true },
+        { name: falseText, value: false },
+      ]}
+    />
+  </div>
+);
 
 export class AlertEditSchedule extends Component {
-    render() {
-        const { alertType, schedule } = this.props;
-
-        return (
-            <div>
-                <h3 className="mt4 mb3 text-dark">How often should we check for results?</h3>
-
-                <div className="bordered rounded mb2">
-                    { alertType === ALERT_TYPE_ROWS && <RawDataAlertTip /> }
-                    <div className="p3 bg-grey-0">
-                        <SchedulePicker
-                            schedule={schedule}
-                            scheduleOptions={["hourly", "daily", "weekly"]}
-                            onScheduleChange={this.props.onScheduleChange}
-                            textBeforeInterval="Check"
-                        />
-                    </div>
-                </div>
-            </div>
-        )
-    }
+  render() {
+    const { alertType, schedule } = this.props;
+
+    return (
+      <div>
+        <h3 className="mt4 mb3 text-dark">
+          How often should we check for results?
+        </h3>
+
+        <div className="bordered rounded mb2">
+          {alertType === ALERT_TYPE_ROWS && <RawDataAlertTip />}
+          <div className="p3 bg-grey-0">
+            <SchedulePicker
+              schedule={schedule}
+              scheduleOptions={["hourly", "daily", "weekly"]}
+              onScheduleChange={this.props.onScheduleChange}
+              textBeforeInterval="Check"
+            />
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
 
 @connect(
-    (state) => ({ user: getUser(state), userList: userListSelector(state), formInput: formInputSelector(state) }),
-    { fetchPulseFormInput, fetchUsers }
+  state => ({
+    user: getUser(state),
+    userList: userListSelector(state),
+    formInput: formInputSelector(state),
+  }),
+  { fetchPulseFormInput, fetchUsers },
 )
 export class AlertEditChannels extends Component {
-    props: {
-        onChannelsChange: (any) => void,
-        user: any,
-        userList: any[],
-        // this stupidly named property contains different channel options, nothing else
-        formInput: any,
-        fetchPulseFormInput: () => void,
-        fetchUsers: () => void
-    }
-
-    componentDidMount() {
-        this.props.fetchPulseFormInput();
-        this.props.fetchUsers();
-    }
-
-    // Technically pulse definition is equal to alert definition
-    onSetPulse = (alert) => {
-        // If the pulse channel has been added, it PulseEditChannels puts the default schedule to it
-        // We want to have same schedule for all channels
-        const schedule = getScheduleFromChannel(alert.channels.find((c) => c.channel_type === "email"))
-
-        this.props.onAlertChange({
-            ...alert,
-            channels: alert.channels.map((channel) => ({ ...channel, ...schedule }))
-        })
-    }
-
-    render() {
-        const { alert, user, userList, formInput } = this.props;
-        return (
-            <div className="mt4 pt2">
-                <h3 className="text-dark mb3">{jt`Where do you want to send these alerts?`}</h3>
-                <div className="mb2">
-                    <PulseEditChannels
-                        pulse={alert}
-                        pulseId={alert.id}
-                        pulseIsValid={true}
-                        formInput={formInput}
-                        user={user}
-                        userList={userList}
-                        setPulse={this.onSetPulse}
-                        hideSchedulePicker={true}
-                        emailRecipientText={t`Email alerts to:`}
-                     />
-                </div>
-            </div>
-        )
-    }
+  props: {
+    onChannelsChange: any => void,
+    user: any,
+    userList: any[],
+    // this stupidly named property contains different channel options, nothing else
+    formInput: any,
+    fetchPulseFormInput: () => void,
+    fetchUsers: () => void,
+  };
+
+  componentDidMount() {
+    this.props.fetchPulseFormInput();
+    this.props.fetchUsers();
+  }
+
+  // Technically pulse definition is equal to alert definition
+  onSetPulse = alert => {
+    // If the pulse channel has been added, it PulseEditChannels puts the default schedule to it
+    // We want to have same schedule for all channels
+    const schedule = getScheduleFromChannel(
+      alert.channels.find(c => c.channel_type === "email"),
+    );
+
+    this.props.onAlertChange({
+      ...alert,
+      channels: alert.channels.map(channel => ({ ...channel, ...schedule })),
+    });
+  };
+
+  render() {
+    const { alert, user, userList, formInput } = this.props;
+    return (
+      <div className="mt4 pt2">
+        <h3 className="text-dark mb3">{jt`Where do you want to send these alerts?`}</h3>
+        <div className="mb2">
+          <PulseEditChannels
+            pulse={alert}
+            pulseId={alert.id}
+            pulseIsValid={true}
+            formInput={formInput}
+            user={user}
+            userList={userList}
+            setPulse={this.onSetPulse}
+            hideSchedulePicker={true}
+            emailRecipientText={t`Email alerts to:`}
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
 // TODO: Not sure how to translate text with formatting properly
-@connect((state) => ({ question: getQuestion(state), visualizationSettings: getVisualizationSettings(state) }))
+@connect(state => ({
+  question: getQuestion(state),
+  visualizationSettings: getVisualizationSettings(state),
+}))
 export class RawDataAlertTip extends Component {
-    render() {
-        const display = this.props.question.display()
-        const vizSettings = this.props.visualizationSettings
-        const goalEnabled = vizSettings["graph.show_goal"]
-        const isLineAreaBar = display === "line" || display === "area" || display === "bar"
-        const isMultiSeries =
-            isLineAreaBar && vizSettings["graph.metrics"] && vizSettings["graph.metrics"].length > 1
-        const showMultiSeriesGoalAlert = goalEnabled && isMultiSeries
-
-        return (
-            <div className="border-row-divider p3 flex align-center">
-                <div className="circle flex align-center justify-center bg-grey-0 p2 mr2 text-grey-3">
-                    <Icon name="lightbulb" size="20" />
-                </div>
-                { showMultiSeriesGoalAlert ? <MultiSeriesAlertTip /> : <NormalAlertTip /> }
-            </div>
-        )
-    }
+  render() {
+    const display = this.props.question.display();
+    const vizSettings = this.props.visualizationSettings;
+    const goalEnabled = vizSettings["graph.show_goal"];
+    const isLineAreaBar =
+      display === "line" || display === "area" || display === "bar";
+    const isMultiSeries =
+      isLineAreaBar &&
+      vizSettings["graph.metrics"] &&
+      vizSettings["graph.metrics"].length > 1;
+    const showMultiSeriesGoalAlert = goalEnabled && isMultiSeries;
+
+    return (
+      <div className="border-row-divider p3 flex align-center">
+        <div className="circle flex align-center justify-center bg-grey-0 p2 mr2 text-grey-3">
+          <Icon name="lightbulb" size="20" />
+        </div>
+        {showMultiSeriesGoalAlert ? (
+          <MultiSeriesAlertTip />
+        ) : (
+          <NormalAlertTip />
+        )}
+      </div>
+    );
+  }
 }
 
-export const MultiSeriesAlertTip = () => <div>{jt`${<strong>Heads up:</strong>} Goal-based alerts aren't yet supported for charts with more than one line, so this alert will be sent whenever the chart has ${<em>results</em>}.`}</div>
-export const NormalAlertTip  = () => <div>{jt`${<strong>Tip:</strong>} This kind of alert is most useful when your saved question doesn’t ${<em>usually</em>} return any results, but you want to know when it does.`}</div>
+export const MultiSeriesAlertTip = () => (
+  <div>{jt`${(
+    <strong>Heads up:</strong>
+  )} Goal-based alerts aren't yet supported for charts with more than one line, so this alert will be sent whenever the chart has ${(
+    <em>results</em>
+  )}.`}</div>
+);
+export const NormalAlertTip = () => (
+  <div>{jt`${(
+    <strong>Tip:</strong>
+  )} This kind of alert is most useful when your saved question doesn’t ${(
+    <em>usually</em>
+  )} return any results, but you want to know when it does.`}</div>
+);
diff --git a/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx b/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx
index 2483b1308779a122d823e279ca0dc7a10720d22b..ec3963b58d563f2156f9ac99459ca2d0dae7268f 100644
--- a/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/BreakoutWidget.jsx
@@ -7,98 +7,97 @@ import Popover from "metabase/components/Popover.jsx";
 
 import _ from "underscore";
 
-
 export default class BreakoutWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            isOpen: props.isInitiallyOpen || false
-        };
+    this.state = {
+      isOpen: props.isInitiallyOpen || false,
+    };
 
-        _.bindAll(this, "open", "close", "setBreakout");
-    }
+    _.bindAll(this, "open", "close", "setBreakout");
+  }
 
-    static propTypes = {
-        addButton: PropTypes.object,
-        field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
-        fieldOptions: PropTypes.object.isRequired,
-        customFieldOptions: PropTypes.object,
-        setField: PropTypes.func.isRequired,
-        isInitiallyOpen: PropTypes.bool,
-        tableMetadata: PropTypes.object.isRequired,
-        enableSubDimensions: PropTypes.bool
-    };
+  static propTypes = {
+    addButton: PropTypes.object,
+    field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
+    fieldOptions: PropTypes.object.isRequired,
+    customFieldOptions: PropTypes.object,
+    setField: PropTypes.func.isRequired,
+    isInitiallyOpen: PropTypes.bool,
+    tableMetadata: PropTypes.object.isRequired,
+    enableSubDimensions: PropTypes.bool,
+  };
 
-    static defaultProps = {
-        enableSubDimensions: true
-    };
+  static defaultProps = {
+    enableSubDimensions: true,
+  };
 
-    setBreakout(value) {
-        this.props.setField(value);
-        this.close();
-    }
+  setBreakout(value) {
+    this.props.setField(value);
+    this.close();
+  }
 
-    open() {
-        this.setState({ isOpen: true });
-    }
+  open() {
+    this.setState({ isOpen: true });
+  }
 
-    close() {
-        this.setState({ isOpen: false });
-    }
+  close() {
+    this.setState({ isOpen: false });
+  }
 
-    renderPopover() {
-        if (this.state.isOpen) {
-            return (
-                <Popover
-                    id="BreakoutPopover"
-                    ref="popover"
-                    className="FieldPopover"
-                    onClose={this.close}
-                >
-                    <FieldList
-                        className={"text-green"}
-                        tableMetadata={this.props.tableMetadata}
-                        field={this.props.field}
-                        fieldOptions={this.props.fieldOptions}
-                        customFieldOptions={this.props.customFieldOptions}
-                        onFieldChange={this.setBreakout}
-                        enableSubDimensions={this.props.enableSubDimensions}
-                    />
-                </Popover>
-            );
-        }
+  renderPopover() {
+    if (this.state.isOpen) {
+      return (
+        <Popover
+          id="BreakoutPopover"
+          ref="popover"
+          className="FieldPopover"
+          onClose={this.close}
+        >
+          <FieldList
+            className={"text-green"}
+            tableMetadata={this.props.tableMetadata}
+            field={this.props.field}
+            fieldOptions={this.props.fieldOptions}
+            customFieldOptions={this.props.customFieldOptions}
+            onFieldChange={this.setBreakout}
+            enableSubDimensions={this.props.enableSubDimensions}
+          />
+        </Popover>
+      );
     }
+  }
 
-    render() {
-        // if we have a field then render FieldName, otherwise display our + option if enabled
-        const { addButton, field, fieldOptions } = this.props;
+  render() {
+    // if we have a field then render FieldName, otherwise display our + option if enabled
+    const { addButton, field, fieldOptions } = this.props;
 
-        if (field) {
-            return (
-                <div className="flex align-center">
-                    <FieldName
-                        className={this.props.className}
-                        tableMetadata={this.props.tableMetadata}
-                        field={field}
-                        fieldOptions={this.props.fieldOptions}
-                        customFieldOptions={this.props.customFieldOptions}
-                        removeField={() => this.setBreakout(null)}
-                        onClick={this.open}
-                    />
-                    {this.renderPopover()}
-                </div>
-            );
-        } else if (addButton && fieldOptions && fieldOptions.count > 0) {
-            return (
-                <div id="BreakoutWidget" onClick={this.open}>
-                    {addButton}
-                    {this.renderPopover()}
-                </div>
-            );
-        } else {
-            // this needs to be here to prevent React error (#2304)
-            return null;
-        }
+    if (field) {
+      return (
+        <div className="flex align-center">
+          <FieldName
+            className={this.props.className}
+            tableMetadata={this.props.tableMetadata}
+            field={field}
+            fieldOptions={this.props.fieldOptions}
+            customFieldOptions={this.props.customFieldOptions}
+            removeField={() => this.setBreakout(null)}
+            onClick={this.open}
+          />
+          {this.renderPopover()}
+        </div>
+      );
+    } else if (addButton && fieldOptions && fieldOptions.count > 0) {
+      return (
+        <div id="BreakoutWidget" onClick={this.open}>
+          {addButton}
+          {this.renderPopover()}
+        </div>
+      );
+    } else {
+      // this needs to be here to prevent React error (#2304)
+      return null;
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/Clearable.jsx b/frontend/src/metabase/query_builder/components/Clearable.jsx
index 2016fe1240ff061d2246be1e89bd0a3c92c49835..c1a28c472630392fabda8824398d3c1e1aa08722 100644
--- a/frontend/src/metabase/query_builder/components/Clearable.jsx
+++ b/frontend/src/metabase/query_builder/components/Clearable.jsx
@@ -3,14 +3,18 @@ import cx from "classnames";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const Clearable = ({ onClear, children, className }) =>
-    <div className={cx("flex align-center", className)}>
-        {children}
-        { onClear &&
-            <a className="text-grey-2 no-decoration pr1 flex align-center" onClick={onClear}>
-                <Icon name='close' size={14} />
-            </a>
-        }
-    </div>
+const Clearable = ({ onClear, children, className }) => (
+  <div className={cx("flex align-center", className)}>
+    {children}
+    {onClear && (
+      <a
+        className="text-grey-2 no-decoration pr1 flex align-center"
+        onClick={onClear}
+      >
+        <Icon name="close" size={14} />
+      </a>
+    )}
+  </div>
+);
 
 export default Clearable;
diff --git a/frontend/src/metabase/query_builder/components/DataSelector.jsx b/frontend/src/metabase/query_builder/components/DataSelector.jsx
index 4f7fdf818da04392dd6ee016c96159e0ffbe2580..e3707c57190548b93ec9c9b5d8d61bea5c5dcf1e 100644
--- a/frontend/src/metabase/query_builder/components/DataSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/DataSelector.jsx
@@ -1,15 +1,15 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
-import cx from 'classnames'
+import { t } from "c-3po";
+import cx from "classnames";
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import AccordianList from "metabase/components/AccordianList.jsx";
-import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper"
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
-import { isQueryable } from 'metabase/lib/table';
-import { titleize, humanize } from 'metabase/lib/formatting';
+import { isQueryable } from "metabase/lib/table";
+import { titleize, humanize } from "metabase/lib/formatting";
 
 import { fetchTableMetadata } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
@@ -17,741 +17,975 @@ import { getMetadata } from "metabase/selectors/metadata";
 import _ from "underscore";
 
 // chooses a database
-const DATABASE_STEP = 'DATABASE';
+const DATABASE_STEP = "DATABASE";
 // chooses a database and a schema inside that database
-const DATABASE_SCHEMA_STEP = 'DATABASE_SCHEMA';
+const DATABASE_SCHEMA_STEP = "DATABASE_SCHEMA";
 // chooses a schema (given that a database has already been selected)
-const SCHEMA_STEP = 'SCHEMA';
+const SCHEMA_STEP = "SCHEMA";
 // chooses a database and a schema and provides additional "Segments" option for jumping to SEGMENT_STEP
-const SCHEMA_AND_SEGMENTS_STEP = 'SCHEMA_AND_SEGMENTS';
+const SCHEMA_AND_SEGMENTS_STEP = "SCHEMA_AND_SEGMENTS";
 // chooses a table (database has already been selected)
-const TABLE_STEP = 'TABLE';
+const TABLE_STEP = "TABLE";
 // chooses a table field (table has already been selected)
-const FIELD_STEP = 'FIELD';
+const FIELD_STEP = "FIELD";
 // shows either table or segment list depending on which one is selected
-const SEGMENT_OR_TABLE_STEP = 'SEGMENT_OR_TABLE_STEP';
-
-export const SchemaTableAndSegmentDataSelector = (props) =>
-    <DataSelector
-        steps={[SCHEMA_AND_SEGMENTS_STEP, SEGMENT_OR_TABLE_STEP]}
-        getTriggerElementContent={SchemaAndSegmentTriggerContent}
-        {...props}
-    />
-export const SchemaAndSegmentTriggerContent = ({ selectedTable, selectedSegment }) => {
-    if (selectedTable) {
-        return  <span className="text-grey no-decoration">{selectedTable.display_name || selectedTable.name}</span>;
-    } else if (selectedSegment) {
-        return <span className="text-grey no-decoration">{selectedSegment.name}</span>;
-    } else {
-        return <span className="text-grey-4 no-decoration">{t`Pick a segment or table`}</span>;
-    }
-}
-
-export const DatabaseDataSelector = (props) =>
-    <DataSelector
-        steps={[DATABASE_STEP]}
-        getTriggerElementContent={DatabaseTriggerContent}
-        {...props}
-    />
+const SEGMENT_OR_TABLE_STEP = "SEGMENT_OR_TABLE_STEP";
+
+export const SchemaTableAndSegmentDataSelector = props => (
+  <DataSelector
+    steps={[SCHEMA_AND_SEGMENTS_STEP, SEGMENT_OR_TABLE_STEP]}
+    getTriggerElementContent={SchemaAndSegmentTriggerContent}
+    {...props}
+  />
+);
+export const SchemaAndSegmentTriggerContent = ({
+  selectedTable,
+  selectedSegment,
+}) => {
+  if (selectedTable) {
+    return (
+      <span className="text-grey no-decoration">
+        {selectedTable.display_name || selectedTable.name}
+      </span>
+    );
+  } else if (selectedSegment) {
+    return (
+      <span className="text-grey no-decoration">{selectedSegment.name}</span>
+    );
+  } else {
+    return (
+      <span className="text-grey-4 no-decoration">{t`Pick a segment or table`}</span>
+    );
+  }
+};
+
+export const DatabaseDataSelector = props => (
+  <DataSelector
+    steps={[DATABASE_STEP]}
+    getTriggerElementContent={DatabaseTriggerContent}
+    {...props}
+  />
+);
 export const DatabaseTriggerContent = ({ selectedDatabase }) =>
-    selectedDatabase
-        ? <span className="text-grey no-decoration">{selectedDatabase.name}</span>
-        : <span className="text-grey-4 no-decoration">{t`Select a database`}</span>
-
-export const SchemaTableAndFieldDataSelector = (props) =>
-    <DataSelector
-        steps={[SCHEMA_STEP, TABLE_STEP, FIELD_STEP]}
-        getTriggerElementContent={FieldTriggerContent}
-        triggerIconSize={12}
-        renderAsSelect={true}
-        {...props}
-    />
+  selectedDatabase ? (
+    <span className="text-grey no-decoration">{selectedDatabase.name}</span>
+  ) : (
+    <span className="text-grey-4 no-decoration">{t`Select a database`}</span>
+  );
+
+export const SchemaTableAndFieldDataSelector = props => (
+  <DataSelector
+    steps={[SCHEMA_STEP, TABLE_STEP, FIELD_STEP]}
+    getTriggerElementContent={FieldTriggerContent}
+    triggerIconSize={12}
+    renderAsSelect={true}
+    {...props}
+  />
+);
 export const FieldTriggerContent = ({ selectedDatabase, selectedField }) => {
-    if (!selectedField || !selectedField.table) {
-        return <span className="flex-full text-grey-4 no-decoration">{t`Select...`}</span>
-    } else {
-        const hasMultipleSchemas = selectedDatabase && _.uniq(selectedDatabase.tables, (t) => t.schema).length > 1;
-        return (
-            <div className="flex-full cursor-pointer">
-                <div className="h6 text-bold text-uppercase text-grey-2">
-                    {hasMultipleSchemas && (selectedField.table.schema + " > ")}{selectedField.table.display_name}
-                </div>
-                <div className="h4 text-bold text-default">{selectedField.display_name}</div>
-            </div>
-        )
-    }
-}
-
-export const DatabaseSchemaAndTableDataSelector = (props) =>
-    <DataSelector
-        steps={[DATABASE_SCHEMA_STEP, TABLE_STEP]}
-        getTriggerElementContent={TableTriggerContent}
-        {...props}
-    />
-export const SchemaAndTableDataSelector = (props) =>
-    <DataSelector
-        steps={[SCHEMA_STEP, TABLE_STEP]}
-        getTriggerElementContent={TableTriggerContent}
-        {...props}
-    />
+  if (!selectedField || !selectedField.table) {
+    return (
+      <span className="flex-full text-grey-4 no-decoration">{t`Select...`}</span>
+    );
+  } else {
+    const hasMultipleSchemas =
+      selectedDatabase &&
+      _.uniq(selectedDatabase.tables, t => t.schema).length > 1;
+    return (
+      <div className="flex-full cursor-pointer">
+        <div className="h6 text-bold text-uppercase text-grey-2">
+          {hasMultipleSchemas && selectedField.table.schema + " > "}
+          {selectedField.table.display_name}
+        </div>
+        <div className="h4 text-bold text-default">
+          {selectedField.display_name}
+        </div>
+      </div>
+    );
+  }
+};
+
+export const DatabaseSchemaAndTableDataSelector = props => (
+  <DataSelector
+    steps={[DATABASE_SCHEMA_STEP, TABLE_STEP]}
+    getTriggerElementContent={TableTriggerContent}
+    {...props}
+  />
+);
+export const SchemaAndTableDataSelector = props => (
+  <DataSelector
+    steps={[SCHEMA_STEP, TABLE_STEP]}
+    getTriggerElementContent={TableTriggerContent}
+    {...props}
+  />
+);
 export const TableTriggerContent = ({ selectedTable }) =>
-    selectedTable
-        ? <span className="text-grey no-decoration">{selectedTable.display_name || selectedTable.name}</span>
-        : <span className="text-grey-4 no-decoration">{t`Select a table`}</span>
-
-@connect(state => ({metadata: getMetadata(state)}), { fetchTableMetadata })
+  selectedTable ? (
+    <span className="text-grey no-decoration">
+      {selectedTable.display_name || selectedTable.name}
+    </span>
+  ) : (
+    <span className="text-grey-4 no-decoration">{t`Select a table`}</span>
+  );
+
+@connect(state => ({ metadata: getMetadata(state) }), { fetchTableMetadata })
 export default class DataSelector extends Component {
-    constructor(props) {
-        super()
+  constructor(props) {
+    super();
 
-        this.state = {
-            ...this.getStepsAndSelectedEntities(props),
-            activeStep: null,
-            isLoading: false
+    this.state = {
+      ...this.getStepsAndSelectedEntities(props),
+      activeStep: null,
+      isLoading: false,
+    };
+  }
+
+  getStepsAndSelectedEntities = props => {
+    let selectedSchema, selectedTable;
+    let selectedDatabaseId = props.selectedDatabaseId;
+    // augment databases with schemas
+    const databases =
+      props.databases &&
+      props.databases.map(database => {
+        let schemas = {};
+        for (let table of database.tables.filter(isQueryable)) {
+          let name = table.schema || "";
+          schemas[name] = schemas[name] || {
+            name: titleize(humanize(name)),
+            database: database,
+            tables: [],
+          };
+          schemas[name].tables.push(table);
+          if (props.selectedTableId && table.id === props.selectedTableId) {
+            selectedSchema = schemas[name];
+            selectedDatabaseId = selectedSchema.database.id;
+            selectedTable = table;
+          }
         }
-    }
-
-    getStepsAndSelectedEntities = (props) => {
-        let selectedSchema, selectedTable;
-        let selectedDatabaseId = props.selectedDatabaseId;
-        // augment databases with schemas
-        const databases = props.databases && props.databases.map(database => {
-            let schemas = {};
-            for (let table of database.tables.filter(isQueryable)) {
-                let name = table.schema || "";
-                schemas[name] = schemas[name] || {
-                    name: titleize(humanize(name)),
-                    database: database,
-                    tables: []
-                }
-                schemas[name].tables.push(table);
-                if (props.selectedTableId && table.id === props.selectedTableId) {
-                    selectedSchema = schemas[name];
-                    selectedDatabaseId = selectedSchema.database.id;
-                    selectedTable = table;
-                }
-            }
-            schemas = Object.values(schemas);
-            // Hide the schema name if there is only one schema
-            if (schemas.length === 1) {
-                schemas[0].name = "";
-            }
-            return {
-                ...database,
-                schemas: schemas.sort((a, b) => a.name.localeCompare(b.name))
-            };
-        });
-
-        const selectedDatabase = selectedDatabaseId ? databases.find(db => db.id === selectedDatabaseId) : null;
-        const hasMultipleSchemas = selectedDatabase && _.uniq(selectedDatabase.tables, (t) => t.schema).length > 1;
-
-        // remove the schema step if a database is already selected and the database does not have more than one schema.
-        let steps = [...props.steps]
-        if (selectedDatabase && !hasMultipleSchemas && steps.includes(SCHEMA_STEP)) {
-            steps.splice(props.steps.indexOf(SCHEMA_STEP), 1);
-            selectedSchema = selectedDatabase.schemas[0];
+        schemas = Object.values(schemas);
+        // Hide the schema name if there is only one schema
+        if (schemas.length === 1) {
+          schemas[0].name = "";
         }
-
-        // if a db is selected but schema isn't, default to the first schema
-        selectedSchema = selectedSchema || (selectedDatabase && selectedDatabase.schemas[0]);
-
-        const selectedSegmentId = props.selectedSegmentId
-        const selectedSegment = selectedSegmentId ? props.segments.find(segment => segment.id === selectedSegmentId) : null;
-        const selectedField = props.selectedFieldId ? props.metadata.fields[props.selectedFieldId] : null
-
         return {
-            databases,
-            selectedDatabase,
-            selectedSchema,
-            selectedTable,
-            selectedSegment,
-            selectedField,
-            steps
-        }
+          ...database,
+          schemas: schemas.sort((a, b) => a.name.localeCompare(b.name)),
+        };
+      });
+
+    const selectedDatabase = selectedDatabaseId
+      ? databases.find(db => db.id === selectedDatabaseId)
+      : null;
+    const hasMultipleSchemas =
+      selectedDatabase &&
+      _.uniq(selectedDatabase.tables, t => t.schema).length > 1;
+
+    // remove the schema step if a database is already selected and the database does not have more than one schema.
+    let steps = [...props.steps];
+    if (
+      selectedDatabase &&
+      !hasMultipleSchemas &&
+      steps.includes(SCHEMA_STEP)
+    ) {
+      steps.splice(props.steps.indexOf(SCHEMA_STEP), 1);
+      selectedSchema = selectedDatabase.schemas[0];
     }
 
-    static propTypes = {
-        selectedDatabaseId: PropTypes.number,
-        selectedTableId: PropTypes.number,
-        selectedFieldId: PropTypes.number,
-        selectedSegmentId: PropTypes.number,
-        databases: PropTypes.array.isRequired,
-        segments: PropTypes.array,
-        disabledTableIds: PropTypes.array,
-        disabledSegmentIds: PropTypes.array,
-        setDatabaseFn: PropTypes.func,
-        setFieldFn: PropTypes.func,
-        setSourceTableFn: PropTypes.func,
-        setSourceSegmentFn: PropTypes.func,
-        isInitiallyOpen: PropTypes.bool,
-        renderAsSelect: PropTypes.bool,
-    };
-
-    static defaultProps = {
-        isInitiallyOpen: false,
-        renderAsSelect: false,
+    // if a db is selected but schema isn't, default to the first schema
+    selectedSchema =
+      selectedSchema || (selectedDatabase && selectedDatabase.schemas[0]);
+
+    const selectedSegmentId = props.selectedSegmentId;
+    const selectedSegment = selectedSegmentId
+      ? props.segments.find(segment => segment.id === selectedSegmentId)
+      : null;
+    const selectedField = props.selectedFieldId
+      ? props.metadata.fields[props.selectedFieldId]
+      : null;
+
+    return {
+      databases,
+      selectedDatabase,
+      selectedSchema,
+      selectedTable,
+      selectedSegment,
+      selectedField,
+      steps,
     };
-
-    componentWillMount() {
-        const useOnlyAvailableDatabase =
-            !this.props.selectedDatabaseId && this.props.databases.length === 1 && !this.props.segments
-        if (useOnlyAvailableDatabase) {
-            setTimeout(() => this.onChangeDatabase(0));
-        }
-
-        this.hydrateActiveStep();
+  };
+
+  static propTypes = {
+    selectedDatabaseId: PropTypes.number,
+    selectedTableId: PropTypes.number,
+    selectedFieldId: PropTypes.number,
+    selectedSegmentId: PropTypes.number,
+    databases: PropTypes.array.isRequired,
+    segments: PropTypes.array,
+    disabledTableIds: PropTypes.array,
+    disabledSegmentIds: PropTypes.array,
+    setDatabaseFn: PropTypes.func,
+    setFieldFn: PropTypes.func,
+    setSourceTableFn: PropTypes.func,
+    setSourceSegmentFn: PropTypes.func,
+    isInitiallyOpen: PropTypes.bool,
+    renderAsSelect: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    isInitiallyOpen: false,
+    renderAsSelect: false,
+  };
+
+  componentWillMount() {
+    const useOnlyAvailableDatabase =
+      !this.props.selectedDatabaseId &&
+      this.props.databases.length === 1 &&
+      !this.props.segments;
+    if (useOnlyAvailableDatabase) {
+      setTimeout(() => this.onChangeDatabase(0));
     }
 
-    componentWillReceiveProps(nextProps) {
-        const newStateProps = this.getStepsAndSelectedEntities(nextProps)
-
-        // only update non-empty properties
-        this.setState(_.pick(newStateProps, (propValue) => !!propValue))
-    }
-
-    hydrateActiveStep() {
-        if (this.props.selectedFieldId) {
-            this.switchToStep(FIELD_STEP);
-        } else if (this.props.selectedSegmentId) {
-            this.switchToStep(SEGMENT_OR_TABLE_STEP);
-        } else if (this.props.selectedTableId) {
-            if (this.props.segments) {
-                this.switchToStep(SEGMENT_OR_TABLE_STEP);
-            } else {
-                this.switchToStep(TABLE_STEP);
-            }
-        } else {
-            let firstStep = this.state.steps[0];
-            this.switchToStep(firstStep)
-        }
+    this.hydrateActiveStep();
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const newStateProps = this.getStepsAndSelectedEntities(nextProps);
+
+    // only update non-empty properties
+    this.setState(_.pick(newStateProps, propValue => !!propValue));
+  }
+
+  hydrateActiveStep() {
+    if (this.props.selectedFieldId) {
+      this.switchToStep(FIELD_STEP);
+    } else if (this.props.selectedSegmentId) {
+      this.switchToStep(SEGMENT_OR_TABLE_STEP);
+    } else if (this.props.selectedTableId) {
+      if (this.props.segments) {
+        this.switchToStep(SEGMENT_OR_TABLE_STEP);
+      } else {
+        this.switchToStep(TABLE_STEP);
+      }
+    } else {
+      let firstStep = this.state.steps[0];
+      this.switchToStep(firstStep);
     }
+  }
 
-    nextStep = (stateChange = {}) => {
-        let activeStepIndex = this.state.steps.indexOf(this.state.activeStep);
-        if (activeStepIndex + 1 >= this.state.steps.length) {
-            this.setState(stateChange)
-            this.refs.popover.toggle();
-        } else {
-            const nextStep = this.state.steps[activeStepIndex + 1]
-            this.switchToStep(nextStep, stateChange);
-        }
+  nextStep = (stateChange = {}) => {
+    let activeStepIndex = this.state.steps.indexOf(this.state.activeStep);
+    if (activeStepIndex + 1 >= this.state.steps.length) {
+      this.setState(stateChange);
+      this.refs.popover.toggle();
+    } else {
+      const nextStep = this.state.steps[activeStepIndex + 1];
+      this.switchToStep(nextStep, stateChange);
     }
-    
-    switchToStep = async (stepName, stateChange = {}) => {
-        const updatedState =  { ...this.state, ...stateChange, activeStep: stepName }
+  };
 
-        const loadersForSteps = {
-            [FIELD_STEP]: () => updatedState.selectedTable && this.props.fetchTableMetadata(updatedState.selectedTable.id)
-        }
-
-        if (loadersForSteps[stepName]) {
-            this.setState({ ...updatedState, isLoading: true });
-            await loadersForSteps[stepName]();
-        }
+  switchToStep = async (stepName, stateChange = {}) => {
+    const updatedState = {
+      ...this.state,
+      ...stateChange,
+      activeStep: stepName,
+    };
 
-        this.setState({
-            ...updatedState,
-            isLoading: false
-        });
-    }
+    const loadersForSteps = {
+      [FIELD_STEP]: () =>
+        updatedState.selectedTable &&
+        this.props.fetchTableMetadata(updatedState.selectedTable.id),
+    };
 
-    hasPreviousStep = () => {
-        return !!this.state.steps[this.state.steps.indexOf(this.state.activeStep) - 1];
+    if (loadersForSteps[stepName]) {
+      this.setState({ ...updatedState, isLoading: true });
+      await loadersForSteps[stepName]();
     }
 
-    hasAdjacentStep = () => {
-        return !!this.state.steps[this.state.steps.indexOf(this.state.activeStep) + 1];
+    this.setState({
+      ...updatedState,
+      isLoading: false,
+    });
+  };
+
+  hasPreviousStep = () => {
+    return !!this.state.steps[
+      this.state.steps.indexOf(this.state.activeStep) - 1
+    ];
+  };
+
+  hasAdjacentStep = () => {
+    return !!this.state.steps[
+      this.state.steps.indexOf(this.state.activeStep) + 1
+    ];
+  };
+
+  onBack = () => {
+    if (!this.hasPreviousStep()) {
+      return;
     }
-
-    onBack = () => {
-        if (!this.hasPreviousStep()) { return; }
-        const previousStep = this.state.steps[this.state.steps.indexOf(this.state.activeStep) - 1];
-        this.switchToStep(previousStep)
+    const previousStep = this.state.steps[
+      this.state.steps.indexOf(this.state.activeStep) - 1
+    ];
+    this.switchToStep(previousStep);
+  };
+
+  onChangeDatabase = (index, schemaInSameStep) => {
+    let database = this.state.databases[index];
+    let schema =
+      database && (database.schemas.length > 1 ? null : database.schemas[0]);
+    if (database && database.tables.length === 0) {
+      schema = {
+        database: database,
+        name: "",
+        tables: [],
+      };
     }
+    const stateChange = {
+      selectedDatabase: database,
+      selectedSchema: schema,
+    };
 
-    onChangeDatabase = (index, schemaInSameStep) => {
-        let database = this.state.databases[index];
-        let schema = database && (database.schemas.length > 1 ? null : database.schemas[0]);
-        if (database && database.tables.length === 0) {
-            schema = {
-                database: database,
-                name: "",
-                tables: []
-            };
-        }
-        const stateChange = {
-            selectedDatabase: database,
-            selectedSchema: schema
-        };
-
-        this.props.setDatabaseFn && this.props.setDatabaseFn(database.id);
+    this.props.setDatabaseFn && this.props.setDatabaseFn(database.id);
 
-        if (schemaInSameStep) {
-            if (database.schemas.length > 1) {
-                this.setState(stateChange)
-            } else {
-                this.nextStep(stateChange)
-            }
-        } else {
-            this.nextStep(stateChange)
-        }
-    }
-
-    onChangeSchema = (schema) => {
-        this.nextStep({selectedSchema: schema});
+    if (schemaInSameStep) {
+      if (database.schemas.length > 1) {
+        this.setState(stateChange);
+      } else {
+        this.nextStep(stateChange);
+      }
+    } else {
+      this.nextStep(stateChange);
     }
+  };
 
-    onChangeTable = (item) => {
-        if (item.table != null) {
-            this.props.setSourceTableFn && this.props.setSourceTableFn(item.table.id);
-            this.nextStep({selectedTable: item.table});
-        }
-    }
+  onChangeSchema = schema => {
+    this.nextStep({ selectedSchema: schema });
+  };
 
-    onChangeField = (item) => {
-        if (item.field != null) {
-            this.props.setFieldFn && this.props.setFieldFn(item.field.id);
-            this.nextStep({selectedField: item.field});
-        }
+  onChangeTable = item => {
+    if (item.table != null) {
+      this.props.setSourceTableFn && this.props.setSourceTableFn(item.table.id);
+      this.nextStep({ selectedTable: item.table });
     }
+  };
 
-    onChangeSegment = (item) => {
-        if (item.segment != null) {
-            this.props.setSourceSegmentFn && this.props.setSourceSegmentFn(item.segment.id);
-            this.nextStep({ selectedTable: null, selectedSegment: item.segment })
-        }
+  onChangeField = item => {
+    if (item.field != null) {
+      this.props.setFieldFn && this.props.setFieldFn(item.field.id);
+      this.nextStep({ selectedField: item.field });
     }
+  };
 
-    onShowSegmentSection = () => {
-        // Jumping to the next step SEGMENT_OR_TABLE_STEP without a db/schema
-        // indicates that we want to show the segment section
-        this.nextStep({ selectedDatabase: null, selectedSchema: null })
+  onChangeSegment = item => {
+    if (item.segment != null) {
+      this.props.setSourceSegmentFn &&
+        this.props.setSourceSegmentFn(item.segment.id);
+      this.nextStep({ selectedTable: null, selectedSegment: item.segment });
     }
+  };
+
+  onShowSegmentSection = () => {
+    // Jumping to the next step SEGMENT_OR_TABLE_STEP without a db/schema
+    // indicates that we want to show the segment section
+    this.nextStep({ selectedDatabase: null, selectedSchema: null });
+  };
+
+  getTriggerElement() {
+    const {
+      className,
+      style,
+      triggerIconSize,
+      getTriggerElementContent,
+    } = this.props;
+    const {
+      selectedDatabase,
+      selectedSegment,
+      selectedTable,
+      selectedField,
+    } = this.state;
 
-    getTriggerElement() {
-        const { className, style, triggerIconSize, getTriggerElementContent } = this.props
-        const { selectedDatabase, selectedSegment, selectedTable, selectedField } = this.state;
-
+    return (
+      <span
+        className={className || "px2 py2 text-bold cursor-pointer text-default"}
+        style={style}
+      >
+        {React.createElement(getTriggerElementContent, {
+          selectedDatabase,
+          selectedSegment,
+          selectedTable,
+          selectedField,
+        })}
+        <Icon className="ml1" name="chevrondown" size={triggerIconSize || 8} />
+      </span>
+    );
+  }
+
+  renderActiveStep() {
+    const {
+      segments,
+      skipDatabaseSelection,
+      disabledTableIds,
+      disabledSegmentIds,
+    } = this.props;
+    const {
+      databases,
+      isLoading,
+      selectedDatabase,
+      selectedSchema,
+      selectedTable,
+      selectedField,
+      selectedSegment,
+    } = this.state;
+
+    const hasAdjacentStep = this.hasAdjacentStep();
+
+    switch (this.state.activeStep) {
+      case DATABASE_STEP:
         return (
-            <span className={className || "px2 py2 text-bold cursor-pointer text-default"} style={style}>
-                { React.createElement(getTriggerElementContent, { selectedDatabase, selectedSegment, selectedTable, selectedField }) }
-                <Icon className="ml1" name="chevrondown" size={triggerIconSize || 8}/>
-            </span>
+          <DatabasePicker
+            databases={databases}
+            selectedDatabase={selectedDatabase}
+            onChangeDatabase={this.onChangeDatabase}
+            hasAdjacentStep={hasAdjacentStep}
+          />
         );
-    }
-
-    renderActiveStep() {
-        const { segments, skipDatabaseSelection, disabledTableIds, disabledSegmentIds } = this.props
-        const { databases, isLoading, selectedDatabase, selectedSchema, selectedTable, selectedField, selectedSegment } = this.state
-
-        const hasAdjacentStep = this.hasAdjacentStep()
-
-        switch(this.state.activeStep) {
-            case DATABASE_STEP: return <DatabasePicker
-                databases={databases}
-                selectedDatabase={selectedDatabase}
-                onChangeDatabase={this.onChangeDatabase}
-                hasAdjacentStep={hasAdjacentStep}
-            />;
-            case DATABASE_SCHEMA_STEP: return <DatabaseSchemaPicker
-                skipDatabaseSelection={skipDatabaseSelection}
-                databases={databases}
-                selectedDatabase={selectedDatabase}
-                selectedSchema={selectedSchema}
-                onChangeSchema={this.onChangeSchema}
-                onChangeDatabase={this.onChangeDatabase}
-                hasAdjacentStep={hasAdjacentStep}
-            />;
-            case SCHEMA_STEP: return <SchemaPicker
-                 selectedDatabase={selectedDatabase}
-                 selectedSchema={selectedSchema}
-                 onChangeSchema={this.onChangeSchema}
-                 hasAdjacentStep={hasAdjacentStep}
-            />;
-            case SCHEMA_AND_SEGMENTS_STEP: return <SegmentAndDatabasePicker
-                databases={databases}
-                selectedSchema={selectedSchema}
-                onChangeSchema={this.onChangeSchema}
-                onShowSegmentSection={this.onShowSegmentSection}
-                onChangeDatabase={this.onChangeDatabase}
-                hasAdjacentStep={hasAdjacentStep}
-            />;
-            case TABLE_STEP:
-                const canGoBack = this.hasPreviousStep()
-
-                return <TablePicker
-                     selectedDatabase={selectedDatabase}
-                     selectedSchema={selectedSchema}
-                     selectedTable={selectedTable}
-                     databases={databases}
-                     segments={segments}
-                     disabledTableIds={disabledTableIds}
-                     onChangeTable={this.onChangeTable}
-                     onBack={canGoBack && this.onBack}
-                     hasAdjacentStep={hasAdjacentStep}
-                />;
-            case FIELD_STEP: return <FieldPicker
-                     isLoading={isLoading}
-                     selectedTable={selectedTable}
-                     selectedField={selectedField}
-                     onChangeField={this.onChangeField}
-                     onBack={this.onBack}
-                />;
-            case SEGMENT_OR_TABLE_STEP:
-                if (selectedDatabase && selectedSchema) {
-                    return <TablePicker
-                         selectedDatabase={selectedDatabase}
-                         selectedSchema={selectedSchema}
-                         selectedTable={selectedTable}
-                         databases={databases}
-                         segments={segments}
-                         disabledTableIds={disabledTableIds}
-                         onChangeTable={this.onChangeTable}
-                         hasPreviousStep={this.hasPreviousStep}
-                         onBack={this.onBack}
-                    />
-                } else {
-                    return <SegmentPicker
-                        segments={segments}
-                        selectedSegment={selectedSegment}
-                        disabledSegmentIds={disabledSegmentIds}
-                        onBack={this.onBack}
-                        onChangeSegment={this.onChangeSegment}
-                    />
-                }
-        }
-
-        return null;
-    }
-
-    render() {
-        const triggerClasses = this.props.renderAsSelect ? "border-med bg-white block no-decoration" : "flex align-center";
+      case DATABASE_SCHEMA_STEP:
         return (
-            <PopoverWithTrigger
-                id="DataPopover"
-                ref="popover"
-                isInitiallyOpen={this.props.isInitiallyOpen}
-                triggerElement={this.getTriggerElement()}
-                triggerClasses={triggerClasses}
-                horizontalAttachments={["center", "left", "right"]}
-            >
-                { this.renderActiveStep() }
-            </PopoverWithTrigger>
+          <DatabaseSchemaPicker
+            skipDatabaseSelection={skipDatabaseSelection}
+            databases={databases}
+            selectedDatabase={selectedDatabase}
+            selectedSchema={selectedSchema}
+            onChangeSchema={this.onChangeSchema}
+            onChangeDatabase={this.onChangeDatabase}
+            hasAdjacentStep={hasAdjacentStep}
+          />
         );
-    }
-}
+      case SCHEMA_STEP:
+        return (
+          <SchemaPicker
+            selectedDatabase={selectedDatabase}
+            selectedSchema={selectedSchema}
+            onChangeSchema={this.onChangeSchema}
+            hasAdjacentStep={hasAdjacentStep}
+          />
+        );
+      case SCHEMA_AND_SEGMENTS_STEP:
+        return (
+          <SegmentAndDatabasePicker
+            databases={databases}
+            selectedSchema={selectedSchema}
+            onChangeSchema={this.onChangeSchema}
+            onShowSegmentSection={this.onShowSegmentSection}
+            onChangeDatabase={this.onChangeDatabase}
+            hasAdjacentStep={hasAdjacentStep}
+          />
+        );
+      case TABLE_STEP:
+        const canGoBack = this.hasPreviousStep();
 
-const DatabasePicker = ({ databases, selectedDatabase, onChangeDatabase, hasAdjacentStep }) => {
-    if (databases.length === 0) {
-        return <DataSelectorLoading />
+        return (
+          <TablePicker
+            selectedDatabase={selectedDatabase}
+            selectedSchema={selectedSchema}
+            selectedTable={selectedTable}
+            databases={databases}
+            segments={segments}
+            disabledTableIds={disabledTableIds}
+            onChangeTable={this.onChangeTable}
+            onBack={canGoBack && this.onBack}
+            hasAdjacentStep={hasAdjacentStep}
+          />
+        );
+      case FIELD_STEP:
+        return (
+          <FieldPicker
+            isLoading={isLoading}
+            selectedTable={selectedTable}
+            selectedField={selectedField}
+            onChangeField={this.onChangeField}
+            onBack={this.onBack}
+          />
+        );
+      case SEGMENT_OR_TABLE_STEP:
+        if (selectedDatabase && selectedSchema) {
+          return (
+            <TablePicker
+              selectedDatabase={selectedDatabase}
+              selectedSchema={selectedSchema}
+              selectedTable={selectedTable}
+              databases={databases}
+              segments={segments}
+              disabledTableIds={disabledTableIds}
+              onChangeTable={this.onChangeTable}
+              hasPreviousStep={this.hasPreviousStep}
+              onBack={this.onBack}
+            />
+          );
+        } else {
+          return (
+            <SegmentPicker
+              segments={segments}
+              selectedSegment={selectedSegment}
+              disabledSegmentIds={disabledSegmentIds}
+              onBack={this.onBack}
+              onChangeSegment={this.onChangeSegment}
+            />
+          );
+        }
     }
 
-    let sections = [{
-        items: databases.map((database, index) => ({
-            name: database.name,
-            index,
-            database: database
-        }))
-    }];
+    return null;
+  }
 
+  render() {
+    const triggerClasses = this.props.renderAsSelect
+      ? "border-med bg-white block no-decoration"
+      : "flex align-center";
     return (
-        <AccordianList
-            id="DatabasePicker"
-            key="databasePicker"
-            className="text-brand"
-            sections={sections}
-            onChange={(db) => onChangeDatabase(db.index)}
-            itemIsSelected={(item) => selectedDatabase && item.database.id === selectedDatabase.id}
-            renderItemIcon={() => <Icon className="Icon text-default" name="database" size={18} />}
-            showItemArrows={hasAdjacentStep}
-        />
+      <PopoverWithTrigger
+        id="DataPopover"
+        ref="popover"
+        isInitiallyOpen={this.props.isInitiallyOpen}
+        triggerElement={this.getTriggerElement()}
+        triggerClasses={triggerClasses}
+        horizontalAttachments={["center", "left", "right"]}
+      >
+        {this.renderActiveStep()}
+      </PopoverWithTrigger>
     );
+  }
 }
 
-const SegmentAndDatabasePicker = ({ databases, selectedSchema, onChangeSchema, onShowSegmentSection, onChangeDatabase, hasAdjacentStep }) => {
-    const segmentItem = [{ name: 'Segments', items: [], icon: 'segment'}];
-
-    const sections = segmentItem.concat(databases.map(database => {
-        return {
-            name: database.name,
-            items: database.schemas.length > 1 ? database.schemas : []
-        };
-    }));
-
-    // FIXME: this seems a bit brittle and hard to follow
-    let openSection = selectedSchema && (_.findIndex(databases, (db) => _.find(db.schemas, selectedSchema)) + segmentItem.length);
-    if (openSection >= 0 && databases[openSection - segmentItem.length] && databases[openSection - segmentItem.length].schemas.length === 1) {
-        openSection = -1;
-    }
-
-    return (
-        <AccordianList
-            id="SegmentAndDatabasePicker"
-            key="segmentAndDatabasePicker"
-            className="text-brand"
-            sections={sections}
-            onChange={onChangeSchema}
-            onChangeSection={(index) => {
-                index === 0
-                    ? onShowSegmentSection()
-                    : onChangeDatabase(index - segmentItem.length, true)
-            }}
-            itemIsSelected={(schema) => selectedSchema === schema}
-            renderSectionIcon={(section) => <Icon className="Icon text-default" name={section.icon || "database"} size={18} />}
-            renderItemIcon={() => <Icon name="folder" size={16} />}
-            initiallyOpenSection={openSection}
-            showItemArrows={hasAdjacentStep}
-            alwaysTogglable={true}
+const DatabasePicker = ({
+  databases,
+  selectedDatabase,
+  onChangeDatabase,
+  hasAdjacentStep,
+}) => {
+  if (databases.length === 0) {
+    return <DataSelectorLoading />;
+  }
+
+  let sections = [
+    {
+      items: databases.map((database, index) => ({
+        name: database.name,
+        index,
+        database: database,
+      })),
+    },
+  ];
+
+  return (
+    <AccordianList
+      id="DatabasePicker"
+      key="databasePicker"
+      className="text-brand"
+      sections={sections}
+      onChange={db => onChangeDatabase(db.index)}
+      itemIsSelected={item =>
+        selectedDatabase && item.database.id === selectedDatabase.id
+      }
+      renderItemIcon={() => (
+        <Icon className="Icon text-default" name="database" size={18} />
+      )}
+      showItemArrows={hasAdjacentStep}
+    />
+  );
+};
+
+const SegmentAndDatabasePicker = ({
+  databases,
+  selectedSchema,
+  onChangeSchema,
+  onShowSegmentSection,
+  onChangeDatabase,
+  hasAdjacentStep,
+}) => {
+  const segmentItem = [{ name: "Segments", items: [], icon: "segment" }];
+
+  const sections = segmentItem.concat(
+    databases.map(database => {
+      return {
+        name: database.name,
+        items: database.schemas.length > 1 ? database.schemas : [],
+      };
+    }),
+  );
+
+  // FIXME: this seems a bit brittle and hard to follow
+  let openSection =
+    selectedSchema &&
+    _.findIndex(databases, db => _.find(db.schemas, selectedSchema)) +
+      segmentItem.length;
+  if (
+    openSection >= 0 &&
+    databases[openSection - segmentItem.length] &&
+    databases[openSection - segmentItem.length].schemas.length === 1
+  ) {
+    openSection = -1;
+  }
+
+  return (
+    <AccordianList
+      id="SegmentAndDatabasePicker"
+      key="segmentAndDatabasePicker"
+      className="text-brand"
+      sections={sections}
+      onChange={onChangeSchema}
+      onChangeSection={index => {
+        index === 0
+          ? onShowSegmentSection()
+          : onChangeDatabase(index - segmentItem.length, true);
+      }}
+      itemIsSelected={schema => selectedSchema === schema}
+      renderSectionIcon={section => (
+        <Icon
+          className="Icon text-default"
+          name={section.icon || "database"}
+          size={18}
         />
-    );
-}
-
-export const SchemaPicker = ({ selectedDatabase, selectedSchema, onChangeSchema, hasAdjacentStep }) => {
-    let sections = [{
-        items: selectedDatabase.schemas
-    }];
+      )}
+      renderItemIcon={() => <Icon name="folder" size={16} />}
+      initiallyOpenSection={openSection}
+      showItemArrows={hasAdjacentStep}
+      alwaysTogglable={true}
+    />
+  );
+};
+
+export const SchemaPicker = ({
+  selectedDatabase,
+  selectedSchema,
+  onChangeSchema,
+  hasAdjacentStep,
+}) => {
+  let sections = [
+    {
+      items: selectedDatabase.schemas,
+    },
+  ];
+  return (
+    <div style={{ width: 300 }}>
+      <AccordianList
+        id="DatabaseSchemaPicker"
+        key="databaseSchemaPicker"
+        className="text-brand"
+        sections={sections}
+        searchable
+        onChange={onChangeSchema}
+        itemIsSelected={schema => schema === selectedSchema}
+        renderItemIcon={() => <Icon name="folder" size={16} />}
+        showItemArrows={hasAdjacentStep}
+      />
+    </div>
+  );
+};
+
+export const DatabaseSchemaPicker = ({
+  skipDatabaseSelection,
+  databases,
+  selectedDatabase,
+  selectedSchema,
+  onChangeSchema,
+  onChangeDatabase,
+  hasAdjacentStep,
+}) => {
+  if (databases.length === 0) {
+    return <DataSelectorLoading />;
+  }
+
+  const sections = databases.map(database => ({
+    name: database.name,
+    items: database.schemas.length > 1 ? database.schemas : [],
+    className: database.is_saved_questions ? "bg-slate-extra-light" : null,
+    icon: database.is_saved_questions ? "all" : "database",
+  }));
+
+  let openSection =
+    selectedSchema &&
+    _.findIndex(databases, db => _.find(db.schemas, selectedSchema));
+  if (
+    openSection >= 0 &&
+    databases[openSection] &&
+    databases[openSection].schemas.length === 1
+  ) {
+    openSection = -1;
+  }
+
+  return (
+    <div>
+      <AccordianList
+        id="DatabaseSchemaPicker"
+        key="databaseSchemaPicker"
+        className="text-brand"
+        sections={sections}
+        onChange={onChangeSchema}
+        onChangeSection={dbId => onChangeDatabase(dbId, true)}
+        itemIsSelected={schema => schema === selectedSchema}
+        renderSectionIcon={item => (
+          <Icon className="Icon text-default" name={item.icon} size={18} />
+        )}
+        renderItemIcon={() => <Icon name="folder" size={16} />}
+        initiallyOpenSection={openSection}
+        alwaysTogglable={true}
+        showItemArrows={hasAdjacentStep}
+      />
+    </div>
+  );
+};
+
+export const TablePicker = ({
+  selectedDatabase,
+  selectedSchema,
+  selectedTable,
+  disabledTableIds,
+  onChangeTable,
+  hasAdjacentStep,
+  onBack,
+}) => {
+  // In case DataSelector props get reseted
+  if (!selectedDatabase) {
+    if (onBack) onBack();
+    return null;
+  }
+
+  const isSavedQuestionList = selectedDatabase.is_saved_questions;
+  let header = (
+    <div className="flex flex-wrap align-center">
+      <span
+        className={cx("flex align-center", {
+          "text-brand-hover cursor-pointer": onBack,
+        })}
+        onClick={onBack}
+      >
+        {onBack && <Icon name="chevronleft" size={18} />}
+        <span className="ml1">{selectedDatabase.name}</span>
+      </span>
+      {selectedSchema.name && (
+        <span className="ml1 text-slate">- {selectedSchema.name}</span>
+      )}
+    </div>
+  );
+
+  if (selectedSchema.tables.length === 0) {
+    // this is a database with no tables!
     return (
-        <div style={{ width: 300 }}>
-            <AccordianList
-                id="DatabaseSchemaPicker"
-                key="databaseSchemaPicker"
-                className="text-brand"
-                sections={sections}
-                searchable
-                onChange={onChangeSchema}
-                itemIsSelected={(schema) => schema === selectedSchema}
-                renderItemIcon={() => <Icon name="folder" size={16} />}
-                showItemArrows={hasAdjacentStep}
-            />
+      <section
+        className="List-section List-section--open"
+        style={{ width: 300 }}
+      >
+        <div className="p1 border-bottom">
+          <div className="px1 py1 flex align-center">
+            <h3 className="text-default">{header}</h3>
+          </div>
         </div>
+        <div className="p4 text-centered">{t`No tables found in this database.`}</div>
+      </section>
     );
-}
-
-export const DatabaseSchemaPicker = ({ skipDatabaseSelection, databases, selectedDatabase, selectedSchema, onChangeSchema, onChangeDatabase, hasAdjacentStep }) => {
-        if (databases.length === 0) {
-            return <DataSelectorLoading />
-        }
-
-        const sections = databases.map(database => ({
-            name: database.name,
-            items: database.schemas.length > 1 ? database.schemas : [],
-            className: database.is_saved_questions ? "bg-slate-extra-light" : null,
-            icon: database.is_saved_questions ? 'all' : 'database'
-        }));
-
-        let openSection = selectedSchema && _.findIndex(databases, (db) => _.find(db.schemas, selectedSchema));
-        if (openSection >= 0 && databases[openSection] && databases[openSection].schemas.length === 1) {
-            openSection = -1;
-        }
-
-        return (
-            <div>
-                <AccordianList
-                    id="DatabaseSchemaPicker"
-                    key="databaseSchemaPicker"
-                    className="text-brand"
-                    sections={sections}
-                    onChange={onChangeSchema}
-                    onChangeSection={(dbId) => onChangeDatabase(dbId, true)}
-                    itemIsSelected={(schema) => schema === selectedSchema}
-                    renderSectionIcon={item =>
-                        <Icon
-                            className="Icon text-default"
-                            name={item.icon}
-                            size={18}
-                        />
-                    }
-                    renderItemIcon={() => <Icon name="folder" size={16} />}
-                    initiallyOpenSection={openSection}
-                    alwaysTogglable={true}
-                    showItemArrows={hasAdjacentStep}
-                />
-            </div>
-        );
-
-    }
-
-export const TablePicker = ({ selectedDatabase, selectedSchema, selectedTable, disabledTableIds, onChangeTable, hasAdjacentStep, onBack }) => {
-    // In case DataSelector props get reseted
-    if (!selectedDatabase) {
-        if (onBack) onBack()
-        return null
-    }
-
-    const isSavedQuestionList = selectedDatabase.is_saved_questions;
-    let header = (
-        <div className="flex flex-wrap align-center">
-                <span className={cx("flex align-center", { "text-brand-hover cursor-pointer": onBack })} onClick={onBack}>
-                    {onBack && <Icon name="chevronleft" size={18} /> }
-                    <span className="ml1">{selectedDatabase.name}</span>
-                </span>
-            { selectedSchema.name && <span className="ml1 text-slate">- {selectedSchema.name}</span>}
-        </div>
+  } else {
+    let sections = [
+      {
+        name: header,
+        items: selectedSchema.tables.map(table => ({
+          name: table.display_name,
+          disabled: disabledTableIds && disabledTableIds.includes(table.id),
+          table: table,
+          database: selectedDatabase,
+        })),
+      },
+    ];
+    return (
+      <div style={{ width: 300 }}>
+        <AccordianList
+          id="TablePicker"
+          key="tablePicker"
+          className="text-brand"
+          sections={sections}
+          searchable
+          onChange={onChangeTable}
+          itemIsSelected={item =>
+            item.table && selectedTable
+              ? item.table.id === selectedTable.id
+              : false
+          }
+          itemIsClickable={item => item.table && !item.disabled}
+          renderItemIcon={item =>
+            item.table ? <Icon name="table2" size={18} /> : null
+          }
+          showItemArrows={hasAdjacentStep}
+        />
+        {isSavedQuestionList && (
+          <div className="bg-slate-extra-light p2 text-centered border-top">
+            {t`Is a question missing?`}
+            <a
+              href="http://metabase.com/docs/latest/users-guide/04-asking-questions.html#source-data"
+              className="block link"
+            >{t`Learn more about nested queries`}</a>
+          </div>
+        )}
+      </div>
     );
+  }
+};
 
-    if (selectedSchema.tables.length === 0) {
-        // this is a database with no tables!
-        return (
-            <section className="List-section List-section--open" style={{width: 300}}>
-                <div className="p1 border-bottom">
-                    <div className="px1 py1 flex align-center">
-                        <h3 className="text-default">{header}</h3>
-                    </div>
-                </div>
-                <div className="p4 text-centered">{t`No tables found in this database.`}</div>
-            </section>
-        );
-    } else {
-        let sections = [{
-            name: header,
-            items: selectedSchema.tables
-                .map(table => ({
-                    name: table.display_name,
-                    disabled: disabledTableIds && disabledTableIds.includes(table.id),
-                    table: table,
-                    database: selectedDatabase
-                }))
-        }];
-        return (
-            <div style={{ width: 300 }}>
-                <AccordianList
-                    id="TablePicker"
-                    key="tablePicker"
-                    className="text-brand"
-                    sections={sections}
-                    searchable
-                    onChange={onChangeTable}
-                    itemIsSelected={(item) => (item.table && selectedTable) ? item.table.id === selectedTable.id : false}
-                    itemIsClickable={(item) => item.table && !item.disabled}
-                    renderItemIcon={(item) => item.table ? <Icon name="table2" size={18} /> : null}
-                    showItemArrows={hasAdjacentStep}
-                />
-                { isSavedQuestionList && (
-                    <div className="bg-slate-extra-light p2 text-centered border-top">
-                        {t`Is a question missing?`}
-                        <a href="http://metabase.com/docs/latest/users-guide/04-asking-questions.html#source-data" className="block link">{t`Learn more about nested queries`}</a>
-                    </div>
-                )}
-            </div>
-        );
-    }
-}
-
-@connect(state => ({metadata: getMetadata(state)}))
+@connect(state => ({ metadata: getMetadata(state) }))
 export class FieldPicker extends Component {
-    render() {
-        const { isLoading, selectedTable, selectedField, onChangeField, metadata, onBack } = this.props
-        // In case DataSelector props get reseted
-        if (!selectedTable) {
-            if (onBack) onBack()
-            return null
-        }
-
-        const header = (
-            <span className="flex align-center">
-                    <span className="flex align-center text-slate cursor-pointer" onClick={onBack}>
-                        <Icon name="chevronleft" size={18} />
-                        <span className="ml1">{ selectedTable.display_name || t`Fields`}</span>
-                    </span>
-                </span>
-        );
-
-        if (isLoading) {
-            return <DataSelectorLoading header={header} />
-        }
-
-        const table = metadata.tables[selectedTable.id];
-        const fields = (table && table.fields) || [];
-        const sections = [{
-            name: header,
-            items: fields.map(field => ({
-                name: field.display_name,
-                field: field,
-            }))
-        }];
-
-        return (
-            <div style={{ width: 300 }}>
-                <AccordianList
-                    id="FieldPicker"
-                    key="fieldPicker"
-                    className="text-brand"
-                    sections={sections}
-                    searchable
-                    onChange={onChangeField}
-                    itemIsSelected={(item) => (item.field && selectedField) ? (item.field.id === selectedField.id) : false}
-                    itemIsClickable={(item) => item.field && !item.disabled}
-                    renderItemIcon={(item) => item.field ? <Icon name={item.field.dimension().icon()} size={18} /> : null}
-                />
-            </div>
-        );
+  render() {
+    const {
+      isLoading,
+      selectedTable,
+      selectedField,
+      onChangeField,
+      metadata,
+      onBack,
+    } = this.props;
+    // In case DataSelector props get reseted
+    if (!selectedTable) {
+      if (onBack) onBack();
+      return null;
     }
-}
 
-//TODO: refactor this. lots of shared code with renderTablePicker = () =>
-export const SegmentPicker = ({ segments, selectedSegment, disabledSegmentIds, onBack, onChangeSegment }) => {
     const header = (
-        <span className="flex align-center">
-                <span className="flex align-center text-slate cursor-pointer" onClick={onBack}>
-                    <Icon name="chevronleft" size={18} />
-                    <span className="ml1">{t`Segments`}</span>
-                </span>
-            </span>
+      <span className="flex align-center">
+        <span
+          className="flex align-center text-slate cursor-pointer"
+          onClick={onBack}
+        >
+          <Icon name="chevronleft" size={18} />
+          <span className="ml1">{selectedTable.display_name || t`Fields`}</span>
+        </span>
+      </span>
     );
 
-    if (!segments || segments.length === 0) {
-        return (
-            <section className="List-section List-section--open" style={{width: '300px'}}>
-                <div className="p1 border-bottom">
-                    <div className="px1 py1 flex align-center">
-                        <h3 className="text-default">{header}</h3>
-                    </div>
-                </div>
-                <div className="p4 text-centered">{t`No segments were found.`}</div>
-            </section>
-        );
+    if (isLoading) {
+      return <DataSelectorLoading header={header} />;
     }
 
-    const sections = [{
+    const table = metadata.tables[selectedTable.id];
+    const fields = (table && table.fields) || [];
+    const sections = [
+      {
         name: header,
-        items: segments
-            .map(segment => ({
-                name: segment.name,
-                segment: segment,
-                disabled: disabledSegmentIds && disabledSegmentIds.includes(segment.id)
-            }))
-    }];
+        items: fields.map(field => ({
+          name: field.display_name,
+          field: field,
+        })),
+      },
+    ];
 
     return (
+      <div style={{ width: 300 }}>
         <AccordianList
-            id="SegmentPicker"
-            key="segmentPicker"
-            className="text-brand"
-            sections={sections}
-            searchable
-            searchPlaceholder={t`Find a segment`}
-            onChange={onChangeSegment}
-            itemIsSelected={(item) => selectedSegment && item.segment ? item.segment.id === selectedSegment.id : false}
-            itemIsClickable={(item) => item.segment && !item.disabled}
-            renderItemIcon={(item) => item.segment ? <Icon name="segment" size={18} /> : null}
+          id="FieldPicker"
+          key="fieldPicker"
+          className="text-brand"
+          sections={sections}
+          searchable
+          onChange={onChangeField}
+          itemIsSelected={item =>
+            item.field && selectedField
+              ? item.field.id === selectedField.id
+              : false
+          }
+          itemIsClickable={item => item.field && !item.disabled}
+          renderItemIcon={item =>
+            item.field ? (
+              <Icon name={item.field.dimension().icon()} size={18} />
+            ) : null
+          }
         />
+      </div>
     );
+  }
 }
 
-const DataSelectorLoading = ({ header }) => {
-    if (header) {
-        return (
-            <section className="List-section List-section--open" style={{width: 300}}>
-                <div className="p1 border-bottom">
-                    <div className="px1 py1 flex align-center">
-                        <h3 className="text-default">{header}</h3>
-                    </div>
-                </div>
-                <LoadingAndErrorWrapper loading />;
-            </section>
-        );
-    } else {
-        return <LoadingAndErrorWrapper loading />;
-    }
-}
+//TODO: refactor this. lots of shared code with renderTablePicker = () =>
+export const SegmentPicker = ({
+  segments,
+  selectedSegment,
+  disabledSegmentIds,
+  onBack,
+  onChangeSegment,
+}) => {
+  const header = (
+    <span className="flex align-center">
+      <span
+        className="flex align-center text-slate cursor-pointer"
+        onClick={onBack}
+      >
+        <Icon name="chevronleft" size={18} />
+        <span className="ml1">{t`Segments`}</span>
+      </span>
+    </span>
+  );
+
+  if (!segments || segments.length === 0) {
+    return (
+      <section
+        className="List-section List-section--open"
+        style={{ width: "300px" }}
+      >
+        <div className="p1 border-bottom">
+          <div className="px1 py1 flex align-center">
+            <h3 className="text-default">{header}</h3>
+          </div>
+        </div>
+        <div className="p4 text-centered">{t`No segments were found.`}</div>
+      </section>
+    );
+  }
+
+  const sections = [
+    {
+      name: header,
+      items: segments.map(segment => ({
+        name: segment.name,
+        segment: segment,
+        disabled: disabledSegmentIds && disabledSegmentIds.includes(segment.id),
+      })),
+    },
+  ];
+
+  return (
+    <AccordianList
+      id="SegmentPicker"
+      key="segmentPicker"
+      className="text-brand"
+      sections={sections}
+      searchable
+      searchPlaceholder={t`Find a segment`}
+      onChange={onChangeSegment}
+      itemIsSelected={item =>
+        selectedSegment && item.segment
+          ? item.segment.id === selectedSegment.id
+          : false
+      }
+      itemIsClickable={item => item.segment && !item.disabled}
+      renderItemIcon={item =>
+        item.segment ? <Icon name="segment" size={18} /> : null
+      }
+    />
+  );
+};
 
+const DataSelectorLoading = ({ header }) => {
+  if (header) {
+    return (
+      <section
+        className="List-section List-section--open"
+        style={{ width: 300 }}
+      >
+        <div className="p1 border-bottom">
+          <div className="px1 py1 flex align-center">
+            <h3 className="text-default">{header}</h3>
+          </div>
+        </div>
+        <LoadingAndErrorWrapper loading />;
+      </section>
+    );
+  } else {
+    return <LoadingAndErrorWrapper loading />;
+  }
+};
diff --git a/frontend/src/metabase/query_builder/components/ExpandableString.jsx b/frontend/src/metabase/query_builder/components/ExpandableString.jsx
index 6fcc7fc66bf2355b347364f67d166420e49f2bd9..7195247d59809aa1ea8a2db70f17e94ab422237f 100644
--- a/frontend/src/metabase/query_builder/components/ExpandableString.jsx
+++ b/frontend/src/metabase/query_builder/components/ExpandableString.jsx
@@ -1,45 +1,61 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Humanize from "humanize-plus";
 
 export default class ExpandableString extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.toggleExpansion = this.toggleExpansion.bind(this);
+  constructor(props, context) {
+    super(props, context);
+    this.toggleExpansion = this.toggleExpansion.bind(this);
 
-        this.state = {
-            expanded: false
-        };
-    }
-
-    static defaultProps = {
-        length: 140,
-        expanded: false
+    this.state = {
+      expanded: false,
     };
-
-    componentWillReceiveProps(newProps) {
-        this.setState({
-            expanded: newProps.expanded
-        });
-    }
-
-    toggleExpansion() {
-        this.setState({
-            expanded: !this.state.expanded
-        });
-    }
-
-    render() {
-        if (!this.props.str) return false;
-
-        var truncated = Humanize.truncate(this.props.str || "", 140);
-
-        if (this.state.expanded) {
-            return (<span>{this.props.str} <span className="block mt1 link" onClick={this.toggleExpansion}>{t`View less`}</span></span>);
-        } else if (truncated !== this.props.str) {
-            return (<span>{truncated} <span className="block mt1 link" onClick={this.toggleExpansion}>{t`View more`}</span></span>);
-        } else {
-            return (<span>{this.props.str}</span>);
-        }
+  }
+
+  static defaultProps = {
+    length: 140,
+    expanded: false,
+  };
+
+  componentWillReceiveProps(newProps) {
+    this.setState({
+      expanded: newProps.expanded,
+    });
+  }
+
+  toggleExpansion() {
+    this.setState({
+      expanded: !this.state.expanded,
+    });
+  }
+
+  render() {
+    if (!this.props.str) return false;
+
+    var truncated = Humanize.truncate(this.props.str || "", 140);
+
+    if (this.state.expanded) {
+      return (
+        <span>
+          {this.props.str}{" "}
+          <span
+            className="block mt1 link"
+            onClick={this.toggleExpansion}
+          >{t`View less`}</span>
+        </span>
+      );
+    } else if (truncated !== this.props.str) {
+      return (
+        <span>
+          {truncated}{" "}
+          <span
+            className="block mt1 link"
+            onClick={this.toggleExpansion}
+          >{t`View more`}</span>
+        </span>
+      );
+    } else {
+      return <span>{this.props.str}</span>;
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
index d44cdd935392bdaab299226dc3ed911a86534651..0817e2bf61afadb985cd5a6250b3cc464e109d39 100644
--- a/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/ExtendedOptions.jsx
@@ -4,10 +4,10 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import _ from "underscore";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import AddClauseButton from "./AddClauseButton.jsx";
 import Expressions from "./expressions/Expressions.jsx";
-import ExpressionWidget from './expressions/ExpressionWidget.jsx';
+import ExpressionWidget from "./expressions/ExpressionWidget.jsx";
 import LimitWidget from "./LimitWidget.jsx";
 import SortWidget from "./SortWidget.jsx";
 import Popover from "metabase/components/Popover.jsx";
@@ -15,180 +15,207 @@ import Popover from "metabase/components/Popover.jsx";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-import type { DatasetQuery }  from "metabase/meta/types/Card";
+import type { DatasetQuery } from "metabase/meta/types/Card";
 import type { GuiQueryEditorFeatures } from "./GuiQueryEditor";
 
 type Props = {
-    query: StructuredQuery,
-    setDatasetQuery: (
-        datasetQuery: DatasetQuery,
-        options: { run: boolean }
-    ) => void,
-    features: GuiQueryEditorFeatures
-}
+  query: StructuredQuery,
+  setDatasetQuery: (
+    datasetQuery: DatasetQuery,
+    options: { run: boolean },
+  ) => void,
+  features: GuiQueryEditorFeatures,
+};
 
 type State = {
-    isOpen: boolean,
-    editExpression: any
-}
+  isOpen: boolean,
+  editExpression: any,
+};
 
 export default class ExtendedOptions extends Component {
-    props: Props;
-    state: State = {
-        isOpen: false,
-        editExpression: null
-    };
-
-    static propTypes = {
-        features: PropTypes.object.isRequired,
-        datasetQuery: PropTypes.object.isRequired,
-        tableMetadata: PropTypes.object,
-        setDatasetQuery: PropTypes.func.isRequired
-    };
-
-    static defaultProps = {
-        expressions: {}
-    };
-
-
-    setExpression(name, expression, previousName) {
-        let { query, setDatasetQuery } = this.props;
-        query.updateExpression(name, expression, previousName).update(setDatasetQuery);
-        this.setState({ editExpression: null });
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Expression', !_.isEmpty(previousName));
-    }
-
-    removeExpression(name) {
-        let { query, setDatasetQuery } = this.props;
-        query.removeExpression(name).update(setDatasetQuery);
-        this.setState({editExpression: null});
-
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Remove Expression');
+  props: Props;
+  state: State = {
+    isOpen: false,
+    editExpression: null,
+  };
+
+  static propTypes = {
+    features: PropTypes.object.isRequired,
+    datasetQuery: PropTypes.object.isRequired,
+    tableMetadata: PropTypes.object,
+    setDatasetQuery: PropTypes.func.isRequired,
+  };
+
+  static defaultProps = {
+    expressions: {},
+  };
+
+  setExpression(name, expression, previousName) {
+    let { query, setDatasetQuery } = this.props;
+    query
+      .updateExpression(name, expression, previousName)
+      .update(setDatasetQuery);
+    this.setState({ editExpression: null });
+    MetabaseAnalytics.trackEvent(
+      "QueryBuilder",
+      "Set Expression",
+      !_.isEmpty(previousName),
+    );
+  }
+
+  removeExpression(name) {
+    let { query, setDatasetQuery } = this.props;
+    query.removeExpression(name).update(setDatasetQuery);
+    this.setState({ editExpression: null });
+
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Remove Expression");
+  }
+
+  setLimit = limit => {
+    let { query, setDatasetQuery } = this.props;
+    query.updateLimit(limit).update(setDatasetQuery);
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Set Limit", limit);
+    this.setState({ isOpen: false });
+  };
+
+  renderSort() {
+    const { query, setDatasetQuery } = this.props;
+
+    if (!this.props.features.limit) {
+      return;
     }
 
-    setLimit = (limit) => {
-        let { query, setDatasetQuery } = this.props;
-        query.updateLimit(limit).update(setDatasetQuery);
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Set Limit', limit);
-        this.setState({ isOpen: false });
-    }
-
-    renderSort() {
-        const { query, setDatasetQuery } = this.props;
-
-        if (!this.props.features.limit) {
-            return;
-        }
-
-        let sortList, addSortButton;
-
-        const tableMetadata = query.table();
-        if (tableMetadata) {
-            const sorts = query.sorts();
-
-            sortList = sorts.map((sort, index) =>
-                <SortWidget
-                    key={index}
-                    query={query}
-                    tableMetadata={query.table()}
-                    sort={sort}
-                    fieldOptions={query.sortOptions(sort)}
-                    removeOrderBy={() => query.removeSort(index).update(setDatasetQuery)}
-                    updateOrderBy={(orderBy) => query.updateSort(index, orderBy).update(setDatasetQuery)}
-                />
-            );
-
-
-            if (query.canAddSort()) {
-                addSortButton = (
-                    <AddClauseButton text={t`Pick a field to sort by`} onClick={() => {
-                        // $FlowFixMe: shouldn't be adding a sort with null field
-                        query.addSort([null, "ascending"]).update(setDatasetQuery)
-                    }} />
-                );
-            }
-        }
-
-        if ((sortList && sortList.length > 0) || addSortButton) {
-            return (
-                <div className="pb3">
-                    <div className="pb1 h6 text-uppercase text-grey-3 text-bold">{t`Sort`}</div>
-                    {sortList}
-                    {addSortButton}
-                </div>
-            );
-        }
-    }
-
-    renderExpressionWidget() {
-        // if we aren't editing any expression then there is nothing to do
-        if (!this.state.editExpression || !this.props.tableMetadata) return null;
-
-        const { query } = this.props;
-
-        const expressions = query.expressions();
-        const expression = expressions && expressions[this.state.editExpression];
-        const name = _.isString(this.state.editExpression) ? this.state.editExpression : "";
-
-        return (
-            <Popover onClose={() => this.setState({editExpression: null})}>
-                <ExpressionWidget
-                    name={name}
-                    expression={expression}
-                    tableMetadata={query.table()}
-                    onSetExpression={(newName, newExpression) => this.setExpression(newName, newExpression, name)}
-                    onRemoveExpression={(name) => this.removeExpression(name)}
-                    onCancel={() => this.setState({editExpression: null})}
-                />
-            </Popover>
+    let sortList, addSortButton;
+
+    const tableMetadata = query.table();
+    if (tableMetadata) {
+      const sorts = query.sorts();
+
+      sortList = sorts.map((sort, index) => (
+        <SortWidget
+          key={index}
+          query={query}
+          tableMetadata={query.table()}
+          sort={sort}
+          fieldOptions={query.sortOptions(sort)}
+          removeOrderBy={() => query.removeSort(index).update(setDatasetQuery)}
+          updateOrderBy={orderBy =>
+            query.updateSort(index, orderBy).update(setDatasetQuery)
+          }
+        />
+      ));
+
+      if (query.canAddSort()) {
+        addSortButton = (
+          <AddClauseButton
+            text={t`Pick a field to sort by`}
+            onClick={() => {
+              // $FlowFixMe: shouldn't be adding a sort with null field
+              query.addSort([null, "ascending"]).update(setDatasetQuery);
+            }}
+          />
         );
+      }
     }
 
-    renderPopover() {
-        if (!this.state.isOpen) return null;
-
-        const { features, query } = this.props;
-
-        return (
-            <Popover onClose={() => this.setState({isOpen: false})}>
-                <div className="p3">
-                    {this.renderSort()}
-
-                    {_.contains(query.table().db.features, "expressions") ?
-                        <Expressions
-                            expressions={query.expressions()}
-                            tableMetadata={query.table()}
-                            onAddExpression={() => this.setState({isOpen: false, editExpression: true})}
-                            onEditExpression={(name) => {
-                                this.setState({isOpen: false, editExpression: name});
-                                MetabaseAnalytics.trackEvent("QueryBuilder", "Show Edit Custom Field");
-                            }}
-                        />
-                    : null}
-
-                    { features.limit &&
-                        <div>
-                            <div className="mb1 h6 text-uppercase text-grey-3 text-bold">{t`Row limit`}</div>
-                            <LimitWidget limit={query.limit()} onChange={this.setLimit} />
-                        </div>
-                    }
-                </div>
-            </Popover>
-        );
+    if ((sortList && sortList.length > 0) || addSortButton) {
+      return (
+        <div className="pb3">
+          <div className="pb1 h6 text-uppercase text-grey-3 text-bold">{t`Sort`}</div>
+          {sortList}
+          {addSortButton}
+        </div>
+      );
     }
-
-    render() {
-        const { features } = this.props;
-        if (!features.sort && !features.limit) return null;
-
-        const onClick = this.props.tableMetadata ? () => this.setState({isOpen: true}) : null;
-
-        return (
-            <div className="GuiBuilder-section GuiBuilder-sort-limit flex align-center">
-                <span className={cx("EllipsisButton no-decoration text-grey-1 px1", {"cursor-pointer": onClick})} onClick={onClick}>…</span>
-                {this.renderPopover()}
-                {this.renderExpressionWidget()}
+  }
+
+  renderExpressionWidget() {
+    // if we aren't editing any expression then there is nothing to do
+    if (!this.state.editExpression || !this.props.tableMetadata) return null;
+
+    const { query } = this.props;
+
+    const expressions = query.expressions();
+    const expression = expressions && expressions[this.state.editExpression];
+    const name = _.isString(this.state.editExpression)
+      ? this.state.editExpression
+      : "";
+
+    return (
+      <Popover onClose={() => this.setState({ editExpression: null })}>
+        <ExpressionWidget
+          name={name}
+          expression={expression}
+          tableMetadata={query.table()}
+          onSetExpression={(newName, newExpression) =>
+            this.setExpression(newName, newExpression, name)
+          }
+          onRemoveExpression={name => this.removeExpression(name)}
+          onCancel={() => this.setState({ editExpression: null })}
+        />
+      </Popover>
+    );
+  }
+
+  renderPopover() {
+    if (!this.state.isOpen) return null;
+
+    const { features, query } = this.props;
+
+    return (
+      <Popover onClose={() => this.setState({ isOpen: false })}>
+        <div className="p3">
+          {this.renderSort()}
+
+          {_.contains(query.table().db.features, "expressions") ? (
+            <Expressions
+              expressions={query.expressions()}
+              tableMetadata={query.table()}
+              onAddExpression={() =>
+                this.setState({ isOpen: false, editExpression: true })
+              }
+              onEditExpression={name => {
+                this.setState({ isOpen: false, editExpression: name });
+                MetabaseAnalytics.trackEvent(
+                  "QueryBuilder",
+                  "Show Edit Custom Field",
+                );
+              }}
+            />
+          ) : null}
+
+          {features.limit && (
+            <div>
+              <div className="mb1 h6 text-uppercase text-grey-3 text-bold">{t`Row limit`}</div>
+              <LimitWidget limit={query.limit()} onChange={this.setLimit} />
             </div>
-        );
-    }
+          )}
+        </div>
+      </Popover>
+    );
+  }
+
+  render() {
+    const { features } = this.props;
+    if (!features.sort && !features.limit) return null;
+
+    const onClick = this.props.tableMetadata
+      ? () => this.setState({ isOpen: true })
+      : null;
+
+    return (
+      <div className="GuiBuilder-section GuiBuilder-sort-limit flex align-center">
+        <span
+          className={cx("EllipsisButton no-decoration text-grey-1 px1", {
+            "cursor-pointer": onClick,
+          })}
+          onClick={onClick}
+        >
+          …
+        </span>
+        {this.renderPopover()}
+        {this.renderExpressionWidget()}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/FieldList.jsx b/frontend/src/metabase/query_builder/components/FieldList.jsx
index 09cc3bc3c2ded170818743c5a2889cc9b9525f7c..eca0c89a8aa02cca1c5dabb475951e4eb25eda1a 100644
--- a/frontend/src/metabase/query_builder/components/FieldList.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldList.jsx
@@ -16,228 +16,270 @@ import type { ConcreteField } from "metabase/meta/types/Query";
 import type Table from "metabase-lib/lib/metadata/Table";
 
 // import type { Section } from "metabase/components/AccordianList";
-export type AccordianListItem = {
-}
+export type AccordianListItem = {};
 
 export type AccordianListSection = {
-    name: ?string;
-    items: AccordianListItem[]
-}
+  name: ?string,
+  items: AccordianListItem[],
+};
 
 type Props = {
-    className?: string,
-    maxHeight?: number,
+  className?: string,
+  maxHeight?: number,
 
-    field: ?ConcreteField,
-    onFieldChange: (field: ConcreteField) => void,
+  field: ?ConcreteField,
+  onFieldChange: (field: ConcreteField) => void,
 
-    // HACK: for segments
-    onFilterChange?: (filter: any) => void,
+  // HACK: for segments
+  onFilterChange?: (filter: any) => void,
 
-    tableMetadata: Table,
+  tableMetadata: Table,
 
-    alwaysExpanded?: boolean,
-    enableSubDimensions?: boolean,
+  alwaysExpanded?: boolean,
+  enableSubDimensions?: boolean,
 
-    hideSectionHeader?: boolean
-}
+  hideSectionHeader?: boolean,
+};
 
 type State = {
-    sections: AccordianListSection[]
-}
+  sections: AccordianListSection[],
+};
 
 export default class FieldList extends Component {
-    props: Props;
-    state: State = {
-        sections: []
-    }
-
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
+  props: Props;
+  state: State = {
+    sections: [],
+  };
+
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
+
+  componentWillReceiveProps(newProps) {
+    let {
+      tableMetadata,
+      fieldOptions,
+      segmentOptions,
+      hideSectionHeader,
+    } = newProps;
+    let tableName = tableMetadata.display_name;
+
+    let specialOptions = [];
+    if (segmentOptions) {
+      specialOptions = segmentOptions.map(segment => ({
+        name: segment.name,
+        value: ["SEGMENT", segment.id],
+        segment: segment,
+      }));
     }
 
-    componentWillReceiveProps(newProps) {
-        let { tableMetadata, fieldOptions, segmentOptions, hideSectionHeader } = newProps;
-        let tableName = tableMetadata.display_name;
-
-        let specialOptions = [];
-        if (segmentOptions) {
-            specialOptions = segmentOptions.map(segment => ({
-                name: segment.name,
-                value: ["SEGMENT", segment.id],
-                segment: segment
-            }));
-        }
-
-        const getSectionItems = (sectionOptions) =>
-            sectionOptions.dimensions.map(dimension => ({
-                name: dimension.displayName(),
-                dimension: dimension
-            }))
-
-        let mainSection = {
-            name: hideSectionHeader ? null : singularize(tableName),
-            items: specialOptions.concat(getSectionItems(fieldOptions))
-        };
-
-        let fkSections = fieldOptions.fks.map(fkOptions => ({
-            name: hideSectionHeader ? null : stripId(fkOptions.field.display_name),
-            items: getSectionItems(fkOptions)
-        }));
-
-        let sections = []
-        if (mainSection.items.length > 0 ) {
-            sections.push(mainSection);
-        }
-        sections.push(...fkSections);
-
-        this.setState({ sections });
+    const getSectionItems = sectionOptions =>
+      sectionOptions.dimensions.map(dimension => ({
+        name: dimension.displayName(),
+        dimension: dimension,
+      }));
+
+    let mainSection = {
+      name: hideSectionHeader ? null : singularize(tableName),
+      items: specialOptions.concat(getSectionItems(fieldOptions)),
+    };
+
+    let fkSections = fieldOptions.fks.map(fkOptions => ({
+      name: hideSectionHeader ? null : stripId(fkOptions.field.display_name),
+      items: getSectionItems(fkOptions),
+    }));
+
+    let sections = [];
+    if (mainSection.items.length > 0) {
+      sections.push(mainSection);
     }
+    sections.push(...fkSections);
 
-    itemIsSelected = (item) => {
-        return item.dimension && item.dimension.isSameBaseDimension(this.props.field);
-    }
-
-    renderItemExtra = (item) => {
-        const { field, enableSubDimensions, tableMetadata: { metadata } } = this.props;
-
-        return (
-            <div className="Field-extra flex align-center">
-                { item.segment &&
-                    this.renderSegmentTooltip(item.segment)
-                }
-                { item.dimension && item.dimension.tag &&
-                    <span className="h5 text-grey-2 px1">{item.dimension.tag}</span>
-                }
-                { enableSubDimensions && item.dimension && item.dimension.dimensions().length > 0 ?
-                    <PopoverWithTrigger
-                        className={this.props.className}
-                        hasArrow={false}
-                        triggerElement={this.renderSubDimensionTrigger(item.dimension)}
-                        tetherOptions={{
-                            attachment: 'top left',
-                            targetAttachment: 'top right',
-                            targetOffset: '0 0',
-                            constraints: [{ to: 'window', attachment: 'together', pin: ['left', 'right']}]
-                        }}
-                    >
-                        <DimensionPicker
-                            dimension={Dimension.parseMBQL(field, metadata)}
-                            dimensions={item.dimension.dimensions()}
-                            onChangeDimension={dimension => this.props.onFieldChange(dimension.mbql())}
-                        />
-                    </PopoverWithTrigger>
-                : null }
-            </div>
-        );
-    }
+    this.setState({ sections });
+  }
 
-    renderItemIcon = (item) => {
-        let name;
-        if (item.segment) {
-            name = "staroutline";
-        } else if (item.dimension) {
-            name = item.dimension.icon();
-        }
-        return <Icon name={name || 'unknown'} size={18} />;
-    }
+  itemIsSelected = item => {
+    return (
+      item.dimension && item.dimension.isSameBaseDimension(this.props.field)
+    );
+  };
 
-    renderSubDimensionTrigger(dimension) {
-        const { field, tableMetadata: { metadata } } = this.props;
-        const subDimension = dimension.isSameBaseDimension(field) ?
-            Dimension.parseMBQL(field, metadata) :
-            dimension.defaultDimension();
-        const name = subDimension ? subDimension.subTriggerDisplayName() : null;
-        return (
-            <div className="FieldList-grouping-trigger flex align-center p1 cursor-pointer">
-                {name && <h4 className="mr1">{name}</h4> }
-                <Icon name="chevronright" size={16} />
-            </div>
-        );
-    }
+  renderItemExtra = item => {
+    const {
+      field,
+      enableSubDimensions,
+      tableMetadata: { metadata },
+    } = this.props;
 
-    renderSegmentTooltip(segment) {
-        let { tableMetadata } = this.props;
-        return (
-            <div className="p1">
-                <Tooltip tooltip={<QueryDefinitionTooltip object={segment} tableMetadata={tableMetadata} />}>
-                    <span className="QuestionTooltipTarget" />
-                </Tooltip>
-            </div>
-        );
+    return (
+      <div className="Field-extra flex align-center">
+        {item.segment && this.renderSegmentTooltip(item.segment)}
+        {item.dimension &&
+          item.dimension.tag && (
+            <span className="h5 text-grey-2 px1">{item.dimension.tag}</span>
+          )}
+        {enableSubDimensions &&
+        item.dimension &&
+        item.dimension.dimensions().length > 0 ? (
+          <PopoverWithTrigger
+            className={this.props.className}
+            hasArrow={false}
+            triggerElement={this.renderSubDimensionTrigger(item.dimension)}
+            tetherOptions={{
+              attachment: "top left",
+              targetAttachment: "top right",
+              targetOffset: "0 0",
+              constraints: [
+                {
+                  to: "window",
+                  attachment: "together",
+                  pin: ["left", "right"],
+                },
+              ],
+            }}
+          >
+            <DimensionPicker
+              dimension={Dimension.parseMBQL(field, metadata)}
+              dimensions={item.dimension.dimensions()}
+              onChangeDimension={dimension =>
+                this.props.onFieldChange(dimension.mbql())
+              }
+            />
+          </PopoverWithTrigger>
+        ) : null}
+      </div>
+    );
+  };
+
+  renderItemIcon = item => {
+    let name;
+    if (item.segment) {
+      name = "staroutline";
+    } else if (item.dimension) {
+      name = item.dimension.icon();
     }
-
-    getItemClasses = (item, itemIndex) => {
-        if (item.segment) {
-            return "List-item--segment";
-        } else {
-            return null;
-        }
+    return <Icon name={name || "unknown"} size={18} />;
+  };
+
+  renderSubDimensionTrigger(dimension) {
+    const { field, tableMetadata: { metadata } } = this.props;
+    const subDimension = dimension.isSameBaseDimension(field)
+      ? Dimension.parseMBQL(field, metadata)
+      : dimension.defaultDimension();
+    const name = subDimension ? subDimension.subTriggerDisplayName() : null;
+    return (
+      <div className="FieldList-grouping-trigger flex align-center p1 cursor-pointer">
+        {name && <h4 className="mr1">{name}</h4>}
+        <Icon name="chevronright" size={16} />
+      </div>
+    );
+  }
+
+  renderSegmentTooltip(segment) {
+    let { tableMetadata } = this.props;
+    return (
+      <div className="p1">
+        <Tooltip
+          tooltip={
+            <QueryDefinitionTooltip
+              object={segment}
+              tableMetadata={tableMetadata}
+            />
+          }
+        >
+          <span className="QuestionTooltipTarget" />
+        </Tooltip>
+      </div>
+    );
+  }
+
+  getItemClasses = (item, itemIndex) => {
+    if (item.segment) {
+      return "List-item--segment";
+    } else {
+      return null;
     }
+  };
 
-    renderSectionIcon = (section, sectionIndex) => {
-        if (sectionIndex > 0) {
-            return <Icon name="connections" size={18} />
-        } else {
-            return <Icon name="table2" size={18} />;
-        }
+  renderSectionIcon = (section, sectionIndex) => {
+    if (sectionIndex > 0) {
+      return <Icon name="connections" size={18} />;
+    } else {
+      return <Icon name="table2" size={18} />;
     }
-
-    onChange = (item) => {
-        const { field, enableSubDimensions, onFilterChange, onFieldChange} = this.props;
-        if (item.segment && onFilterChange) {
-            onFilterChange(item.value);
-        } else if (field != null && this.itemIsSelected(item)) {
-            // ensure if we select the same item we don't reset datetime-field's unit
-            onFieldChange(field);
-        } else {
-            const dimension = item.dimension.defaultDimension() || item.dimension;
-            const shouldExcludeBinning = !enableSubDimensions && dimension instanceof BinnedDimension
-
-            if (shouldExcludeBinning) {
-                // If we don't let user choose the sub-dimension, we don't want to treat the field
-                // as a binned field (which would use the default binning)
-                // Let's unwrap the base field of the binned field instead
-                onFieldChange(dimension.baseDimension().mbql());
-            } else {
-                onFieldChange(dimension.mbql());
-            }
-        }
+  };
+
+  onChange = item => {
+    const {
+      field,
+      enableSubDimensions,
+      onFilterChange,
+      onFieldChange,
+    } = this.props;
+    if (item.segment && onFilterChange) {
+      onFilterChange(item.value);
+    } else if (field != null && this.itemIsSelected(item)) {
+      // ensure if we select the same item we don't reset datetime-field's unit
+      onFieldChange(field);
+    } else {
+      const dimension = item.dimension.defaultDimension() || item.dimension;
+      const shouldExcludeBinning =
+        !enableSubDimensions && dimension instanceof BinnedDimension;
+
+      if (shouldExcludeBinning) {
+        // If we don't let user choose the sub-dimension, we don't want to treat the field
+        // as a binned field (which would use the default binning)
+        // Let's unwrap the base field of the binned field instead
+        onFieldChange(dimension.baseDimension().mbql());
+      } else {
+        onFieldChange(dimension.mbql());
+      }
     }
+  };
 
-    render() {
-        return (
-            <AccordianList
-                className={this.props.className}
-                maxHeight={this.props.maxHeight}
-                sections={this.state.sections}
-                onChange={this.onChange}
-                itemIsSelected={this.itemIsSelected}
-                renderSectionIcon={this.renderSectionIcon}
-                renderItemExtra={this.renderItemExtra}
-                renderItemIcon={this.renderItemIcon}
-                getItemClasses={this.getItemClasses}
-                alwaysExpanded={this.props.alwaysExpanded}
-            />
-        )
-    }
+  render() {
+    return (
+      <AccordianList
+        className={this.props.className}
+        maxHeight={this.props.maxHeight}
+        sections={this.state.sections}
+        onChange={this.onChange}
+        itemIsSelected={this.itemIsSelected}
+        renderSectionIcon={this.renderSectionIcon}
+        renderItemExtra={this.renderItemExtra}
+        renderItemIcon={this.renderItemIcon}
+        getItemClasses={this.getItemClasses}
+        alwaysExpanded={this.props.alwaysExpanded}
+      />
+    );
+  }
 }
 
 import cx from "classnames";
 
-export const DimensionPicker = ({ className, dimension, dimensions, onChangeDimension }) => {
-    return (
-        <ul className="px2 py1">
-            { dimensions.map((d, index) =>
-                <li
-                    key={index}
-                    className={cx("List-item", { "List-item--selected": d.isEqual(dimension) })}
-                >
-                    <a className="List-item-title full px2 py1 cursor-pointer" onClick={() => onChangeDimension(d)}>
-                        {d.subDisplayName()}
-                    </a>
-                </li>
-            )}
-        </ul>
-    )
-}
+export const DimensionPicker = ({
+  className,
+  dimension,
+  dimensions,
+  onChangeDimension,
+}) => {
+  return (
+    <ul className="px2 py1">
+      {dimensions.map((d, index) => (
+        <li
+          key={index}
+          className={cx("List-item", {
+            "List-item--selected": d.isEqual(dimension),
+          })}
+        >
+          <a
+            className="List-item-title full px2 py1 cursor-pointer"
+            onClick={() => onChangeDimension(d)}
+          >
+            {d.subDisplayName()}
+          </a>
+        </li>
+      ))}
+    </ul>
+  );
+};
diff --git a/frontend/src/metabase/query_builder/components/FieldName.jsx b/frontend/src/metabase/query_builder/components/FieldName.jsx
index 1e2684c2e1c1053566cd90768cee482046aed325..b4a582f497ce72472ca16ea5cb997eef834f5660 100644
--- a/frontend/src/metabase/query_builder/components/FieldName.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldName.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Clearable from "./Clearable.jsx";
 
 import Query from "metabase/lib/query";
@@ -11,63 +11,83 @@ import _ from "underscore";
 import cx from "classnames";
 
 export default class FieldName extends Component {
-    static propTypes = {
-        field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
-        onClick: PropTypes.func,
-        removeField: PropTypes.func,
-        tableMetadata: PropTypes.object.isRequired,
-        query: PropTypes.object
-    };
+  static propTypes = {
+    field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
+    onClick: PropTypes.func,
+    removeField: PropTypes.func,
+    tableMetadata: PropTypes.object.isRequired,
+    query: PropTypes.object,
+  };
 
-    static defaultProps = {
-        className: ""
-    };
+  static defaultProps = {
+    className: "",
+  };
 
-    displayNameForFieldLiteral(tableMetadata, fieldLiteral) {
-        // see if we can find an entry in the table metadata that matches the field literal
-        let matchingField = _.find(tableMetadata.fields, (field) => Query.isFieldLiteral(field.id) && field.id[1] === fieldLiteral[1]); // check whether names of field literals match
+  displayNameForFieldLiteral(tableMetadata, fieldLiteral) {
+    // see if we can find an entry in the table metadata that matches the field literal
+    let matchingField = _.find(
+      tableMetadata.fields,
+      field =>
+        Query.isFieldLiteral(field.id) && field.id[1] === fieldLiteral[1],
+    ); // check whether names of field literals match
 
-        return (matchingField && matchingField.display_name) || fieldLiteral[1];
-    }
+    return (matchingField && matchingField.display_name) || fieldLiteral[1];
+  }
 
-    render() {
-        let { field, tableMetadata, query, className } = this.props;
+  render() {
+    let { field, tableMetadata, query, className } = this.props;
 
-        let parts = [];
+    let parts = [];
 
-        if (field) {
-            const dimension = Dimension.parseMBQL(field, tableMetadata && tableMetadata.metadata);
-            if (dimension) {
-                if (dimension instanceof AggregationDimension) {
-                    // Aggregation dimension doesn't know about its relation to the current query
-                    // so we have to infer the display name of aggregation here
-                    parts = <span key="field">{query.aggregations()[dimension.aggregationIndex()][0]}</span>
-                } else {
-                    parts = <span key="field">{dimension.render()}</span>;
-                }
-            }
-            // TODO Atte Keinänen 6/23/17: Move nested queries logic to Dimension subclasses
-            // if the Field in question is a field literal, e.g. ["field-literal", <name>, <type>] just use name as-is
-            else if (Query.isFieldLiteral(field)) {
-                parts.push(<span key="field">{this.displayNameForFieldLiteral(tableMetadata, field)}</span>);
-            }
-            // otherwise if for some weird reason we wound up with a Field Literal inside a field ID,
-            // e.g. ["field-id", ["field-literal", <name>, <type>], still just use the name as-is
-            else if (Query.isLocalField(field) && Query.isFieldLiteral(field[1])) {
-                parts.push(<span key="field">{this.displayNameForFieldLiteral(tableMetadata, field[1])}</span>);
-            } else {
-                parts.push(<span key="field">{t`Unknown Field`}</span>);
-            }
+    if (field) {
+      const dimension = Dimension.parseMBQL(
+        field,
+        tableMetadata && tableMetadata.metadata,
+      );
+      if (dimension) {
+        if (dimension instanceof AggregationDimension) {
+          // Aggregation dimension doesn't know about its relation to the current query
+          // so we have to infer the display name of aggregation here
+          parts = (
+            <span key="field">
+              {query.aggregations()[dimension.aggregationIndex()][0]}
+            </span>
+          );
         } else {
-            parts.push(<span key="field" className={"text-grey-2"}>{t`field`}</span>)
+          parts = <span key="field">{dimension.render()}</span>;
         }
-
-        return (
-            <Clearable onClear={this.props.removeField}>
-                <div className={cx(className, { selected: Query.isValidField(field) })} onClick={this.props.onClick}>
-                    <span className="QueryOption">{parts}</span>
-                </div>
-            </Clearable>
+      } else if (Query.isFieldLiteral(field)) {
+        // TODO Atte Keinänen 6/23/17: Move nested queries logic to Dimension subclasses
+        // if the Field in question is a field literal, e.g. ["field-literal", <name>, <type>] just use name as-is
+        parts.push(
+          <span key="field">
+            {this.displayNameForFieldLiteral(tableMetadata, field)}
+          </span>,
         );
+      } else if (Query.isLocalField(field) && Query.isFieldLiteral(field[1])) {
+        // otherwise if for some weird reason we wound up with a Field Literal inside a field ID,
+        // e.g. ["field-id", ["field-literal", <name>, <type>], still just use the name as-is
+        parts.push(
+          <span key="field">
+            {this.displayNameForFieldLiteral(tableMetadata, field[1])}
+          </span>,
+        );
+      } else {
+        parts.push(<span key="field">{t`Unknown Field`}</span>);
+      }
+    } else {
+      parts.push(<span key="field" className={"text-grey-2"}>{t`field`}</span>);
     }
+
+    return (
+      <Clearable onClear={this.props.removeField}>
+        <div
+          className={cx(className, { selected: Query.isValidField(field) })}
+          onClick={this.props.onClick}
+        >
+          <span className="QueryOption">{parts}</span>
+        </div>
+      </Clearable>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/FieldWidget.jsx b/frontend/src/metabase/query_builder/components/FieldWidget.jsx
index 10179b50a6a4eb3703ac653f137faa448bd7bd91..0d42941a6e892c492b1997df32688b627cbcb570 100644
--- a/frontend/src/metabase/query_builder/components/FieldWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/FieldWidget.jsx
@@ -10,81 +10,77 @@ import Query from "metabase/lib/query";
 import _ from "underscore";
 
 export default class FieldWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            isOpen: props.isInitiallyOpen || false
-        };
+    this.state = {
+      isOpen: props.isInitiallyOpen || false,
+    };
 
-        _.bindAll(this, "toggle", "setField");
-    }
+    _.bindAll(this, "toggle", "setField");
+  }
 
-    static propTypes = {
-        field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
-        fieldOptions: PropTypes.object.isRequired,
-        customFieldOptions: PropTypes.object,
-        setField: PropTypes.func.isRequired,
-        removeField: PropTypes.func,
-        isInitiallyOpen: PropTypes.bool,
-        tableMetadata: PropTypes.object.isRequired,
-        enableSubDimensions: PropTypes.bool
-    };
+  static propTypes = {
+    field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
+    fieldOptions: PropTypes.object.isRequired,
+    customFieldOptions: PropTypes.object,
+    setField: PropTypes.func.isRequired,
+    removeField: PropTypes.func,
+    isInitiallyOpen: PropTypes.bool,
+    tableMetadata: PropTypes.object.isRequired,
+    enableSubDimensions: PropTypes.bool,
+  };
 
-    static defaultProps = {
-        color: "brand",
-        enableSubDimensions: true
-    };
+  static defaultProps = {
+    color: "brand",
+    enableSubDimensions: true,
+  };
 
-    setField(value) {
-        this.props.setField(value);
-        if (Query.isValidField(value)) {
-            this.toggle();
-        }
+  setField(value) {
+    this.props.setField(value);
+    if (Query.isValidField(value)) {
+      this.toggle();
     }
+  }
 
-    toggle() {
-        this.setState({ isOpen: !this.state.isOpen });
-    }
+  toggle() {
+    this.setState({ isOpen: !this.state.isOpen });
+  }
 
-    renderPopover() {
-        if (this.state.isOpen) {
-            return (
-                <Popover
-                    ref="popover"
-                    className="FieldPopover"
-                    onClose={this.toggle}
-                >
-                    <FieldList
-                        className={"text-" + this.props.color}
-                        tableMetadata={this.props.tableMetadata}
-                        field={this.props.field}
-                        fieldOptions={this.props.fieldOptions}
-                        customFieldOptions={this.props.customFieldOptions}
-                        onFieldChange={this.setField}
-                        enableSubDimensions={this.props.enableSubDimensions}
-                    />
-                </Popover>
-            );
-        }
+  renderPopover() {
+    if (this.state.isOpen) {
+      return (
+        <Popover ref="popover" className="FieldPopover" onClose={this.toggle}>
+          <FieldList
+            className={"text-" + this.props.color}
+            tableMetadata={this.props.tableMetadata}
+            field={this.props.field}
+            fieldOptions={this.props.fieldOptions}
+            customFieldOptions={this.props.customFieldOptions}
+            onFieldChange={this.setField}
+            enableSubDimensions={this.props.enableSubDimensions}
+          />
+        </Popover>
+      );
     }
+  }
 
-    render() {
-        const { className, field, query } = this.props;
-        return (
-            <div className="flex align-center">
-                <FieldName
-                    className={className}
-                    field={field}
-                    query={query}
-                    tableMetadata={this.props.tableMetadata}
-                    fieldOptions={this.props.fieldOptions}
-                    customFieldOptions={this.props.customFieldOptions}
-                    removeField={this.props.removeField}
-                    onClick={this.toggle}
-                />
-                {this.renderPopover()}
-            </div>
-        );
-    }
+  render() {
+    const { className, field, query } = this.props;
+    return (
+      <div className="flex align-center">
+        <FieldName
+          className={className}
+          field={field}
+          query={query}
+          tableMetadata={this.props.tableMetadata}
+          fieldOptions={this.props.fieldOptions}
+          customFieldOptions={this.props.customFieldOptions}
+          removeField={this.props.removeField}
+          onClick={this.toggle}
+        />
+        {this.renderPopover()}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
index a87fc1edccc8e5375c4f00b8150786af160d6d94..3134d5503f9d65bb1c63c5bd677151513d734d65 100644
--- a/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/GuiQueryEditor.jsx
@@ -3,14 +3,14 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
-import AggregationWidget_LEGACY from './AggregationWidget.jsx';
-import BreakoutWidget_LEGACY from './BreakoutWidget.jsx';
+import { t } from "c-3po";
+import AggregationWidget_LEGACY from "./AggregationWidget.jsx";
+import BreakoutWidget_LEGACY from "./BreakoutWidget.jsx";
 import ExtendedOptions from "./ExtendedOptions.jsx";
-import FilterList from './filters/FilterList.jsx';
-import FilterPopover from './filters/FilterPopover.jsx';
+import FilterList from "./filters/FilterList.jsx";
+import FilterPopover from "./filters/FilterPopover.jsx";
 import Icon from "metabase/components/Icon.jsx";
-import IconBorder from 'metabase/components/IconBorder.jsx';
+import IconBorder from "metabase/components/IconBorder.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import { DatabaseSchemaAndTableDataSelector } from "metabase/query_builder/components/DataSelector";
 
@@ -20,395 +20,469 @@ import _ from "underscore";
 import type { TableId } from "metabase/meta/types/Table";
 import type { DatabaseId } from "metabase/meta/types/Database";
 import type { DatasetQuery } from "metabase/meta/types/Card";
-import type { TableMetadata, DatabaseMetadata } from "metabase/meta/types/Metadata";
-import type { Children } from 'react';
+import type {
+  TableMetadata,
+  DatabaseMetadata,
+} from "metabase/meta/types/Metadata";
+import type { Children } from "react";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 export type GuiQueryEditorFeatures = {
-    data?: boolean,
-    filter?: boolean,
-    aggregation?: boolean,
-    breakout?: boolean,
-    sort?: boolean,
-    limit?: boolean
-}
+  data?: boolean,
+  filter?: boolean,
+  aggregation?: boolean,
+  breakout?: boolean,
+  sort?: boolean,
+  limit?: boolean,
+};
 
 type Props = {
-    children?: Children,
+  children?: Children,
 
-    features: GuiQueryEditorFeatures,
+  features: GuiQueryEditorFeatures,
 
-    query: StructuredQuery,
+  query: StructuredQuery,
 
-    databases: DatabaseMetadata[],
-    tables: TableMetadata[],
+  databases: DatabaseMetadata[],
+  tables: TableMetadata[],
 
-    supportMultipleAggregations?: boolean,
+  supportMultipleAggregations?: boolean,
 
-    setDatabaseFn: (id: DatabaseId) => void,
-    setSourceTableFn: (id: TableId) => void,
-    setDatasetQuery: (datasetQuery: DatasetQuery) => void,
+  setDatabaseFn: (id: DatabaseId) => void,
+  setSourceTableFn: (id: TableId) => void,
+  setDatasetQuery: (datasetQuery: DatasetQuery) => void,
 
-    isShowingTutorial: boolean,
-    isShowingDataReference: boolean,
-}
+  isShowingTutorial: boolean,
+  isShowingDataReference: boolean,
+};
 
 type State = {
-    expanded: boolean
-}
+  expanded: boolean,
+};
 
 export default class GuiQueryEditor extends Component {
-    props: Props;
-    state: State = {
-        expanded: true
-    }
-
-    static propTypes = {
-        databases: PropTypes.array,
-        isShowingDataReference: PropTypes.bool.isRequired,
-        setDatasetQuery: PropTypes.func.isRequired,
-        setDatabaseFn: PropTypes.func,
-        setSourceTableFn: PropTypes.func,
-        features: PropTypes.object,
-        supportMultipleAggregations: PropTypes.bool
-    };
-
-    static defaultProps = {
-        features: {
-            data: true,
-            filter: true,
-            aggregation: true,
-            breakout: true,
-            sort: true,
-            limit: true
-        },
-        supportMultipleAggregations: true
-    };
-
-    renderAdd(text: ?string, onClick: ?(() => void), targetRefName?: string) {
-        let className = "AddButton text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color";
-        if (onClick) {
-            return (
-                <a className={className} onClick={onClick}>
-                    { text && <span className="mr1">{text}</span> }
-                    {this.renderAddIcon(targetRefName)}
-                </a>
-            );
-        } else {
-            return (
-                <span className={className}>
-                    { text && <span className="mr1">{text}</span> }
-                    {this.renderAddIcon(targetRefName)}
-                </span>
-            );
-        }
+  props: Props;
+  state: State = {
+    expanded: true,
+  };
+
+  static propTypes = {
+    databases: PropTypes.array,
+    isShowingDataReference: PropTypes.bool.isRequired,
+    setDatasetQuery: PropTypes.func.isRequired,
+    setDatabaseFn: PropTypes.func,
+    setSourceTableFn: PropTypes.func,
+    features: PropTypes.object,
+    supportMultipleAggregations: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    features: {
+      data: true,
+      filter: true,
+      aggregation: true,
+      breakout: true,
+      sort: true,
+      limit: true,
+    },
+    supportMultipleAggregations: true,
+  };
+
+  renderAdd(text: ?string, onClick: ?() => void, targetRefName?: string) {
+    let className =
+      "AddButton text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color";
+    if (onClick) {
+      return (
+        <a className={className} onClick={onClick}>
+          {text && <span className="mr1">{text}</span>}
+          {this.renderAddIcon(targetRefName)}
+        </a>
+      );
+    } else {
+      return (
+        <span className={className}>
+          {text && <span className="mr1">{text}</span>}
+          {this.renderAddIcon(targetRefName)}
+        </span>
+      );
     }
-
-    renderAddIcon(targetRefName?: string) {
-        return (
-            <IconBorder borderRadius="3px" ref={targetRefName}>
-                <Icon name="add" size={14} />
-            </IconBorder>
-        )
-    }
-
-    renderFilters() {
-        const { query, features, setDatasetQuery } = this.props;
-
-        if (!features.filter) return;
-
-        let enabled;
-        let filterList;
-        let addFilterButton;
-
-        if (query.isEditable()) {
-            enabled = true;
-
-            let filters = query.filters();
-            if (filters && filters.length > 0) {
-                filterList = (
-                    <FilterList
-                        query={query}
-                        filters={filters}
-                        removeFilter={(index) => query.removeFilter(index).update(setDatasetQuery)}
-                        updateFilter={(index, filter) => query.updateFilter(index, filter).update(setDatasetQuery)}
-                    />
-                );
+  }
+
+  renderAddIcon(targetRefName?: string) {
+    return (
+      <IconBorder borderRadius="3px" ref={targetRefName}>
+        <Icon name="add" size={14} />
+      </IconBorder>
+    );
+  }
+
+  renderFilters() {
+    const { query, features, setDatasetQuery } = this.props;
+
+    if (!features.filter) return;
+
+    let enabled;
+    let filterList;
+    let addFilterButton;
+
+    if (query.isEditable()) {
+      enabled = true;
+
+      let filters = query.filters();
+      if (filters && filters.length > 0) {
+        filterList = (
+          <FilterList
+            query={query}
+            filters={filters}
+            removeFilter={index =>
+              query.removeFilter(index).update(setDatasetQuery)
             }
-
-            if (query.canAddFilter()) {
-                addFilterButton = this.renderAdd((filterList ? null : t`Add filters to narrow your answer`), null, "addFilterTarget");
+            updateFilter={(index, filter) =>
+              query.updateFilter(index, filter).update(setDatasetQuery)
             }
-        } else {
-            enabled = false;
-            addFilterButton = this.renderAdd(t`Add filters to narrow your answer`, null, "addFilterTarget");
-        }
+          />
+        );
+      }
 
-        return (
-            <div className={cx("Query-section", { disabled: !enabled })}>
-                <div className="Query-filters">
-                    {filterList}
-                </div>
-                <div className="mx2">
-                    <PopoverWithTrigger
-                        id="FilterPopover"
-                        ref="filterPopover"
-                        triggerElement={addFilterButton}
-                        triggerClasses="flex align-center"
-                        getTarget={() => this.refs.addFilterTarget}
-                        horizontalAttachments={["left"]}
-                        autoWidth
-                    >
-                        <FilterPopover
-                            isNew
-                            query={query}
-                            onCommitFilter={(filter) => query.addFilter(filter).update(setDatasetQuery)}
-                            onClose={() => this.refs.filterPopover.close()}
-                        />
-                    </PopoverWithTrigger>
-                </div>
-            </div>
+      if (query.canAddFilter()) {
+        addFilterButton = this.renderAdd(
+          filterList ? null : t`Add filters to narrow your answer`,
+          null,
+          "addFilterTarget",
         );
+      }
+    } else {
+      enabled = false;
+      addFilterButton = this.renderAdd(
+        t`Add filters to narrow your answer`,
+        null,
+        "addFilterTarget",
+      );
     }
 
-    renderAggregation() {
-        const { query, features, setDatasetQuery, supportMultipleAggregations } = this.props;
-
-        if (!features.aggregation) {
-            return;
-        }
-
-        // aggregation clause.  must have table details available
-        if (query.isEditable()) {
-            // $FlowFixMe
-            let aggregations: (Aggregation|null)[] = query.aggregations();
-
-            if (aggregations.length === 0) {
-                // add implicit rows aggregation
-                aggregations.push(["rows"]);
-            }
-
-            // Placeholder aggregation for showing the add button
-            if (supportMultipleAggregations && !query.isBareRows()) {
-                aggregations.push([]);
-            }
-
-            let aggregationList = [];
-            for (const [index, aggregation] of aggregations.entries()) {
-                aggregationList.push(
-                    <AggregationWidget
-                        key={"agg"+index}
-                        index={index}
-                        aggregation={aggregation}
-                        query={query}
-                        updateQuery={setDatasetQuery}
-                        addButton={this.renderAdd(null)}
-                    />
-                );
-                if (aggregations[index + 1] != null && aggregations[index + 1].length > 0) {
-                    aggregationList.push(
-                        <span key={"and"+index} className="text-bold">{t`and`}</span>
-                    );
-                }
-            }
-            return aggregationList
-        } else {
-            // TODO: move this into AggregationWidget?
-            return (
-                <div className="Query-section Query-section-aggregation disabled">
-                    <a className="QueryOption p1 flex align-center">{t`Raw data`}</a>
-                </div>
-            );
-        }
+    return (
+      <div className={cx("Query-section", { disabled: !enabled })}>
+        <div className="Query-filters">{filterList}</div>
+        <div className="mx2">
+          <PopoverWithTrigger
+            id="FilterPopover"
+            ref="filterPopover"
+            triggerElement={addFilterButton}
+            triggerClasses="flex align-center"
+            getTarget={() => this.refs.addFilterTarget}
+            horizontalAttachments={["left", "center"]}
+            autoWidth
+          >
+            <FilterPopover
+              isNew
+              query={query}
+              onCommitFilter={filter =>
+                query.addFilter(filter).update(setDatasetQuery)
+              }
+              onClose={() => this.refs.filterPopover.close()}
+            />
+          </PopoverWithTrigger>
+        </div>
+      </div>
+    );
+  }
+
+  renderAggregation() {
+    const {
+      query,
+      features,
+      setDatasetQuery,
+      supportMultipleAggregations,
+    } = this.props;
+
+    if (!features.aggregation) {
+      return;
     }
 
-    renderBreakouts() {
-        const { query, setDatasetQuery, features } = this.props;
-
-        if (!features.breakout) {
-            return;
+    // aggregation clause.  must have table details available
+    if (query.isEditable()) {
+      // $FlowFixMe
+      let aggregations: (Aggregation | null)[] = query.aggregations();
+
+      if (aggregations.length === 0) {
+        // add implicit rows aggregation
+        aggregations.push(["rows"]);
+      }
+
+      // Placeholder aggregation for showing the add button
+      if (supportMultipleAggregations && !query.isBareRows()) {
+        aggregations.push([]);
+      }
+
+      let aggregationList = [];
+      for (const [index, aggregation] of aggregations.entries()) {
+        aggregationList.push(
+          <AggregationWidget
+            key={"agg" + index}
+            index={index}
+            aggregation={aggregation}
+            query={query}
+            updateQuery={setDatasetQuery}
+            addButton={this.renderAdd(null)}
+          />,
+        );
+        if (
+          aggregations[index + 1] != null &&
+          aggregations[index + 1].length > 0
+        ) {
+          aggregationList.push(
+            <span key={"and" + index} className="text-bold">{t`and`}</span>,
+          );
         }
+      }
+      return aggregationList;
+    } else {
+      // TODO: move this into AggregationWidget?
+      return (
+        <div className="Query-section Query-section-aggregation disabled">
+          <a className="QueryOption p1 flex align-center">{t`Raw data`}</a>
+        </div>
+      );
+    }
+  }
 
-        const breakoutList = [];
-
-        // $FlowFixMe
-        const breakouts: (Breakout|null)[] = query.breakouts();
-
-        // Placeholder breakout for showing the add button
-        if (query.canAddBreakout()) {
-            breakouts.push(null);
-        }
+  renderBreakouts() {
+    const { query, setDatasetQuery, features } = this.props;
 
-        for (let i = 0; i < breakouts.length; i++) {
-            const breakout = breakouts[i];
+    if (!features.breakout) {
+      return;
+    }
 
-            if (breakout == null) {
-                breakoutList.push(<span key="nullBreakout" className="ml1" />);
-            }
+    const breakoutList = [];
 
-            breakoutList.push(
-                <BreakoutWidget
-                    key={"breakout"+i}
-                    className="View-section-breakout SelectionModule p1"
-                    index={i}
-                    breakout={breakout}
-                    query={query}
-                    updateQuery={setDatasetQuery}
-                    addButton={this.renderAdd(i === 0 ? t`Add a grouping` : null)}
-                />
-            );
-
-            if (breakouts[i + 1] != null) {
-                breakoutList.push(
-                    <span key={"and"+i} className="text-bold">{t`and`}</span>
-                );
-            }
-        }
+    // $FlowFixMe
+    const breakouts: (Breakout | null)[] = query.breakouts();
 
-        return (
-            <div className={cx("Query-section Query-section-breakout", { disabled: breakoutList.length === 0 })}>
-                {breakoutList}
-            </div>
-        );
+    // Placeholder breakout for showing the add button
+    if (query.canAddBreakout()) {
+      breakouts.push(null);
     }
 
-    renderDataSection() {
-        const { databases, query, isShowingTutorial } = this.props;
-        const tableMetadata = query.tableMetadata();
-        const datasetQuery = query.datasetQuery();
-        const databaseId = datasetQuery && datasetQuery.database
-        const sourceTableId = datasetQuery && datasetQuery.query && datasetQuery.query.source_table;
-        const isInitiallyOpen = (!datasetQuery.database || !sourceTableId) && !isShowingTutorial;
-
-        return (
-            <div className={"GuiBuilder-section GuiBuilder-data flex align-center arrow-right"}>
-                <span className="GuiBuilder-section-label Query-label">{t`Data`}</span>
-                { this.props.features.data ?
-                    <DatabaseSchemaAndTableDataSelector
-                        databases={databases}
-                        selected={sourceTableId}
-                        selectedDatabaseId={databaseId}
-                        selectedTableId={sourceTableId}
-                        setDatabaseFn={this.props.setDatabaseFn}
-                        setSourceTableFn={this.props.setSourceTableFn}
-                        isInitiallyOpen={isInitiallyOpen}
-                    />
-                    :
-                    <span className="flex align-center px2 py2 text-bold text-grey">
-                        {tableMetadata && tableMetadata.display_name}
-                    </span>
-                }
-            </div>
+    for (let i = 0; i < breakouts.length; i++) {
+      const breakout = breakouts[i];
+
+      if (breakout == null) {
+        breakoutList.push(<span key="nullBreakout" className="ml1" />);
+      }
+
+      breakoutList.push(
+        <BreakoutWidget
+          key={"breakout" + i}
+          className="View-section-breakout SelectionModule p1"
+          index={i}
+          breakout={breakout}
+          query={query}
+          updateQuery={setDatasetQuery}
+          addButton={this.renderAdd(i === 0 ? t`Add a grouping` : null)}
+        />,
+      );
+
+      if (breakouts[i + 1] != null) {
+        breakoutList.push(
+          <span key={"and" + i} className="text-bold">{t`and`}</span>,
         );
+      }
     }
 
-    renderFilterSection() {
-        if (!this.props.features.filter) {
-            return;
+    return (
+      <div
+        className={cx("Query-section Query-section-breakout", {
+          disabled: breakoutList.length === 0,
+        })}
+      >
+        {breakoutList}
+      </div>
+    );
+  }
+
+  renderDataSection() {
+    const { databases, query, isShowingTutorial } = this.props;
+    const tableMetadata = query.tableMetadata();
+    const datasetQuery = query.datasetQuery();
+    const databaseId = datasetQuery && datasetQuery.database;
+    const sourceTableId =
+      datasetQuery && datasetQuery.query && datasetQuery.query.source_table;
+    const isInitiallyOpen =
+      (!datasetQuery.database || !sourceTableId) && !isShowingTutorial;
+
+    return (
+      <div
+        className={
+          "GuiBuilder-section GuiBuilder-data flex align-center arrow-right"
         }
-
-        return (
-            <div className="GuiBuilder-section GuiBuilder-filtered-by flex align-center" ref="filterSection">
-                <span className="GuiBuilder-section-label Query-label">{t`Filtered by`}</span>
-                {this.renderFilters()}
-            </div>
-        );
+      >
+        <span className="GuiBuilder-section-label Query-label">{t`Data`}</span>
+        {this.props.features.data ? (
+          <DatabaseSchemaAndTableDataSelector
+            databases={databases}
+            selected={sourceTableId}
+            selectedDatabaseId={databaseId}
+            selectedTableId={sourceTableId}
+            setDatabaseFn={this.props.setDatabaseFn}
+            setSourceTableFn={this.props.setSourceTableFn}
+            isInitiallyOpen={isInitiallyOpen}
+          />
+        ) : (
+          <span className="flex align-center px2 py2 text-bold text-grey">
+            {tableMetadata && tableMetadata.display_name}
+          </span>
+        )}
+      </div>
+    );
+  }
+
+  renderFilterSection() {
+    if (!this.props.features.filter) {
+      return;
     }
 
-    renderViewSection() {
-        const { features } = this.props;
-        if (!features.aggregation && !features.breakout) {
-            return;
-        }
-
-        return (
-            <div className="GuiBuilder-section GuiBuilder-view flex align-center px1 pr2" ref="viewSection">
-                <span className="GuiBuilder-section-label Query-label">{t`View`}</span>
-                {this.renderAggregation()}
-            </div>
-        );
+    return (
+      <div
+        className="GuiBuilder-section GuiBuilder-filtered-by flex align-center"
+        ref="filterSection"
+      >
+        <span className="GuiBuilder-section-label Query-label">{t`Filtered by`}</span>
+        {this.renderFilters()}
+      </div>
+    );
+  }
+
+  renderViewSection() {
+    const { features } = this.props;
+    if (!features.aggregation && !features.breakout) {
+      return;
     }
 
-    renderGroupedBySection() {
-        const { features } = this.props;
-        if (!features.aggregation && !features.breakout) {
-            return;
-        }
-
-        return (
-            <div className="GuiBuilder-section GuiBuilder-groupedBy flex align-center px1" ref="viewSection">
-                <span className="GuiBuilder-section-label Query-label">{t`Grouped By`}</span>
-                {this.renderBreakouts()}
-            </div>
-        );
+    return (
+      <div
+        className="GuiBuilder-section GuiBuilder-view flex align-center px1 pr2"
+        ref="viewSection"
+      >
+        <span className="GuiBuilder-section-label Query-label">{t`View`}</span>
+        {this.renderAggregation()}
+      </div>
+    );
+  }
+
+  renderGroupedBySection() {
+    const { features } = this.props;
+    if (!features.aggregation && !features.breakout) {
+      return;
     }
 
-    componentDidUpdate() {
-        const guiBuilder = ReactDOM.findDOMNode(this.refs.guiBuilder);
-        if (!guiBuilder) {
-            return;
-        }
-
-        // HACK: magic number "5" accounts for the borders between the sections?
-        let contentWidth = ["data", "filter", "view", "groupedBy","sortLimit"].reduce((acc, ref) => {
-            let node = ReactDOM.findDOMNode(this.refs[`${ref}Section`]);
-            return acc + (node ? node.offsetWidth : 0);
-        }, 0) + 5;
-        let guiBuilderWidth = guiBuilder.offsetWidth;
-
-        let expanded = (contentWidth < guiBuilderWidth);
-        if (this.state.expanded !== expanded) {
-            this.setState({ expanded });
-        }
+    return (
+      <div
+        className="GuiBuilder-section GuiBuilder-groupedBy flex align-center px1"
+        ref="viewSection"
+      >
+        <span className="GuiBuilder-section-label Query-label">{t`Grouped By`}</span>
+        {this.renderBreakouts()}
+      </div>
+    );
+  }
+
+  componentDidUpdate() {
+    const guiBuilder = ReactDOM.findDOMNode(this.refs.guiBuilder);
+    if (!guiBuilder) {
+      return;
     }
 
-    render() {
-        const { databases, query } = this.props;
-        const datasetQuery = query.datasetQuery()
-        const readOnly = datasetQuery.database != null && !_.findWhere(databases, { id: datasetQuery.database });
-        if (readOnly) {
-            return <div className="border-bottom border-med" />
-        }
+    // HACK: magic number "5" accounts for the borders between the sections?
+    let contentWidth =
+      ["data", "filter", "view", "groupedBy", "sortLimit"].reduce(
+        (acc, ref) => {
+          let node = ReactDOM.findDOMNode(this.refs[`${ref}Section`]);
+          return acc + (node ? node.offsetWidth : 0);
+        },
+        0,
+      ) + 5;
+    let guiBuilderWidth = guiBuilder.offsetWidth;
 
-        return (
-            <div className={cx("GuiBuilder rounded shadowed", { "GuiBuilder--expand": this.state.expanded, disabled: readOnly })} ref="guiBuilder">
-                <div className="GuiBuilder-row flex">
-                    {this.renderDataSection()}
-                    {this.renderFilterSection()}
-                </div>
-                <div className="GuiBuilder-row flex flex-full">
-                    {this.renderViewSection()}
-                    {this.renderGroupedBySection()}
-                    <div className="flex-full"></div>
-                    {this.props.children}
-                    <ExtendedOptions
-                        {...this.props}
-                    />
-                </div>
-            </div>
-        );
+    let expanded = contentWidth < guiBuilderWidth;
+    if (this.state.expanded !== expanded) {
+      this.setState({ expanded });
+    }
+  }
+
+  render() {
+    const { databases, query } = this.props;
+    const datasetQuery = query.datasetQuery();
+    const readOnly =
+      datasetQuery.database != null &&
+      !_.findWhere(databases, { id: datasetQuery.database });
+    if (readOnly) {
+      return <div className="border-bottom border-med" />;
     }
+
+    return (
+      <div
+        className={cx("GuiBuilder rounded shadowed", {
+          "GuiBuilder--expand": this.state.expanded,
+          disabled: readOnly,
+        })}
+        ref="guiBuilder"
+      >
+        <div className="GuiBuilder-row flex">
+          {this.renderDataSection()}
+          {this.renderFilterSection()}
+        </div>
+        <div className="GuiBuilder-row flex flex-full">
+          {this.renderViewSection()}
+          {this.renderGroupedBySection()}
+          <div className="flex-full" />
+          {this.props.children}
+          <ExtendedOptions {...this.props} />
+        </div>
+      </div>
+    );
+  }
 }
 
-export const AggregationWidget = ({ index, aggregation, query, updateQuery, addButton }: Object) =>
-    <AggregationWidget_LEGACY
-        query={query}
-        aggregation={aggregation}
-        tableMetadata={query.tableMetadata()}
-        customFields={query.expressions()}
-        updateAggregation={(aggregation) => query.updateAggregation(index, aggregation).update(updateQuery)}
-        removeAggregation={query.canRemoveAggregation() ? (() => query.removeAggregation(index).update(updateQuery)) : null}
-        addButton={addButton}
-    />
-
-export const BreakoutWidget = ({ className, index, breakout, query, updateQuery, addButton }: Object) =>
-    <BreakoutWidget_LEGACY
-        className={className}
-        field={breakout}
-        fieldOptions={query.breakoutOptions(breakout)}
-        customFieldOptions={query.expressions()}
-        tableMetadata={query.tableMetadata()}
-        setField={(field) => query.updateBreakout(index, field).update(updateQuery)}
-        addButton={addButton}
-    />
+export const AggregationWidget = ({
+  index,
+  aggregation,
+  query,
+  updateQuery,
+  addButton,
+}: Object) => (
+  <AggregationWidget_LEGACY
+    query={query}
+    aggregation={aggregation}
+    tableMetadata={query.tableMetadata()}
+    customFields={query.expressions()}
+    updateAggregation={aggregation =>
+      query.updateAggregation(index, aggregation).update(updateQuery)
+    }
+    removeAggregation={
+      query.canRemoveAggregation()
+        ? () => query.removeAggregation(index).update(updateQuery)
+        : null
+    }
+    addButton={addButton}
+  />
+);
+
+export const BreakoutWidget = ({
+  className,
+  index,
+  breakout,
+  query,
+  updateQuery,
+  addButton,
+}: Object) => (
+  <BreakoutWidget_LEGACY
+    className={className}
+    field={breakout}
+    fieldOptions={query.breakoutOptions(breakout)}
+    customFieldOptions={query.expressions()}
+    tableMetadata={query.tableMetadata()}
+    setField={field => query.updateBreakout(index, field).update(updateQuery)}
+    addButton={addButton}
+  />
+);
diff --git a/frontend/src/metabase/query_builder/components/LimitWidget.jsx b/frontend/src/metabase/query_builder/components/LimitWidget.jsx
index f6f40be07cc7a99a26fa00a67d4312d16fa8c635..64342da9eb30a58b56dc4a364e2bca174be7ae77 100644
--- a/frontend/src/metabase/query_builder/components/LimitWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/LimitWidget.jsx
@@ -1,28 +1,33 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class LimitWidget extends Component {
+  static propTypes = {
+    limit: PropTypes.number,
+    onChange: PropTypes.func.isRequired,
+  };
 
-    static propTypes = {
-        limit: PropTypes.number,
-        onChange: PropTypes.func.isRequired,
-    };
+  static defaultProps = {
+    options: [undefined, 1, 10, 25, 100, 1000],
+  };
 
-    static defaultProps = {
-        options: [undefined, 1, 10, 25, 100, 1000]
-    };
-
-    render() {
-        return (
-            <ul className="Button-group Button-group--blue">
-                {this.props.options.map(count =>
-                    <li key={count || "None"} className={cx("Button", { "Button--active":  count === this.props.limit })} onClick={() => this.props.onChange(count)}>
-                        {count || t`None`}
-                    </li>
-                )}
-            </ul>
-        );
-    }
+  render() {
+    return (
+      <ul className="Button-group Button-group--blue">
+        {this.props.options.map(count => (
+          <li
+            key={count || "None"}
+            className={cx("Button", {
+              "Button--active": count === this.props.limit,
+            })}
+            onClick={() => this.props.onChange(count)}
+          >
+            {count || t`None`}
+          </li>
+        ))}
+      </ul>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.css b/frontend/src/metabase/query_builder/components/NativeQueryEditor.css
index 9370e0b0e8a2263db12255d536da50eafdb61ffc..a20b288fc2b8b3bc1c3c8edc8f3cdaae5c3abed6 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.css
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.css
@@ -1,6 +1,5 @@
-
 .NativeQueryEditor .ace_editor {
-    height: 100%;
+  height: 100%;
 }
 
 .NativeQueryEditor .react-resizable {
@@ -16,23 +15,23 @@
 }
 
 .NativeQueryEditor .ace_editor.read-only .ace_cursor {
-    display: none;
+  display: none;
 }
 
 .NativeQueryEditor .ace_editor .ace_gutter-cell {
-    padding-top: 2px;
-    font-size: 10px;
-    font-weight: 700;
-    color: color(var(--base-grey) shade(30%));
-    padding-left: 0;
-    padding-right: 0;
-    display: block;
-    text-align: center;
+  padding-top: 2px;
+  font-size: 10px;
+  font-weight: 700;
+  color: color(var(--base-grey) shade(30%));
+  padding-left: 0;
+  padding-right: 0;
+  display: block;
+  text-align: center;
 }
 
 .NativeQueryEditor .ace_editor .ace_gutter {
-    background-color: #F9FBFC;
-    border-right: 1px solid var(--border-color);
-    padding-left: 5px;
-    padding-right: 5px;
+  background-color: #f9fbfc;
+  border-right: 1px solid var(--border-color);
+  padding-left: 5px;
+  padding-right: 5px;
 }
diff --git a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
index 68f8bf32ac0a8d61196bf10af5907e3e5884a369..c03cf80286d4f7b311216e26da680ee4326fad3c 100644
--- a/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
+++ b/frontend/src/metabase/query_builder/components/NativeQueryEditor.jsx
@@ -7,23 +7,23 @@ import ReactDOM from "react-dom";
 import "./NativeQueryEditor.css";
 
 // $FlowFixMe react-resizable causes Flow errors
-import { ResizableBox } from 'react-resizable';
+import { ResizableBox } from "react-resizable";
 
-import 'ace/ace';
-import 'ace/ext-language_tools';
+import "ace/ace";
+import "ace/ext-language_tools";
 
-import 'ace/mode-sql';
-import 'ace/mode-mysql';
-import 'ace/mode-pgsql';
-import 'ace/mode-sqlserver';
-import 'ace/mode-json';
+import "ace/mode-sql";
+import "ace/mode-mysql";
+import "ace/mode-pgsql";
+import "ace/mode-sqlserver";
+import "ace/mode-json";
 
-import 'ace/snippets/sql';
-import 'ace/snippets/mysql';
-import 'ace/snippets/pgsql';
-import 'ace/snippets/sqlserver';
-import 'ace/snippets/json';
-import { t } from 'c-3po';
+import "ace/snippets/sql";
+import "ace/snippets/mysql";
+import "ace/snippets/pgsql";
+import "ace/snippets/sqlserver";
+import "ace/snippets/json";
+import { t } from "c-3po";
 
 import { SQLBehaviour } from "metabase/lib/ace/sql_behaviour";
 
@@ -38,7 +38,7 @@ const LINE_HEIGHT = 16;
 const MIN_HEIGHT_LINES = 1;
 const MAX_AUTO_SIZE_LINES = 12;
 
-const getEditorLineHeight = (lines) => lines * LINE_HEIGHT + 2 * SCROLL_MARGIN;
+const getEditorLineHeight = lines => lines * LINE_HEIGHT + 2 * SCROLL_MARGIN;
 
 import Question from "metabase-lib/lib/Question";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
@@ -50,305 +50,339 @@ import type { ParameterId } from "metabase/meta/types/Parameter";
 import type { LocationDescriptor } from "metabase/meta/types";
 import type { RunQueryParams } from "metabase/query_builder/actions";
 import {
-    DatabaseDataSelector,
-    SchemaAndTableDataSelector,
+  DatabaseDataSelector,
+  SchemaAndTableDataSelector,
 } from "metabase/query_builder/components/DataSelector";
 
 type AutoCompleteResult = [string, string, string];
 type AceEditor = any; // TODO;
 
 type Props = {
-    location:               LocationDescriptor,
+  location: LocationDescriptor,
 
-    question:               Question,
-    query:                  NativeQuery,
+  question: Question,
+  query: NativeQuery,
 
-    runQuestionQuery:       (options?: RunQueryParams) => void,
-    setDatasetQuery:        (datasetQuery: DatasetQuery) => void,
+  runQuestionQuery: (options?: RunQueryParams) => void,
+  setDatasetQuery: (datasetQuery: DatasetQuery) => void,
 
-    setDatabaseFn:          (databaseId: DatabaseId) => void,
-    setParameterValue:      (parameterId: ParameterId, value: string) => void,
+  setDatabaseFn: (databaseId: DatabaseId) => void,
+  setParameterValue: (parameterId: ParameterId, value: string) => void,
 
-    autocompleteResultsFn:  (input: string) => Promise<AutoCompleteResult[]>
+  autocompleteResultsFn: (input: string) => Promise<AutoCompleteResult[]>,
 };
 type State = {
-    showEditor: boolean,
-    initialHeight: number
+  showEditor: boolean,
+  initialHeight: number,
 };
 
 export default class NativeQueryEditor extends Component {
-    props: Props;
-    state: State;
-
-    _editor: AceEditor;
-    _localUpdate: boolean = false;
-
-    constructor(props: Props) {
-        super(props);
-
-        const lines = Math.min(MAX_AUTO_SIZE_LINES, props.query && props.query.lineCount() || MAX_AUTO_SIZE_LINES);
-
-        this.state = {
-            showEditor: !props.question || !props.question.isSaved(),
-            initialHeight: getEditorLineHeight(lines)
-        };
-
-        // Ace sometimes fires mutliple "change" events in rapid succession
-        // e.x. https://github.com/metabase/metabase/issues/2801
-        // $FlowFixMe
-        this.onChange = _.debounce(this.onChange.bind(this), 1);
-    }
-
-    static defaultProps = {
-        isOpen: false
+  props: Props;
+  state: State;
+
+  _editor: AceEditor;
+  _localUpdate: boolean = false;
+
+  constructor(props: Props) {
+    super(props);
+
+    const lines = Math.min(
+      MAX_AUTO_SIZE_LINES,
+      (props.query && props.query.lineCount()) || MAX_AUTO_SIZE_LINES,
+    );
+
+    this.state = {
+      showEditor: !props.question || !props.question.isSaved(),
+      initialHeight: getEditorLineHeight(lines),
+    };
+
+    // Ace sometimes fires mutliple "change" events in rapid succession
+    // e.x. https://github.com/metabase/metabase/issues/2801
+    // $FlowFixMe
+    this.onChange = _.debounce(this.onChange.bind(this), 1);
+  }
+
+  static defaultProps = {
+    isOpen: false,
+  };
+
+  componentDidMount() {
+    this.loadAceEditor();
+    document.addEventListener("keydown", this.handleKeyDown);
+  }
+
+  componentDidUpdate() {
+    const { query } = this.props;
+    if (!query || !this._editor) {
+      return;
     }
 
-    componentDidMount() {
-        this.loadAceEditor();
-        document.addEventListener("keydown", this.handleKeyDown);
+    if (this._editor.getValue() !== query.queryText()) {
+      // This is a weird hack, but the purpose is to avoid an infinite loop caused by the fact that calling editor.setValue()
+      // will trigger the editor 'change' event, update the query, and cause another rendering loop which we don't want, so
+      // we need a way to update the editor without causing the onChange event to go through as well
+      this._localUpdate = true;
+      this._editor.setValue(query.queryText());
+      this._editor.clearSelection();
+      this._localUpdate = false;
     }
 
-    componentDidUpdate() {
-        const { query } = this.props;
-        if (!query || !this._editor) {
-            return;
-        }
-
-        if (this._editor.getValue() !== query.queryText()) {
-            // This is a weird hack, but the purpose is to avoid an infinite loop caused by the fact that calling editor.setValue()
-            // will trigger the editor 'change' event, update the query, and cause another rendering loop which we don't want, so
-            // we need a way to update the editor without causing the onChange event to go through as well
-            this._localUpdate = true;
-            this._editor.setValue(query.queryText());
-            this._editor.clearSelection();
-            this._localUpdate = false;
-        }
-
-        let editorElement = ReactDOM.findDOMNode(this.refs.editor);
-        if (query.hasWritePermission()) {
-            this._editor.setReadOnly(false);
-            editorElement.classList.remove("read-only");
-        } else {
-            this._editor.setReadOnly(true);
-            editorElement.classList.add("read-only");
-        }
-        const aceMode = query.aceMode();
-        if (this._editor.getSession().$modeId !== aceMode) {
-            this._editor.getSession().setMode(aceMode);
-            // monkey patch the mode to add our bracket/paren/braces-matching behavior
-            if (aceMode.indexOf("sql") >= 0) {
-                this._editor.getSession().$mode.$behaviour = new SQLBehaviour();
-            }
-        }
+    let editorElement = ReactDOM.findDOMNode(this.refs.editor);
+    if (query.hasWritePermission()) {
+      this._editor.setReadOnly(false);
+      editorElement.classList.remove("read-only");
+    } else {
+      this._editor.setReadOnly(true);
+      editorElement.classList.add("read-only");
     }
-
-    componentWillUnmount() {
-        document.removeEventListener("keydown", this.handleKeyDown);
+    const aceMode = query.aceMode();
+    if (this._editor.getSession().$modeId !== aceMode) {
+      this._editor.getSession().setMode(aceMode);
+      // monkey patch the mode to add our bracket/paren/braces-matching behavior
+      if (aceMode.indexOf("sql") >= 0) {
+        this._editor.getSession().$mode.$behaviour = new SQLBehaviour();
+      }
     }
-
-    handleKeyDown = (e: KeyboardEvent) => {
-        const { query, runQuestionQuery } = this.props;
-
-        const ENTER_KEY = 13;
-        if (e.keyCode === ENTER_KEY && (e.metaKey || e.ctrlKey) && query.canRun()) {
-            const { query } = this.props;
-            if (e.altKey) {
-                // run just the selected text, if any
-                const selectedText = this._editor.getSelectedText();
-                if (selectedText) {
-                    const temporaryCard = query.updateQueryText(selectedText).question().card();
-                    runQuestionQuery({ overrideWithCard: temporaryCard, shouldUpdateUrl: false });
-                }
-            } else {
-                runQuestionQuery();
-            }
+  }
+
+  componentWillUnmount() {
+    document.removeEventListener("keydown", this.handleKeyDown);
+  }
+
+  handleKeyDown = (e: KeyboardEvent) => {
+    const { query, runQuestionQuery } = this.props;
+
+    const ENTER_KEY = 13;
+    if (e.keyCode === ENTER_KEY && (e.metaKey || e.ctrlKey) && query.canRun()) {
+      const { query } = this.props;
+      if (e.altKey) {
+        // run just the selected text, if any
+        const selectedText = this._editor.getSelectedText();
+        if (selectedText) {
+          const temporaryCard = query
+            .updateQueryText(selectedText)
+            .question()
+            .card();
+          runQuestionQuery({
+            overrideWithCard: temporaryCard,
+            shouldUpdateUrl: false,
+          });
         }
+      } else {
+        runQuestionQuery();
+      }
     }
-
-    loadAceEditor() {
-        const { query } = this.props;
-
-        let editorElement = ReactDOM.findDOMNode(this.refs.editor);
-        // $FlowFixMe
-        this._editor = ace.edit(editorElement);
-
-        // listen to onChange events
-        this._editor.getSession().on('change', this.onChange);
-
-        // initialize the content
-        this._editor.setValue(query ? query.queryText() : "");
-
-        this._editor.renderer.setScrollMargin(SCROLL_MARGIN, SCROLL_MARGIN);
-
-        // clear the editor selection, otherwise we start with the whole editor selected
-        this._editor.clearSelection();
-
-        // hmmm, this could be dangerous
-        this._editor.focus();
-
-        let aceLanguageTools = ace.require('ace/ext/language_tools');
-        this._editor.setOptions({
-            enableBasicAutocompletion: true,
-            enableSnippets: true,
-            enableLiveAutocompletion: true,
-            showPrintMargin: false,
-            highlightActiveLine: false,
-            highlightGutterLine: false,
-            showLineNumbers: true
-        });
-
-        aceLanguageTools.addCompleter({
-            getCompletions: async (editor, session, pos, prefix, callback) => {
-                try {
-                    // HACK: call this.props.autocompleteResultsFn rather than caching the prop since it might change
-                    let results = await this.props.autocompleteResultsFn(prefix);
-                    // transform results of the API call into what ACE expects
-                    let js_results = results.map(function(result) {
-                        return {
-                            name: result[0],
-                            value: result[0],
-                            meta: result[1]
-                        };
-                    });
-                    callback(null, js_results);
-                } catch (error) {
-                    console.log('error getting autocompletion data', error);
-                    callback(null, []);
-                }
-            }
-        });
-    }
-
-    _updateSize() {
-         const doc = this._editor.getSession().getDocument();
-         const element = ReactDOM.findDOMNode(this.refs.resizeBox);
-         const newHeight = getEditorLineHeight(doc.getLength());
-         if (newHeight > element.offsetHeight && newHeight <= getEditorLineHeight(MAX_AUTO_SIZE_LINES)) {
-             element.style.height = newHeight + "px";
-             this._editor.resize();
-         }
-     }
-
-    onChange() {
-        const { query } = this.props;
-        if (this._editor && !this._localUpdate) {
-            this._updateSize();
-            if (query.queryText() !== this._editor.getValue()) {
-                query.updateQueryText(this._editor.getValue()).update(this.props.setDatasetQuery);
-            }
+  };
+
+  loadAceEditor() {
+    const { query } = this.props;
+
+    let editorElement = ReactDOM.findDOMNode(this.refs.editor);
+    // $FlowFixMe
+    this._editor = ace.edit(editorElement);
+
+    // listen to onChange events
+    this._editor.getSession().on("change", this.onChange);
+
+    // initialize the content
+    this._editor.setValue(query ? query.queryText() : "");
+
+    this._editor.renderer.setScrollMargin(SCROLL_MARGIN, SCROLL_MARGIN);
+
+    // clear the editor selection, otherwise we start with the whole editor selected
+    this._editor.clearSelection();
+
+    // hmmm, this could be dangerous
+    this._editor.focus();
+
+    let aceLanguageTools = ace.require("ace/ext/language_tools");
+    this._editor.setOptions({
+      enableBasicAutocompletion: true,
+      enableSnippets: true,
+      enableLiveAutocompletion: true,
+      showPrintMargin: false,
+      highlightActiveLine: false,
+      highlightGutterLine: false,
+      showLineNumbers: true,
+    });
+
+    aceLanguageTools.addCompleter({
+      getCompletions: async (editor, session, pos, prefix, callback) => {
+        try {
+          // HACK: call this.props.autocompleteResultsFn rather than caching the prop since it might change
+          let results = await this.props.autocompleteResultsFn(prefix);
+          // transform results of the API call into what ACE expects
+          let js_results = results.map(function(result) {
+            return {
+              name: result[0],
+              value: result[0],
+              meta: result[1],
+            };
+          });
+          callback(null, js_results);
+        } catch (error) {
+          console.log("error getting autocompletion data", error);
+          callback(null, []);
         }
+      },
+    });
+  }
+
+  _updateSize() {
+    const doc = this._editor.getSession().getDocument();
+    const element = ReactDOM.findDOMNode(this.refs.resizeBox);
+    const newHeight = getEditorLineHeight(doc.getLength());
+    if (
+      newHeight > element.offsetHeight &&
+      newHeight <= getEditorLineHeight(MAX_AUTO_SIZE_LINES)
+    ) {
+      element.style.height = newHeight + "px";
+      this._editor.resize();
     }
-
-    toggleEditor = () => {
-        this.setState({ showEditor: !this.state.showEditor })
+  }
+
+  onChange() {
+    const { query } = this.props;
+    if (this._editor && !this._localUpdate) {
+      this._updateSize();
+      if (query.queryText() !== this._editor.getValue()) {
+        query
+          .updateQueryText(this._editor.getValue())
+          .update(this.props.setDatasetQuery);
+      }
     }
-
-    /// Change the Database we're currently editing a query for.
-    setDatabaseId = (databaseId: DatabaseId) => {
-        // TODO: use metabase-lib
-        this.props.setDatabaseFn(databaseId);
+  }
+
+  toggleEditor = () => {
+    this.setState({ showEditor: !this.state.showEditor });
+  };
+
+  /// Change the Database we're currently editing a query for.
+  setDatabaseId = (databaseId: DatabaseId) => {
+    // TODO: use metabase-lib
+    this.props.setDatabaseFn(databaseId);
+  };
+
+  setTableId = (tableId: TableId) => {
+    // TODO: push more of this into metabase-lib?
+    const { query } = this.props;
+    const table = query._metadata.tables[tableId];
+    if (table && table.name !== query.collection()) {
+      query.updateCollection(table.name).update(this.props.setDatasetQuery);
     }
-
-    setTableId = (tableId: TableId) => {
-        // TODO: push more of this into metabase-lib?
-        const { query } = this.props;
-        const table = query._metadata.tables[tableId];
-        if (table && table.name !== query.collection()) {
-            query.updateCollection(table.name).update(this.props.setDatasetQuery);
-        }
+  };
+
+  render() {
+    const { query, setParameterValue, location } = this.props;
+    const database = query.database();
+    const databases = query.databases();
+    const parameters = query.question().parameters();
+
+    let dataSelectors = [];
+    if (this.state.showEditor && databases.length > 0) {
+      // we only render a db selector if there are actually multiple to choose from
+      if (
+        databases.length > 1 &&
+        (database == null || _.any(databases, db => db.id === database.id))
+      ) {
+        dataSelectors.push(
+          <div
+            key="db_selector"
+            className="GuiBuilder-section GuiBuilder-data flex align-center"
+          >
+            <span className="GuiBuilder-section-label Query-label">{t`Database`}</span>
+            <DatabaseDataSelector
+              databases={databases}
+              selectedDatabaseId={database && database.id}
+              setDatabaseFn={this.setDatabaseId}
+              isInitiallyOpen={database == null}
+            />
+          </div>,
+        );
+      } else if (database) {
+        dataSelectors.push(
+          <span key="db" className="p2 text-bold text-grey">
+            {database.name}
+          </span>,
+        );
+      }
+      if (query.requiresTable()) {
+        const selectedTable = query.table();
+        const tables = query.tables() || [];
+
+        dataSelectors.push(
+          <div
+            key="table_selector"
+            className="GuiBuilder-section GuiBuilder-data flex align-center"
+          >
+            <span className="GuiBuilder-section-label Query-label">{t`Table`}</span>
+            <SchemaAndTableDataSelector
+              selectedTableId={selectedTable ? selectedTable.id : null}
+              selectedDatabaseId={database && database.id}
+              databases={[database]}
+              tables={tables}
+              setSourceTableFn={this.setTableId}
+              isInitiallyOpen={false}
+            />
+          </div>,
+        );
+      }
+    } else {
+      dataSelectors = (
+        <span className="p2 text-grey-4">{t`This question is written in ${query.nativeQueryLanguage()}.`}</span>
+      );
     }
 
-    render() {
-        const { query, setParameterValue, location } = this.props;
-        const database = query.database();
-        const databases = query.databases();
-        const parameters = query.question().parameters();
-
-        let dataSelectors = [];
-        if (this.state.showEditor && databases.length > 0) {
-            // we only render a db selector if there are actually multiple to choose from
-            if (databases.length > 1 && (database == null || _.any(databases, (db) => db.id === database.id))) {
-                dataSelectors.push(
-                    <div key="db_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
-                        <span className="GuiBuilder-section-label Query-label">{t`Database`}</span>
-                        <DatabaseDataSelector
-                            databases={databases}
-                            selectedDatabaseId={database && database.id}
-                            setDatabaseFn={this.setDatabaseId}
-                            isInitiallyOpen={database == null}
-                        />
-                    </div>
-                )
-            } else if (database) {
-                dataSelectors.push(
-                    <span key="db" className="p2 text-bold text-grey">{database.name}</span>
-                );
-            }
-            if (query.requiresTable()) {
-                const selectedTable    = query.table();
-                const tables           = query.tables() || [];
-
-                dataSelectors.push(
-                    <div key="table_selector" className="GuiBuilder-section GuiBuilder-data flex align-center">
-                        <span className="GuiBuilder-section-label Query-label">{t`Table`}</span>
-                        <SchemaAndTableDataSelector
-                            selectedTableId={selectedTable ? selectedTable.id : null}
-                            selectedDatabaseId={database && database.id}
-                            databases={[database]}
-                            tables={tables}
-                            setSourceTableFn={this.setTableId}
-                            isInitiallyOpen={false}
-                        />
-                    </div>
-                );
-            }
-        } else {
-            dataSelectors = <span className="p2 text-grey-4">{t`This question is written in ${query.nativeQueryLanguage()}.`}</span>;
-        }
-
-        let editorClasses, toggleEditorText, toggleEditorIcon;
-        if (this.state.showEditor) {
-            editorClasses = "";
-            toggleEditorText = query.hasWritePermission() ? t`Hide Editor` : t`Hide Query`;
-            toggleEditorIcon = "contract";
-        } else {
-            editorClasses = "hide";
-            toggleEditorText = query.hasWritePermission() ? t`Open Editor` : t`Show Query`;
-            toggleEditorIcon = "expand";
-        }
-
-        return (
-            <div className="wrapper">
-                <div className="NativeQueryEditor bordered rounded shadowed">
-                    <div className="flex align-center" style={{ minHeight: 50 }}>
-                        {dataSelectors}
-                        <Parameters
-                            parameters={parameters}
-                            query={location.query}
-                            setParameterValue={setParameterValue}
-                            syncQueryString
-                            isQB
-                            commitImmediately
-                        />
-                        <a className="Query-label no-decoration flex-align-right flex align-center px2" onClick={this.toggleEditor}>
-                            <span className="mx2">{toggleEditorText}</span>
-                            <Icon name={toggleEditorIcon} size={20}/>
-                        </a>
-                    </div>
-                    <ResizableBox
-                        ref="resizeBox"
-                        className={"border-top " + editorClasses}
-                        height={this.state.initialHeight}
-                        minConstraints={[Infinity, getEditorLineHeight(MIN_HEIGHT_LINES)]}
-                        axis="y"
-                        onResizeStop={(e, data) => {
-                            this._editor.resize();
-                        }}
-                    >
-                        <div id="id_sql" ref="editor"></div>
-                    </ResizableBox>
-                </div>
-            </div>
-        );
+    let editorClasses, toggleEditorText, toggleEditorIcon;
+    if (this.state.showEditor) {
+      editorClasses = "";
+      toggleEditorText = query.hasWritePermission()
+        ? t`Hide Editor`
+        : t`Hide Query`;
+      toggleEditorIcon = "contract";
+    } else {
+      editorClasses = "hide";
+      toggleEditorText = query.hasWritePermission()
+        ? t`Open Editor`
+        : t`Show Query`;
+      toggleEditorIcon = "expand";
     }
+
+    return (
+      <div className="wrapper">
+        <div className="NativeQueryEditor bordered rounded shadowed">
+          <div className="flex align-center" style={{ minHeight: 50 }}>
+            {dataSelectors}
+            <Parameters
+              parameters={parameters}
+              query={location.query}
+              setParameterValue={setParameterValue}
+              syncQueryString
+              isQB
+              commitImmediately
+            />
+            <a
+              className="Query-label no-decoration flex-align-right flex align-center px2"
+              onClick={this.toggleEditor}
+            >
+              <span className="mx2">{toggleEditorText}</span>
+              <Icon name={toggleEditorIcon} size={20} />
+            </a>
+          </div>
+          <ResizableBox
+            ref="resizeBox"
+            className={"border-top " + editorClasses}
+            height={this.state.initialHeight}
+            minConstraints={[Infinity, getEditorLineHeight(MIN_HEIGHT_LINES)]}
+            axis="y"
+            onResizeStop={(e, data) => {
+              this._editor.resize();
+            }}
+          >
+            <div id="id_sql" ref="editor" />
+          </ResizableBox>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
index 6214ff691f9edfbb398ba934150fa65190206f7c..2dd155ae0e2fcec09c063700db535f8e6be9d6c1 100644
--- a/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDefinitionTooltip.jsx
@@ -6,48 +6,45 @@ import AggregationWidget from "./AggregationWidget.jsx";
 import FieldSet from "metabase/components/FieldSet.jsx";
 
 import Query from "metabase/lib/query";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class QueryDefinitionTooltip extends Component {
+  static propTypes = {
+    type: PropTypes.string,
+    object: PropTypes.object.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+  };
 
-    static propTypes = {
-        type: PropTypes.string,
-        object: PropTypes.object.isRequired,
-        tableMetadata: PropTypes.object.isRequired
-    };
+  render() {
+    const { type, object, tableMetadata } = this.props;
 
-    render() {
-        const { type, object, tableMetadata } = this.props;
-
-        return (
-            <div className="p2" style={{width: 250}}>
-                <div>
-                    { type && type === "metric" && !object.is_active ?
-                        t`This metric has been retired.  It's no longer available for use.`
-                    :
-                        object.description
-                    }
-                </div>
-                { object.definition &&
-                    <div className="mt2">
-                        <FieldSet legend={t`Definition`} className="border-light">
-                            <div className="TooltipFilterList">
-                                { Query.getAggregations(object.definition).map(aggregation =>
-                                    <AggregationWidget
-                                        aggregation={aggregation}
-                                        tableMetadata={tableMetadata}
-                                    />
-                                )}
-                                <FilterList
-                                    filters={Query.getFilters(object.definition)}
-                                    tableMetadata={tableMetadata}
-                                    maxDisplayValues={Infinity}
-                                />
-                            </div>
-                        </FieldSet>
-                    </div>
-                }
-            </div>
-        );
-    }
+    return (
+      <div className="p2" style={{ width: 250 }}>
+        <div>
+          {type && type === "metric" && !object.is_active
+            ? t`This metric has been retired.  It's no longer available for use.`
+            : object.description}
+        </div>
+        {object.definition && (
+          <div className="mt2">
+            <FieldSet legend={t`Definition`} className="border-light">
+              <div className="TooltipFilterList">
+                {Query.getAggregations(object.definition).map(aggregation => (
+                  <AggregationWidget
+                    aggregation={aggregation}
+                    tableMetadata={tableMetadata}
+                  />
+                ))}
+                <FilterList
+                  filters={Query.getFilters(object.definition)}
+                  tableMetadata={tableMetadata}
+                  maxDisplayValues={Infinity}
+                />
+              </div>
+            </FieldSet>
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
index 359e48ec1d18fd343d72a456163a5db2bac11300..aeed7f18dc541ea6487bca45792a9aff6d4d9448 100644
--- a/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryDownloadWidget.jsx
@@ -1,7 +1,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { parse as urlParse } from "url";
 import querystring from "querystring";
 
@@ -19,98 +19,139 @@ import cx from "classnames";
 
 const EXPORT_FORMATS = ["csv", "xlsx", "json"];
 
-const QueryDownloadWidget = ({ className, card, result, uuid, token }) =>
-    <PopoverWithTrigger
-        triggerElement={
-            <Tooltip tooltip={t`Download full results`}>
-                <Icon title={t`Download this data`} name="downarrow" size={16} />
-            </Tooltip>
-        }
-        triggerClasses={cx(className, "text-brand-hover")}
-    >
-        <div className="p2" style={{ maxWidth: 320 }}>
-            <h4>{t`Download full results`}</h4>
-            { result.data.rows_truncated != null &&
-                <FieldSet className="my2 text-gold border-gold" legend={t`Warning`}>
-                    <div className="my1">{t`Your answer has a large number of rows so it could take a while to download.`}</div>
-                    <div>{t`The maximum download size is 1 million rows.`}</div>
-                </FieldSet>
-            }
-            <div className="flex flex-row mt2">
-                {EXPORT_FORMATS.map(type =>
-                    uuid ?
-                        <PublicQueryButton key={type} type={type} uuid={uuid} result={result} className="mr1 text-uppercase text-default" />
-                    : token ?
-                        <EmbedQueryButton key={type} type={type} token={token} className="mr1 text-uppercase text-default" />
-                    : card && card.id ?
-                        <SavedQueryButton key={type} type={type} card={card} result={result} className="mr1 text-uppercase text-default" />
-                    : card && !card.id ?
-                        <UnsavedQueryButton key={type} type={type} card={card} result={result} className="mr1 text-uppercase text-default" />
-                    :
-                      null
-                )}
-            </div>
-        </div>
-    </PopoverWithTrigger>
+const QueryDownloadWidget = ({ className, card, result, uuid, token }) => (
+  <PopoverWithTrigger
+    triggerElement={
+      <Tooltip tooltip={t`Download full results`}>
+        <Icon title={t`Download this data`} name="downarrow" size={16} />
+      </Tooltip>
+    }
+    triggerClasses={cx(className, "text-brand-hover")}
+  >
+    <div className="p2" style={{ maxWidth: 320 }}>
+      <h4>{t`Download full results`}</h4>
+      {result.data.rows_truncated != null && (
+        <FieldSet className="my2 text-gold border-gold" legend={t`Warning`}>
+          <div className="my1">{t`Your answer has a large number of rows so it could take a while to download.`}</div>
+          <div>{t`The maximum download size is 1 million rows.`}</div>
+        </FieldSet>
+      )}
+      <div className="flex flex-row mt2">
+        {EXPORT_FORMATS.map(
+          type =>
+            uuid ? (
+              <PublicQueryButton
+                key={type}
+                type={type}
+                uuid={uuid}
+                result={result}
+                className="mr1 text-uppercase text-default"
+              />
+            ) : token ? (
+              <EmbedQueryButton
+                key={type}
+                type={type}
+                token={token}
+                className="mr1 text-uppercase text-default"
+              />
+            ) : card && card.id ? (
+              <SavedQueryButton
+                key={type}
+                type={type}
+                card={card}
+                result={result}
+                className="mr1 text-uppercase text-default"
+              />
+            ) : card && !card.id ? (
+              <UnsavedQueryButton
+                key={type}
+                type={type}
+                card={card}
+                result={result}
+                className="mr1 text-uppercase text-default"
+              />
+            ) : null,
+        )}
+      </div>
+    </div>
+  </PopoverWithTrigger>
+);
 
-const UnsavedQueryButton = ({ className, type, result: { json_query }, card }) =>
-    <DownloadButton
-        className={className}
-        url={`api/dataset/${type}`}
-        params={{ query: JSON.stringify(_.omit(json_query, "constraints")) }}
-        extensions={[type]}
-    >
-        {type}
-    </DownloadButton>
+const UnsavedQueryButton = ({
+  className,
+  type,
+  result: { json_query },
+  card,
+}) => (
+  <DownloadButton
+    className={className}
+    url={`api/dataset/${type}`}
+    params={{ query: JSON.stringify(_.omit(json_query, "constraints")) }}
+    extensions={[type]}
+  >
+    {type}
+  </DownloadButton>
+);
 
-const SavedQueryButton = ({ className, type, result: { json_query }, card }) =>
-    <DownloadButton
-        className={className}
-        url={`api/card/${card.id}/query/${type}`}
-        params={{ parameters: JSON.stringify(json_query.parameters) }}
-        extensions={[type]}
-    >
-        {type}
-    </DownloadButton>
-
-const PublicQueryButton = ({ className, type, uuid, result: { json_query }}) =>
-    <DownloadButton
-        className={className}
-        method="GET"
-        url={Urls.publicCard(uuid, type)}
-        params={{ parameters: JSON.stringify(json_query.parameters) }}
-        extensions={[type]}
-    >
-        {type}
-    </DownloadButton>
+const SavedQueryButton = ({
+  className,
+  type,
+  result: { json_query },
+  card,
+}) => (
+  <DownloadButton
+    className={className}
+    url={`api/card/${card.id}/query/${type}`}
+    params={{ parameters: JSON.stringify(json_query.parameters) }}
+    extensions={[type]}
+  >
+    {type}
+  </DownloadButton>
+);
 
+const PublicQueryButton = ({
+  className,
+  type,
+  uuid,
+  result: { json_query },
+}) => (
+  <DownloadButton
+    className={className}
+    method="GET"
+    url={Urls.publicCard(uuid, type)}
+    params={{ parameters: JSON.stringify(json_query.parameters) }}
+    extensions={[type]}
+  >
+    {type}
+  </DownloadButton>
+);
 
 const EmbedQueryButton = ({ className, type, token }) => {
-    // Parse the query string part of the URL (e.g. the `?key=value` part) into an object. We need to pass them this
-    // way to the `DownloadButton` because it's a form which means we need to insert a hidden `<input>` for each param
-    // we want to pass along. For whatever wacky reason the /api/embed endpoint expect params like ?key=value instead
-    // of like ?params=<json-encoded-params-array> like the other endpoints do.
-    const query  = urlParse(window.location.href).query; // get the part of the URL that looks like key=value
-    const params = query && querystring.parse(query);    // expand them out into a map
+  // Parse the query string part of the URL (e.g. the `?key=value` part) into an object. We need to pass them this
+  // way to the `DownloadButton` because it's a form which means we need to insert a hidden `<input>` for each param
+  // we want to pass along. For whatever wacky reason the /api/embed endpoint expect params like ?key=value instead
+  // of like ?params=<json-encoded-params-array> like the other endpoints do.
+  const query = urlParse(window.location.href).query; // get the part of the URL that looks like key=value
+  const params = query && querystring.parse(query); // expand them out into a map
 
-    return (
-        <DownloadButton
-            className={className}
-            method="GET"
-            url={Urls.embedCard(token, type)}
-            params={params}
-            extensions={[type]}
-        >
-            {type}
-        </DownloadButton>
-    );
-}
+  return (
+    <DownloadButton
+      className={className}
+      method="GET"
+      url={Urls.embedCard(token, type)}
+      params={params}
+      extensions={[type]}
+    >
+      {type}
+    </DownloadButton>
+  );
+};
 
 QueryDownloadWidget.propTypes = {
-    className: PropTypes.string,
-    card: PropTypes.object,
-    result: PropTypes.object,
-    uuid: PropTypes.string
+  className: PropTypes.string,
+  card: PropTypes.object,
+  result: PropTypes.object,
+  uuid: PropTypes.string,
 };
 
 export default QueryDownloadWidget;
diff --git a/frontend/src/metabase/query_builder/components/QueryHeader.jsx b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
index ceeb7204d7676f162a5b2b44881d00d7014afc00..834879cfbb314d8f6cea99baa45158859cc4b12d 100644
--- a/frontend/src/metabase/query_builder/components/QueryHeader.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryHeader.jsx
@@ -2,23 +2,23 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import QueryModeButton from "./QueryModeButton.jsx";
 
-import ActionButton from 'metabase/components/ActionButton.jsx';
-import AddToDashSelectDashModal from 'metabase/containers/AddToDashSelectDashModal.jsx';
+import ActionButton from "metabase/components/ActionButton.jsx";
+import AddToDashSelectDashModal from "metabase/containers/AddToDashSelectDashModal.jsx";
 import ButtonBar from "metabase/components/ButtonBar.jsx";
 import HeaderBar from "metabase/components/HeaderBar.jsx";
 import HistoryModal from "metabase/components/HistoryModal.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Modal from "metabase/components/Modal.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
-import QuestionSavedModal from 'metabase/components/QuestionSavedModal.jsx';
+import QuestionSavedModal from "metabase/components/QuestionSavedModal.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import MoveToCollection from "metabase/questions/containers/MoveToCollection.jsx";
-import ArchiveQuestionModal from "metabase/query_builder/containers/ArchiveQuestionModal"
+import ArchiveQuestionModal from "metabase/query_builder/containers/ArchiveQuestionModal";
 
-import SaveQuestionModal from 'metabase/containers/SaveQuestionModal.jsx';
+import SaveQuestionModal from "metabase/containers/SaveQuestionModal.jsx";
 
 import { clearRequestState } from "metabase/redux/requests";
 
@@ -33,472 +33,596 @@ import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import EntityMenu from "metabase/components/EntityMenu";
 import { CreateAlertModalContent } from "metabase/query_builder/components/AlertModals";
 import { AlertListPopoverContent } from "metabase/query_builder/components/AlertListPopoverContent";
-import { getQuestionAlerts, getVisualizationSettings } from "metabase/query_builder/selectors";
+import {
+  getQuestionAlerts,
+  getVisualizationSettings,
+} from "metabase/query_builder/selectors";
 import { getUser } from "metabase/home/selectors";
 import { fetchAlertsForQuestion } from "metabase/alert/alert";
 
 const mapStateToProps = (state, props) => ({
-    questionAlerts: getQuestionAlerts(state),
-    visualizationSettings: getVisualizationSettings(state),
-    user: getUser(state)
-})
+  questionAlerts: getQuestionAlerts(state),
+  visualizationSettings: getVisualizationSettings(state),
+  user: getUser(state),
+});
 
 const mapDispatchToProps = {
-    fetchAlertsForQuestion,
-    clearRequestState
+  fetchAlertsForQuestion,
+  clearRequestState,
 };
-const ICON_SIZE = 16
+const ICON_SIZE = 16;
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class QueryHeader extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            recentlySaved: null,
-            modal: null,
-            revisions: null
-        };
-
-        _.bindAll(this, "resetStateOnTimeout",
-            "onCreate", "onSave", "onBeginEditing", "onCancel", "onDelete",
-            "onFollowBreadcrumb", "onToggleDataReference",
-            "onFetchRevisions", "onRevertToRevision", "onRevertedRevision"
-        );
+  constructor(props, context) {
+    super(props, context);
+
+    this.state = {
+      recentlySaved: null,
+      modal: null,
+      revisions: null,
+    };
+
+    _.bindAll(
+      this,
+      "resetStateOnTimeout",
+      "onCreate",
+      "onSave",
+      "onBeginEditing",
+      "onCancel",
+      "onDelete",
+      "onFollowBreadcrumb",
+      "onToggleDataReference",
+      "onFetchRevisions",
+      "onRevertToRevision",
+      "onRevertedRevision",
+    );
+  }
+
+  static propTypes = {
+    question: PropTypes.object.isRequired,
+    card: PropTypes.object.isRequired,
+    originalCard: PropTypes.object,
+    isEditing: PropTypes.bool.isRequired,
+    tableMetadata: PropTypes.object, // can't be required, sometimes null
+    onSetCardAttribute: PropTypes.func.isRequired,
+    reloadCardFn: PropTypes.func.isRequired,
+    setQueryModeFn: PropTypes.func.isRequired,
+    isShowingDataReference: PropTypes.bool.isRequired,
+    toggleDataReferenceFn: PropTypes.func.isRequired,
+    isNew: PropTypes.bool.isRequired,
+    isDirty: PropTypes.bool.isRequired,
+  };
+
+  componentWillUnmount() {
+    clearTimeout(this.timeout);
+  }
+
+  resetStateOnTimeout() {
+    // clear any previously set timeouts then start a new one
+    clearTimeout(this.timeout);
+    this.timeout = setTimeout(
+      () => this.setState({ recentlySaved: null }),
+      5000,
+    );
+  }
+
+  onCreate = async (card, showSavedModal = true) => {
+    const { question, apiCreateQuestion } = this.props;
+    const questionWithUpdatedCard = question.setCard(card);
+    await apiCreateQuestion(questionWithUpdatedCard);
+
+    this.setState(
+      {
+        recentlySaved: "created",
+        ...(showSavedModal ? { modal: "saved" } : {}),
+      },
+      this.resetStateOnTimeout,
+    );
+  };
+
+  onSave = async (card, showSavedModal = true) => {
+    const { question, apiUpdateQuestion } = this.props;
+    const questionWithUpdatedCard = question.setCard(card);
+    await apiUpdateQuestion(questionWithUpdatedCard);
+
+    if (this.props.fromUrl) {
+      this.onGoBack();
+      return;
     }
 
-    static propTypes = {
-        question: PropTypes.object.isRequired,
-        card: PropTypes.object.isRequired,
-        originalCard: PropTypes.object,
-        isEditing: PropTypes.bool.isRequired,
-        tableMetadata: PropTypes.object, // can't be required, sometimes null
-        onSetCardAttribute: PropTypes.func.isRequired,
-        reloadCardFn: PropTypes.func.isRequired,
-        setQueryModeFn: PropTypes.func.isRequired,
-        isShowingDataReference: PropTypes.bool.isRequired,
-        toggleDataReferenceFn: PropTypes.func.isRequired,
-        isNew: PropTypes.bool.isRequired,
-        isDirty: PropTypes.bool.isRequired
+    this.setState(
+      {
+        recentlySaved: "updated",
+        ...(showSavedModal ? { modal: "saved" } : {}),
+      },
+      this.resetStateOnTimeout,
+    );
+  };
+
+  onBeginEditing() {
+    this.props.onBeginEditing();
+  }
+
+  async onCancel() {
+    if (this.props.fromUrl) {
+      this.onGoBack();
+    } else {
+      this.props.onCancelEditing();
     }
-
-    componentWillUnmount() {
-        clearTimeout(this.timeout);
-    }
-
-    resetStateOnTimeout() {
-        // clear any previously set timeouts then start a new one
-        clearTimeout(this.timeout);
-        this.timeout = setTimeout(() =>
-            this.setState({ recentlySaved: null })
-        , 5000);
+  }
+
+  async onDelete() {
+    // TODO: reduxify
+    await CardApi.delete({ cardId: this.props.card.id });
+    this.onGoBack();
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Delete");
+  }
+
+  onFollowBreadcrumb() {
+    this.props.onRestoreOriginalQuery();
+  }
+
+  onToggleDataReference() {
+    this.props.toggleDataReferenceFn();
+  }
+
+  onGoBack() {
+    this.props.onChangeLocation(this.props.fromUrl || "/");
+  }
+
+  async onFetchRevisions({ entity, id }) {
+    // TODO: reduxify
+    var revisions = await RevisionApi.list({ entity, id });
+    this.setState({ revisions });
+  }
+
+  onRevertToRevision({ entity, id, revision_id }) {
+    // TODO: reduxify
+    return RevisionApi.revert({ entity, id, revision_id });
+  }
+
+  onRevertedRevision() {
+    this.props.reloadCardFn();
+    this.refs.cardHistory.toggle();
+  }
+
+  getHeaderButtons() {
+    const {
+      question,
+      questionAlerts,
+      visualizationSettings,
+      card,
+      isNew,
+      isDirty,
+      isEditing,
+      tableMetadata,
+      databases,
+    } = this.props;
+    const database = _.findWhere(databases, {
+      id: card && card.dataset_query && card.dataset_query.database,
+    });
+
+    var buttonSections = [];
+
+    // A card that is either completely new or it has been derived from a saved question
+    if (isNew && isDirty) {
+      buttonSections.push([
+        <ModalWithTrigger
+          form
+          key="save"
+          ref="saveModal"
+          triggerClasses="h4 text-grey-4 text-brand-hover text-uppercase"
+          triggerElement="Save"
+        >
+          <SaveQuestionModal
+            card={this.props.card}
+            originalCard={this.props.originalCard}
+            tableMetadata={this.props.tableMetadata}
+            // if saving modified question, don't show "add to dashboard" modal
+            saveFn={card => this.onSave(card, false)}
+            createFn={this.onCreate}
+            onClose={() => this.refs.saveModal.toggle()}
+          />
+        </ModalWithTrigger>,
+      ]);
     }
 
-    onCreate = async (card, showSavedModal = true) => {
-        const { question, apiCreateQuestion } = this.props
-        const questionWithUpdatedCard = question.setCard(card)
-        await apiCreateQuestion(questionWithUpdatedCard)
-
-        this.setState({
-            recentlySaved: "created",
-            ...(showSavedModal ? { modal: "saved" } : {})
-        }, this.resetStateOnTimeout);
-    }
-
-    onSave = async (card, showSavedModal = true) => {
-        const { question, apiUpdateQuestion } = this.props
-        const questionWithUpdatedCard = question.setCard(card)
-        await apiUpdateQuestion(questionWithUpdatedCard)
-
-        if (this.props.fromUrl) {
-            this.onGoBack();
-            return;
-        }
-
-        this.setState({
-            recentlySaved: "updated",
-            ...(showSavedModal ? { modal: "saved" } : {})
-        }, this.resetStateOnTimeout);
-    }
-
-    onBeginEditing() {
-        this.props.onBeginEditing();
-    }
-
-    async onCancel() {
-        if (this.props.fromUrl) {
-            this.onGoBack();
+    // persistence buttons on saved cards
+    if (!isNew && card.can_write) {
+      if (!isEditing) {
+        if (this.state.recentlySaved) {
+          // existing card + not editing + recently saved = save confirmation
+          buttonSections.push([
+            <button
+              key="recentlySaved"
+              className="cursor-pointer bg-white text-success text-bold text-uppercase"
+            >
+              <span>
+                <Icon name="check" size={12} />
+                <span className="ml1">{t`Saved`}</span>
+              </span>
+            </button>,
+          ]);
         } else {
-            this.props.onCancelEditing();
-        }
-    }
-
-    async onDelete() {
-        // TODO: reduxify
-        await CardApi.delete({ 'cardId': this.props.card.id });
-        this.onGoBack();
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Delete");
-    }
-
-    onFollowBreadcrumb() {
-        this.props.onRestoreOriginalQuery();
-    }
-
-    onToggleDataReference() {
-        this.props.toggleDataReferenceFn();
-    }
-
-    onGoBack() {
-        this.props.onChangeLocation(this.props.fromUrl || "/");
-    }
-
-    async onFetchRevisions({ entity, id }) {
-        // TODO: reduxify
-        var revisions = await RevisionApi.list({ entity, id });
-        this.setState({ revisions });
-    }
-
-    onRevertToRevision({ entity, id, revision_id }) {
-        // TODO: reduxify
-        return RevisionApi.revert({ entity, id, revision_id });
-    }
-
-    onRevertedRevision() {
-        this.props.reloadCardFn();
-        this.refs.cardHistory.toggle();
-    }
-
-    getHeaderButtons() {
-        const { question, questionAlerts, visualizationSettings, card ,isNew, isDirty, isEditing, tableMetadata, databases } = this.props;
-        const database = _.findWhere(databases, { id: card && card.dataset_query && card.dataset_query.database });
-
-        var buttonSections = [];
-
-        // A card that is either completely new or it has been derived from a saved question
-        if (isNew && isDirty) {
-            buttonSections.push([
-                <ModalWithTrigger
-                    form
-                    key="save"
-                    ref="saveModal"
-                    triggerClasses="h4 text-grey-4 text-brand-hover text-uppercase"
-                    triggerElement="Save"
-                >
-                    <SaveQuestionModal
-                        card={this.props.card}
-                        originalCard={this.props.originalCard}
-                        tableMetadata={this.props.tableMetadata}
-                        // if saving modified question, don't show "add to dashboard" modal
-                        saveFn={(card) => this.onSave(card, false)}
-                        createFn={this.onCreate}
-                        onClose={() => this.refs.saveModal.toggle()}
-                    />
-                </ModalWithTrigger>
-            ]);
+          // edit button
+          buttonSections.push([
+            <Tooltip key="edit" tooltip={t`Edit question`}>
+              <a
+                className="cursor-pointer text-brand-hover"
+                onClick={this.onBeginEditing}
+              >
+                <Icon name="pencil" size={16} />
+              </a>
+            </Tooltip>,
+          ]);
         }
-
-        // persistence buttons on saved cards
-        if (!isNew && card.can_write) {
-            if (!isEditing) {
-                if (this.state.recentlySaved) {
-                    // existing card + not editing + recently saved = save confirmation
-                    buttonSections.push([
-                        <button
-                            key="recentlySaved"
-                            className="cursor-pointer bg-white text-success text-bold text-uppercase"
-                        >
-                            <span>
-                                <Icon name='check' size={12} />
-                                <span className="ml1">{t`Saved`}</span>
-                            </span>
-                        </button>
-                    ]);
-                } else {
-                    // edit button
-                    buttonSections.push([
-                        <Tooltip key="edit" tooltip={t`Edit question`}>
-                            <a className="cursor-pointer text-brand-hover" onClick={this.onBeginEditing}>
-                                <Icon name="pencil" size={16} />
-                            </a>
-                        </Tooltip>
-                    ]);
-                }
-
-            } else {
-                // save button
-                buttonSections.push([
-                    <ActionButton
-                        key="save"
-                        actionFn={() => this.onSave(this.props.card, false)}
-                        className="cursor-pointer text-brand-hover bg-white text-grey-4 text-uppercase"
-                        normalText={t`SAVE CHANGES`}
-                        activeText={t`Saving…`}
-                        failedText={t`Save failed`}
-                        successText={t`Saved`}
-                    />
-                ]);
-
-                // cancel button
-                buttonSections.push([
-                    <a key="cancel" className="cursor-pointer text-brand-hover text-grey-4 text-uppercase" onClick={this.onCancel}>
-                        {t`CANCEL`}
-                    </a>
-                ]);
-
-                // delete button
-                buttonSections.push([
-                    <ArchiveQuestionModal questionId={this.props.card.id} />
-                ]);
-
-                buttonSections.push([
-                    <ModalWithTrigger
-                        ref="move"
-                        key="move"
-                        full
-                        triggerElement={
-                            <Tooltip tooltip={t`Move question`}>
-                                <Icon name="move" />
-                            </Tooltip>
-                        }
-                    >
-                        <MoveToCollection
-                            questionId={this.props.card.id}
-                            initialCollectionId={this.props.card && this.props.card.collection_id}
-                            setCollection={(questionId, collection) => {
-                                this.props.onSetCardAttribute('collection', collection)
-                                this.props.onSetCardAttribute('collection_id', collection.id)
-                            }}
-                        />
-                    </ModalWithTrigger>
-                ]);
-            }
-        }
-
-        // parameters
-        if (question.query() instanceof NativeQuery && database && _.contains(database.features, "native-parameters")) {
-            const parametersButtonClasses = cx('transition-color', {
-                'text-brand': this.props.uiControls.isShowingTemplateTagsEditor,
-                'text-brand-hover': !this.props.uiControls.isShowingTemplateTagsEditor
-            });
-            buttonSections.push([
-                <Tooltip key="parameterEdititor" tooltip={t`Variables`}>
-                    <a className={parametersButtonClasses}>
-                        <Icon name="variable" size={16} onClick={this.props.toggleTemplateTagsEditor}></Icon>
-                    </a>
-                </Tooltip>
-            ]);
-        }
-
-        // add to dashboard
-        if (!isNew && !isEditing) {
-            // simply adding an existing saved card to a dashboard, so show the modal to do so
-            buttonSections.push([
-                <Tooltip key="addtodash" tooltip={t`Add to dashboard`}>
-                    <span data-metabase-event={"QueryBuilder;AddToDash Modal;normal"} className="cursor-pointer text-brand-hover" onClick={() => this.setState({ modal: "add-to-dashboard" })}>
-                        <Icon name="addtodash" size={ICON_SIZE} />
-                    </span>
-                </Tooltip>
-            ]);
-        } else if (isNew && isDirty) {
-            // this is a new card, so we need the user to save first then they can add to dash
-            buttonSections.push([
-                <Tooltip key="addtodashsave" tooltip={t`Add to dashboard`}>
-                    <ModalWithTrigger
-                        ref="addToDashSaveModal"
-                        triggerClasses="h4 text-brand-hover text-uppercase"
-                        triggerElement={<span data-metabase-event={"QueryBuilder;AddToDash Modal;pre-save"} className="text-brand-hover"><Icon name="addtodash" size={ICON_SIZE} /></span>}
-                    >
-                        <SaveQuestionModal
-                            card={this.props.card}
-                            originalCard={this.props.originalCard}
-                            tableMetadata={this.props.tableMetadata}
-                            saveFn={async (card) => {
-                                await this.onSave(card, false);
-                                this.setState({ modal: "add-to-dashboard"})
-                            }}
-                            createFn={async (card) => {
-                                await this.onCreate(card, false);
-                                this.setState({ modal: "add-to-dashboard"})
-                            }}
-                            onClose={() => this.refs.addToDashSaveModal.toggle()}
-                            multiStep
-                        />
-                    </ModalWithTrigger>
-                </Tooltip>
-            ]);
-        }
-
-        // history icon on saved cards
-        if (!isNew) {
-            buttonSections.push([
-                <Tooltip key="history" tooltip={t`Revision history`}>
-                    <ModalWithTrigger
-                        ref="cardHistory"
-                        triggerElement={<span className="text-brand-hover"><Icon name="history" size={18} /></span>}
-                    >
-                        <HistoryModal
-                            revisions={this.state.revisions}
-                            entityType="card"
-                            entityId={this.props.card.id}
-                            onFetchRevisions={this.onFetchRevisions}
-                            onRevertToRevision={this.onRevertToRevision}
-                            onClose={() => this.refs.cardHistory.toggle()}
-                            onReverted={this.onRevertedRevision}
-                        />
-                    </ModalWithTrigger>
-                </Tooltip>
-            ]);
-        }
-
-        // query mode toggle
+      } else {
+        // save button
         buttonSections.push([
-            <QueryModeButton
-                key="queryModeToggle"
-                mode={this.props.card.dataset_query.type}
-                allowNativeToQuery={isNew && !isDirty}
-                allowQueryToNative={tableMetadata ?
-                    // if a table is selected, only enable if user has native write permissions for THAT database
-                    tableMetadata.db && tableMetadata.db.native_permissions === "write" :
-                    // if no table is selected, only enable if user has native write permissions for ANY database
-                    _.any(databases, (db) => db.native_permissions === "write")
-                }
-                nativeForm={this.props.result && this.props.result.data && this.props.result.data.native_form}
-                onSetMode={this.props.setQueryModeFn}
-                tableMetadata={tableMetadata}
-            />
+          <ActionButton
+            key="save"
+            actionFn={() => this.onSave(this.props.card, false)}
+            className="cursor-pointer text-brand-hover bg-white text-grey-4 text-uppercase"
+            normalText={t`SAVE CHANGES`}
+            activeText={t`Saving…`}
+            failedText={t`Save failed`}
+            successText={t`Saved`}
+          />,
         ]);
 
-        // data reference button
-        var dataReferenceButtonClasses = cx('transition-color', {
-            'text-brand': this.props.isShowingDataReference,
-            'text-brand-hover': !this.state.isShowingDataReference
-        });
+        // cancel button
         buttonSections.push([
-            <Tooltip key="dataReference" tooltip={t`Learn about your data`}>
-                <a className={dataReferenceButtonClasses}>
-                    <Icon name='reference' size={ICON_SIZE} onClick={this.onToggleDataReference}></Icon>
-                </a>
-            </Tooltip>
+          <a
+            key="cancel"
+            className="cursor-pointer text-brand-hover text-grey-4 text-uppercase"
+            onClick={this.onCancel}
+          >
+            {t`CANCEL`}
+          </a>,
         ]);
 
-        if (!isEditing && card && question.alertType(visualizationSettings) !== null) {
-            const createAlertItem = {
-                title: t`Get alerts about this`,
-                icon: "alert",
-                action: () => this.setState({ modal: "create-alert" })
-            }
-            const createAlertAfterSavingQuestionItem = {
-                title: t`Get alerts about this`,
-                icon: "alert",
-                action: () => this.setState({ modal: "save-question-before-alert" })
-            }
+        // delete button
+        buttonSections.push([
+          <ArchiveQuestionModal questionId={this.props.card.id} />,
+        ]);
 
-            const updateAlertItem = {
-                title: t`Alerts are on`,
-                icon: "alert",
-                content: (toggleMenu, setMenuFreeze) => <AlertListPopoverContent closeMenu={toggleMenu} setMenuFreeze={setMenuFreeze} />
+        buttonSections.push([
+          <ModalWithTrigger
+            ref="move"
+            key="move"
+            full
+            triggerElement={
+              <Tooltip tooltip={t`Move question`}>
+                <Icon name="move" />
+              </Tooltip>
             }
-
-            buttonSections.push([
-                <div className="mr1" style={{ marginLeft: "-15px" }}>
-                    <EntityMenu
-                        triggerIcon='burger'
-                        items={[
-                            (!isNew && Object.values(questionAlerts).length > 0)
-                                ? updateAlertItem
-                                : (isNew ? createAlertAfterSavingQuestionItem : createAlertItem)
-                        ]}
-                    />
-                </div>
-            ]);
-        }
-
-        return (
-            <ButtonBar buttons={buttonSections} className="Header-buttonSection borderless" />
-        );
+          >
+            <MoveToCollection
+              questionId={this.props.card.id}
+              initialCollectionId={
+                this.props.card && this.props.card.collection_id
+              }
+              setCollection={(questionId, collection) => {
+                this.props.onSetCardAttribute("collection", collection);
+                this.props.onSetCardAttribute("collection_id", collection.id);
+              }}
+            />
+          </ModalWithTrigger>,
+        ]);
+      }
     }
 
-    onCloseModal = () => {
-        this.setState({ modal: null });
+    // parameters
+    if (
+      question.query() instanceof NativeQuery &&
+      database &&
+      _.contains(database.features, "native-parameters")
+    ) {
+      const parametersButtonClasses = cx("transition-color", {
+        "text-brand": this.props.uiControls.isShowingTemplateTagsEditor,
+        "text-brand-hover": !this.props.uiControls.isShowingTemplateTagsEditor,
+      });
+      buttonSections.push([
+        <Tooltip key="parameterEdititor" tooltip={t`Variables`}>
+          <a className={parametersButtonClasses}>
+            <Icon
+              name="variable"
+              size={16}
+              onClick={this.props.toggleTemplateTagsEditor}
+            />
+          </a>
+        </Tooltip>,
+      ]);
     }
 
-    showAlertsAfterQuestionSaved = () => {
-        const { questionAlerts, user } = this.props
+    // add to dashboard
+    if (!isNew && !isEditing) {
+      // simply adding an existing saved card to a dashboard, so show the modal to do so
+      buttonSections.push([
+        <Tooltip key="addtodash" tooltip={t`Add to dashboard`}>
+          <span
+            data-metabase-event={"QueryBuilder;AddToDash Modal;normal"}
+            className="cursor-pointer text-brand-hover"
+            onClick={() => this.setState({ modal: "add-to-dashboard" })}
+          >
+            <Icon name="addtodash" size={ICON_SIZE} />
+          </span>
+        </Tooltip>,
+      ]);
+    } else if (isNew && isDirty) {
+      // this is a new card, so we need the user to save first then they can add to dash
+      buttonSections.push([
+        <Tooltip key="addtodashsave" tooltip={t`Add to dashboard`}>
+          <ModalWithTrigger
+            ref="addToDashSaveModal"
+            triggerClasses="h4 text-brand-hover text-uppercase"
+            triggerElement={
+              <span
+                data-metabase-event={"QueryBuilder;AddToDash Modal;pre-save"}
+                className="text-brand-hover"
+              >
+                <Icon name="addtodash" size={ICON_SIZE} />
+              </span>
+            }
+          >
+            <SaveQuestionModal
+              card={this.props.card}
+              originalCard={this.props.originalCard}
+              tableMetadata={this.props.tableMetadata}
+              saveFn={async card => {
+                await this.onSave(card, false);
+                this.setState({ modal: "add-to-dashboard" });
+              }}
+              createFn={async card => {
+                await this.onCreate(card, false);
+                this.setState({ modal: "add-to-dashboard" });
+              }}
+              onClose={() => this.refs.addToDashSaveModal.toggle()}
+              multiStep
+            />
+          </ModalWithTrigger>
+        </Tooltip>,
+      ]);
+    }
 
-        const hasAlertsCreatedByCurrentUser =
-            Object.values(questionAlerts).some((alert) => alert.creator.id === user.id)
+    // history icon on saved cards
+    if (!isNew) {
+      buttonSections.push([
+        <Tooltip key="history" tooltip={t`Revision history`}>
+          <ModalWithTrigger
+            ref="cardHistory"
+            triggerElement={
+              <span className="text-brand-hover">
+                <Icon name="history" size={18} />
+              </span>
+            }
+          >
+            <HistoryModal
+              revisions={this.state.revisions}
+              entityType="card"
+              entityId={this.props.card.id}
+              onFetchRevisions={this.onFetchRevisions}
+              onRevertToRevision={this.onRevertToRevision}
+              onClose={() => this.refs.cardHistory.toggle()}
+              onReverted={this.onRevertedRevision}
+            />
+          </ModalWithTrigger>
+        </Tooltip>,
+      ]);
+    }
 
-        if (hasAlertsCreatedByCurrentUser) {
-            // TODO Atte Keinänen 11/10/17: The question was replaced and there is already an alert created by current user.
-            // Should we show pop up the alerts list in this case or do nothing (as we do currently)?
-            this.setState({ modal: null })
-        } else {
-            this.setState({ modal: "create-alert" })
+    // query mode toggle
+    buttonSections.push([
+      <QueryModeButton
+        key="queryModeToggle"
+        mode={this.props.card.dataset_query.type}
+        allowNativeToQuery={isNew && !isDirty}
+        allowQueryToNative={
+          tableMetadata
+            ? // if a table is selected, only enable if user has native write permissions for THAT database
+              tableMetadata.db &&
+              tableMetadata.db.native_permissions === "write"
+            : // if no table is selected, only enable if user has native write permissions for ANY database
+              _.any(databases, db => db.native_permissions === "write")
+        }
+        nativeForm={
+          this.props.result &&
+          this.props.result.data &&
+          this.props.result.data.native_form
         }
+        onSetMode={this.props.setQueryModeFn}
+        tableMetadata={tableMetadata}
+      />,
+    ]);
+
+    // data reference button
+    var dataReferenceButtonClasses = cx("transition-color", {
+      "text-brand": this.props.isShowingDataReference,
+      "text-brand-hover": !this.state.isShowingDataReference,
+    });
+    buttonSections.push([
+      <Tooltip key="dataReference" tooltip={t`Learn about your data`}>
+        <a className={dataReferenceButtonClasses}>
+          <Icon
+            name="reference"
+            size={ICON_SIZE}
+            onClick={this.onToggleDataReference}
+          />
+        </a>
+      </Tooltip>,
+    ]);
+
+    if (
+      !isEditing &&
+      card &&
+      question.alertType(visualizationSettings) !== null
+    ) {
+      const createAlertItem = {
+        title: t`Get alerts about this`,
+        icon: "alert",
+        action: () => this.setState({ modal: "create-alert" }),
+      };
+      const createAlertAfterSavingQuestionItem = {
+        title: t`Get alerts about this`,
+        icon: "alert",
+        action: () => this.setState({ modal: "save-question-before-alert" }),
+      };
+
+      const updateAlertItem = {
+        title: t`Alerts are on`,
+        icon: "alert",
+        content: (toggleMenu, setMenuFreeze) => (
+          <AlertListPopoverContent
+            closeMenu={toggleMenu}
+            setMenuFreeze={setMenuFreeze}
+          />
+        ),
+      };
+
+      buttonSections.push([
+        <div className="mr1" style={{ marginLeft: "-15px" }}>
+          <EntityMenu
+            triggerIcon="burger"
+            items={[
+              !isNew && Object.values(questionAlerts).length > 0
+                ? updateAlertItem
+                : isNew ? createAlertAfterSavingQuestionItem : createAlertItem,
+            ]}
+          />
+        </div>,
+      ]);
     }
 
-    render() {
-        return (
-            <div className="relative">
-                <HeaderBar
-                    isEditing={this.props.isEditing}
-                    name={this.props.isNew ? t`New question` : this.props.card.name}
-                    description={this.props.card ? this.props.card.description : null}
-                    breadcrumb={(!this.props.card.id && this.props.originalCard) ? (<span className="pl2">{t`started from`} <a className="link" onClick={this.onFollowBreadcrumb}>{this.props.originalCard.name}</a></span>) : null }
-                    buttons={this.getHeaderButtons()}
-                    setItemAttributeFn={this.props.onSetCardAttribute}
-                    badge={this.props.card.collection &&
-                        <Link
-                            to={Urls.collection(this.props.card.collection)}
-                            className="text-uppercase flex align-center no-decoration"
-                            style={{ color: this.props.card.collection.color, fontSize: 12 }}
-                        >
-                            <Icon name="collection" size={12} style={{ marginRight: "0.5em" }} />
-                            {this.props.card.collection.name}
-                        </Link>
-                    }
-                />
-
-                <Modal small isOpen={this.state.modal === "saved"} onClose={this.onCloseModal}>
-                    <QuestionSavedModal
-                        addToDashboardFn={() => this.setState({ modal: "add-to-dashboard" })}
-                        onClose={this.onCloseModal}
-                    />
-                </Modal>
-
-
-                <Modal isOpen={this.state.modal === "add-to-dashboard"} onClose={this.onCloseModal}>
-                    <AddToDashSelectDashModal
-                        card={this.props.card}
-                        onClose={this.onCloseModal}
-                        onChangeLocation={this.props.onChangeLocation}
-                    />
-                </Modal>
-
-                <Modal full isOpen={this.state.modal === "create-alert"} onClose={this.onCloseModal}>
-                    <CreateAlertModalContent onCancel={this.onCloseModal} onAlertCreated={this.onCloseModal} />
-                </Modal>
-
-                <Modal isOpen={this.state.modal === "save-question-before-alert"} onClose={this.onCloseModal}>
-                    <SaveQuestionModal
-                        card={this.props.card}
-                        originalCard={this.props.originalCard}
-                        tableMetadata={this.props.tableMetadata}
-                        saveFn={async (card) => {
-                            await this.onSave(card, false);
-                            this.showAlertsAfterQuestionSaved()
-                        }}
-                        createFn={async (card) => {
-                            await this.onCreate(card, false);
-                            this.showAlertsAfterQuestionSaved()
-                        }}
-                        // only close the modal if we are closing the dialog without saving
-                        // otherwise we are in some alerts modal already
-                        onClose={() => this.state.modal === "save-question-before-alert" && this.setState({ modal: null }) }
-                        multiStep
-                    />
-                </Modal>
-            </div>
-        );
+    return (
+      <ButtonBar
+        buttons={buttonSections}
+        className="Header-buttonSection borderless"
+      />
+    );
+  }
+
+  onCloseModal = () => {
+    this.setState({ modal: null });
+  };
+
+  showAlertsAfterQuestionSaved = () => {
+    const { questionAlerts, user } = this.props;
+
+    const hasAlertsCreatedByCurrentUser = Object.values(questionAlerts).some(
+      alert => alert.creator.id === user.id,
+    );
+
+    if (hasAlertsCreatedByCurrentUser) {
+      // TODO Atte Keinänen 11/10/17: The question was replaced and there is already an alert created by current user.
+      // Should we show pop up the alerts list in this case or do nothing (as we do currently)?
+      this.setState({ modal: null });
+    } else {
+      this.setState({ modal: "create-alert" });
     }
+  };
+
+  render() {
+    return (
+      <div className="relative">
+        <HeaderBar
+          isEditing={this.props.isEditing}
+          name={this.props.isNew ? t`New question` : this.props.card.name}
+          description={this.props.card ? this.props.card.description : null}
+          breadcrumb={
+            !this.props.card.id && this.props.originalCard ? (
+              <span className="pl2">
+                {t`started from`}{" "}
+                <a className="link" onClick={this.onFollowBreadcrumb}>
+                  {this.props.originalCard.name}
+                </a>
+              </span>
+            ) : null
+          }
+          buttons={this.getHeaderButtons()}
+          setItemAttributeFn={this.props.onSetCardAttribute}
+          badge={
+            this.props.card.collection && (
+              <Link
+                to={Urls.collection(this.props.card.collection)}
+                className="text-uppercase flex align-center no-decoration"
+                style={{
+                  color: this.props.card.collection.color,
+                  fontSize: 12,
+                }}
+              >
+                <Icon
+                  name="collection"
+                  size={12}
+                  style={{ marginRight: "0.5em" }}
+                />
+                {this.props.card.collection.name}
+              </Link>
+            )
+          }
+        />
+
+        <Modal
+          small
+          isOpen={this.state.modal === "saved"}
+          onClose={this.onCloseModal}
+        >
+          <QuestionSavedModal
+            addToDashboardFn={() =>
+              this.setState({ modal: "add-to-dashboard" })
+            }
+            onClose={this.onCloseModal}
+          />
+        </Modal>
+
+        <Modal
+          isOpen={this.state.modal === "add-to-dashboard"}
+          onClose={this.onCloseModal}
+        >
+          <AddToDashSelectDashModal
+            card={this.props.card}
+            onClose={this.onCloseModal}
+            onChangeLocation={this.props.onChangeLocation}
+          />
+        </Modal>
+
+        <Modal
+          full
+          isOpen={this.state.modal === "create-alert"}
+          onClose={this.onCloseModal}
+        >
+          <CreateAlertModalContent
+            onCancel={this.onCloseModal}
+            onAlertCreated={this.onCloseModal}
+          />
+        </Modal>
+
+        <Modal
+          isOpen={this.state.modal === "save-question-before-alert"}
+          onClose={this.onCloseModal}
+        >
+          <SaveQuestionModal
+            card={this.props.card}
+            originalCard={this.props.originalCard}
+            tableMetadata={this.props.tableMetadata}
+            saveFn={async card => {
+              await this.onSave(card, false);
+              this.showAlertsAfterQuestionSaved();
+            }}
+            createFn={async card => {
+              await this.onCreate(card, false);
+              this.showAlertsAfterQuestionSaved();
+            }}
+            // only close the modal if we are closing the dialog without saving
+            // otherwise we are in some alerts modal already
+            onClose={() =>
+              this.state.modal === "save-question-before-alert" &&
+              this.setState({ modal: null })
+            }
+            multiStep
+          />
+        </Modal>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
index 26a45ea9f9755c453403637b87343ec7141fe9cf..3e94101aa0ab10784f2ce928cfeedc6c58c9a9c3 100644
--- a/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryModeButton.jsx
@@ -7,83 +7,112 @@ import { getEngineNativeType, formatJsonQuery } from "metabase/lib/engine";
 import Icon from "metabase/components/Icon.jsx";
 import Modal from "metabase/components/Modal.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class QueryModeButton extends Component {
+  constructor(props, context) {
+    super(props, context);
 
-    constructor(props, context) {
-        super(props, context);
-
-        this.state = {
-            isOpen: false,
-        };
-    }
-
-    static propTypes = {
-        mode: PropTypes.string.isRequired,
-        allowNativeToQuery: PropTypes.bool,
-        allowQueryToNative: PropTypes.bool,
-        nativeForm: PropTypes.object,
-        onSetMode: PropTypes.func.isRequired
+    this.state = {
+      isOpen: false,
     };
+  }
 
-    static defaultProps = {
-        allowNativeToQuery: false,
-    }
+  static propTypes = {
+    mode: PropTypes.string.isRequired,
+    allowNativeToQuery: PropTypes.bool,
+    allowQueryToNative: PropTypes.bool,
+    nativeForm: PropTypes.object,
+    onSetMode: PropTypes.func.isRequired,
+  };
 
-    render() {
-        const { allowQueryToNative, allowNativeToQuery, mode, nativeForm, onSetMode, tableMetadata } = this.props;
+  static defaultProps = {
+    allowNativeToQuery: false,
+  };
 
-        // determine the type to switch to based on the type
-        var targetType = (mode === "query") ? "native" : "query";
+  render() {
+    const {
+      allowQueryToNative,
+      allowNativeToQuery,
+      mode,
+      nativeForm,
+      onSetMode,
+      tableMetadata,
+    } = this.props;
 
-        const engine = tableMetadata && tableMetadata.db.engine;
-        const nativeQueryName = getEngineNativeType(engine) === "sql" ? t`SQL` : t`native query`;
+    // determine the type to switch to based on the type
+    var targetType = mode === "query" ? "native" : "query";
 
-        // maybe switch up the icon based on mode?
-        let onClick = null;
-        let tooltip = t`Not Supported`;
-        if (mode === "query" && allowQueryToNative) {
-            onClick = nativeForm ? () => this.setState({isOpen: true}) : () => onSetMode("native");
-            tooltip = nativeForm ? t`View the ${nativeQueryName}` : t`Switch to ${nativeQueryName}`;
-        } else if (mode === "native" && allowNativeToQuery) {
-            onClick = () => onSetMode("query");
-            tooltip = t`Switch to Builder`;
-        }
+    const engine = tableMetadata && tableMetadata.db.engine;
+    const nativeQueryName =
+      getEngineNativeType(engine) === "sql" ? t`SQL` : t`native query`;
 
-        return (
-            <div>
-                <Tooltip tooltip={tooltip}>
-                    <span data-metabase-event={"QueryBuilder;Toggle Mode"} className={cx("cursor-pointer", {"text-brand-hover": onClick, "text-grey-1": !onClick})} onClick={onClick}>
-                        <Icon name="sql" size={16} />
-                    </span>
-                </Tooltip>
+    // maybe switch up the icon based on mode?
+    let onClick = null;
+    let tooltip = t`Not Supported`;
+    if (mode === "query" && allowQueryToNative) {
+      onClick = nativeForm
+        ? () => this.setState({ isOpen: true })
+        : () => onSetMode("native");
+      tooltip = nativeForm
+        ? t`View the ${nativeQueryName}`
+        : t`Switch to ${nativeQueryName}`;
+    } else if (mode === "native" && allowNativeToQuery) {
+      onClick = () => onSetMode("query");
+      tooltip = t`Switch to Builder`;
+    }
+
+    return (
+      <div>
+        <Tooltip tooltip={tooltip}>
+          <span
+            data-metabase-event={"QueryBuilder;Toggle Mode"}
+            className={cx("cursor-pointer", {
+              "text-brand-hover": onClick,
+              "text-grey-1": !onClick,
+            })}
+            onClick={onClick}
+          >
+            <Icon name="sql" size={16} />
+          </span>
+        </Tooltip>
 
-                <Modal medium isOpen={this.state.isOpen} onClose={() => this.setState({isOpen: false})}>
-                    <div className="p4">
-                        <div className="mb3 flex flex-row flex-full align-center justify-between">
-                            <h2>{t`${capitalize(nativeQueryName)} for this question`}</h2>
-                            <span className="cursor-pointer" onClick={() => this.setState({isOpen: false})}><Icon name="close" size={16} /></span>
-                        </div>
+        <Modal
+          medium
+          isOpen={this.state.isOpen}
+          onClose={() => this.setState({ isOpen: false })}
+        >
+          <div className="p4">
+            <div className="mb3 flex flex-row flex-full align-center justify-between">
+              <h2>{t`${capitalize(nativeQueryName)} for this question`}</h2>
+              <span
+                className="cursor-pointer"
+                onClick={() => this.setState({ isOpen: false })}
+              >
+                <Icon name="close" size={16} />
+              </span>
+            </div>
 
-                        <pre className="mb3 p2 sql-code">
-                            {nativeForm && nativeForm.query && (
-                                getEngineNativeType(engine) === "json" ?
-                                    formatJsonQuery(nativeForm.query, engine)
-                                :
-                                    formatSQL(nativeForm.query)
-                            )}
-                        </pre>
+            <pre className="mb3 p2 sql-code">
+              {nativeForm &&
+                nativeForm.query &&
+                (getEngineNativeType(engine) === "json"
+                  ? formatJsonQuery(nativeForm.query, engine)
+                  : formatSQL(nativeForm.query))}
+            </pre>
 
-                        <div className="text-centered">
-                            <a className="Button Button--primary" onClick={() => {
-                                onSetMode(targetType);
-                                this.setState({isOpen: false});
-                            }}>{t`Convert this question to ${nativeQueryName}`}</a>
-                        </div>
-                    </div>
-                </Modal>
+            <div className="text-centered">
+              <a
+                className="Button Button--primary"
+                onClick={() => {
+                  onSetMode(targetType);
+                  this.setState({ isOpen: false });
+                }}
+              >{t`Convert this question to ${nativeQueryName}`}</a>
             </div>
-        );
-    }
+          </div>
+        </Modal>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
index f7e38151264517fed2b7896f5acf6c2cf69558fd..817247b80ce41f629fe9e9fa3f33a59d3313d263 100644
--- a/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
+++ b/frontend/src/metabase/query_builder/components/QueryVisualization.jsx
@@ -2,14 +2,14 @@
 
 import React, { Component } from "react";
 import { Link } from "react-router";
-import { t, jt } from 'c-3po';
-import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx';
+import { t, jt } from "c-3po";
+import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 import Tooltip from "metabase/components/Tooltip";
 import Icon from "metabase/components/Icon";
 import ShrinkableList from "metabase/components/ShrinkableList";
 
-import RunButton from './RunButton.jsx';
-import VisualizationSettings from './VisualizationSettings.jsx';
+import RunButton from "./RunButton.jsx";
+import VisualizationSettings from "./VisualizationSettings.jsx";
 
 import VisualizationError from "./VisualizationError.jsx";
 import VisualizationResult from "./VisualizationResult.jsx";
@@ -28,7 +28,7 @@ import _ from "underscore";
 import moment from "moment";
 
 import Question from "metabase-lib/lib/Question";
-import type  { Database } from "metabase/meta/types/Database";
+import type { Database } from "metabase/meta/types/Database";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
 import type { DatasetQuery } from "metabase/meta/types/Card";
 import type { ParameterValues } from "metabase/meta/types/Parameter";
@@ -36,213 +36,266 @@ import type { ParameterValues } from "metabase/meta/types/Parameter";
 const REFRESH_TOOLTIP_THRESHOLD = 30 * 1000; // 30 seconds
 
 type Props = {
-    question: Question,
-    originalQuestion: Question,
-    result?: Object,
-    databases?: Database[],
-    tableMetadata?: TableMetadata,
-    tableForeignKeys?: [],
-    tableForeignKeyReferences?: {},
-    setDisplayFn: (any) => void,
-    onUpdateVisualizationSettings: (any) => void,
-    onReplaceAllVisualizationSettings: (any) => void,
-    cellIsClickableFn?: (any) => void,
-    cellClickedFn?: (any) => void,
-    isRunning: boolean,
-    isRunnable: boolean,
-    isAdmin: boolean,
-    isObjectDetail: boolean,
-    isResultDirty: boolean,
-    runQuestionQuery: (any) => void,
-    cancelQuery?: (any) => void,
-    className: string
+  question: Question,
+  originalQuestion: Question,
+  result?: Object,
+  databases?: Database[],
+  tableMetadata?: TableMetadata,
+  tableForeignKeys?: [],
+  tableForeignKeyReferences?: {},
+  setDisplayFn: any => void,
+  onUpdateVisualizationSettings: any => void,
+  onReplaceAllVisualizationSettings: any => void,
+  cellIsClickableFn?: any => void,
+  cellClickedFn?: any => void,
+  isRunning: boolean,
+  isRunnable: boolean,
+  isAdmin: boolean,
+  isObjectDetail: boolean,
+  isResultDirty: boolean,
+  runQuestionQuery: any => void,
+  cancelQuery?: any => void,
+  className: string,
 };
 
 type State = {
-    lastRunDatasetQuery: DatasetQuery,
-    lastRunParameterValues: ParameterValues,
-    warnings: string[]
-}
+  lastRunDatasetQuery: DatasetQuery,
+  lastRunParameterValues: ParameterValues,
+  warnings: string[],
+};
 
 export default class QueryVisualization extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    constructor(props, context) {
-        super(props, context);
-        this.state = this._getStateFromProps(props);
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = this._getStateFromProps(props);
+  }
+
+  static defaultProps = {
+    // NOTE: this should be more dynamic from the backend, it's set based on the query lang
+    maxTableRows: 2000,
+  };
 
-    static defaultProps = {
-        // NOTE: this should be more dynamic from the backend, it's set based on the query lang
-        maxTableRows: 2000
+  _getStateFromProps(props) {
+    return {
+      lastRunDatasetQuery: Utils.copy(props.question.query().datasetQuery()),
+      lastRunParameterValues: Utils.copy(props.parameterValues),
     };
+  }
 
-    _getStateFromProps(props) {
-        return {
-            lastRunDatasetQuery: Utils.copy(props.question.query().datasetQuery()),
-            lastRunParameterValues: Utils.copy(props.parameterValues)
-        };
+  componentWillReceiveProps(nextProps) {
+    // whenever we are told that we are running a query lets update our understanding of the "current" query
+    if (nextProps.isRunning) {
+      this.setState(this._getStateFromProps(nextProps));
     }
+  }
 
-    componentWillReceiveProps(nextProps) {
-        // whenever we are told that we are running a query lets update our understanding of the "current" query
-        if (nextProps.isRunning) {
-            this.setState(this._getStateFromProps(nextProps));
-        }
+  isChartDisplay(display) {
+    return display !== "table" && display !== "scalar";
+  }
+
+  runQuery = () => {
+    this.props.runQuestionQuery({ ignoreCache: true });
+  };
+
+  renderHeader() {
+    const {
+      question,
+      isObjectDetail,
+      isRunnable,
+      isRunning,
+      isResultDirty,
+      isAdmin,
+      result,
+      cancelQuery,
+    } = this.props;
+
+    let runButtonTooltip;
+    if (
+      !isResultDirty &&
+      result &&
+      result.cached &&
+      result.average_execution_time > REFRESH_TOOLTIP_THRESHOLD
+    ) {
+      runButtonTooltip = t`This question will take approximately ${duration(
+        result.average_execution_time,
+      )} to refresh`;
     }
 
-    isChartDisplay(display) {
-        return (display !== "table" && display !== "scalar");
+    const messages = [];
+    if (result && result.cached) {
+      messages.push({
+        icon: "clock",
+        message: <div>{t`Updated ${moment(result.updated_at).fromNow()}`}</div>,
+      });
+    }
+    if (
+      result &&
+      result.data &&
+      !isObjectDetail &&
+      question.display() === "table"
+    ) {
+      messages.push({
+        icon: "table2",
+        message: (
+          // class name is included for the sake of making targeting the element in tests easier
+          <div className="ShownRowCount">
+            {jt`${
+              result.data.rows_truncated != null ? t`Showing first` : t`Showing`
+            } ${<strong>{formatNumber(result.row_count)}</strong>} ${inflect(
+              "row",
+              result.data.rows.length,
+            )}`}
+          </div>
+        ),
+      });
     }
 
-    runQuery = () => {
-        this.props.runQuestionQuery({ ignoreCache: true });
-    };
+    const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
+    const isEmbeddingEnabled = MetabaseSettings.get("embedding");
+    return (
+      <div className="relative flex align-center flex-no-shrink mt2 mb1 sm-py3">
+        <div className="z4 absolute left hide sm-show">
+          {!isObjectDetail && (
+            <VisualizationSettings ref="settings" {...this.props} />
+          )}
+        </div>
+        <div className="z3 absolute left right">
+          <Tooltip tooltip={runButtonTooltip}>
+            <RunButton
+              isRunnable={isRunnable}
+              isDirty={isResultDirty}
+              isRunning={isRunning}
+              onRun={this.runQuery}
+              onCancel={cancelQuery}
+            />
+          </Tooltip>
+        </div>
+        <div
+          className="z4 absolute right flex align-center justify-end"
+          style={{ lineHeight: 0 /* needed to align icons :-/ */ }}
+        >
+          <ShrinkableList
+            className="flex"
+            items={messages}
+            renderItem={item => (
+              <div className="flex-no-shrink flex align-center mx2 h5 text-grey-4">
+                <Icon className="mr1" name={item.icon} size={12} />
+                {item.message}
+              </div>
+            )}
+            renderItemSmall={item => (
+              <Tooltip tooltip={<div className="p1">{item.message}</div>}>
+                <Icon className="mx1" name={item.icon} size={16} />
+              </Tooltip>
+            )}
+          />
+          {!isObjectDetail && (
+            <Warnings
+              warnings={this.state.warnings}
+              className="mx1"
+              size={18}
+            />
+          )}
+          {!isResultDirty && result && !result.error ? (
+            <QueryDownloadWidget
+              className="mx1 hide sm-show"
+              card={question.card()}
+              result={result}
+            />
+          ) : null}
+          {question.isSaved() &&
+          ((isPublicLinksEnabled && (isAdmin || question.publicUUID())) ||
+            (isEmbeddingEnabled && isAdmin)) ? (
+            <QuestionEmbedWidget
+              className="mx1 hide sm-show"
+              card={question.card()}
+            />
+          ) : null}
+        </div>
+      </div>
+    );
+  }
 
-    renderHeader() {
-        const { question, isObjectDetail, isRunnable, isRunning, isResultDirty, isAdmin, result, cancelQuery } = this.props;
-
-        let runButtonTooltip;
-        if (!isResultDirty && result && result.cached && result.average_execution_time > REFRESH_TOOLTIP_THRESHOLD) {
-            runButtonTooltip = t`This question will take approximately ${duration(result.average_execution_time)} to refresh`;
-        }
-
-        const messages = [];
-        if (result && result.cached) {
-            messages.push({
-                icon: "clock",
-                message: (
-                    <div>
-                        {t`Updated ${moment(result.updated_at).fromNow()}`}
-                    </div>
-                )
-            })
-        }
-        if (result && result.data && !isObjectDetail && question.display() === "table") {
-            messages.push({
-                icon: "table2",
-                message: (
-                    // class name is included for the sake of making targeting the element in tests easier
-                    <div className="ShownRowCount">
-                        {jt`${ result.data.rows_truncated != null ? (t`Showing first`) : (t`Showing`)} ${<strong>{formatNumber(result.row_count)}</strong>} ${inflect("row", result.data.rows.length)}`}
-                    </div>
-                )
-            })
-        }
-
-        const isPublicLinksEnabled = MetabaseSettings.get("public_sharing");
-        const isEmbeddingEnabled = MetabaseSettings.get("embedding");
-        return (
-            <div className="relative flex align-center flex-no-shrink mt2 mb1 sm-py3">
-                <div className="z4 absolute left hide sm-show">
-                  { !isObjectDetail && <VisualizationSettings ref="settings" {...this.props} /> }
-                </div>
-                <div className="z3 absolute left right">
-                    <Tooltip tooltip={runButtonTooltip}>
-                        <RunButton
-                            isRunnable={isRunnable}
-                            isDirty={isResultDirty}
-                            isRunning={isRunning}
-                            onRun={this.runQuery}
-                            onCancel={cancelQuery}
-                        />
-                    </Tooltip>
-                </div>
-                <div className="z4 absolute right flex align-center justify-end" style={{ lineHeight: 0 /* needed to align icons :-/ */ }}>
-                    <ShrinkableList
-                        className="flex"
-                        items={messages}
-                        renderItem={(item) =>
-                            <div className="flex-no-shrink flex align-center mx2 h5 text-grey-4">
-                                <Icon className="mr1" name={item.icon} size={12} />
-                                {item.message}
-                            </div>
-                        }
-                        renderItemSmall={(item) =>
-                            <Tooltip tooltip={<div className="p1">{item.message}</div>}>
-                                <Icon className="mx1" name={item.icon} size={16} />
-                            </Tooltip>
-                        }
-                    />
-                    { !isObjectDetail &&
-                        <Warnings warnings={this.state.warnings} className="mx1" size={18} />
-                    }
-                    { !isResultDirty && result && !result.error ?
-                        <QueryDownloadWidget
-                            className="mx1 hide sm-show"
-                            card={question.card()}
-                            result={result}
-                        />
-                    : null }
-                    { question.isSaved() && (
-                        (isPublicLinksEnabled && (isAdmin || question.publicUUID())) ||
-                        (isEmbeddingEnabled && isAdmin)
-                    ) ?
-                        <QuestionEmbedWidget
-                            className="mx1 hide sm-show"
-                            card={question.card()}
-                        />
-                    : null }
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const {
+      className,
+      question,
+      databases,
+      isObjectDetail,
+      isRunning,
+      result,
+    } = this.props;
+    let viz;
+
+    if (!result) {
+      let hasSampleDataset = !!_.findWhere(databases, { is_sample: true });
+      viz = <VisualizationEmptyState showTutorialLink={hasSampleDataset} />;
+    } else {
+      let error = result.error;
 
-    render() {
-        const { className, question, databases, isObjectDetail, isRunning, result } = this.props;
-        let viz;
-
-        if (!result) {
-            let hasSampleDataset = !!_.findWhere(databases, { is_sample: true });
-            viz = <VisualizationEmptyState showTutorialLink={hasSampleDataset} />
-        } else {
-            let error = result.error;
-
-            if (error) {
-                viz = <VisualizationError error={error} card={question.card()} duration={result.duration} />
-            } else if (result.data) {
-                viz = (
-                    <VisualizationResult
-                        lastRunDatasetQuery={this.state.lastRunDatasetQuery}
-                        onUpdateWarnings={(warnings) => this.setState({ warnings })}
-                        onOpenChartSettings={() => this.refs.settings.open()}
-                        {...this.props}
-                        className="spread"
-                    />
-                );
-            }
-        }
-
-        const wrapperClasses = cx(className, 'relative', {
-            'flex': !isObjectDetail,
-            'flex-column': !isObjectDetail
-        });
-
-        const visualizationClasses = cx('flex flex-full Visualization z1 relative', {
-            'Visualization--errors': (result && result.error),
-            'Visualization--loading': isRunning
-        });
-
-        return (
-            <div className={wrapperClasses}>
-                { !this.props.noHeader && this.renderHeader()}
-                { isRunning && (
-                    <div className="Loading spread flex flex-column layout-centered text-brand z2">
-                        <LoadingSpinner />
-                        <h2 className="Loading-message text-brand text-uppercase my3">{t`Doing science`}...</h2>
-                    </div>
-                )}
-                <div className={visualizationClasses}>
-                    {viz}
-                </div>
-            </div>
+      if (error) {
+        viz = (
+          <VisualizationError
+            error={error}
+            card={question.card()}
+            duration={result.duration}
+          />
         );
+      } else if (result.data) {
+        viz = (
+          <VisualizationResult
+            lastRunDatasetQuery={this.state.lastRunDatasetQuery}
+            onUpdateWarnings={warnings => this.setState({ warnings })}
+            onOpenChartSettings={() => this.refs.settings.open()}
+            {...this.props}
+            className="spread"
+          />
+        );
+      }
     }
+
+    const wrapperClasses = cx(className, "relative", {
+      flex: !isObjectDetail,
+      "flex-column": !isObjectDetail,
+    });
+
+    const visualizationClasses = cx(
+      "flex flex-full Visualization z1 relative",
+      {
+        "Visualization--errors": result && result.error,
+        "Visualization--loading": isRunning,
+      },
+    );
+
+    return (
+      <div className={wrapperClasses}>
+        {!this.props.noHeader && this.renderHeader()}
+        {isRunning && (
+          <div className="Loading spread flex flex-column layout-centered text-brand z2">
+            <LoadingSpinner />
+            <h2 className="Loading-message text-brand text-uppercase my3">
+              {t`Doing science`}...
+            </h2>
+          </div>
+        )}
+        <div className={visualizationClasses}>{viz}</div>
+      </div>
+    );
+  }
 }
 
-export const VisualizationEmptyState = ({showTutorialLink}) =>
-    <div className="flex full layout-centered text-grey-1 flex-column">
-        <h1>{t`If you give me some data I can show you something cool. Run a Query!`}</h1>
-        { showTutorialLink && <Link to={Urls.question(null, "?tutorial")} className="link cursor-pointer my2">{t`How do I use this thing?`}</Link> }
-    </div>;
+export const VisualizationEmptyState = ({ showTutorialLink }) => (
+  <div className="flex full layout-centered text-grey-1 flex-column">
+    <h1
+    >{t`If you give me some data I can show you something cool. Run a Query!`}</h1>
+    {showTutorialLink && (
+      <Link
+        to={Urls.question(null, "?tutorial")}
+        className="link cursor-pointer my2"
+      >{t`How do I use this thing?`}</Link>
+    )}
+  </div>
+);
diff --git a/frontend/src/metabase/query_builder/components/RunButton.jsx b/frontend/src/metabase/query_builder/components/RunButton.jsx
index 579e96c1be69958b0831fa8aa3115deaa74f989f..08409ac0cee29a021a82211cb5e67b8f7a630642 100644
--- a/frontend/src/metabase/query_builder/components/RunButton.jsx
+++ b/frontend/src/metabase/query_builder/components/RunButton.jsx
@@ -1,40 +1,53 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 
 import cx from "classnames";
 
 export default class RunButton extends Component {
-    static propTypes = {
-        isRunnable: PropTypes.bool.isRequired,
-        isRunning: PropTypes.bool.isRequired,
-        isDirty: PropTypes.bool.isRequired,
-        onRun: PropTypes.func.isRequired,
-        onCancel: PropTypes.func
-    };
+  static propTypes = {
+    isRunnable: PropTypes.bool.isRequired,
+    isRunning: PropTypes.bool.isRequired,
+    isDirty: PropTypes.bool.isRequired,
+    onRun: PropTypes.func.isRequired,
+    onCancel: PropTypes.func,
+  };
 
-    render() {
-        let { isRunnable, isRunning, isDirty, onRun, onCancel } = this.props;
-        let buttonText = null;
-        if (isRunning) {
-            buttonText = <div className="flex align-center"><Icon className="mr1" name="close" />{t`Cancel`}</div>;
-        } else if (isRunnable && isDirty) {
-            buttonText = t`Get Answer`;
-        } else if (isRunnable && !isDirty) {
-            buttonText = <div className="flex align-center"><Icon className="mr1" name="refresh" />{t`Refresh`}</div>;
-        }
-        let actionFn = isRunning ? onCancel : onRun;
-        let classes = cx("Button Button--medium circular RunButton ml-auto mr-auto block", {
-            "RunButton--hidden": !buttonText,
-            "Button--primary": isDirty,
-            "text-grey-2": !isDirty,
-            "text-grey-4-hover": !isDirty,
-        });
-        return (
-            <button className={classes} onClick={() => actionFn()}>
-            {buttonText}
-            </button>
-        );
+  render() {
+    let { isRunnable, isRunning, isDirty, onRun, onCancel } = this.props;
+    let buttonText = null;
+    if (isRunning) {
+      buttonText = (
+        <div className="flex align-center">
+          <Icon className="mr1" name="close" />
+          {t`Cancel`}
+        </div>
+      );
+    } else if (isRunnable && isDirty) {
+      buttonText = t`Get Answer`;
+    } else if (isRunnable && !isDirty) {
+      buttonText = (
+        <div className="flex align-center">
+          <Icon className="mr1" name="refresh" />
+          {t`Refresh`}
+        </div>
+      );
     }
+    let actionFn = isRunning ? onCancel : onRun;
+    let classes = cx(
+      "Button Button--medium circular RunButton ml-auto mr-auto block",
+      {
+        "RunButton--hidden": !buttonText,
+        "Button--primary": isDirty,
+        "text-grey-2": !isDirty,
+        "text-grey-4-hover": !isDirty,
+      },
+    );
+    return (
+      <button className={classes} onClick={() => actionFn()}>
+        {buttonText}
+      </button>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
index 7a7cad744561fd3e98ce6b6cdfb456e011892522..32758c6a1a2b3732bad38b5c78cbb85c1d76a9cd 100644
--- a/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
+++ b/frontend/src/metabase/query_builder/components/SavedQuestionIntroModal.jsx
@@ -1,25 +1,28 @@
 import React, { Component } from "react";
 
 import Modal from "metabase/components/Modal.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class SavedQuestionIntroModal extends Component {
+  render() {
+    return (
+      <Modal small isOpen={this.props.isShowingNewbModal}>
+        <div className="Modal-content Modal-content--small NewForm">
+          <div className="Modal-header Form-header">
+            <h2 className="pb2 text-dark">{t`It's okay to play around with saved questions`}</h2>
 
-    render() {
-        return (
-            <Modal small isOpen={this.props.isShowingNewbModal}>
-                <div className="Modal-content Modal-content--small NewForm">
-                    <div className="Modal-header Form-header">
-                        <h2 className="pb2 text-dark">{t`It's okay to play around with saved questions`}</h2>
+            <div className="pb1 text-grey-4">{t`You won't make any permanent changes to a saved question unless you click the edit icon in the top-right.`}</div>
+          </div>
 
-                        <div className="pb1 text-grey-4">{t`You won't make any permanent changes to a saved question unless you click the edit icon in the top-right.`}</div>
-                    </div>
-
-                    <div className="Form-actions flex justify-center py1">
-                        <button data-metabase-event={"QueryBuilder;IntroModal"} className="Button Button--primary" onClick={() => this.props.onClose()}>{t`Okay`}</button>
-                    </div>
-                </div>
-            </Modal>
-        );
-    }
+          <div className="Form-actions flex justify-center py1">
+            <button
+              data-metabase-event={"QueryBuilder;IntroModal"}
+              className="Button Button--primary"
+              onClick={() => this.props.onClose()}
+            >{t`Okay`}</button>
+          </div>
+        </div>
+      </Modal>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/SearchBar.jsx b/frontend/src/metabase/query_builder/components/SearchBar.jsx
index 4a99c146d12b198bd5f8b9783ed995cabff76a0b..aa20f73049d81f7402cedf2284d98bc0eba4cfb0 100644
--- a/frontend/src/metabase/query_builder/components/SearchBar.jsx
+++ b/frontend/src/metabase/query_builder/components/SearchBar.jsx
@@ -1,26 +1,33 @@
 import React from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 export default class SearchBar extends React.Component {
-    constructor(props, context) {
-        super(props, context);
-        this.handleInputChange = this.handleInputChange.bind(this);
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.handleInputChange = this.handleInputChange.bind(this);
+  }
 
-    static propTypes = {
-        filter: PropTypes.string.isRequired,
-        onFilter: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    filter: PropTypes.string.isRequired,
+    onFilter: PropTypes.func.isRequired,
+  };
 
-    handleInputChange() {
-        this.props.onFilter(ReactDOM.findDOMNode(this.refs.filterTextInput).value);
-    }
+  handleInputChange() {
+    this.props.onFilter(ReactDOM.findDOMNode(this.refs.filterTextInput).value);
+  }
 
-    render() {
-        return (
-            <input className="SearchBar" type="text" ref="filterTextInput" value={this.props.filter} placeholder={t`Search for`} onChange={this.handleInputChange}/>
-        );
-    }
+  render() {
+    return (
+      <input
+        className="SearchBar"
+        type="text"
+        ref="filterTextInput"
+        value={this.props.filter}
+        placeholder={t`Search for`}
+        onChange={this.handleInputChange}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/SelectionModule.jsx b/frontend/src/metabase/query_builder/components/SelectionModule.jsx
index 7273413707ca625449b11cec3c11e03a54fdac14..8f5a93eb83f195392093dbc3b3f091efbbf6ddb3 100644
--- a/frontend/src/metabase/query_builder/components/SelectionModule.jsx
+++ b/frontend/src/metabase/query_builder/components/SelectionModule.jsx
@@ -3,58 +3,58 @@ import PropTypes from "prop-types";
 
 import Popover from "metabase/components/Popover.jsx";
 import Icon from "metabase/components/Icon.jsx";
-import SearchBar from './SearchBar.jsx';
-import { t } from 'c-3po';
+import SearchBar from "./SearchBar.jsx";
+import { t } from "c-3po";
 import _ from "underscore";
 import cx from "classnames";
 
 export default class SelectionModule extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this._expand = this._expand.bind(this);
-        this._select = this._select.bind(this);
-        this._toggleOpen = this._toggleOpen.bind(this);
-        this.onClose = this.onClose.bind(this);
-        // a selection module can be told to be open on initialization but otherwise is closed
-        var isInitiallyOpen = props.isInitiallyOpen || false;
-
-        this.state = {
-            open: isInitiallyOpen,
-            expanded: false,
-            searchThreshold: 20,
-            searchEnabled: false,
-            filterTerm: null
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this._expand = this._expand.bind(this);
+    this._select = this._select.bind(this);
+    this._toggleOpen = this._toggleOpen.bind(this);
+    this.onClose = this.onClose.bind(this);
+    // a selection module can be told to be open on initialization but otherwise is closed
+    var isInitiallyOpen = props.isInitiallyOpen || false;
 
-    static propTypes = {
-        action: PropTypes.func.isRequired,
-        display: PropTypes.string.isRequired,
-        descriptionKey: PropTypes.string,
-        expandFilter: PropTypes.func,
-        expandTitle: PropTypes.string,
-        isInitiallyOpen: PropTypes.bool,
-        items: PropTypes.array,
-        remove: PropTypes.func,
-        selectedKey: PropTypes.string,
-        selectedValue: PropTypes.node,
-        parentIndex: PropTypes.number,
-        placeholder: PropTypes.string
+    this.state = {
+      open: isInitiallyOpen,
+      expanded: false,
+      searchThreshold: 20,
+      searchEnabled: false,
+      filterTerm: null,
     };
+  }
 
-    static defaultProps = {
-        className: ""
-    };
+  static propTypes = {
+    action: PropTypes.func.isRequired,
+    display: PropTypes.string.isRequired,
+    descriptionKey: PropTypes.string,
+    expandFilter: PropTypes.func,
+    expandTitle: PropTypes.string,
+    isInitiallyOpen: PropTypes.bool,
+    items: PropTypes.array,
+    remove: PropTypes.func,
+    selectedKey: PropTypes.string,
+    selectedValue: PropTypes.node,
+    parentIndex: PropTypes.number,
+    placeholder: PropTypes.string,
+  };
 
-    onClose() {
-        this.setState({
-            open: false,
-            expanded: false
-        });
-    }
+  static defaultProps = {
+    className: "",
+  };
+
+  onClose() {
+    this.setState({
+      open: false,
+      expanded: false,
+    });
+  }
 
-    _enableSearch() {
-        /*
+  _enableSearch() {
+    /*
         not showing search for now
         if(this.props.items.length > this.state.searchThreshold) {
             return true
@@ -62,187 +62,209 @@ export default class SelectionModule extends Component {
             return false
         }
         */
-        return false;
-    }
+    return false;
+  }
 
-    _toggleOpen() {
-        this.setState({
-            open: !this.state.open,
-            expanded: !this.state.open ? this.state.expanded : false
-        });
-    }
+  _toggleOpen() {
+    this.setState({
+      open: !this.state.open,
+      expanded: !this.state.open ? this.state.expanded : false,
+    });
+  }
 
-    _expand() {
-        this.setState({
-            expanded: true
-        });
-    }
+  _expand() {
+    this.setState({
+      expanded: true,
+    });
+  }
 
-    _isExpanded() {
-        if (this.state.expanded || !this.props.expandFilter) {
-            return true;
-        }
-        // if an item that is normally in the expansion is selected then show the expansion
-        for (var i = 0; i < this.props.items.length; i++) {
-            var item = this.props.items[i];
-            if (this._itemIsSelected(item) && !this.props.expandFilter(item, i)) {
-                return true;
-            }
-        }
-        return false;
+  _isExpanded() {
+    if (this.state.expanded || !this.props.expandFilter) {
+      return true;
+    }
+    // if an item that is normally in the expansion is selected then show the expansion
+    for (var i = 0; i < this.props.items.length; i++) {
+      var item = this.props.items[i];
+      if (this._itemIsSelected(item) && !this.props.expandFilter(item, i)) {
+        return true;
+      }
     }
+    return false;
+  }
+
+  _displayCustom(values) {
+    var custom = [];
+    this.props.children.forEach(function(element) {
+      var newElement = element;
+      newElement.props.children = values[newElement.props.content];
+      custom.push(element);
+    });
+    return custom;
+  }
+
+  _listItems(selection) {
+    if (this.props.items) {
+      var sourceItems = this.props.items;
 
-    _displayCustom(values) {
-        var custom = [];
-        this.props.children.forEach(function (element) {
-            var newElement = element;
-            newElement.props.children = values[newElement.props.content];
-            custom.push(element);
+      var isExpanded = this._isExpanded();
+      if (!isExpanded) {
+        sourceItems = sourceItems.filter(this.props.expandFilter);
+      }
+
+      var items = sourceItems.map(function(item, index) {
+        var display = item ? item[this.props.display] || item : item;
+        var itemClassName = cx({
+          SelectionItem: true,
+          "SelectionItem--selected": selection === display,
         });
-        return custom;
+        var description = null;
+        if (
+          this.props.descriptionKey &&
+          item &&
+          item[this.props.descriptionKey]
+        ) {
+          description = (
+            <div className="SelectionModule-description">
+              {item[this.props.descriptionKey]}
+            </div>
+          );
+        }
+        // if children are provided, use the custom layout display
+        return (
+          <li
+            className={itemClassName}
+            onClick={this._select.bind(null, item)}
+            key={index}
+          >
+            <Icon name="check" size={12} />
+            <div className="flex-full">
+              <div className="SelectionModule-display">{display}</div>
+              {description}
+            </div>
+          </li>
+        );
+      }, this);
+
+      if (!isExpanded && items.length !== this.props.items.length) {
+        items.push(
+          <li
+            className="SelectionItem border-top"
+            onClick={this._expand}
+            key="expand"
+          >
+            <Icon name="chevrondown" size={12} />
+            <div>
+              <div className="SelectionModule-display">
+                {this.props.expandedTitle || t`Advanced...`}
+              </div>
+            </div>
+          </li>,
+        );
+      }
+
+      return items;
+    } else {
+      return t`Sorry. Something went wrong.`;
     }
+  }
 
-    _listItems(selection) {
-        if (this.props.items) {
-            var sourceItems = this.props.items;
-
-            var isExpanded = this._isExpanded();
-            if (!isExpanded) {
-                sourceItems = sourceItems.filter(this.props.expandFilter);
-            }
-
-            var items = sourceItems.map(function (item, index) {
-                var display = (item) ? item[this.props.display] || item : item;
-                var itemClassName = cx({
-                    'SelectionItem' : true,
-                    'SelectionItem--selected': selection === display
-                });
-                var description = null;
-                if (this.props.descriptionKey && item && item[this.props.descriptionKey]) {
-                    description = (
-                        <div className="SelectionModule-description">
-                            {item[this.props.descriptionKey]}
-                        </div>
-                    );
-                }
-                // if children are provided, use the custom layout display
-                return (
-                    <li className={itemClassName} onClick={this._select.bind(null, item)} key={index}>
-                        <Icon name="check" size={12} />
-                        <div className="flex-full">
-                            <div className="SelectionModule-display">
-                                {display}
-                            </div>
-                            {description}
-                        </div>
-                    </li>
-                );
-            }, this);
-
-            if (!isExpanded && items.length !== this.props.items.length) {
-                items.push(
-                    <li className="SelectionItem border-top" onClick={this._expand} key="expand">
-                        <Icon name="chevrondown" size={12} />
-                        <div>
-                            <div className="SelectionModule-display">
-                                {this.props.expandedTitle || t`Advanced...`}
-                            </div>
-                        </div>
-                    </li>
-                );
-            }
-
-            return items;
+  _select(item) {
+    var index = this.props.index;
+    // send back the item with the specified action
+    if (this.props.action) {
+      if (index !== undefined) {
+        if (this.props.parentIndex) {
+          this.props.action(
+            item[this.props.selectedKey],
+            index,
+            this.props.parentIndex,
+          );
         } else {
-            return t`Sorry. Something went wrong.`;
+          this.props.action(item[this.props.selectedKey], index);
         }
+      } else {
+        this.props.action(item[this.props.selectedKey]);
+      }
     }
+    this._toggleOpen();
+  }
 
-    _select(item) {
-        var index = this.props.index;
-        // send back the item with the specified action
-        if (this.props.action) {
-            if (index !== undefined) {
-                if (this.props.parentIndex) {
-                    this.props.action(item[this.props.selectedKey], index, this.props.parentIndex);
-                } else {
-                    this.props.action(item[this.props.selectedKey], index);
-                }
-            } else {
-                this.props.action(item[this.props.selectedKey]);
-            }
-        }
-        this._toggleOpen();
-    }
+  _itemIsSelected(item) {
+    return (
+      item && _.isEqual(item[this.props.selectedKey], this.props.selectedValue)
+    );
+  }
 
-    _itemIsSelected(item) {
-        return item && _.isEqual(item[this.props.selectedKey], this.props.selectedValue);
-    }
+  renderPopover(selection) {
+    if (this.state.open) {
+      var itemListClasses = cx("SelectionItems", {
+        "SelectionItems--open": this.state.open,
+        "SelectionItems--expanded": this.state.expanded,
+      });
 
-    renderPopover(selection) {
-        if(this.state.open) {
-            var itemListClasses = cx("SelectionItems", {
-                'SelectionItems--open': this.state.open,
-                'SelectionItems--expanded': this.state.expanded
-            });
-
-            var searchBar;
-            if(this._enableSearch()) {
-                searchBar = <SearchBar onFilter={this._filterSelections} />;
-            }
-
-            return (
-                <Popover
-                    className={"SelectionModule " + this.props.className}
-                    onClose={this.onClose}
-                >
-                    <div className={itemListClasses}>
-                        {searchBar}
-                        <ul className="SelectionList scroll-show scroll-y">
-                            {this._listItems(selection)}
-                        </ul>
-                    </div>
-                </Popover>
-            );
-        }
+      var searchBar;
+      if (this._enableSearch()) {
+        searchBar = <SearchBar onFilter={this._filterSelections} />;
+      }
+
+      return (
+        <Popover
+          className={"SelectionModule " + this.props.className}
+          onClose={this.onClose}
+        >
+          <div className={itemListClasses}>
+            {searchBar}
+            <ul className="SelectionList scroll-show scroll-y">
+              {this._listItems(selection)}
+            </ul>
+          </div>
+        </Popover>
+      );
     }
+  }
 
-    render() {
-        var selection;
-        this.props.items.forEach(function (item) {
-            if (this._itemIsSelected(item)) {
-                selection = item[this.props.display];
-            }
-        }, this);
-
-        var placeholder = selection || this.props.placeholder,
-            remove,
-            removeable = !!this.props.remove;
-
-        var moduleClasses = cx({
-            'SelectionModule': true,
-            'selected': selection,
-            'removeable': removeable
-        });
+  render() {
+    var selection;
+    this.props.items.forEach(function(item) {
+      if (this._itemIsSelected(item)) {
+        selection = item[this.props.display];
+      }
+    }, this);
 
-        if(this.props.remove) {
-            remove = (
-                <a className="text-grey-2 no-decoration pr1 flex align-center" onClick={this.props.remove.bind(null, this.props.index)}>
-                    <Icon name='close' size={14} />
-                </a>
-            );
-        }
+    var placeholder = selection || this.props.placeholder,
+      remove,
+      removeable = !!this.props.remove;
 
-        return (
-            <div className={moduleClasses + " " + this.props.className}>
-                <div className="SelectionModule-trigger flex align-center">
-                    <a className="QueryOption p1 flex align-center" onClick={this._toggleOpen}>
-                        {placeholder}
-                    </a>
-                    {remove}
-                </div>
-                {this.renderPopover(selection)}
-            </div>
-        );
+    var moduleClasses = cx({
+      SelectionModule: true,
+      selected: selection,
+      removeable: removeable,
+    });
+
+    if (this.props.remove) {
+      remove = (
+        <a
+          className="text-grey-2 no-decoration pr1 flex align-center"
+          onClick={this.props.remove.bind(null, this.props.index)}
+        >
+          <Icon name="close" size={14} />
+        </a>
+      );
     }
+
+    return (
+      <div className={moduleClasses + " " + this.props.className}>
+        <div className="SelectionModule-trigger flex align-center">
+          <a
+            className="QueryOption p1 flex align-center"
+            onClick={this._toggleOpen}
+          >
+            {placeholder}
+          </a>
+          {remove}
+        </div>
+        {this.renderPopover(selection)}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/SortWidget.jsx b/frontend/src/metabase/query_builder/components/SortWidget.jsx
index cb9dcc9553196349ba41a95621e720d30e5a8c56..48fe4a840f1e7b5f1669667411e92aa270ecae54 100644
--- a/frontend/src/metabase/query_builder/components/SortWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/SortWidget.jsx
@@ -2,97 +2,97 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import Icon from "metabase/components/Icon.jsx";
-import FieldWidget from './FieldWidget.jsx';
-import SelectionModule from './SelectionModule.jsx';
+import FieldWidget from "./FieldWidget.jsx";
+import SelectionModule from "./SelectionModule.jsx";
 
 import _ from "underscore";
 
 export default class SortWidget extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        _.bindAll(this, "setDirection", "setField");
-    }
+    _.bindAll(this, "setDirection", "setField");
+  }
 
-    static propTypes = {
-        sort: PropTypes.array.isRequired,
-        fieldOptions: PropTypes.object.isRequired,
-        customFieldOptions: PropTypes.object,
-        tableName: PropTypes.string,
-        updateOrderBy: PropTypes.func.isRequired,
-        removeOrderBy: PropTypes.func.isRequired,
-        tableMetadata: PropTypes.object.isRequired
-    };
+  static propTypes = {
+    sort: PropTypes.array.isRequired,
+    fieldOptions: PropTypes.object.isRequired,
+    customFieldOptions: PropTypes.object,
+    tableName: PropTypes.string,
+    updateOrderBy: PropTypes.func.isRequired,
+    removeOrderBy: PropTypes.func.isRequired,
+    tableMetadata: PropTypes.object.isRequired,
+  };
 
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
-    }
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
 
-    componentWillReceiveProps(newProps) {
-        this.setState({
-            field: newProps.sort[0],           // id of the field
-            direction: newProps.sort[1]        // sort direction
-        });
-    }
+  componentWillReceiveProps(newProps) {
+    this.setState({
+      field: newProps.sort[0], // id of the field
+      direction: newProps.sort[1], // sort direction
+    });
+  }
 
-    componentWillUnmount() {
-        // Remove partially completed sort if the widget is removed
-        if (this.state.field == null || this.state.direction == null) {
-            this.props.removeOrderBy();
-        }
+  componentWillUnmount() {
+    // Remove partially completed sort if the widget is removed
+    if (this.state.field == null || this.state.direction == null) {
+      this.props.removeOrderBy();
     }
+  }
 
-    setField(value) {
-        if (this.state.field !== value) {
-            this.props.updateOrderBy([value, this.state.direction]);
-            // Optimistically set field state so componentWillUnmount logic works correctly
-            this.setState({ field: value });
-        }
+  setField(value) {
+    if (this.state.field !== value) {
+      this.props.updateOrderBy([value, this.state.direction]);
+      // Optimistically set field state so componentWillUnmount logic works correctly
+      this.setState({ field: value });
     }
+  }
 
-    setDirection(value) {
-        if (this.state.direction !== value) {
-            this.props.updateOrderBy([this.state.field, value]);
-            // Optimistically set direction state so componentWillUnmount logic works correctly
-            this.setState({ direction: value });
-        }
+  setDirection(value) {
+    if (this.state.direction !== value) {
+      this.props.updateOrderBy([this.state.field, value]);
+      // Optimistically set direction state so componentWillUnmount logic works correctly
+      this.setState({ direction: value });
     }
+  }
 
-    render() {
-        var directionOptions = [
-            {key: "ascending", val: "ascending"},
-            {key: "descending", val: "descending"},
-        ];
+  render() {
+    var directionOptions = [
+      { key: "ascending", val: "ascending" },
+      { key: "descending", val: "descending" },
+    ];
 
-        return (
-            <div className="flex align-center">
-                <FieldWidget
-                    query={this.props.query}
-                    className="Filter-section Filter-section-sort-field SelectionModule"
-                    tableMetadata={this.props.tableMetadata}
-                    field={this.state.field}
-                    fieldOptions={this.props.fieldOptions}
-                    customFieldOptions={this.props.customFieldOptions}
-                    setField={this.setField}
-                    isInitiallyOpen={this.state.field === null}
-                    enableSubDimensions={false}
-                />
+    return (
+      <div className="flex align-center">
+        <FieldWidget
+          query={this.props.query}
+          className="Filter-section Filter-section-sort-field SelectionModule"
+          tableMetadata={this.props.tableMetadata}
+          field={this.state.field}
+          fieldOptions={this.props.fieldOptions}
+          customFieldOptions={this.props.customFieldOptions}
+          setField={this.setField}
+          isInitiallyOpen={this.state.field === null}
+          enableSubDimensions={false}
+        />
 
-                <SelectionModule
-                    className="Filter-section Filter-section-sort-direction"
-                    placeholder="..."
-                    items={directionOptions}
-                    display="key"
-                    selectedValue={this.state.direction}
-                    selectedKey="val"
-                    isInitiallyOpen={false}
-                    action={this.setDirection}
-                />
+        <SelectionModule
+          className="Filter-section Filter-section-sort-direction"
+          placeholder="..."
+          items={directionOptions}
+          display="key"
+          selectedValue={this.state.direction}
+          selectedKey="val"
+          isInitiallyOpen={false}
+          action={this.setDirection}
+        />
 
-                <a onClick={this.props.removeOrderBy}>
-                    <Icon name='close' size={12} />
-                </a>
-            </div>
-        );
-    }
+        <a onClick={this.props.removeOrderBy}>
+          <Icon name="close" size={12} />
+        </a>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
index 580e4bb713cedf71a987e503daad6647a1c459d1..f9379d7bdae49da182da5a3f98bc22fe714dd18e 100644
--- a/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/TimeGroupingPopover.jsx
@@ -2,87 +2,101 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 
 import { parseFieldBucketing, formatBucketing } from "metabase/lib/query_time";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 const BUCKETINGS = [
-    "default",
-    "minute",
-    "hour",
-    "day",
-    "week",
-    "month",
-    "quarter",
-    "year",
-    null,
-    "minute-of-hour",
-    "hour-of-day",
-    "day-of-week",
-    "day-of-month",
-    "day-of-year",
-    "week-of-year",
-    "month-of-year",
-    "quarter-of-year",
+  "default",
+  "minute",
+  "hour",
+  "day",
+  "week",
+  "month",
+  "quarter",
+  "year",
+  null,
+  "minute-of-hour",
+  "hour-of-day",
+  "day-of-week",
+  "day-of-month",
+  "day-of-year",
+  "week-of-year",
+  "month-of-year",
+  "quarter-of-year",
 ];
 
 export default class TimeGroupingPopover extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {};
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {};
+  }
 
-    static propTypes = {
-        field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
-        onFieldChange: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    field: PropTypes.oneOfType([PropTypes.number, PropTypes.array]),
+    onFieldChange: PropTypes.func.isRequired,
+  };
 
-    static defaultProps = {
-        title: t`Group time by`,
-        groupingOptions: [
-            // "default",
-            "minute",
-            "hour",
-            "day",
-            "week",
-            "month",
-            "quarter",
-            "year",
-            // "minute-of-hour",
-            "hour-of-day",
-            "day-of-week",
-            "day-of-month",
-            // "day-of-year",
-            "week-of-year",
-            "month-of-year",
-            "quarter-of-year",
-        ]
-    }
+  static defaultProps = {
+    title: t`Group time by`,
+    groupingOptions: [
+      // "default",
+      "minute",
+      "hour",
+      "day",
+      "week",
+      "month",
+      "quarter",
+      "year",
+      // "minute-of-hour",
+      "hour-of-day",
+      "day-of-week",
+      "day-of-month",
+      // "day-of-year",
+      "week-of-year",
+      "month-of-year",
+      "quarter-of-year",
+    ],
+  };
 
-    setField(bucketing) {
-        this.props.onFieldChange(["datetime-field", this.props.field[1], "as", bucketing]);
-    }
+  setField(bucketing) {
+    this.props.onFieldChange([
+      "datetime-field",
+      this.props.field[1],
+      "as",
+      bucketing,
+    ]);
+  }
 
-    render() {
-        const { title, field, className, groupingOptions } = this.props;
-        const enabledOptions = new Set(groupingOptions);
-        return (
-            <div className={cx(className, "px2 py1")} style={{width:"250px"}}>
-                { title &&
-                    <h3 className="List-section-header pt1 mx2">{title}</h3>
-                }
-                <ul className="py1">
-                { BUCKETINGS.filter(o => o == null || enabledOptions.has(o)).map((bucketing, bucketingIndex) =>
-                    bucketing == null ?
-                        <hr key={bucketingIndex} style={{ "border": "none" }}/>
-                    :
-                        <li key={bucketingIndex} className={cx("List-item", { "List-item--selected": parseFieldBucketing(field) === bucketing })}>
-                            <a className="List-item-title full px2 py1 cursor-pointer" onClick={this.setField.bind(this, bucketing)}>
-                                {formatBucketing(bucketing)}
-                            </a>
-                        </li>
-                )}
-                </ul>
-            </div>
-        );
-    }
+  render() {
+    const { title, field, className, groupingOptions } = this.props;
+    const enabledOptions = new Set(groupingOptions);
+    return (
+      <div className={cx(className, "px2 py1")} style={{ width: "250px" }}>
+        {title && <h3 className="List-section-header pt1 mx2">{title}</h3>}
+        <ul className="py1">
+          {BUCKETINGS.filter(o => o == null || enabledOptions.has(o)).map(
+            (bucketing, bucketingIndex) =>
+              bucketing == null ? (
+                <hr key={bucketingIndex} style={{ border: "none" }} />
+              ) : (
+                <li
+                  key={bucketingIndex}
+                  className={cx("List-item", {
+                    "List-item--selected":
+                      parseFieldBucketing(field) === bucketing,
+                  })}
+                >
+                  <a
+                    className="List-item-title full px2 py1 cursor-pointer"
+                    onClick={this.setField.bind(this, bucketing)}
+                  >
+                    {formatBucketing(bucketing)}
+                  </a>
+                </li>
+              ),
+          )}
+        </ul>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/VisualizationError.jsx b/frontend/src/metabase/query_builder/components/VisualizationError.jsx
index 9dccff47adb05031e3938b790815f653917b4d81..9c26d23e35dcce0a310fdb76f4d2e870d17c9248 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationError.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationError.jsx
@@ -1,88 +1,114 @@
 /* eslint "react/prop-types": "warn" */
 
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import MetabaseSettings from "metabase/lib/settings";
-import VisualizationErrorMessage from './VisualizationErrorMessage';
+import VisualizationErrorMessage from "./VisualizationErrorMessage";
 
 const EmailAdmin = () => {
-  const adminEmail = MetabaseSettings.adminEmail()
-  return adminEmail && (
+  const adminEmail = MetabaseSettings.adminEmail();
+  return (
+    adminEmail && (
       <span className="QueryError-adminEmail">
-          <a className="no-decoration" href={`mailto:${adminEmail}`}>
-              {adminEmail}
-          </a>
+        <a className="no-decoration" href={`mailto:${adminEmail}`}>
+          {adminEmail}
+        </a>
       </span>
-  )
-}
+    )
+  );
+};
 
 class VisualizationError extends Component {
-
   constructor(props) {
-      super(props);
-      this.state = {
-          showError: false
-      }
+    super(props);
+    this.state = {
+      showError: false,
+    };
   }
   static propTypes = {
-      card:     PropTypes.object.isRequired,
-      duration: PropTypes.number.isRequired,
-      error:    PropTypes.object.isRequired,
-  }
+    card: PropTypes.object.isRequired,
+    duration: PropTypes.number.isRequired,
+    error: PropTypes.object.isRequired,
+  };
 
-  render () {
-      const { card, duration, error } = this.props
+  render() {
+    const { card, duration, error } = this.props;
 
-      if (error && typeof error.status === "number") {
-          // Assume if the request took more than 15 seconds it was due to a timeout
-          // Some platforms like Heroku return a 503 for numerous types of errors so we can't use the status code to distinguish between timeouts and other failures.
-          if (duration > 15*1000) {
-              return <VisualizationErrorMessage
-                        type="timeout"
-                        title={t`Your question took too long`}
-                        message={t`We didn't get an answer back from your database in time, so we had to stop. You can try again in a minute, or if the problem persists, you can email an admin to let them know.`}
-                        action={<EmailAdmin />}
-                    />
-          } else {
-              return <VisualizationErrorMessage
-                        type="serverError"
-                        title={t`We're experiencing server issues`}
-                        message={t`Try refreshing the page after waiting a minute or two. If the problem persists we'd recommend you contact an admin.`}
-                        action={<EmailAdmin />}
-                    />
-          }
-      } else if (card && card.dataset_query && card.dataset_query.type === 'native') {
-          // always show errors for native queries
-          return (
-              <div className="QueryError flex full align-center text-error">
-                  <div className="QueryError-iconWrapper">
-                      <svg className="QueryError-icon" viewBox="0 0 32 32" width="64" height="64" fill="currentcolor">
-                          <path d="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 "></path>
-                      </svg>
-                  </div>
-                  <span className="QueryError-message">{error}</span>
-              </div>
-          );
+    if (error && typeof error.status === "number") {
+      // Assume if the request took more than 15 seconds it was due to a timeout
+      // Some platforms like Heroku return a 503 for numerous types of errors so we can't use the status code to distinguish between timeouts and other failures.
+      if (duration > 15 * 1000) {
+        return (
+          <VisualizationErrorMessage
+            type="timeout"
+            title={t`Your question took too long`}
+            message={t`We didn't get an answer back from your database in time, so we had to stop. You can try again in a minute, or if the problem persists, you can email an admin to let them know.`}
+            action={<EmailAdmin />}
+          />
+        );
       } else {
-          return (
-              <div className="QueryError2 flex full justify-center">
-                  <div className="QueryError-image QueryError-image--queryError mr4" />
-                  <div className="QueryError2-details">
-                      <h1 className="text-bold">{t`There was a problem with your question`}</h1>
-                      <p className="QueryError-messageText">{t`Most of the time this is caused by an invalid selection or bad input value. Double check your inputs and retry your query.`}</p>
-                      <div className="pt2">
-                          <a onClick={() => this.setState({ showError: true })} className="link cursor-pointer">{t`Show error details`}</a>
-                      </div>
-                      <div style={{ display: this.state.showError? 'inherit': 'none'}} className="pt3 text-left">
-                          <h2>{t`Here's the full error message`}</h2>
-                          <div style={{fontFamily: "monospace"}} className="QueryError2-detailBody bordered rounded bg-grey-0 text-bold p2 mt1">{error}</div>
-                      </div>
-                  </div>
-              </div>
-          );
+        return (
+          <VisualizationErrorMessage
+            type="serverError"
+            title={t`We're experiencing server issues`}
+            message={t`Try refreshing the page after waiting a minute or two. If the problem persists we'd recommend you contact an admin.`}
+            action={<EmailAdmin />}
+          />
+        );
       }
+    } else if (
+      card &&
+      card.dataset_query &&
+      card.dataset_query.type === "native"
+    ) {
+      // always show errors for native queries
+      return (
+        <div className="QueryError flex full align-center text-error">
+          <div className="QueryError-iconWrapper">
+            <svg
+              className="QueryError-icon"
+              viewBox="0 0 32 32"
+              width="64"
+              height="64"
+              fill="currentcolor"
+            >
+              <path d="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 " />
+            </svg>
+          </div>
+          <span className="QueryError-message">{error}</span>
+        </div>
+      );
+    } else {
+      return (
+        <div className="QueryError2 flex full justify-center">
+          <div className="QueryError-image QueryError-image--queryError mr4" />
+          <div className="QueryError2-details">
+            <h1 className="text-bold">{t`There was a problem with your question`}</h1>
+            <p className="QueryError-messageText">{t`Most of the time this is caused by an invalid selection or bad input value. Double check your inputs and retry your query.`}</p>
+            <div className="pt2">
+              <a
+                onClick={() => this.setState({ showError: true })}
+                className="link cursor-pointer"
+              >{t`Show error details`}</a>
+            </div>
+            <div
+              style={{ display: this.state.showError ? "inherit" : "none" }}
+              className="pt3 text-left"
+            >
+              <h2>{t`Here's the full error message`}</h2>
+              <div
+                style={{ fontFamily: "monospace" }}
+                className="QueryError2-detailBody bordered rounded bg-grey-0 text-bold p2 mt1"
+              >
+                {error}
+              </div>
+            </div>
+          </div>
+        </div>
+      );
+    }
   }
 }
 
-export default VisualizationError
+export default VisualizationError;
diff --git a/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx b/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx
index 47fb7cdcc6c120e08df7f108b272ec21fc30df60..41ec6628e7962070089998bcde40dfaeb2e5d573 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationErrorMessage.jsx
@@ -1,26 +1,26 @@
 /* eslint "react/prop-types": "warn" */
 
-import React from 'react';
+import React from "react";
 import PropTypes from "prop-types";
 
-const VisualizationErrorMessage = ({title, type, message, action}) => {
-    return (
-        <div className="QueryError flex full align-center">
-            <div className={`QueryError-image QueryError-image--${type}`}></div>
-            <div className="QueryError-message text-centered">
-                { title && <h1 className="text-bold">{title}</h1> }
-                <p className="QueryError-messageText">{message}</p>
-                {action}
-            </div>
-        </div>
-    )
-}
+const VisualizationErrorMessage = ({ title, type, message, action }) => {
+  return (
+    <div className="QueryError flex full align-center">
+      <div className={`QueryError-image QueryError-image--${type}`} />
+      <div className="QueryError-message text-centered">
+        {title && <h1 className="text-bold">{title}</h1>}
+        <p className="QueryError-messageText">{message}</p>
+        {action}
+      </div>
+    </div>
+  );
+};
 
 VisualizationErrorMessage.propTypes = {
-  title:    PropTypes.string.isRequired,
-  type:     PropTypes.string.isRequired,
-  message:  PropTypes.string.isRequired,
-  action:   PropTypes.node
-}
+  title: PropTypes.string.isRequired,
+  type: PropTypes.string.isRequired,
+  message: PropTypes.string.isRequired,
+  action: PropTypes.node,
+};
 
 export default VisualizationErrorMessage;
diff --git a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
index 68b4901269163a20513427cd59ee61cdc2791215..fc3abe19269981bcaa524db43927fd763e7d5bc8 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationResult.jsx
@@ -1,7 +1,9 @@
 /* eslint "react/prop-types": "warn" */
 
 import React from "react";
-import VisualizationErrorMessage from './VisualizationErrorMessage';
+import { t, jt } from "c-3po";
+
+import VisualizationErrorMessage from "./VisualizationErrorMessage";
 import Visualization from "metabase/visualizations/components/Visualization.jsx";
 import { datasetContainsNoResults } from "metabase/lib/dataset";
 import { DatasetQuery } from "metabase/meta/types/Card";
@@ -11,72 +13,94 @@ import Modal from "metabase/components/Modal";
 import { ALERT_TYPE_ROWS } from "metabase-lib/lib/Alert";
 
 type Props = {
-    question: Question,
-    isObjectDetail: boolean,
-    result: any,
-    results: any[],
-    isDirty: boolean,
-    lastRunDatasetQuery: DatasetQuery,
-    navigateToNewCardInsideQB: (any) => void,
-    rawSeries: any
-}
+  question: Question,
+  isObjectDetail: boolean,
+  result: any,
+  results: any[],
+  isDirty: boolean,
+  lastRunDatasetQuery: DatasetQuery,
+  navigateToNewCardInsideQB: any => void,
+  rawSeries: any,
+};
 
 export default class VisualizationResult extends Component {
-    props: Props
-    state = {
-        showCreateAlertModal: false
-    }
+  props: Props;
+  state = {
+    showCreateAlertModal: false,
+  };
 
-    showCreateAlertModal = () => {
-        this.setState({ showCreateAlertModal: true })
-    }
+  showCreateAlertModal = () => {
+    this.setState({ showCreateAlertModal: true });
+  };
 
-    onCloseCreateAlertModal = () =>  {
-        this.setState({ showCreateAlertModal: false })
-    }
+  onCloseCreateAlertModal = () => {
+    this.setState({ showCreateAlertModal: false });
+  };
 
-    render() {
-        const { question, isDirty, navigateToNewCardInsideQB, result, rawSeries, ...props } = this.props
-        const { showCreateAlertModal } = this.state
+  render() {
+    const {
+      question,
+      isDirty,
+      navigateToNewCardInsideQB,
+      result,
+      rawSeries,
+      ...props
+    } = this.props;
+    const { showCreateAlertModal } = this.state;
 
-        const noResults = datasetContainsNoResults(result.data);
-        if (noResults) {
-            const supportsRowsPresentAlert = question.alertType() === ALERT_TYPE_ROWS
+    const noResults = datasetContainsNoResults(result.data);
+    if (noResults) {
+      const supportsRowsPresentAlert = question.alertType() === ALERT_TYPE_ROWS;
 
-            // successful query but there were 0 rows returned with the result
-            return <div className="flex flex-full">
-                <VisualizationErrorMessage
-                    type='noRows'
-                    title='No results!'
-                    message={t`This may be the answer you’re looking for. If not, try removing or changing your filters to make them less specific.`}
-                    action={
-                        <div>
-                            { supportsRowsPresentAlert && !isDirty && (
-                                <p>
-                                    {jt`You can also ${<a className="link" onClick={this.showCreateAlertModal}>get an alert</a>} when there are any results.`}
-                                </p>
-                                )}
-                            <button className="Button" onClick={() => window.history.back() }>
-                                {t`Back to last run`}
-                            </button>
-                        </div>
-                    }
-                />
-                { showCreateAlertModal && <Modal full onClose={this.onCloseCreateAlertModal}>
-                    <CreateAlertModalContent onCancel={this.onCloseCreateAlertModal} onAlertCreated={this.onCloseCreateAlertModal} />
-                </Modal> }
-            </div>
-        } else {
-            return (
-                <Visualization
-                    rawSeries={rawSeries}
-                    onChangeCardAndRun={navigateToNewCardInsideQB}
-                    isEditing={true}
-                    card={question.card()}
-                    // Table:
-                    {...props}
-                />
-            )
-        }
+      // successful query but there were 0 rows returned with the result
+      return (
+        <div className="flex flex-full">
+          <VisualizationErrorMessage
+            type="noRows"
+            title="No results!"
+            message={t`This may be the answer you’re looking for. If not, try removing or changing your filters to make them less specific.`}
+            action={
+              <div>
+                {supportsRowsPresentAlert &&
+                  !isDirty && (
+                    <p>
+                      {jt`You can also ${(
+                        <a className="link" onClick={this.showCreateAlertModal}>
+                          get an alert
+                        </a>
+                      )} when there are any results.`}
+                    </p>
+                  )}
+                <button
+                  className="Button"
+                  onClick={() => window.history.back()}
+                >
+                  {t`Back to last run`}
+                </button>
+              </div>
+            }
+          />
+          {showCreateAlertModal && (
+            <Modal full onClose={this.onCloseCreateAlertModal}>
+              <CreateAlertModalContent
+                onCancel={this.onCloseCreateAlertModal}
+                onAlertCreated={this.onCloseCreateAlertModal}
+              />
+            </Modal>
+          )}
+        </div>
+      );
+    } else {
+      return (
+        <Visualization
+          rawSeries={rawSeries}
+          onChangeCardAndRun={navigateToNewCardInsideQB}
+          isEditing={true}
+          card={question.card()}
+          // Table:
+          {...props}
+        />
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
index da1f76b73e816d5c49b05630256c6bd41053441f..896677dd22c70e104afb92dbf9074453bca067fb 100644
--- a/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
+++ b/frontend/src/metabase/query_builder/components/VisualizationSettings.jsx
@@ -1,6 +1,6 @@
 import React from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 import PopoverWithTrigger from "metabase/components/PopoverWithTrigger.jsx";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
@@ -12,106 +12,113 @@ import visualizations, { getVisualizationRaw } from "metabase/visualizations";
 import cx from "classnames";
 
 export default class VisualizationSettings extends React.Component {
-    constructor(props, context) {
-        super(props, context);
-    }
+  constructor(props, context) {
+    super(props, context);
+  }
 
-    static propTypes = {
-        card: PropTypes.object.isRequired,
-        result: PropTypes.object,
-        setDisplayFn: PropTypes.func.isRequired,
-        onUpdateVisualizationSettings: PropTypes.func.isRequired,
-        onReplaceAllVisualizationSettings: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    card: PropTypes.object.isRequired,
+    result: PropTypes.object,
+    setDisplayFn: PropTypes.func.isRequired,
+    onUpdateVisualizationSettings: PropTypes.func.isRequired,
+    onReplaceAllVisualizationSettings: PropTypes.func.isRequired,
+  };
 
-    setDisplay = (type) => {
-        // notify our parent about our change
-        this.props.setDisplayFn(type);
-        this.refs.displayPopover.toggle();
-    }
+  setDisplay = type => {
+    // notify our parent about our change
+    this.props.setDisplayFn(type);
+    this.refs.displayPopover.toggle();
+  };
 
-    renderChartTypePicker() {
-        let { result, card } = this.props;
-        let { CardVisualization } = getVisualizationRaw([{ card, data: result.data }]);
+  renderChartTypePicker() {
+    let { result, card } = this.props;
+    let { CardVisualization } = getVisualizationRaw([
+      { card, data: result.data },
+    ]);
 
-        var triggerElement = (
-            <span className="px2 py1 text-bold cursor-pointer text-default flex align-center">
-                <Icon
-                    className="mr1"
-                    name={CardVisualization.iconName}
-                    size={12}
-                />
-                {CardVisualization.uiName}
-                <Icon className="ml1" name="chevrondown" size={8} />
-            </span>
-        );
+    var triggerElement = (
+      <span className="px2 py1 text-bold cursor-pointer text-default flex align-center">
+        <Icon className="mr1" name={CardVisualization.iconName} size={12} />
+        {CardVisualization.uiName}
+        <Icon className="ml1" name="chevrondown" size={8} />
+      </span>
+    );
 
-        return (
-            <div className="relative">
-                <span
-                    className="GuiBuilder-section-label pl0 Query-label"
-                    style={{ marginLeft: 4 }}
-                >
-                    {t`Visualization`}
-                </span>
-                <PopoverWithTrigger
-                    id="VisualizationPopover"
-                    ref="displayPopover"
-                    className="ChartType-popover"
-                    triggerId="VisualizationTrigger"
-                    triggerElement={triggerElement}
-                    triggerClasses="flex align-center"
-                    sizeToFit
-                >
-                    <ul className="pt1 pb1">
-                        { Array.from(visualizations).map(([vizType, viz], index) =>
-                            <li
-                                key={index}
-                                className={cx('p2 flex align-center cursor-pointer bg-brand-hover text-white-hover', {
-                                    'ChartType--selected': vizType === card.display,
-                                    'ChartType--notSensible': !(result && result.data && viz.isSensible && viz.isSensible(result.data.cols, result.data.rows)),
-                                    'hide': viz.hidden
-                                })}
-                                onClick={this.setDisplay.bind(null, vizType)}
-                            >
-                                <Icon name={viz.iconName} size={12} />
-                                <span className="ml1">{viz.uiName}</span>
-                            </li>
-                        )}
-                    </ul>
-                </PopoverWithTrigger>
-            </div>
-        );
-    }
+    return (
+      <div className="relative">
+        <span
+          className="GuiBuilder-section-label pl0 Query-label"
+          style={{ marginLeft: 4 }}
+        >
+          {t`Visualization`}
+        </span>
+        <PopoverWithTrigger
+          id="VisualizationPopover"
+          ref="displayPopover"
+          className="ChartType-popover"
+          triggerId="VisualizationTrigger"
+          triggerElement={triggerElement}
+          triggerClasses="flex align-center"
+          sizeToFit
+        >
+          <ul className="pt1 pb1">
+            {Array.from(visualizations).map(([vizType, viz], index) => (
+              <li
+                key={index}
+                className={cx(
+                  "p2 flex align-center cursor-pointer bg-brand-hover text-white-hover",
+                  {
+                    "ChartType--selected": vizType === card.display,
+                    "ChartType--notSensible": !(
+                      result &&
+                      result.data &&
+                      viz.isSensible &&
+                      viz.isSensible(result.data.cols, result.data.rows)
+                    ),
+                    hide: viz.hidden,
+                  },
+                )}
+                onClick={this.setDisplay.bind(null, vizType)}
+              >
+                <Icon name={viz.iconName} size={12} />
+                <span className="ml1">{viz.uiName}</span>
+              </li>
+            ))}
+          </ul>
+        </PopoverWithTrigger>
+      </div>
+    );
+  }
 
-    open = () => {
-        this.refs.popover.open();
-    }
+  open = () => {
+    this.refs.popover.open();
+  };
 
-    render() {
-        if (this.props.result && this.props.result.error === undefined) {
-            return (
-                <div className="VisualizationSettings flex align-center">
-                    {this.renderChartTypePicker()}
-                    <ModalWithTrigger
-                        wide tall
-                        triggerElement={
-                            <span data-metabase-event="Query Builder;Chart Settings">
-                                <Icon name="gear"/>
-                            </span>
-                        }
-                        triggerClasses="text-brand-hover"
-                        ref="popover"
-                    >
-                        <ChartSettings
-                            series={[{ card: this.props.card, data: this.props.result.data }]}
-                            onChange={this.props.onReplaceAllVisualizationSettings}
-                        />
-                    </ModalWithTrigger>
-                </div>
-            );
-        } else {
-            return false;
-        }
+  render() {
+    if (this.props.result && this.props.result.error === undefined) {
+      return (
+        <div className="VisualizationSettings flex align-center">
+          {this.renderChartTypePicker()}
+          <ModalWithTrigger
+            wide
+            tall
+            triggerElement={
+              <span data-metabase-event="Query Builder;Chart Settings">
+                <Icon name="gear" />
+              </span>
+            }
+            triggerClasses="text-brand-hover"
+            ref="popover"
+          >
+            <ChartSettings
+              series={[{ card: this.props.card, data: this.props.result.data }]}
+              onChange={this.props.onReplaceAllVisualizationSettings}
+            />
+          </ModalWithTrigger>
+        </div>
+      );
+    } else {
+      return false;
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/Warnings.jsx b/frontend/src/metabase/query_builder/components/Warnings.jsx
index 97d41970bf28ec0d1d5ca54d787d57a270a1341f..c8c8e726576a4f237e353323ac10df08074e80d0 100644
--- a/frontend/src/metabase/query_builder/components/Warnings.jsx
+++ b/frontend/src/metabase/query_builder/components/Warnings.jsx
@@ -4,25 +4,24 @@ import Tooltip from "metabase/components/Tooltip.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
 const Warnings = ({ warnings, className, size = 16 }) => {
-    if (!warnings || warnings.length === 0) {
-        return null;
-    }
-    const tooltip = (
-        <ul className="px2 pt2 pb1" style={{ maxWidth: 350 }}>
-            {warnings.map((warning) =>
-                <li className="pb1" key={warning}>
-                    {warning}
-                </li>
-            )}
-        </ul>
-    );
-
-    return (
-        <Tooltip tooltip={tooltip}>
-            <Icon className={className} name="warning2" size={size} />
-        </Tooltip>
-    )
-}
+  if (!warnings || warnings.length === 0) {
+    return null;
+  }
+  const tooltip = (
+    <ul className="px2 pt2 pb1" style={{ maxWidth: 350 }}>
+      {warnings.map(warning => (
+        <li className="pb1" key={warning}>
+          {warning}
+        </li>
+      ))}
+    </ul>
+  );
 
+  return (
+    <Tooltip tooltip={tooltip}>
+      <Icon className={className} name="warning2" size={size} />
+    </Tooltip>
+  );
+};
 
 export default Warnings;
diff --git a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
index 7da4f03cb7e31d3e36a6b4eeb466c590c9ae7960..2480a3a9e6cc95ca412b4f998da2a88307ba19f6 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DataReference.jsx
@@ -1,98 +1,110 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
-import MainPane from './MainPane.jsx';
-import TablePane from './TablePane.jsx';
-import FieldPane from './FieldPane.jsx';
-import SegmentPane from './SegmentPane.jsx';
-import MetricPane from './MetricPane.jsx';
+import { t } from "c-3po";
+import MainPane from "./MainPane.jsx";
+import TablePane from "./TablePane.jsx";
+import FieldPane from "./FieldPane.jsx";
+import SegmentPane from "./SegmentPane.jsx";
+import MetricPane from "./MetricPane.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
 import _ from "underscore";
 
 export default class DataReference extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
 
-        this.state = {
-            stack: [],
-            tables: {},
-            fields: {}
-        };
-
-        _.bindAll(this, "back", "close", "show");
-    }
-
-    static propTypes = {
-        query: PropTypes.object.isRequired,
-        onClose: PropTypes.func.isRequired,
-        runQuestionQuery: PropTypes.func.isRequired,
-        setDatasetQuery: PropTypes.func.isRequired,
-        setDatabaseFn: PropTypes.func.isRequired,
-        setSourceTableFn: PropTypes.func.isRequired,
-        setDisplayFn: PropTypes.func.isRequired
+    this.state = {
+      stack: [],
+      tables: {},
+      fields: {},
     };
 
-    close() {
-        this.props.onClose();
-    }
+    _.bindAll(this, "back", "close", "show");
+  }
 
-    back() {
-        this.setState({
-            stack: this.state.stack.slice(0, -1)
-        });
-    }
+  static propTypes = {
+    query: PropTypes.object.isRequired,
+    onClose: PropTypes.func.isRequired,
+    runQuestionQuery: PropTypes.func.isRequired,
+    setDatasetQuery: PropTypes.func.isRequired,
+    setDatabaseFn: PropTypes.func.isRequired,
+    setSourceTableFn: PropTypes.func.isRequired,
+    setDisplayFn: PropTypes.func.isRequired,
+  };
 
-    show(type, item) {
-        this.setState({
-            stack: this.state.stack.concat({ type, item })
-        });
-    }
+  close() {
+    this.props.onClose();
+  }
 
-    render() {
-        var content;
-        if (this.state.stack.length === 0) {
-            content = <MainPane {...this.props} show={this.show} />
-        } else {
-            var page = this.state.stack[this.state.stack.length - 1];
-            if (page.type === "table") {
-                content = <TablePane {...this.props} show={this.show} table={page.item} />
-            } else if (page.type === "field") {
-                content = <FieldPane {...this.props} show={this.show} field={page.item}/>
-            } else if (page.type === "segment") {
-                content = <SegmentPane {...this.props} show={this.show} segment={page.item}/>
-            } else if (page.type === "metric") {
-                content = <MetricPane {...this.props} show={this.show} metric={page.item}/>
-            }
-        }
+  back() {
+    this.setState({
+      stack: this.state.stack.slice(0, -1),
+    });
+  }
 
-        var backButton;
-        if (this.state.stack.length > 0) {
-            backButton = (
-                <a className="flex align-center mb2 text-default text-brand-hover no-decoration" onClick={this.back}>
-                    <Icon name="chevronleft" size={18} />
-                    <span className="text-uppercase">{t`Back`}</span>
-                </a>
-            )
-        }
+  show(type, item) {
+    this.setState({
+      stack: this.state.stack.concat({ type, item }),
+    });
+  }
 
-        var closeButton = (
-            <a className="flex-align-right text-default text-brand-hover no-decoration" onClick={this.close}>
-                <Icon name="close" size={18} />
-            </a>
+  render() {
+    var content;
+    if (this.state.stack.length === 0) {
+      content = <MainPane {...this.props} show={this.show} />;
+    } else {
+      var page = this.state.stack[this.state.stack.length - 1];
+      if (page.type === "table") {
+        content = (
+          <TablePane {...this.props} show={this.show} table={page.item} />
         );
-
-        return (
-            <div className="DataReference-container p3 full-height scroll-y">
-                <div className="DataReference-header flex mb1">
-                    {backButton}
-                    {closeButton}
-                </div>
-                <div className="DataReference-content">
-                    {content}
-                </div>
-            </div>
+      } else if (page.type === "field") {
+        content = (
+          <FieldPane {...this.props} show={this.show} field={page.item} />
         );
+      } else if (page.type === "segment") {
+        content = (
+          <SegmentPane {...this.props} show={this.show} segment={page.item} />
+        );
+      } else if (page.type === "metric") {
+        content = (
+          <MetricPane {...this.props} show={this.show} metric={page.item} />
+        );
+      }
+    }
+
+    var backButton;
+    if (this.state.stack.length > 0) {
+      backButton = (
+        <a
+          className="flex align-center mb2 text-default text-brand-hover no-decoration"
+          onClick={this.back}
+        >
+          <Icon name="chevronleft" size={18} />
+          <span className="text-uppercase">{t`Back`}</span>
+        </a>
+      );
     }
+
+    var closeButton = (
+      <a
+        className="flex-align-right text-default text-brand-hover no-decoration"
+        onClick={this.close}
+      >
+        <Icon name="close" size={18} />
+      </a>
+    );
+
+    return (
+      <div className="DataReference-container p3 full-height scroll-y">
+        <div className="DataReference-header flex mb1">
+          {backButton}
+          {closeButton}
+        </div>
+        <div className="DataReference-content">{content}</div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
index fe18456d42bfc7394569e5d00ed94c47f8e23082..31d69223eddca7e3bec996ed2247a30b247221ae 100644
--- a/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/DetailPane.jsx
@@ -1,49 +1,56 @@
 /* eslint "react/prop-types": "warn" */
 import React from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
-const DetailPane = ({ name, description, usefulQuestions, useForCurrentQuestion, extra }) =>
-    <div>
-        <h1>{name}</h1>
-        <p className={cx({ "text-grey-3": !description })}>
-            {description || t`No description set.`}
-        </p>
-        { useForCurrentQuestion && useForCurrentQuestion.length > 0 ?
-            <div className="py1">
-                <p className="text-bold">{t`Use for current question`}</p>
-                <ul className="my2">
-                {useForCurrentQuestion.map((item, index) =>
-                    <li className="mt1" key={index}>
-                        {item}
-                    </li>
-                )}
-                </ul>
-            </div>
-        : null }
-        { usefulQuestions && usefulQuestions.length > 0 ?
-            <div className="py1">
-                <p className="text-bold">{t`Potentially useful questions`}</p>
-                <ul>
-                {usefulQuestions.map((item, index) =>
-                    <li className="border-row-divider" key={index}>
-                        {item}
-                    </li>
-                )}
-                </ul>
-            </div>
-        : null }
-        {extra}
-    </div>
+const DetailPane = ({
+  name,
+  description,
+  usefulQuestions,
+  useForCurrentQuestion,
+  extra,
+}) => (
+  <div>
+    <h1>{name}</h1>
+    <p className={cx({ "text-grey-3": !description })}>
+      {description || t`No description set.`}
+    </p>
+    {useForCurrentQuestion && useForCurrentQuestion.length > 0 ? (
+      <div className="py1">
+        <p className="text-bold">{t`Use for current question`}</p>
+        <ul className="my2">
+          {useForCurrentQuestion.map((item, index) => (
+            <li className="mt1" key={index}>
+              {item}
+            </li>
+          ))}
+        </ul>
+      </div>
+    ) : null}
+    {usefulQuestions && usefulQuestions.length > 0 ? (
+      <div className="py1">
+        <p className="text-bold">{t`Potentially useful questions`}</p>
+        <ul>
+          {usefulQuestions.map((item, index) => (
+            <li className="border-row-divider" key={index}>
+              {item}
+            </li>
+          ))}
+        </ul>
+      </div>
+    ) : null}
+    {extra}
+  </div>
+);
 
 DetailPane.propTypes = {
-    name: PropTypes.string.isRequired,
-    description: PropTypes.string,
-    error: PropTypes.string,
-    useForCurrentQuestion: PropTypes.array,
-    usefulQuestions: PropTypes.array,
-    extra: PropTypes.element
-}
+  name: PropTypes.string.isRequired,
+  description: PropTypes.string,
+  error: PropTypes.string,
+  useForCurrentQuestion: PropTypes.array,
+  usefulQuestions: PropTypes.array,
+  extra: PropTypes.element,
+};
 
 export default DetailPane;
diff --git a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
index 7fed60cb8fadcd05d6446e6ff18e516b9ebd085b..9c955d4f2a45f27681831543cb6e731891e86101 100644
--- a/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/FieldPane.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
 import UseForButton from "./UseForButton.jsx";
@@ -11,7 +11,7 @@ import { getMetadata } from "metabase/selectors/metadata";
 import { createCard } from "metabase/lib/card";
 import Query, { createQuery } from "metabase/lib/query";
 import { isDimension, isSummable } from "metabase/lib/schema_metadata";
-import inflection from 'inflection';
+import inflection from "inflection";
 
 import _ from "underscore";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
@@ -19,155 +19,188 @@ import { connect } from "react-redux";
 import Dimension from "metabase-lib/lib/Dimension";
 
 const mapDispatchToProps = {
-    fetchTableMetadata,
+  fetchTableMetadata,
 };
 
 const mapStateToProps = (state, props) => ({
-    metadata: getMetadata(state, props)
-})
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class FieldPane extends Component {
-    constructor(props, context) {
-        super(props, context);
+  constructor(props, context) {
+    super(props, context);
+  }
+
+  static propTypes = {
+    field: PropTypes.object.isRequired,
+    datasetQuery: PropTypes.object,
+    question: PropTypes.object,
+    originalQuestion: PropTypes.object,
+    metadata: PropTypes.object,
+    fetchTableMetadata: PropTypes.func.isRequired,
+    runQuestionQuery: PropTypes.func.isRequired,
+    setDatasetQuery: PropTypes.func.isRequired,
+    setCardAndRun: PropTypes.func.isRequired,
+    updateQuestion: PropTypes.func.isRequired,
+  };
+
+  componentWillMount() {
+    this.props.fetchTableMetadata(this.props.field.table_id);
+  }
+
+  // See the note in render() method about filterBy
+  // filterBy() {
+  //     var datasetQuery = this.setDatabaseAndTable();
+  //     // Add an aggregation so both aggregation and filter popovers aren't visible
+  //     if (!Query.hasValidAggregation(datasetQuery.query)) {
+  //         Query.clearAggregations(datasetQuery.query);
+  //     }
+  //     Query.addFilter(datasetQuery.query, [null, this.props.field.id, null]);
+  //     this.props.setDatasetQuery(datasetQuery);
+  // }
+
+  groupBy = () => {
+    let { question, metadata, field } = this.props;
+    let query = question.query();
+
+    if (query instanceof StructuredQuery) {
+      // Add an aggregation so both aggregation and filter popovers aren't visible
+      if (!Query.hasValidAggregation(query.datasetQuery().query)) {
+        query = query.clearAggregations();
+      }
+
+      const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
+      query = query.addBreakout(defaultBreakout);
+
+      this.props.updateQuestion(query.question());
+      this.props.runQuestionQuery();
     }
-
-    static propTypes = {
-        field: PropTypes.object.isRequired,
-        datasetQuery: PropTypes.object,
-        question: PropTypes.object,
-        originalQuestion: PropTypes.object,
-        metadata: PropTypes.object,
-        fetchTableMetadata: PropTypes.func.isRequired,
-        runQuestionQuery: PropTypes.func.isRequired,
-        setDatasetQuery: PropTypes.func.isRequired,
-        setCardAndRun: PropTypes.func.isRequired,
-        updateQuestion: PropTypes.func.isRequired
-    };
-
-    componentWillMount() {
-        this.props.fetchTableMetadata(this.props.field.table_id);
-    }
-
-    // See the note in render() method about filterBy
-    // filterBy() {
-    //     var datasetQuery = this.setDatabaseAndTable();
-    //     // Add an aggregation so both aggregation and filter popovers aren't visible
-    //     if (!Query.hasValidAggregation(datasetQuery.query)) {
-    //         Query.clearAggregations(datasetQuery.query);
-    //     }
-    //     Query.addFilter(datasetQuery.query, [null, this.props.field.id, null]);
-    //     this.props.setDatasetQuery(datasetQuery);
-    // }
-
-    groupBy = () => {
-
-        let { question, metadata, field } = this.props;
-        let query = question.query();
-
-        if (query instanceof StructuredQuery) {
-            // Add an aggregation so both aggregation and filter popovers aren't visible
-            if (!Query.hasValidAggregation(query.datasetQuery().query)) {
-                query = query.clearAggregations()
-            }
-
-            const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
-            query = query.addBreakout(defaultBreakout);
-
-            this.props.updateQuestion(query.question())
-            this.props.runQuestionQuery();
-        }
-    }
-
-    newCard = () => {
-        const { metadata, field } = this.props;
-        const tableId = field.table_id;
-        const dbId = metadata.tables[tableId].database.id;
-
-        let card = createCard();
-        card.dataset_query = createQuery("query", dbId, tableId);
-        return card;
-    }
-
-    setQuerySum = () => {
-        let card = this.newCard();
-        card.dataset_query.query.aggregation = ["sum", this.props.field.id];
-        this.props.setCardAndRun(card);
+  };
+
+  newCard = () => {
+    const { metadata, field } = this.props;
+    const tableId = field.table_id;
+    const dbId = metadata.tables[tableId].database.id;
+
+    let card = createCard();
+    card.dataset_query = createQuery("query", dbId, tableId);
+    return card;
+  };
+
+  setQuerySum = () => {
+    let card = this.newCard();
+    card.dataset_query.query.aggregation = ["sum", this.props.field.id];
+    this.props.setCardAndRun(card);
+  };
+
+  setQueryDistinct = () => {
+    const { metadata, field } = this.props;
+    const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
+
+    let card = this.newCard();
+    card.dataset_query.query.aggregation = ["rows"];
+    card.dataset_query.query.breakout = [defaultBreakout];
+    this.props.setCardAndRun(card);
+  };
+
+  setQueryCountGroupedBy = chartType => {
+    const { metadata, field } = this.props;
+    const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
+
+    let card = this.newCard();
+    card.dataset_query.query.aggregation = ["count"];
+    card.dataset_query.query.breakout = [defaultBreakout];
+    card.display = chartType;
+    this.props.setCardAndRun(card);
+  };
+
+  isBreakoutWithCurrentField = breakout => {
+    const { field, metadata } = this.props;
+    const dimension = Dimension.parseMBQL(breakout, metadata);
+    return dimension && dimension.field().id === field.id;
+  };
+
+  render() {
+    let { field, question } = this.props;
+
+    const query = question.query();
+
+    let fieldName = field.display_name;
+    let tableName = query.table() ? query.table().display_name : "";
+
+    let useForCurrentQuestion = [],
+      usefulQuestions = [];
+
+    // determine if the selected field is a valid dimension on this table
+    let validBreakout = false;
+    if (query.table()) {
+      const validDimensions = _.filter(query.table().fields, isDimension);
+      validBreakout = _.some(validDimensions, f => f.id === field.id);
     }
 
-    setQueryDistinct = () => {
-        const { metadata, field } = this.props;
-        const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
-
-        let card = this.newCard();
-        card.dataset_query.query.aggregation = ["rows"];
-        card.dataset_query.query.breakout = [defaultBreakout];
-        this.props.setCardAndRun(card);
+    // TODO: allow for filters/grouping via foreign keys
+    if (
+      query instanceof StructuredQuery &&
+      query.tableId() === field.table_id
+    ) {
+      // NOTE: disabled this for now because we need a way to capture the completed filter before adding it to the query, or to pop open the filter widget here?
+      // useForCurrentQuestion.push(<UseForButton title={"Filter by " + name} onClick={this.filterBy} />);
+
+      // current field must be a valid breakout option for this table AND cannot already be in the breakout clause of our query
+      if (
+        validBreakout &&
+        !_.some(query.breakouts(), this.isBreakoutWithCurrentField)
+      ) {
+        useForCurrentQuestion.push(
+          <UseForButton title={t`Group by ${name}`} onClick={this.groupBy} />,
+        );
+      }
     }
 
-    setQueryCountGroupedBy = (chartType) => {
-        const { metadata, field } = this.props;
-        const defaultBreakout = metadata.fields[field.id].getDefaultBreakout();
-
-        let card = this.newCard();
-        card.dataset_query.query.aggregation = ["count"];
-        card.dataset_query.query.breakout = [defaultBreakout];
-        card.display = chartType;
-        this.props.setCardAndRun(card);
+    if (isSummable(field)) {
+      usefulQuestions.push(
+        <QueryButton
+          icon="number"
+          text={t`Sum of all values of ${fieldName}`}
+          onClick={this.setQuerySum}
+        />,
+      );
     }
-
-    isBreakoutWithCurrentField = (breakout) => {
-        const { field, metadata } = this.props;
-        const dimension = Dimension.parseMBQL(breakout, metadata);
-        return dimension && dimension.field().id === field.id;
+    usefulQuestions.push(
+      <QueryButton
+        icon="table"
+        text={t`All distinct values of ${fieldName}`}
+        onClick={this.setQueryDistinct}
+      />,
+    );
+    let queryCountGroupedByText = t`Number of ${inflection.pluralize(
+      tableName,
+    )} grouped by ${fieldName}`;
+    if (validBreakout) {
+      usefulQuestions.push(
+        <QueryButton
+          icon="bar"
+          text={queryCountGroupedByText}
+          onClick={this.setQueryCountGroupedBy.bind(null, "bar")}
+        />,
+      );
+      usefulQuestions.push(
+        <QueryButton
+          icon="pie"
+          text={queryCountGroupedByText}
+          onClick={this.setQueryCountGroupedBy.bind(null, "pie")}
+        />,
+      );
     }
 
-    render() {
-        let { field, question } = this.props;
-
-        const query = question.query();
-
-        let fieldName = field.display_name;
-        let tableName = query.table() ? query.table().display_name : "";
-
-        let useForCurrentQuestion = [],
-            usefulQuestions = [];
-
-        // determine if the selected field is a valid dimension on this table
-        let validBreakout = false;
-        if (query.table()) {
-            const validDimensions = _.filter(query.table().fields, isDimension);
-            validBreakout = _.some(validDimensions, f => f.id === field.id);
-        }
-
-        // TODO: allow for filters/grouping via foreign keys
-        if (query instanceof StructuredQuery && query.tableId() === field.table_id) {
-            // NOTE: disabled this for now because we need a way to capture the completed filter before adding it to the query, or to pop open the filter widget here?
-            // useForCurrentQuestion.push(<UseForButton title={"Filter by " + name} onClick={this.filterBy} />);
-
-            // current field must be a valid breakout option for this table AND cannot already be in the breakout clause of our query
-            if (validBreakout && !_.some(query.breakouts(), this.isBreakoutWithCurrentField)) {
-                useForCurrentQuestion.push(<UseForButton title={t`Group by ${name}`} onClick={this.groupBy} />);
-            }
-        }
-
-        if (isSummable(field)) {
-            usefulQuestions.push(<QueryButton icon="number" text={t`Sum of all values of ${fieldName}`} onClick={this.setQuerySum} />);
-        }
-        usefulQuestions.push(<QueryButton icon="table" text={t`All distinct values of ${fieldName}`} onClick={this.setQueryDistinct} />);
-        let queryCountGroupedByText = t`Number of ${inflection.pluralize(tableName)} grouped by ${fieldName}`;
-        if (validBreakout) {
-            usefulQuestions.push(<QueryButton icon="bar" text={queryCountGroupedByText} onClick={this.setQueryCountGroupedBy.bind(null, "bar")} />);
-            usefulQuestions.push(<QueryButton icon="pie" text={queryCountGroupedByText} onClick={this.setQueryCountGroupedBy.bind(null, "pie")} />);
-        }
-
-        return (
-            <DetailPane
-                name={fieldName}
-                description={field.description}
-                useForCurrentQuestion={useForCurrentQuestion}
-                usefulQuestions={usefulQuestions}
-            />
-        );
-    }
+    return (
+      <DetailPane
+        name={fieldName}
+        description={field.description}
+        useForCurrentQuestion={useForCurrentQuestion}
+        usefulQuestions={usefulQuestions}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
index 33a7680733fc73be2997d2e99d4f65ce0801a8e7..75154deff35035744e9db6df16d8d1891aa881fc 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MainPane.jsx
@@ -1,38 +1,58 @@
 /* eslint "react/prop-types": "warn" */
 import React from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { isQueryable } from "metabase/lib/table";
 
 import inflection from "inflection";
 import cx from "classnames";
 
-const MainPane = ({ databases, show }) =>
-    <div>
-        <h1>{t`Data Reference`}</h1>
-        <p>{t`Learn more about your data structure to ask more useful questions`}.</p>
-        <ul>
-            {databases && databases.filter(db => db.tables && db.tables.length > 0).map(database =>
-                <li key={database.id}>
-                    <div className="my2">
-                        <h2 className="inline-block">{database.name}</h2>
-                        <span className="ml1">{database.tables.length + " " + inflection.inflect("table", database.tables.length)}</span>
-                    </div>
-                    <ul>
-                        {database.tables.filter(isQueryable).map((table, index) =>
-                            <li key={table.id} className={cx("p1", { "border-bottom": index !== database.tables.length - 1 })}>
-                                <a className="text-brand text-brand-darken-hover no-decoration" onClick={() => show("table", table)}>{table.display_name}</a>
-                            </li>
-                        )}
-                    </ul>
-                </li>
-            )}
-        </ul>
-    </div>
+const MainPane = ({ databases, show }) => (
+  <div>
+    <h1>{t`Data Reference`}</h1>
+    <p>
+      {t`Learn more about your data structure to ask more useful questions`}.
+    </p>
+    <ul>
+      {databases &&
+        databases
+          .filter(db => db.tables && db.tables.length > 0)
+          .map(database => (
+            <li key={database.id}>
+              <div className="my2">
+                <h2 className="inline-block">{database.name}</h2>
+                <span className="ml1">
+                  {database.tables.length +
+                    " " +
+                    inflection.inflect("table", database.tables.length)}
+                </span>
+              </div>
+              <ul>
+                {database.tables.filter(isQueryable).map((table, index) => (
+                  <li
+                    key={table.id}
+                    className={cx("p1", {
+                      "border-bottom": index !== database.tables.length - 1,
+                    })}
+                  >
+                    <a
+                      className="text-brand text-brand-darken-hover no-decoration"
+                      onClick={() => show("table", table)}
+                    >
+                      {table.display_name}
+                    </a>
+                  </li>
+                ))}
+              </ul>
+            </li>
+          ))}
+    </ul>
+  </div>
+);
 
 MainPane.propTypes = {
-    show: PropTypes.func.isRequired,
-    databases: PropTypes.array
+  show: PropTypes.func.isRequired,
+  databases: PropTypes.array,
 };
 
 export default MainPane;
diff --git a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
index f86cf3e5897a301346b9278083db2d70e896077f..ac0d1f8bc145bd21e478d73333f6199021505d5d 100644
--- a/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/MetricPane.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import DetailPane from "./DetailPane.jsx";
 import QueryButton from "metabase/components/QueryButton.jsx";
 import QueryDefinition from "./QueryDefinition.jsx";
@@ -16,77 +16,90 @@ import { fetchTableMetadata } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
 
 const mapDispatchToProps = {
-    fetchTableMetadata,
+  fetchTableMetadata,
 };
 
 const mapStateToProps = (state, props) => ({
-    metadata: getMetadata(state, props)
-})
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricPane extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        _.bindAll(this, "setQueryMetric");
-    }
-
-    static propTypes = {
-        metric: PropTypes.object.isRequired,
-        query: PropTypes.object,
-        fetchTableMetadata: PropTypes.func.isRequired,
-        runQuestionQuery: PropTypes.func.isRequired,
-        setDatasetQuery: PropTypes.func.isRequired,
-        setCardAndRun: PropTypes.func.isRequired,
-        metadata: PropTypes.object
-    };
-
-    componentWillMount() {
-        this.props.fetchTableMetadata(this.props.metric.table_id);
+  constructor(props, context) {
+    super(props, context);
+
+    _.bindAll(this, "setQueryMetric");
+  }
+
+  static propTypes = {
+    metric: PropTypes.object.isRequired,
+    query: PropTypes.object,
+    fetchTableMetadata: PropTypes.func.isRequired,
+    runQuestionQuery: PropTypes.func.isRequired,
+    setDatasetQuery: PropTypes.func.isRequired,
+    setCardAndRun: PropTypes.func.isRequired,
+    metadata: PropTypes.object,
+  };
+
+  componentWillMount() {
+    this.props.fetchTableMetadata(this.props.metric.table_id);
+  }
+
+  newCard() {
+    const { metric, metadata } = this.props;
+    const table = metadata && metadata.tables[metric.table_id];
+
+    if (table) {
+      let card = createCard();
+      card.dataset_query = createQuery("query", table.db_id, table.id);
+      return card;
+    } else {
+      throw new Error(
+        t`Could not find the table metadata prior to creating a new question`,
+      );
     }
-
-    newCard() {
-        const { metric, metadata } = this.props;
-        const table = metadata && metadata.tables[metric.table_id];
-
-        if (table) {
-            let card = createCard();
-            card.dataset_query = createQuery("query", table.db_id, table.id);
-            return card;
-        } else {
-            throw new Error(t`Could not find the table metadata prior to creating a new question`)
+  }
+
+  setQueryMetric() {
+    let card = this.newCard();
+    card.dataset_query.query.aggregation = ["METRIC", this.props.metric.id];
+    this.props.setCardAndRun(card);
+  }
+
+  render() {
+    let { metric, metadata } = this.props;
+
+    let metricName = metric.name;
+
+    let useForCurrentQuestion = [];
+    let usefulQuestions = [];
+
+    usefulQuestions.push(
+      <QueryButton
+        icon="number"
+        text={t`See ${metricName}`}
+        onClick={this.setQueryMetric}
+      />,
+    );
+
+    return (
+      <DetailPane
+        name={metricName}
+        description={metric.description}
+        useForCurrentQuestion={useForCurrentQuestion}
+        usefulQuestions={usefulQuestions}
+        extra={
+          metadata && (
+            <div>
+              <p className="text-bold">{t`Metric Definition`}</p>
+              <QueryDefinition
+                object={metric}
+                tableMetadata={metadata.tables[metric.table_id]}
+              />
+            </div>
+          )
         }
-    }
-
-    setQueryMetric() {
-        let card = this.newCard();
-        card.dataset_query.query.aggregation = ["METRIC", this.props.metric.id];
-        this.props.setCardAndRun(card);
-    }
-
-    render() {
-        let { metric, metadata } = this.props;
-
-        let metricName = metric.name;
-
-        let useForCurrentQuestion = [];
-        let usefulQuestions = [];
-
-        usefulQuestions.push(<QueryButton icon="number" text={t`See ${metricName}`} onClick={this.setQueryMetric} />);
-
-        return (
-            <DetailPane
-                name={metricName}
-                description={metric.description}
-                useForCurrentQuestion={useForCurrentQuestion}
-                usefulQuestions={usefulQuestions}
-                extra={metadata &&
-                    <div>
-                        <p className="text-bold">{t`Metric Definition`}</p>
-                        <QueryDefinition object={metric} tableMetadata={metadata.tables[metric.table_id]} />
-                    </div>
-                }
-            />
-        );
-    }
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx b/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx
index 00e289dda749f79720311dba93aa9f63619e73ac..6b675dae380946b66c389d662d62ccc3b2819996 100644
--- a/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/QueryDefinition.jsx
@@ -6,24 +6,24 @@ import AggregationWidget from "../AggregationWidget.jsx";
 import Query from "metabase/lib/query";
 
 const QueryDefinition = ({ className, object, tableMetadata }) => {
-    const filters = Query.getFilters(object.definition);
-    return (
-        <div className={className} style={{ pointerEvents: "none" }}>
-            { object.definition.aggregation &&
-                <AggregationWidget
-                    aggregation={object.definition.aggregation[0]}
-                    tableMetadata={tableMetadata}
-                />
-            }
-            { filters.length > 0 &&
-                <FilterList
-                    filters={filters}
-                    tableMetadata={tableMetadata}
-                    maxDisplayValues={Infinity}
-                />
-            }
-        </div>
-    );
-}
+  const filters = Query.getFilters(object.definition);
+  return (
+    <div className={className} style={{ pointerEvents: "none" }}>
+      {object.definition.aggregation && (
+        <AggregationWidget
+          aggregation={object.definition.aggregation[0]}
+          tableMetadata={tableMetadata}
+        />
+      )}
+      {filters.length > 0 && (
+        <FilterList
+          filters={filters}
+          tableMetadata={tableMetadata}
+          maxDisplayValues={Infinity}
+        />
+      )}
+    </div>
+  );
+};
 
 export default QueryDefinition;
diff --git a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
index d7bb3e26e1f12f294b486d26711848e31569ea23..c78e6de6d5d190ad14df4be2656dfb54606c443b 100644
--- a/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/SegmentPane.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { fetchTableMetadata } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
 
@@ -18,113 +18,142 @@ import _ from "underscore";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 const mapDispatchToProps = {
-    fetchTableMetadata,
+  fetchTableMetadata,
 };
 
 const mapStateToProps = (state, props) => ({
-    metadata: getMetadata(state, props)
-})
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentPane extends Component {
-    constructor(props, context) {
-        super(props, context);
-
-        _.bindAll(this, "filterBy", "setQueryFilteredBy", "setQueryCountFilteredBy");
+  constructor(props, context) {
+    super(props, context);
+
+    _.bindAll(
+      this,
+      "filterBy",
+      "setQueryFilteredBy",
+      "setQueryCountFilteredBy",
+    );
+  }
+
+  static propTypes = {
+    segment: PropTypes.object.isRequired,
+    datasetQuery: PropTypes.object,
+    fetchTableMetadata: PropTypes.func.isRequired,
+    runQuestionQuery: PropTypes.func.isRequired,
+    updateQuestion: PropTypes.func.isRequired,
+    setCardAndRun: PropTypes.func.isRequired,
+    question: PropTypes.object.isRequired,
+    originalQuestion: PropTypes.object.isRequired,
+    metadata: PropTypes.object.isRequired,
+  };
+
+  componentWillMount() {
+    this.props.fetchTableMetadata(this.props.segment.table_id);
+  }
+
+  filterBy() {
+    const { question } = this.props;
+    let query = question.query();
+
+    if (query instanceof StructuredQuery) {
+      // Add an aggregation so both aggregation and filter popovers aren't visible
+      if (!Query.hasValidAggregation(query.datasetQuery().query)) {
+        query = query.clearAggregations();
+      }
+
+      query = query.addFilter(["SEGMENT", this.props.segment.id]);
+
+      this.props.updateQuestion(query.question());
+      this.props.runQuestionQuery();
     }
-
-    static propTypes = {
-        segment: PropTypes.object.isRequired,
-        datasetQuery: PropTypes.object,
-        fetchTableMetadata: PropTypes.func.isRequired,
-        runQuestionQuery: PropTypes.func.isRequired,
-        updateQuestion: PropTypes.func.isRequired,
-        setCardAndRun: PropTypes.func.isRequired,
-        question: PropTypes.object.isRequired,
-        originalQuestion: PropTypes.object.isRequired,
-        metadata: PropTypes.object.isRequired
-    };
-
-    componentWillMount() {
-        this.props.fetchTableMetadata(this.props.segment.table_id);
+  }
+
+  newCard() {
+    const { segment, metadata } = this.props;
+    const table = metadata && metadata.tables[segment.table_id];
+
+    if (table) {
+      let card = createCard();
+      card.dataset_query = createQuery("query", table.db_id, table.id);
+      return card;
+    } else {
+      throw new Error(
+        t`Could not find the table metadata prior to creating a new question`,
+      );
     }
-
-    filterBy() {
-        const { question } = this.props;
-        let query = question.query();
-
-        if (query instanceof StructuredQuery) {
-            // Add an aggregation so both aggregation and filter popovers aren't visible
-            if (!Query.hasValidAggregation(query.datasetQuery().query)) {
-                query = query.clearAggregations()
-            }
-
-            query = query.addFilter(["SEGMENT", this.props.segment.id]);
-
-            this.props.updateQuestion(query.question())
-            this.props.runQuestionQuery();
-        }
+  }
+  setQueryFilteredBy() {
+    let card = this.newCard();
+    card.dataset_query.query.aggregation = ["rows"];
+    card.dataset_query.query.filter = ["SEGMENT", this.props.segment.id];
+    this.props.setCardAndRun(card);
+  }
+
+  setQueryCountFilteredBy() {
+    let card = this.newCard();
+    card.dataset_query.query.aggregation = ["count"];
+    card.dataset_query.query.filter = ["SEGMENT", this.props.segment.id];
+    this.props.setCardAndRun(card);
+  }
+
+  render() {
+    let { segment, metadata, question } = this.props;
+    const query = question.query();
+
+    let segmentName = segment.name;
+
+    let useForCurrentQuestion = [];
+    let usefulQuestions = [];
+
+    if (
+      query instanceof StructuredQuery &&
+      query.tableId() === segment.table_id &&
+      !_.findWhere(query.filters(), { [0]: "SEGMENT", [1]: segment.id })
+    ) {
+      useForCurrentQuestion.push(
+        <UseForButton
+          title={t`Filter by ${segmentName}`}
+          onClick={this.filterBy}
+        />,
+      );
     }
 
-    newCard() {
-        const { segment, metadata } = this.props;
-        const table = metadata && metadata.tables[segment.table_id];
-
-        if (table) {
-            let card = createCard();
-            card.dataset_query = createQuery("query", table.db_id, table.id);
-            return card;
-        } else {
-            throw new Error(t`Could not find the table metadata prior to creating a new question`)
+    usefulQuestions.push(
+      <QueryButton
+        icon="number"
+        text={t`Number of ${segmentName}`}
+        onClick={this.setQueryCountFilteredBy}
+      />,
+    );
+    usefulQuestions.push(
+      <QueryButton
+        icon="table"
+        text={t`See all ${segmentName}`}
+        onClick={this.setQueryFilteredBy}
+      />,
+    );
+
+    return (
+      <DetailPane
+        name={segmentName}
+        description={segment.description}
+        useForCurrentQuestion={useForCurrentQuestion}
+        usefulQuestions={usefulQuestions}
+        extra={
+          metadata && (
+            <div>
+              <p className="text-bold">{t`Segment Definition`}</p>
+              <QueryDefinition
+                object={segment}
+                tableMetadata={metadata.tables[segment.table_id]}
+              />
+            </div>
+          )
         }
-    }
-    setQueryFilteredBy() {
-        let card = this.newCard();
-        card.dataset_query.query.aggregation = ["rows"];
-        card.dataset_query.query.filter = ["SEGMENT", this.props.segment.id];
-        this.props.setCardAndRun(card);
-    }
-
-    setQueryCountFilteredBy() {
-        let card = this.newCard();
-        card.dataset_query.query.aggregation = ["count"];
-        card.dataset_query.query.filter = ["SEGMENT", this.props.segment.id];
-        this.props.setCardAndRun(card);
-    }
-
-    render() {
-        let { segment, metadata, question } = this.props;
-        const query = question.query();
-
-        let segmentName = segment.name;
-
-        let useForCurrentQuestion = [];
-        let usefulQuestions = [];
-
-
-        if (query instanceof StructuredQuery &&
-            query.tableId() === segment.table_id &&
-            !_.findWhere(query.filters(), {[0]: "SEGMENT", [1]: segment.id})) {
-
-            useForCurrentQuestion.push(<UseForButton title={t`Filter by ${segmentName}`} onClick={this.filterBy} />);
-        }
-
-        usefulQuestions.push(<QueryButton icon="number" text={t`Number of ${segmentName}`} onClick={this.setQueryCountFilteredBy} />);
-        usefulQuestions.push(<QueryButton icon="table" text={t`See all ${segmentName}`} onClick={this.setQueryFilteredBy} />);
-
-        return (
-            <DetailPane
-                name={segmentName}
-                description={segment.description}
-                useForCurrentQuestion={useForCurrentQuestion}
-                usefulQuestions={usefulQuestions}
-                extra={metadata &&
-                <div>
-                    <p className="text-bold">{t`Segment Definition`}</p>
-                    <QueryDefinition object={segment} tableMetadata={metadata.tables[segment.table_id]} />
-                </div>
-                }
-            />
-        );
-    }
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
index cc3277642638417747b9cf0a7336feb7d9a5930b..831fe4575a8cc589fff644122cb3c91b10258327 100644
--- a/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/TablePane.jsx
@@ -1,181 +1,228 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import QueryButton from "metabase/components/QueryButton.jsx";
 import { createCard } from "metabase/lib/card";
 import { createQuery } from "metabase/lib/query";
-import { foreignKeyCountsByOriginTable } from 'metabase/lib/schema_metadata';
-import inflection from 'inflection';
+import { foreignKeyCountsByOriginTable } from "metabase/lib/schema_metadata";
+import inflection from "inflection";
 import cx from "classnames";
 
 import Expandable from "metabase/components/Expandable.jsx";
 
 export default class TablePane extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.setQueryAllRows = this.setQueryAllRows.bind(this);
-        this.showPane = this.showPane.bind(this);
+  constructor(props, context) {
+    super(props, context);
+    this.setQueryAllRows = this.setQueryAllRows.bind(this);
+    this.showPane = this.showPane.bind(this);
 
-        this.state = {
-            table: undefined,
-            tableForeignKeys: undefined,
-            pane: "fields"
-        };
-    }
-
-    static propTypes = {
-        query: PropTypes.object.isRequired,
-        loadTableAndForeignKeysFn: PropTypes.func.isRequired,
-        show: PropTypes.func.isRequired,
-        onClose: PropTypes.func.isRequired,
-        setCardAndRun: PropTypes.func.isRequired,
-        table: PropTypes.object
+    this.state = {
+      table: undefined,
+      tableForeignKeys: undefined,
+      pane: "fields",
     };
+  }
+
+  static propTypes = {
+    query: PropTypes.object.isRequired,
+    loadTableAndForeignKeysFn: PropTypes.func.isRequired,
+    show: PropTypes.func.isRequired,
+    onClose: PropTypes.func.isRequired,
+    setCardAndRun: PropTypes.func.isRequired,
+    table: PropTypes.object,
+  };
 
-    componentWillMount() {
-        this.props.loadTableAndForeignKeysFn(this.props.table.id).then((result) => {
-            this.setState({
-                table: result.table,
-                tableForeignKeys: result.foreignKeys
-            });
-        }).catch((error) => {
-            this.setState({
-                error: t`An error occurred loading the table`
-            });
+  componentWillMount() {
+    this.props
+      .loadTableAndForeignKeysFn(this.props.table.id)
+      .then(result => {
+        this.setState({
+          table: result.table,
+          tableForeignKeys: result.foreignKeys,
         });
-    }
+      })
+      .catch(error => {
+        this.setState({
+          error: t`An error occurred loading the table`,
+        });
+      });
+  }
 
-    showPane(name) {
-        this.setState({ pane: name });
-    }
+  showPane(name) {
+    this.setState({ pane: name });
+  }
 
-    setQueryAllRows() {
-        let card = createCard();
-        card.dataset_query = createQuery("query", this.state.table.db_id, this.state.table.id);
-        this.props.setCardAndRun(card);
-    }
+  setQueryAllRows() {
+    let card = createCard();
+    card.dataset_query = createQuery(
+      "query",
+      this.state.table.db_id,
+      this.state.table.id,
+    );
+    this.props.setCardAndRun(card);
+  }
 
-    render() {
-        const { table, error } = this.state;
-        if (table) {
-            var queryButton;
-            if (table.rows != null) {
-                var text = t`See the raw data for ${table.display_name}`
-                queryButton = (<QueryButton className="border-bottom border-top mb3" icon="table" text={text} onClick={this.setQueryAllRows} />);
-            }
-            var panes = {
-                "fields": table.fields.length,
-                // "metrics": table.metrics.length,
-                // "segments": table.segments.length,
-                "connections": this.state.tableForeignKeys.length
-            };
-            var tabs = Object.entries(panes).map(([name, count]) =>
-                <a key={name} className={cx("Button Button--small", { "Button--active": name === this.state.pane })} onClick={this.showPane.bind(null, name)}>
-                    <span className="DataReference-paneCount">{count}</span><span>{inflection.inflect(name, count)}</span>
-                </a>
-            );
-
-            var pane;
-            if (this.state.pane === "connections") {
-                const fkCountsByTable = foreignKeyCountsByOriginTable(this.state.tableForeignKeys);
-                pane = (
-                    <ul>
-                    { this.state.tableForeignKeys
-                        .sort((a, b) => a.origin.table.display_name.localeCompare(b.origin.table.display_name))
-                        .map((fk, index) =>
-                            <ListItem key={fk.id} onClick={() => this.props.show("field", fk.origin)}>
-                                { fk.origin.table.display_name }
-                                { fkCountsByTable[fk.origin.table.id] > 1 ?
-                                    <span className="text-grey-3 text-light h5"> via {fk.origin.display_name}</span>
-                                : null }
-                            </ListItem>
-                        )
-                    }
-                    </ul>
-                );
-            } else if (this.state.pane) {
-                const itemType = this.state.pane.replace(/s$/, "");
-                pane = (
-                    <ul>
-                        { table[this.state.pane].map((item, index) =>
-                            <ListItem key={item.id} onClick={() => this.props.show(itemType, item)}>
-                                {item.display_name || item.name}
-                            </ListItem>
-                        )}
-                    </ul>
-                );
-            } else
+  render() {
+    const { table, error } = this.state;
+    if (table) {
+      var queryButton;
+      if (table.rows != null) {
+        var text = t`See the raw data for ${table.display_name}`;
+        queryButton = (
+          <QueryButton
+            className="border-bottom border-top mb3"
+            icon="table"
+            text={text}
+            onClick={this.setQueryAllRows}
+          />
+        );
+      }
+      var panes = {
+        fields: table.fields.length,
+        // "metrics": table.metrics.length,
+        // "segments": table.segments.length,
+        connections: this.state.tableForeignKeys.length,
+      };
+      var tabs = Object.entries(panes).map(([name, count]) => (
+        <a
+          key={name}
+          className={cx("Button Button--small", {
+            "Button--active": name === this.state.pane,
+          })}
+          onClick={this.showPane.bind(null, name)}
+        >
+          <span className="DataReference-paneCount">{count}</span>
+          <span>{inflection.inflect(name, count)}</span>
+        </a>
+      ));
 
-            var descriptionClasses = cx({ "text-grey-3": !table.description });
-            var description = (<p className={descriptionClasses}>{table.description || t`No description set.`}</p>);
+      var pane;
+      if (this.state.pane === "connections") {
+        const fkCountsByTable = foreignKeyCountsByOriginTable(
+          this.state.tableForeignKeys,
+        );
+        pane = (
+          <ul>
+            {this.state.tableForeignKeys
+              .sort((a, b) =>
+                a.origin.table.display_name.localeCompare(
+                  b.origin.table.display_name,
+                ),
+              )
+              .map((fk, index) => (
+                <ListItem
+                  key={fk.id}
+                  onClick={() => this.props.show("field", fk.origin)}
+                >
+                  {fk.origin.table.display_name}
+                  {fkCountsByTable[fk.origin.table.id] > 1 ? (
+                    <span className="text-grey-3 text-light h5">
+                      {" "}
+                      via {fk.origin.display_name}
+                    </span>
+                  ) : null}
+                </ListItem>
+              ))}
+          </ul>
+        );
+      } else if (this.state.pane) {
+        const itemType = this.state.pane.replace(/s$/, "");
+        pane = (
+          <ul>
+            {table[this.state.pane].map((item, index) => (
+              <ListItem
+                key={item.id}
+                onClick={() => this.props.show(itemType, item)}
+              >
+                {item.display_name || item.name}
+              </ListItem>
+            ))}
+          </ul>
+        );
+      } else var descriptionClasses = cx({ "text-grey-3": !table.description });
+      var description = (
+        <p className={descriptionClasses}>
+          {table.description || t`No description set.`}
+        </p>
+      );
 
-            return (
-                <div>
-                    <h1>{table.display_name}</h1>
-                    {description}
-                    {queryButton}
-                    { table.metrics && (table.metrics.length > 0) &&
-                        <ExpandableItemList
-                            name="Metrics"
-                            type="metrics"
-                            show={this.props.show.bind(null, "metric")}
-                            items={table.metrics.filter((metric) => metric.is_active === true)}
-                        />
-                    }
-                    { table.segments && (table.segments.length > 0) &&
-                        <ExpandableItemList
-                            name="Segments"
-                            type="segments"
-                            show={this.props.show.bind(null, "segment")}
-                            items={table.segments.filter((segment) => segment.is_active === true)}
-                        />
-                    }
-                    <div className="Button-group Button-group--brand text-uppercase">
-                        {tabs}
-                    </div>
-                    {pane}
-                </div>
-            );
-        } else {
-            return (
-                <div>{error}</div>
-            );
-        }
+      return (
+        <div>
+          <h1>{table.display_name}</h1>
+          {description}
+          {queryButton}
+          {table.metrics &&
+            table.metrics.length > 0 && (
+              <ExpandableItemList
+                name="Metrics"
+                type="metrics"
+                show={this.props.show.bind(null, "metric")}
+                items={table.metrics.filter(
+                  metric => metric.is_active === true,
+                )}
+              />
+            )}
+          {table.segments &&
+            table.segments.length > 0 && (
+              <ExpandableItemList
+                name="Segments"
+                type="segments"
+                show={this.props.show.bind(null, "segment")}
+                items={table.segments.filter(
+                  segment => segment.is_active === true,
+                )}
+              />
+            )}
+          <div className="Button-group Button-group--brand text-uppercase">
+            {tabs}
+          </div>
+          {pane}
+        </div>
+      );
+    } else {
+      return <div>{error}</div>;
     }
+  }
 }
 
-const ExpandableItemList = Expandable(({ name, type, show, items, isExpanded, onExpand }) =>
+const ExpandableItemList = Expandable(
+  ({ name, type, show, items, isExpanded, onExpand }) => (
     <div className="mb2">
-        <div className="text-bold mb1">{name}</div>
-        <ul>
-            { items.map((item, index) =>
-                <ListItem key={item.id} onClick={() => show(item)}>
-                    {item.name}
-                </ListItem>
-            ) }
-            { !isExpanded && <ListItem onClick={onExpand}>{t`More`}...</ListItem>}
-        </ul>
+      <div className="text-bold mb1">{name}</div>
+      <ul>
+        {items.map((item, index) => (
+          <ListItem key={item.id} onClick={() => show(item)}>
+            {item.name}
+          </ListItem>
+        ))}
+        {!isExpanded && <ListItem onClick={onExpand}>{t`More`}...</ListItem>}
+      </ul>
     </div>
+  ),
 );
 
 ExpandableItemList.propTypes = {
-    name: PropTypes.string.isRequired,
-    type: PropTypes.string.isRequired,
-    show: PropTypes.func.isRequired,
-    items: PropTypes.array.isRequired,
-    onExpand:  PropTypes.func.isRequired,
-    isExpanded: PropTypes.bool.isRequired
+  name: PropTypes.string.isRequired,
+  type: PropTypes.string.isRequired,
+  show: PropTypes.func.isRequired,
+  items: PropTypes.array.isRequired,
+  onExpand: PropTypes.func.isRequired,
+  isExpanded: PropTypes.bool.isRequired,
 };
 
-const ListItem = ({ onClick, children }) =>
-    <li className="py1 border-row-divider">
-        <a className="text-brand text-brand-darken-hover no-decoration" onClick={onClick}>
-            {children}
-        </a>
-    </li>
+const ListItem = ({ onClick, children }) => (
+  <li className="py1 border-row-divider">
+    <a
+      className="text-brand text-brand-darken-hover no-decoration"
+      onClick={onClick}
+    >
+      {children}
+    </a>
+  </li>
+);
 
 ListItem.propTypes = {
-    children: PropTypes.any,
-    onClick: PropTypes.func
+  children: PropTypes.any,
+  onClick: PropTypes.func,
 };
diff --git a/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx b/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx
index ae3853de049591928f3167fb07426b14fa57507e..c5a470013a53c9ed74d080179539fcb49e69ceb1 100644
--- a/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx
+++ b/frontend/src/metabase/query_builder/components/dataref/UseForButton.jsx
@@ -2,9 +2,13 @@ import React from "react";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const UseForButton = ({ title, onClick }) =>
-    <a className="Button Button--white text-default text-brand-hover border-brand-hover no-decoration" onClick={onClick}>
-        <Icon className="mr1" name="add" size={12} /> {title}
-    </a>
+const UseForButton = ({ title, onClick }) => (
+  <a
+    className="Button Button--white text-default text-brand-hover border-brand-hover no-decoration"
+    onClick={onClick}
+  >
+    <Icon className="mr1" name="add" size={12} /> {title}
+  </a>
+);
 
 export default UseForButton;
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.css b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.css
index eba71492461d64c7f6d030e9b6b0a1ee2410933a..ad1905f617e0740ab0c2905114baa47b7fb3c7f3 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.css
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.css
@@ -1,17 +1,17 @@
 :local(.editor) {
-    position: relative;
+  position: relative;
 }
 
 :local(.input) {
-    padding-left: 1.5em !important;
+  padding-left: 1.5em !important;
 }
 
 :local(.equalSign) {
-    pointer-events: none;
-    padding-left: 0.7em;
+  pointer-events: none;
+  padding-left: 0.7em;
 }
 
 :local(.placeholder) {
-    /* match the placeholder text */
-    color: rgb(192, 192, 192);
+  /* match the placeholder text */
+  color: rgb(192, 192, 192);
 }
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
index f245928ddf252f84b2d6863fa81a68e9bb90eb0d..c3a216b775ae4599e8b429136a86822d99721a38 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionEditorTextfield.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import S from "./ExpressionEditorTextfield.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import _ from "underscore";
 import cx from "classnames";
 
@@ -10,12 +10,12 @@ import { compile, suggest } from "metabase/lib/expressions/parser";
 import { format } from "metabase/lib/expressions/formatter";
 import { setCaretPosition, getSelectionPosition } from "metabase/lib/dom";
 import {
-    KEYCODE_ENTER,
-    KEYCODE_ESCAPE,
-    KEYCODE_LEFT,
-    KEYCODE_UP,
-    KEYCODE_RIGHT,
-    KEYCODE_DOWN
+  KEYCODE_ENTER,
+  KEYCODE_ESCAPE,
+  KEYCODE_LEFT,
+  KEYCODE_UP,
+  KEYCODE_RIGHT,
+  KEYCODE_DOWN,
 } from "metabase/lib/keyboard";
 
 import Popover from "metabase/components/Popover.jsx";
@@ -27,285 +27,342 @@ import { isExpression } from "metabase/lib/expressions";
 const MAX_SUGGESTIONS = 30;
 
 export default class ExpressionEditorTextfield extends Component {
-    constructor(props, context) {
-        super(props, context);
-        _.bindAll(this, '_triggerAutosuggest', 'onInputKeyDown', 'onInputBlur', 'onSuggestionAccepted', 'onSuggestionMouseDown');
-    }
-
-    static propTypes = {
-        expression: PropTypes.array,      // should be an array like [parsedExpressionObj, expressionString]
-        tableMetadata: PropTypes.object.isRequired,
-        customFields: PropTypes.object,
-        onChange: PropTypes.func.isRequired,
-        onError: PropTypes.func.isRequired,
-        startRule: PropTypes.string.isRequired
+  constructor(props, context) {
+    super(props, context);
+    _.bindAll(
+      this,
+      "_triggerAutosuggest",
+      "onInputKeyDown",
+      "onInputBlur",
+      "onSuggestionAccepted",
+      "onSuggestionMouseDown",
+    );
+  }
+
+  static propTypes = {
+    expression: PropTypes.array, // should be an array like [parsedExpressionObj, expressionString]
+    tableMetadata: PropTypes.object.isRequired,
+    customFields: PropTypes.object,
+    onChange: PropTypes.func.isRequired,
+    onError: PropTypes.func.isRequired,
+    startRule: PropTypes.string.isRequired,
+  };
+
+  static defaultProps = {
+    expression: [null, ""],
+    startRule: "expression",
+    placeholder: "write some math!",
+  };
+
+  _getParserInfo(props = this.props) {
+    return {
+      tableMetadata: props.tableMetadata,
+      customFields: props.customFields || {},
+      startRule: props.startRule,
     };
-
-    static defaultProps = {
-        expression: [null, ""],
-        startRule: "expression",
-        placeholder: "write some math!"
-    }
-
-    _getParserInfo(props = this.props) {
-        return {
-            tableMetadata: props.tableMetadata,
-            customFields: props.customFields || {},
-            startRule: props.startRule
+  }
+
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
+
+  componentWillReceiveProps(newProps) {
+    // we only refresh our state if we had no previous state OR if our expression or table has changed
+    if (
+      !this.state ||
+      this.props.expression != newProps.expression ||
+      this.props.tableMetadata != newProps.tableMetadata
+    ) {
+      const parserInfo = this._getParserInfo(newProps);
+      let parsedExpression = newProps.expression;
+      let expressionString = format(newProps.expression, parserInfo);
+      let expressionErrorMessage = null;
+      let suggestions = [];
+      try {
+        if (expressionString) {
+          compile(expressionString, parserInfo);
         }
+      } catch (e) {
+        expressionErrorMessage = e;
+      }
+
+      this.setState({
+        parsedExpression,
+        expressionString,
+        expressionErrorMessage,
+        suggestions,
+        highlightedSuggestion: 0,
+      });
     }
-
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
+  }
+
+  componentDidMount() {
+    this._setCaretPosition(
+      this.state.expressionString.length,
+      this.state.expressionString.length === 0,
+    );
+  }
+
+  onSuggestionAccepted() {
+    const { expressionString } = this.state;
+    const suggestion = this.state.suggestions[this.state.highlightedSuggestion];
+
+    if (suggestion) {
+      let prefix = expressionString.slice(0, suggestion.index);
+      if (suggestion.prefixTrim) {
+        prefix = prefix.replace(suggestion.prefixTrim, "");
+      }
+      let postfix = expressionString.slice(suggestion.index);
+      if (suggestion.postfixTrim) {
+        postfix = postfix.replace(suggestion.postfixTrim, "");
+      }
+      if (!postfix && suggestion.postfixText) {
+        postfix = suggestion.postfixText;
+      }
+
+      this.onExpressionChange(prefix + suggestion.text + postfix);
+      setTimeout(() =>
+        this._setCaretPosition((prefix + suggestion.text).length, true),
+      );
     }
 
-    componentWillReceiveProps(newProps) {
-        // we only refresh our state if we had no previous state OR if our expression or table has changed
-        if (!this.state || this.props.expression != newProps.expression || this.props.tableMetadata != newProps.tableMetadata) {
-            const parserInfo = this._getParserInfo(newProps);
-            let parsedExpression = newProps.expression;
-            let expressionString = format(newProps.expression, parserInfo);
-            let expressionErrorMessage = null;
-            let suggestions = [];
-            try {
-                if (expressionString) {
-                    compile(expressionString, parserInfo);
-                }
-            } catch (e) {
-                expressionErrorMessage = e;
-            }
-
-            this.setState({
-                parsedExpression,
-                expressionString,
-                expressionErrorMessage,
-                suggestions,
-                highlightedSuggestion: 0
-            });
-        }
-    }
+    this.setState({
+      highlightedSuggestion: 0,
+    });
+  }
 
-    componentDidMount() {
-        this._setCaretPosition(this.state.expressionString.length, this.state.expressionString.length === 0)
-    }
+  onSuggestionMouseDown(event, index) {
+    // when a suggestion is clicked, we'll highlight the clicked suggestion and then hand off to the same code that deals with ENTER / TAB keydowns
+    event.preventDefault();
+    event.stopPropagation();
+    this.setState({ highlightedSuggestion: index }, this.onSuggestionAccepted);
+  }
 
-    onSuggestionAccepted() {
-        const { expressionString } = this.state;
-        const suggestion = this.state.suggestions[this.state.highlightedSuggestion];
-
-        if (suggestion) {
-            let prefix = expressionString.slice(0, suggestion.index);
-            if (suggestion.prefixTrim) {
-                prefix = prefix.replace(suggestion.prefixTrim, "");
-            }
-            let postfix = expressionString.slice(suggestion.index);
-            if (suggestion.postfixTrim) {
-                postfix = postfix.replace(suggestion.postfixTrim, "");
-            }
-            if (!postfix && suggestion.postfixText) {
-                postfix = suggestion.postfixText;
-            }
-
-            this.onExpressionChange(prefix + suggestion.text + postfix)
-            setTimeout(() => this._setCaretPosition((prefix + suggestion.text).length, true))
-        }
+  onInputKeyDown(e) {
+    const { suggestions, highlightedSuggestion } = this.state;
 
-        this.setState({
-            highlightedSuggestion: 0
-        });
+    if (e.keyCode === KEYCODE_LEFT || e.keyCode === KEYCODE_RIGHT) {
+      setTimeout(() => this._triggerAutosuggest());
+      return;
     }
-
-    onSuggestionMouseDown(event, index) {
-        // when a suggestion is clicked, we'll highlight the clicked suggestion and then hand off to the same code that deals with ENTER / TAB keydowns
-        event.preventDefault();
-        event.stopPropagation();
-        this.setState({ highlightedSuggestion: index }, this.onSuggestionAccepted);
+    if (e.keyCode === KEYCODE_ESCAPE) {
+      e.stopPropagation();
+      e.preventDefault();
+      this.clearSuggestions();
+      return;
     }
 
-    onInputKeyDown(e) {
-        const { suggestions, highlightedSuggestion } = this.state;
-
-        if (e.keyCode === KEYCODE_LEFT || e.keyCode === KEYCODE_RIGHT) {
-            setTimeout(() => this._triggerAutosuggest());
-            return;
-        }
-        if (e.keyCode === KEYCODE_ESCAPE) {
-            e.stopPropagation();
-            e.preventDefault();
-            this.clearSuggestions();
-            return;
-        }
-
-        if (!suggestions.length) {
-            return;
-        }
-        if (e.keyCode === KEYCODE_ENTER) {
-            this.onSuggestionAccepted();
-            e.preventDefault();
-        } else if (e.keyCode === KEYCODE_UP) {
-            this.setState({
-                highlightedSuggestion: (highlightedSuggestion + suggestions.length - 1) % suggestions.length
-            });
-            e.preventDefault();
-        } else if (e.keyCode === KEYCODE_DOWN) {
-            this.setState({
-                highlightedSuggestion: (highlightedSuggestion + suggestions.length + 1) % suggestions.length
-            });
-            e.preventDefault();
-        }
+    if (!suggestions.length) {
+      return;
     }
-
-    clearSuggestions() {
-        this.setState({
-            suggestions: [],
-            highlightedSuggestion: 0
-        });
+    if (e.keyCode === KEYCODE_ENTER) {
+      this.onSuggestionAccepted();
+      e.preventDefault();
+    } else if (e.keyCode === KEYCODE_UP) {
+      this.setState({
+        highlightedSuggestion:
+          (highlightedSuggestion + suggestions.length - 1) % suggestions.length,
+      });
+      e.preventDefault();
+    } else if (e.keyCode === KEYCODE_DOWN) {
+      this.setState({
+        highlightedSuggestion:
+          (highlightedSuggestion + suggestions.length + 1) % suggestions.length,
+      });
+      e.preventDefault();
     }
-
-    onInputBlur() {
-        this.clearSuggestions();
-
-        // whenever our input blurs we push the updated expression to our parent if valid
-        if (isExpression(this.state.parsedExpression)) {
-            this.props.onChange(this.state.parsedExpression);
-        } else if (this.state.expressionErrorMessage) {
-            this.props.onError(this.state.expressionErrorMessage);
-        } else {
-            this.props.onError({ message: t`Invalid expression` });
-        }
+  }
+
+  clearSuggestions() {
+    this.setState({
+      suggestions: [],
+      highlightedSuggestion: 0,
+    });
+  }
+
+  onInputBlur() {
+    this.clearSuggestions();
+
+    // whenever our input blurs we push the updated expression to our parent if valid
+    if (isExpression(this.state.parsedExpression)) {
+      this.props.onChange(this.state.parsedExpression);
+    } else if (this.state.expressionErrorMessage) {
+      this.props.onError(this.state.expressionErrorMessage);
+    } else {
+      this.props.onError({ message: t`Invalid expression` });
     }
+  }
 
-    onInputClick = () => {
-        this._triggerAutosuggest();
-    }
+  onInputClick = () => {
+    this._triggerAutosuggest();
+  };
 
-    _triggerAutosuggest = () => {
-        this.onExpressionChange(this.state.expressionString);
-    }
+  _triggerAutosuggest = () => {
+    this.onExpressionChange(this.state.expressionString);
+  };
 
-    _setCaretPosition = (position, autosuggest) => {
-        setCaretPosition(ReactDOM.findDOMNode(this.refs.input), position);
-        if (autosuggest) {
-            setTimeout(() => this._triggerAutosuggest());
-        }
+  _setCaretPosition = (position, autosuggest) => {
+    setCaretPosition(ReactDOM.findDOMNode(this.refs.input), position);
+    if (autosuggest) {
+      setTimeout(() => this._triggerAutosuggest());
     }
+  };
 
-    onExpressionChange(expressionString) {
-        let inputElement = ReactDOM.findDOMNode(this.refs.input);
-        if (!inputElement) {
-            return;
-        }
-
-        const parserInfo = this._getParserInfo();
-
-        let expressionErrorMessage = null;
-        let suggestions           = [];
-        let parsedExpression;
+  onExpressionChange(expressionString) {
+    let inputElement = ReactDOM.findDOMNode(this.refs.input);
+    if (!inputElement) {
+      return;
+    }
 
-        try {
-            parsedExpression = compile(expressionString, parserInfo)
-        } catch (e) {
-            expressionErrorMessage = e;
-            console.error("expression error:", expressionErrorMessage);
-        }
+    const parserInfo = this._getParserInfo();
 
-        const isValid = parsedExpression && parsedExpression.length > 0;
-        const [selectionStart, selectionEnd] = getSelectionPosition(inputElement);
-        const hasSelection = selectionStart !== selectionEnd;
-        const isAtEnd = selectionEnd === expressionString.length;
-        const endsWithWhitespace = /\s$/.test(expressionString);
-
-        // don't show suggestions if
-        // * there's a section
-        // * we're at the end of a valid expression, unless the user has typed another space
-        if (!hasSelection && !(isValid && isAtEnd && !endsWithWhitespace)) {
-            try {
-                suggestions = suggest(expressionString, {
-                    ...parserInfo,
-                    index: selectionEnd
-                })
-            } catch (e) {
-                console.error("suggest error:", e);
-            }
-        }
+    let expressionErrorMessage = null;
+    let suggestions = [];
+    let parsedExpression;
 
-        this.setState({
-            expressionErrorMessage,
-            expressionString,
-            parsedExpression,
-            suggestions,
-            showAll: false
-        });
+    try {
+      parsedExpression = compile(expressionString, parserInfo);
+    } catch (e) {
+      expressionErrorMessage = e;
+      console.error("expression error:", expressionErrorMessage);
     }
 
-    onShowMoreMouseDown (e) {
-        e.preventDefault()
-        this.setState({ showAll: true })
+    const isValid = parsedExpression && parsedExpression.length > 0;
+    const [selectionStart, selectionEnd] = getSelectionPosition(inputElement);
+    const hasSelection = selectionStart !== selectionEnd;
+    const isAtEnd = selectionEnd === expressionString.length;
+    const endsWithWhitespace = /\s$/.test(expressionString);
+
+    // don't show suggestions if
+    // * there's a section
+    // * we're at the end of a valid expression, unless the user has typed another space
+    if (!hasSelection && !(isValid && isAtEnd && !endsWithWhitespace)) {
+      try {
+        suggestions = suggest(expressionString, {
+          ...parserInfo,
+          index: selectionEnd,
+        });
+      } catch (e) {
+        console.error("suggest error:", e);
+      }
     }
 
-    render() {
-        let errorMessage = this.state.expressionErrorMessage;
-        if (errorMessage && !errorMessage.length) errorMessage = t`unknown error`;
-
-        const { placeholder } = this.props;
-        const { suggestions, showAll } = this.state;
-
-        return (
-            <div className={cx(S.editor, "relative")}>
-                <TokenizedInput
-                    ref="input"
-                    className={cx(S.input, "my1 input block full", { "border-error": errorMessage })}
-                    type="text"
-                    placeholder={placeholder}
-                    value={this.state.expressionString}
-                    onChange={(e) => this.onExpressionChange(e.target.value)}
-                    onKeyDown={this.onInputKeyDown}
-                    onBlur={this.onInputBlur}
-                    onFocus={(e) => this._triggerAutosuggest()}
-                    onClick={this.onInputClick}
-                    autoFocus
-                    parserInfo={this._getParserInfo()}
-                />
-                <div className={cx(S.equalSign, "spread flex align-center h4 text-dark", { [S.placeholder]: !this.state.expressionString })}>=</div>
-                { suggestions.length ?
-                    <Popover
-                        className="pb1 not-rounded border-dark"
-                        hasArrow={false}
-                        tetherOptions={{
-                            attachment: 'top left',
-                            targetAttachment: 'bottom left'
-                        }}
+    this.setState({
+      expressionErrorMessage,
+      expressionString,
+      parsedExpression,
+      suggestions,
+      showAll: false,
+    });
+  }
+
+  onShowMoreMouseDown(e) {
+    e.preventDefault();
+    this.setState({ showAll: true });
+  }
+
+  render() {
+    let errorMessage = this.state.expressionErrorMessage;
+    if (errorMessage && !errorMessage.length) errorMessage = t`unknown error`;
+
+    const { placeholder } = this.props;
+    const { suggestions, showAll } = this.state;
+
+    return (
+      <div className={cx(S.editor, "relative")}>
+        <TokenizedInput
+          ref="input"
+          className={cx(S.input, "my1 input block full", {
+            "border-error": errorMessage,
+          })}
+          type="text"
+          placeholder={placeholder}
+          value={this.state.expressionString}
+          onChange={e => this.onExpressionChange(e.target.value)}
+          onKeyDown={this.onInputKeyDown}
+          onBlur={this.onInputBlur}
+          onFocus={e => this._triggerAutosuggest()}
+          onClick={this.onInputClick}
+          autoFocus
+          parserInfo={this._getParserInfo()}
+        />
+        <div
+          className={cx(S.equalSign, "spread flex align-center h4 text-dark", {
+            [S.placeholder]: !this.state.expressionString,
+          })}
+        >
+          =
+        </div>
+        {suggestions.length ? (
+          <Popover
+            className="pb1 not-rounded border-dark"
+            hasArrow={false}
+            tetherOptions={{
+              attachment: "top left",
+              targetAttachment: "bottom left",
+            }}
+          >
+            <ul style={{ minWidth: 150, overflow: "hidden" }}>
+              {(showAll
+                ? suggestions
+                : suggestions.slice(0, MAX_SUGGESTIONS)
+              ).map((suggestion, i) =>
+                // insert section title. assumes they're sorted by type
+                [
+                  (i === 0 || suggestion.type !== suggestions[i - 1].type) && (
+                    <li
+                      ref={"header-" + i}
+                      className="mx2 h6 text-uppercase text-bold text-grey-3 py1 pt2"
                     >
-                        <ul style={{minWidth: 150, overflow: "hidden"}}>
-                            {(showAll ? suggestions : suggestions.slice(0,MAX_SUGGESTIONS)).map((suggestion, i) =>
-                                // insert section title. assumes they're sorted by type
-                                [(i === 0 || suggestion.type !== suggestions[i - 1].type) &&
-                                    <li  ref={"header-" + i} className="mx2 h6 text-uppercase text-bold text-grey-3 py1 pt2">
-                                        {suggestion.type}
-                                    </li>
-                                ,
-                                    <li ref={i} style={{ paddingTop: 5, paddingBottom: 5 }}
-                                        className={cx("px2 cursor-pointer text-white-hover bg-brand-hover", {"text-white bg-brand": i === this.state.highlightedSuggestion})}
-                                        onMouseDownCapture={(e) => this.onSuggestionMouseDown(e, i)}
-                                    >
-                                        { suggestion.prefixLength ?
-                                            <span>
-                                                <span className={cx("text-brand text-bold", {"text-white bg-brand": i === this.state.highlightedSuggestion})}>{suggestion.name.slice(0, suggestion.prefixLength)}</span>
-                                                <span>{suggestion.name.slice(suggestion.prefixLength)}</span>
-                                            </span>
-                                        :
-                                            suggestion.name
-                                        }
-                                    </li>
-                                ]
-                            )}
-                            { !showAll && suggestions.length >= MAX_SUGGESTIONS &&
-                                <li style={{ paddingTop: 5, paddingBottom: 5 }} onMouseDownCapture={(e) => this.onShowMoreMouseDown(e)} className="px2 text-italic text-grey-3 cursor-pointer text-brand-hover">and {suggestions.length - MAX_SUGGESTIONS} more</li>
-                            }
-                        </ul>
-                    </Popover>
-                : null}
-            </div>
-        );
-    }
+                      {suggestion.type}
+                    </li>
+                  ),
+                  <li
+                    ref={i}
+                    style={{ paddingTop: 5, paddingBottom: 5 }}
+                    className={cx(
+                      "px2 cursor-pointer text-white-hover bg-brand-hover",
+                      {
+                        "text-white bg-brand":
+                          i === this.state.highlightedSuggestion,
+                      },
+                    )}
+                    onMouseDownCapture={e => this.onSuggestionMouseDown(e, i)}
+                  >
+                    {suggestion.prefixLength ? (
+                      <span>
+                        <span
+                          className={cx("text-brand text-bold", {
+                            "text-white bg-brand":
+                              i === this.state.highlightedSuggestion,
+                          })}
+                        >
+                          {suggestion.name.slice(0, suggestion.prefixLength)}
+                        </span>
+                        <span>
+                          {suggestion.name.slice(suggestion.prefixLength)}
+                        </span>
+                      </span>
+                    ) : (
+                      suggestion.name
+                    )}
+                  </li>,
+                ],
+              )}
+              {!showAll &&
+                suggestions.length >= MAX_SUGGESTIONS && (
+                  <li
+                    style={{ paddingTop: 5, paddingBottom: 5 }}
+                    onMouseDownCapture={e => this.onShowMoreMouseDown(e)}
+                    className="px2 text-italic text-grey-3 cursor-pointer text-brand-hover"
+                  >
+                    and {suggestions.length - MAX_SUGGESTIONS} more
+                  </li>
+                )}
+            </ul>
+          </Popover>
+        ) : null}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
index 532578beef8d1abdbaa88301006be56a83733ace..d24eadf6985fa6c1b03864a95c5840d2c94394e9 100644
--- a/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/ExpressionWidget.jsx
@@ -1,93 +1,111 @@
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import _ from 'underscore';
-import { t } from 'c-3po';
+import _ from "underscore";
+import { t } from "c-3po";
 import ExpressionEditorTextfield from "./ExpressionEditorTextfield.jsx";
 import { isExpression } from "metabase/lib/expressions";
 
-
 export default class ExpressionWidget extends Component {
+  static propTypes = {
+    expression: PropTypes.array,
+    name: PropTypes.string,
+    tableMetadata: PropTypes.object.isRequired,
+    onSetExpression: PropTypes.func.isRequired,
+    onRemoveExpression: PropTypes.func.isRequired,
+    onCancel: PropTypes.func.isRequired,
+  };
 
-    static propTypes = {
-        expression: PropTypes.array,
-        name: PropTypes.string,
-        tableMetadata: PropTypes.object.isRequired,
-        onSetExpression: PropTypes.func.isRequired,
-        onRemoveExpression: PropTypes.func.isRequired,
-        onCancel: PropTypes.func.isRequired
-    };
-
-    static defaultProps = {
-        expression: null,
-        name: ""
-    }
+  static defaultProps = {
+    expression: null,
+    name: "",
+  };
 
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
-    }
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
 
-    componentWillReceiveProps(newProps) {
-        this.setState({
-            name: newProps.name,
-            expression: newProps.expression
-        });
-    }
+  componentWillReceiveProps(newProps) {
+    this.setState({
+      name: newProps.name,
+      expression: newProps.expression,
+    });
+  }
 
-    isValid() {
-        const { name, expression, error } = this.state;
-        return (!_.isEmpty(name) && !error && isExpression(expression));
-    }
+  isValid() {
+    const { name, expression, error } = this.state;
+    return !_.isEmpty(name) && !error && isExpression(expression);
+  }
 
-    render() {
-        const { expression } = this.state;
+  render() {
+    const { expression } = this.state;
 
-        return (
-            <div style={{maxWidth: "600px"}}>
-                <div className="p2">
-                    <div className="h5 text-uppercase text-grey-3 text-bold">{t`Field formula`}</div>
-                    <div>
-                        <ExpressionEditorTextfield
-                            expression={expression}
-                            tableMetadata={this.props.tableMetadata}
-                            onChange={(parsedExpression) => this.setState({expression: parsedExpression, error: null})}
-                            onError={(errorMessage) => this.setState({error: errorMessage})}
-                        />
-                      <p className="h5 text-grey-5">
-                            {t`Think of this as being kind of like writing a formula in a spreadsheet program: you can use numbers, fields in this table, mathematical symbols like +, and some functions. So you could type something like Subtotal &minus; Cost.`}
-                            &nbsp;<a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/04-asking-questions.html#creating-a-custom-field">{t`Learn more`}</a>
-                        </p>
-                    </div>
+    return (
+      <div style={{ maxWidth: "600px" }}>
+        <div className="p2">
+          <div className="h5 text-uppercase text-grey-3 text-bold">{t`Field formula`}</div>
+          <div>
+            <ExpressionEditorTextfield
+              expression={expression}
+              tableMetadata={this.props.tableMetadata}
+              onChange={parsedExpression =>
+                this.setState({ expression: parsedExpression, error: null })
+              }
+              onError={errorMessage => this.setState({ error: errorMessage })}
+            />
+            <p className="h5 text-grey-5">
+              {t`Think of this as being kind of like writing a formula in a spreadsheet program: you can use numbers, fields in this table, mathematical symbols like +, and some functions. So you could type something like Subtotal &minus; Cost.`}
+              &nbsp;<a
+                className="link"
+                target="_blank"
+                href="http://www.metabase.com/docs/latest/users-guide/04-asking-questions.html#creating-a-custom-field"
+              >{t`Learn more`}</a>
+            </p>
+          </div>
 
-                    <div className="mt3 h5 text-uppercase text-grey-3 text-bold">{t`Give it a name`}</div>
-                    <div>
-                        <input
-                            className="my1 input block full"
-                            type="text"
-                            value={this.state.name}
-                            placeholder={t`Something nice and descriptive`}
-                            onChange={(event) => this.setState({name: event.target.value})}
-                        />
-                    </div>
-                </div>
+          <div className="mt3 h5 text-uppercase text-grey-3 text-bold">{t`Give it a name`}</div>
+          <div>
+            <input
+              className="my1 input block full"
+              type="text"
+              value={this.state.name}
+              placeholder={t`Something nice and descriptive`}
+              onChange={event => this.setState({ name: event.target.value })}
+            />
+          </div>
+        </div>
 
-                <div className="mt2 p2 border-top flex flex-row align-center justify-between">
-                    <div className="ml-auto">
-                        <button className="Button" onClick={() => this.props.onCancel()}>{t`Cancel`}</button>
-                          <button
-                              className={cx("Button ml2", {"Button--primary": this.isValid()})}
-                              onClick={() => this.props.onSetExpression(this.state.name, this.state.expression)}
-                              disabled={!this.isValid()}>
-                                {this.props.expression ? t`Update` : t`Done`}
-                          </button>
-                    </div>
-                    <div>
-                        {this.props.expression ?
-                         <a className="pr2 ml2 text-warning link" onClick={() => this.props.onRemoveExpression(this.props.name)}>{t`Remove`}</a>
-                         : null }
-                    </div>
-                </div>
-            </div>
-        );
-    }
+        <div className="mt2 p2 border-top flex flex-row align-center justify-between">
+          <div className="ml-auto">
+            <button
+              className="Button"
+              onClick={() => this.props.onCancel()}
+            >{t`Cancel`}</button>
+            <button
+              className={cx("Button ml2", {
+                "Button--primary": this.isValid(),
+              })}
+              onClick={() =>
+                this.props.onSetExpression(
+                  this.state.name,
+                  this.state.expression,
+                )
+              }
+              disabled={!this.isValid()}
+            >
+              {this.props.expression ? t`Update` : t`Done`}
+            </button>
+          </div>
+          <div>
+            {this.props.expression ? (
+              <a
+                className="pr2 ml2 text-warning link"
+                onClick={() => this.props.onRemoveExpression(this.props.name)}
+              >{t`Remove`}</a>
+            ) : null}
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
index 0806049ac0a87e38794d3d99f99f7c9778814048..73e0dc9bcdf7e6aa9e62dda69fd56e4bf8086b3e 100644
--- a/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/Expressions.jsx
@@ -1,54 +1,65 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 import IconBorder from "metabase/components/IconBorder.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
 import { format } from "metabase/lib/expressions/formatter";
 
-
 export default class Expressions extends Component {
+  static propTypes = {
+    expressions: PropTypes.object,
+    tableMetadata: PropTypes.object,
+    onAddExpression: PropTypes.func.isRequired,
+    onEditExpression: PropTypes.func.isRequired,
+  };
 
-    static propTypes = {
-        expressions: PropTypes.object,
-        tableMetadata: PropTypes.object,
-        onAddExpression: PropTypes.func.isRequired,
-        onEditExpression: PropTypes.func.isRequired
-    };
-
-    static defaultProps = {
-        expressions: {}
-    };
+  static defaultProps = {
+    expressions: {},
+  };
 
-    render() {
-        const { expressions, onAddExpression, onEditExpression } = this.props;
+  render() {
+    const { expressions, onAddExpression, onEditExpression } = this.props;
 
-        let sortedNames = _.sortBy(_.keys(expressions), _.identity);
-        return (
-            <div className="pb3">
-                <div className="pb1 h6 text-uppercase text-grey-3 text-bold">Custom fields</div>
+    let sortedNames = _.sortBy(_.keys(expressions), _.identity);
+    return (
+      <div className="pb3">
+        <div className="pb1 h6 text-uppercase text-grey-3 text-bold">
+          Custom fields
+        </div>
 
-                { sortedNames && sortedNames.map(name =>
-                    <div key={name} className="pb1 text-brand text-bold cursor-pointer flex flex-row align-center justify-between" onClick={() => onEditExpression(name)}>
-                        <span>{name}</span>
-                        <Tooltip tooltip={format(expressions[name], {
-                            tableMetadata: this.props.tableMetadata,
-                            customFields: expressions
-                        })}>
-                            <span className="QuestionTooltipTarget" />
-                        </Tooltip>
-                    </div>
-                  )}
-
-                    <a data-metabase-event={"QueryBuilder;Show Add Custom Field"} className="text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color" onClick={() => onAddExpression()}>
-                        <IconBorder borderRadius="3px">
-                            <Icon name="add" size={14} />
-                        </IconBorder>
-                        <span className="ml1">{t`Add a custom field`}</span>
-                    </a>
+        {sortedNames &&
+          sortedNames.map(name => (
+            <div
+              key={name}
+              className="pb1 text-brand text-bold cursor-pointer flex flex-row align-center justify-between"
+              onClick={() => onEditExpression(name)}
+            >
+              <span>{name}</span>
+              <Tooltip
+                tooltip={format(expressions[name], {
+                  tableMetadata: this.props.tableMetadata,
+                  customFields: expressions,
+                })}
+              >
+                <span className="QuestionTooltipTarget" />
+              </Tooltip>
             </div>
-        );
-    }
+          ))}
+
+        <a
+          data-metabase-event={"QueryBuilder;Show Add Custom Field"}
+          className="text-grey-2 text-bold flex align-center text-grey-4-hover cursor-pointer no-decoration transition-color"
+          onClick={() => onAddExpression()}
+        >
+          <IconBorder borderRadius="3px">
+            <Icon name="add" size={14} />
+          </IconBorder>
+          <span className="ml1">{t`Add a custom field`}</span>
+        </a>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.css b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.css
index 70547cf26f5924218665972dc41276ad4a3f4f30..c85f651f2bba5b4fb6d36c2e48209c1117253610 100644
--- a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.css
+++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.css
@@ -25,13 +25,13 @@
 
 .Expression-aggregation,
 .Expression-metric {
-  border: 1px solid #9CC177;
-  background-color: #E4F7D1;
+  border: 1px solid #9cc177;
+  background-color: #e4f7d1;
 }
 
 .Expression-field {
-  border: 1px solid #509EE3;
-  background-color: #C7E3FB;
+  border: 1px solid #509ee3;
+  background-color: #c7e3fb;
 }
 
 .Expression-selected.Expression-aggregation,
@@ -39,11 +39,11 @@
 .Expression-selected .Expression-aggregation,
 .Expression-selected .Expression-metric {
   color: white;
-  background-color: #9CC177;
+  background-color: #9cc177;
 }
 
 .Expression-selected.Expression-field,
 .Expression-selected .Expression-field {
   color: white;
-  background-color: #509EE3;
+  background-color: #509ee3;
 }
diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx
index 6d56e2b263b0e8d80d4a0d3be703303085023a95..b6c4103179ee53c32595dea7795080234969fae4 100644
--- a/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedExpression.jsx
@@ -5,104 +5,116 @@ import "./TokenizedExpression.css";
 import cx from "classnames";
 
 export default class TokenizedExpression extends React.Component {
-    render() {
-        // TODO: use the Chevrotain parser or tokenizer
-        // let parsed = parse(this.props.source, this.props.parserInfo);
-        const parsed = parse(this.props.source);
-        return renderSyntaxTree(parsed);
-    }
+  render() {
+    // TODO: use the Chevrotain parser or tokenizer
+    // let parsed = parse(this.props.source, this.props.parserInfo);
+    const parsed = parse(this.props.source);
+    return renderSyntaxTree(parsed);
+  }
 }
 
-const renderSyntaxTree = (node, index) =>
-    <span key={index} className={cx("Expression-node", "Expression-" + node.type, { "Expression-tokenized": node.tokenized })}>
-        {node.text != null ?
-            node.text
-        : node.children ?
-            node.children.map(renderSyntaxTree)
-        : null }
-    </span>
-
+const renderSyntaxTree = (node, index) => (
+  <span
+    key={index}
+    className={cx("Expression-node", "Expression-" + node.type, {
+      "Expression-tokenized": node.tokenized,
+    })}
+  >
+    {node.text != null
+      ? node.text
+      : node.children ? node.children.map(renderSyntaxTree) : null}
+  </span>
+);
 
 function nextNonWhitespace(tokens, index) {
-    while (index < tokens.length && /^\s+$/.test(tokens[++index])) {
-    }
-    return tokens[index];
+  while (index < tokens.length && /^\s+$/.test(tokens[++index])) {}
+  return tokens[index];
 }
 
 function parse(expressionString) {
-    let tokens = (expressionString || " ").match(/[a-zA-Z]\w*|"(?:[^\\"]+|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"|\(|\)|\d+|\s+|[*/+-]|.+/g);
+  let tokens = (expressionString || " ").match(
+    /[a-zA-Z]\w*|"(?:[^\\"]+|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"|\(|\)|\d+|\s+|[*/+-]|.+/g,
+  );
 
-    let root = { type: "group", children: [] };
-    let current = root;
-    let outsideAggregation = true;
-    const stack = [];
-    const push = (element) => {
-        current.children.push(element);
-        stack.push(current);
-        current = element;
-    }
-    const pop = () => {
-        if (stack.length === 0) {
-            return;
-        }
-        current = stack.pop();
+  let root = { type: "group", children: [] };
+  let current = root;
+  let outsideAggregation = true;
+  const stack = [];
+  const push = element => {
+    current.children.push(element);
+    stack.push(current);
+    current = element;
+  };
+  const pop = () => {
+    if (stack.length === 0) {
+      return;
     }
-    for (let i = 0; i < tokens.length; i++) {
-        let token = tokens[i];
-        if (/^[a-zA-Z]\w*$/.test(token)) {
-            if (nextNonWhitespace(tokens, i) === "(") {
-                outsideAggregation = false;
-                push({
-                    type: "aggregation",
-                    tokenized: true,
-                    children: []
-                });
-                current.children.push({
-                    type: "aggregation-name",
-                    text: token
-                });
-            } else {
-                current.children.push({
-                    type: outsideAggregation ? "metric" : "field",
-                    tokenized: true,
-                    text: token
-                });
-            }
-        } else if (/^"(?:[^\\"]+|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"$/.test(token)) {
-            current.children.push({
-                type: "string-literal",
-                tokenized: true,
-                children: [
-                    { type: "open-quote", text: "\"" },
-                    { type: outsideAggregation ? "metric" : "field", text: JSON.parse(token) },
-                    { type: "close-quote", text: "\"" }
-                ]
-            });
-        } else if (token === "(") {
-            push({ type: "group", children: [] })
-            current.children.push({ type: "open-paren", text: "(" })
-        } else if (token === ")") {
-            current.children.push({ type: "close-paren", text: ")" })
-            pop();
-            if (current.type === "aggregation") {
-                outsideAggregation = true;
-                pop();
-            }
-        } else {
-            // special handling for unclosed string literals
-            if (i === tokens.length - 1 && /^".+[^"]$/.test(token)) {
-                current.children.push({
-                    type: "string-literal",
-                    tokenized: true,
-                    children: [
-                        { type: "open-quote", text: "\"" },
-                        { type: outsideAggregation ? "metric" : "field", text: JSON.parse(token + "\"") }
-                    ]
-                });
-            } else {
-                current.children.push({ type: "token", text: token });
-            }
-        }
+    current = stack.pop();
+  };
+  for (let i = 0; i < tokens.length; i++) {
+    let token = tokens[i];
+    if (/^[a-zA-Z]\w*$/.test(token)) {
+      if (nextNonWhitespace(tokens, i) === "(") {
+        outsideAggregation = false;
+        push({
+          type: "aggregation",
+          tokenized: true,
+          children: [],
+        });
+        current.children.push({
+          type: "aggregation-name",
+          text: token,
+        });
+      } else {
+        current.children.push({
+          type: outsideAggregation ? "metric" : "field",
+          tokenized: true,
+          text: token,
+        });
+      }
+    } else if (
+      /^"(?:[^\\"]+|\\(?:[bfnrtv"\\/]|u[0-9a-fA-F]{4}))*"$/.test(token)
+    ) {
+      current.children.push({
+        type: "string-literal",
+        tokenized: true,
+        children: [
+          { type: "open-quote", text: '"' },
+          {
+            type: outsideAggregation ? "metric" : "field",
+            text: JSON.parse(token),
+          },
+          { type: "close-quote", text: '"' },
+        ],
+      });
+    } else if (token === "(") {
+      push({ type: "group", children: [] });
+      current.children.push({ type: "open-paren", text: "(" });
+    } else if (token === ")") {
+      current.children.push({ type: "close-paren", text: ")" });
+      pop();
+      if (current.type === "aggregation") {
+        outsideAggregation = true;
+        pop();
+      }
+    } else {
+      // special handling for unclosed string literals
+      if (i === tokens.length - 1 && /^".+[^"]$/.test(token)) {
+        current.children.push({
+          type: "string-literal",
+          tokenized: true,
+          children: [
+            { type: "open-quote", text: '"' },
+            {
+              type: outsideAggregation ? "metric" : "field",
+              text: JSON.parse(token + '"'),
+            },
+          ],
+        });
+      } else {
+        current.children.push({ type: "token", text: token });
+      }
     }
-    return root;
+  }
+  return root;
 }
diff --git a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx
index f431a6c542f00b146a34e34a33deb270b5ef7584..3a5e2958e9f09cd1b49bad8575f1b0bce3a2c243 100644
--- a/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx
+++ b/frontend/src/metabase/query_builder/components/expressions/TokenizedInput.jsx
@@ -3,146 +3,169 @@ import ReactDOM from "react-dom";
 
 import TokenizedExpression from "./TokenizedExpression.jsx";
 
-import { getCaretPosition, saveSelection, getSelectionPosition } from "metabase/lib/dom"
-
-
-const KEYCODE_BACKSPACE      = 8;
-const KEYCODE_LEFT           = 37;
-const KEYCODE_RIGHT          = 39;
+import {
+  getCaretPosition,
+  saveSelection,
+  getSelectionPosition,
+} from "metabase/lib/dom";
+
+const KEYCODE_BACKSPACE = 8;
+const KEYCODE_LEFT = 37;
+const KEYCODE_RIGHT = 39;
 const KEYCODE_FORWARD_DELETE = 46;
 
 export default class TokenizedInput extends Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-            value: ""
-        }
-    }
-
-    _getValue() {
-        if (this.props.value != undefined) {
-            return this.props.value;
-        } else {
-            return this.state.value;
-        }
-    }
-    _setValue(value) {
-        ReactDOM.findDOMNode(this).value = value;
-        if (typeof this.props.onChange === "function") {
-            this.props.onChange({ target: { value }});
-        } else {
-            this.setState({ value });
-        }
-    }
-
-    componentDidMount() {
-        ReactDOM.findDOMNode(this).focus();
-        this.componentDidUpdate()
-
-        document.addEventListener("selectionchange", this.onSelectionChange, false);
-    }
-    componentWillUnmount() {
-        document.removeEventListener("selectionchange", this.onSelectionChange, false);
-    }
-    onSelectionChange = (e) => {
-        ReactDOM.findDOMNode(this).selectionStart = getCaretPosition(ReactDOM.findDOMNode(this))
+  constructor(props) {
+    super(props);
+    this.state = {
+      value: "",
+    };
+  }
+
+  _getValue() {
+    if (this.props.value != undefined) {
+      return this.props.value;
+    } else {
+      return this.state.value;
     }
-    onClick = (e) => {
-        this._isTyping = false;
-        return this.props.onClick(e);
+  }
+  _setValue(value) {
+    ReactDOM.findDOMNode(this).value = value;
+    if (typeof this.props.onChange === "function") {
+      this.props.onChange({ target: { value } });
+    } else {
+      this.setState({ value });
     }
-    onInput = (e) => {
-        this._setValue(e.target.textContent);
+  }
+
+  componentDidMount() {
+    ReactDOM.findDOMNode(this).focus();
+    this.componentDidUpdate();
+
+    document.addEventListener("selectionchange", this.onSelectionChange, false);
+  }
+  componentWillUnmount() {
+    document.removeEventListener(
+      "selectionchange",
+      this.onSelectionChange,
+      false,
+    );
+  }
+  onSelectionChange = e => {
+    ReactDOM.findDOMNode(this).selectionStart = getCaretPosition(
+      ReactDOM.findDOMNode(this),
+    );
+  };
+  onClick = e => {
+    this._isTyping = false;
+    return this.props.onClick(e);
+  };
+  onInput = e => {
+    this._setValue(e.target.textContent);
+  };
+  onKeyDown = e => {
+    // isTyping signals whether the user is typing characters (keyCode >= 65) vs. deleting / navigating with arrows / clicking to select
+    const isTyping = this._isTyping;
+    // also keep isTyping same when deleting
+    this._isTyping =
+      e.keyCode >= 65 || (e.keyCode === KEYCODE_BACKSPACE && isTyping);
+
+    const input = ReactDOM.findDOMNode(this);
+
+    let [start, end] = getSelectionPosition(input);
+    if (start !== end) {
+      return;
     }
-    onKeyDown = (e) => {
-        // isTyping signals whether the user is typing characters (keyCode >= 65) vs. deleting / navigating with arrows / clicking to select
-        const isTyping = this._isTyping;
-        // also keep isTyping same when deleting
-        this._isTyping = e.keyCode >= 65 || (e.keyCode === KEYCODE_BACKSPACE && isTyping);
-
-        const input = ReactDOM.findDOMNode(this);
-
-        let [start, end] = getSelectionPosition(input);
-        if (start !== end) {
-            return;
-        }
 
-        let element = window.getSelection().focusNode;
-        while (element && element !== input) {
-            // check ancestors of the focused node for "Expression-tokenized"
-            // if the element is marked as "tokenized" we might want to intercept keypresses
-            if (element.classList && element.classList.contains("Expression-tokenized")) {
-                const positionInElement = getCaretPosition(element);
-                const atStart = positionInElement === 0;
-                const atEnd = positionInElement === element.textContent.length;
-                const isSelected = element.classList.contains("Expression-selected");
-                if (!isSelected && !isTyping && (
-                    atEnd && e.keyCode === KEYCODE_BACKSPACE ||
-                    atStart && e.keyCode === KEYCODE_FORWARD_DELETE
-                )) {
-                    // not selected, not "typging", and hit backspace, so mark as "selected"
-                    element.classList.add("Expression-selected");
-                    e.stopPropagation();
-                    e.preventDefault();
-                    return;
-                } else if (isSelected && (
-                    atEnd && e.keyCode === KEYCODE_BACKSPACE ||
-                    atStart && e.keyCode === KEYCODE_FORWARD_DELETE
-                )) {
-                    // selected and hit backspace, so delete it
-                    element.parentNode.removeChild(element);
-                    this._setValue(input.textContent);
-                    e.stopPropagation();
-                    e.preventDefault();
-                    return;
-                } else if (isSelected && (
-                    atEnd && e.keyCode === KEYCODE_LEFT ||
-                    atStart && e.keyCode === KEYCODE_RIGHT
-                )) {
-                    // selected and hit left arrow, so enter "typing" mode and unselect it
-                    element.classList.remove("Expression-selected");
-                    this._isTyping = true;
-                    e.stopPropagation();
-                    e.preventDefault();
-                    return;
-                }
-            }
-            // nada, try the next ancestor
-            element = element.parentNode;
+    let element = window.getSelection().focusNode;
+    while (element && element !== input) {
+      // check ancestors of the focused node for "Expression-tokenized"
+      // if the element is marked as "tokenized" we might want to intercept keypresses
+      if (
+        element.classList &&
+        element.classList.contains("Expression-tokenized")
+      ) {
+        const positionInElement = getCaretPosition(element);
+        const atStart = positionInElement === 0;
+        const atEnd = positionInElement === element.textContent.length;
+        const isSelected = element.classList.contains("Expression-selected");
+        if (
+          !isSelected &&
+          !isTyping &&
+          ((atEnd && e.keyCode === KEYCODE_BACKSPACE) ||
+            (atStart && e.keyCode === KEYCODE_FORWARD_DELETE))
+        ) {
+          // not selected, not "typging", and hit backspace, so mark as "selected"
+          element.classList.add("Expression-selected");
+          e.stopPropagation();
+          e.preventDefault();
+          return;
+        } else if (
+          isSelected &&
+          ((atEnd && e.keyCode === KEYCODE_BACKSPACE) ||
+            (atStart && e.keyCode === KEYCODE_FORWARD_DELETE))
+        ) {
+          // selected and hit backspace, so delete it
+          element.parentNode.removeChild(element);
+          this._setValue(input.textContent);
+          e.stopPropagation();
+          e.preventDefault();
+          return;
+        } else if (
+          isSelected &&
+          ((atEnd && e.keyCode === KEYCODE_LEFT) ||
+            (atStart && e.keyCode === KEYCODE_RIGHT))
+        ) {
+          // selected and hit left arrow, so enter "typing" mode and unselect it
+          element.classList.remove("Expression-selected");
+          this._isTyping = true;
+          e.stopPropagation();
+          e.preventDefault();
+          return;
         }
-
-        // if we haven't handled the event yet, pass it on to our parent
-        this.props.onKeyDown(e);
+      }
+      // nada, try the next ancestor
+      element = element.parentNode;
     }
 
-    componentDidUpdate() {
-        const inputNode = ReactDOM.findDOMNode(this);
-        const restore = saveSelection(inputNode);
+    // if we haven't handled the event yet, pass it on to our parent
+    this.props.onKeyDown(e);
+  };
 
-        ReactDOM.unmountComponentAtNode(inputNode);
-        while (inputNode.firstChild) {
-            inputNode.removeChild(inputNode.firstChild);
-        }
-        ReactDOM.render(<TokenizedExpression source={this._getValue()} parserInfo={this.props.parserInfo} />, inputNode);
+  componentDidUpdate() {
+    const inputNode = ReactDOM.findDOMNode(this);
+    const restore = saveSelection(inputNode);
 
-        if (document.activeElement === inputNode) {
-            restore();
-        }
+    ReactDOM.unmountComponentAtNode(inputNode);
+    while (inputNode.firstChild) {
+      inputNode.removeChild(inputNode.firstChild);
     }
-
-    render() {
-        const { className, onFocus, onBlur } = this.props;
-        return (
-            <div
-                className={className}
-                style={{ whiteSpace: "pre-wrap" }}
-                contentEditable
-                onKeyDown={this.onKeyDown}
-                onInput={this.onInput}
-                onFocus={onFocus}
-                onBlur={onBlur}
-                onClick={this.onClick}
-            />
-        );
+    ReactDOM.render(
+      <TokenizedExpression
+        source={this._getValue()}
+        parserInfo={this.props.parserInfo}
+      />,
+      inputNode,
+    );
+
+    if (document.activeElement === inputNode) {
+      restore();
     }
+  }
+
+  render() {
+    const { className, onFocus, onBlur } = this.props;
+    return (
+      <div
+        className={className}
+        style={{ whiteSpace: "pre-wrap" }}
+        contentEditable
+        onKeyDown={this.onKeyDown}
+        onInput={this.onInput}
+        onFocus={onFocus}
+        onBlur={onBlur}
+        onClick={this.onClick}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
index 8d70385d093c7c9583aa4c85b949ed0d507fe95f..527486d99c9a926f10891a71af9f52e33e48d7ac 100644
--- a/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/DateOperatorSelector.jsx
@@ -9,34 +9,33 @@ import Select, { Option } from "metabase/components/Select";
 import type { Operator } from "./pickers/DatePicker";
 
 type Props = {
-    operator: ?string,
-    operators: Operator[],
-    onOperatorChange: (o: Operator) => void,
-    hideTimeSelectors?: bool
-}
+  operator: ?string,
+  operators: Operator[],
+  onOperatorChange: (o: Operator) => void,
+  hideTimeSelectors?: boolean,
+};
 
 export default class DateOperatorSelector extends Component {
-    props: Props;
-
-    render() {
-        const { operator, operators, onOperatorChange } = this.props;
-
-        return (
-            <div
-              className="mx2 mb2 relative z3"
-              style={{ minWidth: 100 }}
-            >
-                <Select
-                  value={_.findWhere(operators, { name: operator })}
-                  onChange={e => onOperatorChange(e.target.value)}
-                  width={150}
-                  compact
-                >
-                  { operators.map(operator =>
-                    <Option value={operator}>{operator.displayName}</Option>
-                  )}
-                </Select>
-            </div>
-        );
-    }
+  props: Props;
+
+  render() {
+    const { operator, operators, onOperatorChange } = this.props;
+
+    return (
+      <div className="mx2 mb2 relative z3" style={{ minWidth: 100 }}>
+        <Select
+          value={_.findWhere(operators, { name: operator })}
+          onChange={e => onOperatorChange(e.target.value)}
+          width={150}
+          compact
+        >
+          {operators.map(operator => (
+            <Option key={operator.name} value={operator}>
+              {operator.displayName}
+            </Option>
+          ))}
+        </Select>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/DateUnitSelector.jsx b/frontend/src/metabase/query_builder/components/filters/DateUnitSelector.jsx
index 7b0a4285db8ffb94e7e7478eaecb6b274554c31b..6a77ed12ed568454b95c2bc6af51c6da71f7b04a 100644
--- a/frontend/src/metabase/query_builder/components/filters/DateUnitSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/DateUnitSelector.jsx
@@ -4,27 +4,36 @@ import Select, { Option } from "metabase/components/Select";
 import { pluralize, capitalize } from "humanize-plus";
 
 type DateUnitSelectorProps = {
-    value: RelativeDatetimeUnit,
-    onChange: (value: RelativeDatetimeUnit) => void,
-    open: bool,
-    intervals?: number,
-    togglePicker: () => void,
-    formatter: (value: ?number) => ?number,
-    periods: RelativeDatetimeUnit[]
-}
+  value: RelativeDatetimeUnit,
+  onChange: (value: RelativeDatetimeUnit) => void,
+  open: boolean,
+  intervals?: number,
+  togglePicker: () => void,
+  formatter: (value: ?number) => ?number,
+  periods: RelativeDatetimeUnit[],
+};
 
-const DateUnitSelector = ({ open, value, onChange, togglePicker, intervals, formatter, periods }: DateUnitSelectorProps) =>
+const DateUnitSelector = ({
+  open,
+  value,
+  onChange,
+  togglePicker,
+  intervals,
+  formatter,
+  periods,
+}: DateUnitSelectorProps) => (
   <Select
     value={value}
     onChange={e => onChange(e.target.value)}
     width={150}
     compact
   >
-    {periods.map(period =>
-        <Option value={period} key={period}>
-          {capitalize(pluralize(formatter(intervals) || 1, period))}
-        </Option>
-    )}
+    {periods.map(period => (
+      <Option value={period} key={period}>
+        {capitalize(pluralize(formatter(intervals) || 1, period))}
+      </Option>
+    ))}
   </Select>
+);
 
 export default DateUnitSelector;
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
index 8eacd228ad113f68fb3e113ef7cadcef9a9fc775..c92425f32e4318de6c18998ca59aa5c5fcc5f670 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterList.jsx
@@ -1,9 +1,9 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { findDOMNode } from 'react-dom';
-import { t } from 'c-3po';
-import FilterWidget from './FilterWidget.jsx';
+import { findDOMNode } from "react-dom";
+import { t } from "c-3po";
+import FilterWidget from "./FilterWidget.jsx";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import type { Filter } from "metabase/meta/types/Query";
@@ -12,67 +12,72 @@ import Dimension from "metabase-lib/lib/Dimension";
 import type { TableMetadata } from "metabase/meta/types/Metadata";
 
 type Props = {
-    query: StructuredQuery,
-    filters: Array<Filter>,
-    removeFilter?: (index: number) => void,
-    updateFilter?: (index: number, filter: Filter) => void,
-    maxDisplayValues?: number,
-    tableMetadata?: TableMetadata // legacy parameter
+  query: StructuredQuery,
+  filters: Array<Filter>,
+  removeFilter?: (index: number) => void,
+  updateFilter?: (index: number, filter: Filter) => void,
+  maxDisplayValues?: number,
+  tableMetadata?: TableMetadata, // legacy parameter
 };
 
 type State = {
-    shouldScroll: bool
+  shouldScroll: boolean,
 };
 
 export default class FilterList extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-          shouldScroll: false
-        };
-    }
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      shouldScroll: false,
+    };
+  }
 
-    componentDidUpdate () {
-      this.state.shouldScroll ? (findDOMNode(this).scrollLeft = findDOMNode(this).scrollWidth) : null;
-    }
+  componentDidUpdate() {
+    this.state.shouldScroll
+      ? (findDOMNode(this).scrollLeft = findDOMNode(this).scrollWidth)
+      : null;
+  }
 
-    componentWillReceiveProps (nextProps: Props) {
-      // only scroll when a filter is added
-      if(nextProps.filters.length > this.props.filters.length) {
-        this.setState({ shouldScroll: true })
-      } else {
-        this.setState({ shouldScroll: false })
-      }
+  componentWillReceiveProps(nextProps: Props) {
+    // only scroll when a filter is added
+    if (nextProps.filters.length > this.props.filters.length) {
+      this.setState({ shouldScroll: true });
+    } else {
+      this.setState({ shouldScroll: false });
     }
+  }
 
-    componentDidMount () {
-      this.componentDidUpdate();
-    }
+  componentDidMount() {
+    this.componentDidUpdate();
+  }
 
-    render() {
-        const { query, filters, tableMetadata } = this.props;
-        return (
-            <div className="Query-filterList scroll-x scroll-show">
-                {filters.map((filter, index) =>
-                    <FilterWidget
-                        key={index}
-                        placeholder={t`Item`}
-                        // TODO: update widgets that are still passing tableMetadata instead of query
-                        query={query || {
-                            table: () => tableMetadata,
-                            parseFieldReference: (fieldRef) => Dimension.parseMBQL(fieldRef, tableMetadata)
-                        }}
-                        filter={filter}
-                        index={index}
-                        removeFilter={this.props.removeFilter}
-                        updateFilter={this.props.updateFilter}
-                        maxDisplayValues={this.props.maxDisplayValues}
-                    />
-                )}
-            </div>
-        );
-    }
+  render() {
+    const { query, filters, tableMetadata } = this.props;
+    return (
+      <div className="Query-filterList scroll-x scroll-show">
+        {filters.map((filter, index) => (
+          <FilterWidget
+            key={index}
+            placeholder={t`Item`}
+            // TODO: update widgets that are still passing tableMetadata instead of query
+            query={
+              query || {
+                table: () => tableMetadata,
+                parseFieldReference: fieldRef =>
+                  Dimension.parseMBQL(fieldRef, tableMetadata),
+              }
+            }
+            filter={filter}
+            index={index}
+            removeFilter={this.props.removeFilter}
+            updateFilter={this.props.updateFilter}
+            maxDisplayValues={this.props.maxDisplayValues}
+          />
+        ))}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx b/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx
index da404da4b91f08db9fd333634db8a135dbf8079b..4fc3909d2fdf858ac01f44592f88de6ae1930f03 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterOptions.jsx
@@ -1,20 +1,31 @@
 import React, { Component } from "react";
+import PropTypes from "prop-types";
 
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
+import { getFilterOptions, setFilterOptions } from "metabase/lib/query/filter";
 
 import CheckBox from "metabase/components/CheckBox";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-import { getOperator } from "./pickers/DatePicker.jsx";
-
+const OPTION_NAMES = {
+  "include-current": filter => {
+    const period = (
+      <strong key="notsurewhythisneedsakey">
+        {getCurrentIntervalName(filter)}
+      </strong>
+    );
+    return jt`Include ${period}`;
+  },
+  "case-sensitive": () => t`Case sensitive`,
+};
 
 const CURRENT_INTERVAL_NAME = {
-    "day":    t`today`,
-    "week":   t`this week`,
-    "month":  t`this month`,
-    "year":   t`this year`,
-    "minute": t`this minute`,
-    "hour":   t`this hour`,
+  day: t`today`,
+  week: t`this week`,
+  month: t`this month`,
+  year: t`this year`,
+  minute: t`this minute`,
+  hour: t`this hour`,
 };
 
 function getCurrentIntervalName(filter: FieldFilter): ?string {
@@ -25,57 +36,73 @@ function getCurrentIntervalName(filter: FieldFilter): ?string {
   return null;
 }
 
-function getFilterOptions(filter: FieldFilter): FilterOptions {
-  if (filter[0] === "time-interval") {
-    // $FlowFixMe:
-    const options: FilterOptions = filter[4] || {};
-    return options;
+export default class FilterOptions extends Component {
+  static propTypes = {
+    filter: PropTypes.array.isRequired,
+    onFilterChange: PropTypes.func.isRequired,
+    // either an operator from schema_metadata or DatePicker
+    operator: PropTypes.object.isRequired,
+  };
+
+  getOptions() {
+    return (this.props.operator && this.props.operator.options) || {};
   }
-  return {};
-}
 
-function setFilterOptions<T: FieldFilter>(filter: T, options: FilterOptions): T {
-  if (filter[0] === "time-interval") {
-    // $FlowFixMe
-    return [...filter.slice(0,4), options];
-  } else {
-    return filter;
+  getOptionName(name) {
+    if (OPTION_NAMES[name]) {
+      return OPTION_NAMES[name](this.props.filter);
+    }
+    return name;
   }
-}
 
-export default class FilterOptions extends Component {
-  hasCurrentPeriod = () => {
-      const { filter } = this.props;
-      return getFilterOptions(filter)["include-current"] || false;
+  getOptionValue(name) {
+    const { filter } = this.props;
+    let value = getFilterOptions(filter)[name];
+    if (value !== undefined) {
+      return value;
+    }
+    const option = this.getOptions()[name];
+    if (option && option.defaultValue !== undefined) {
+      return option.defaultValue;
+    }
+    // for now values are always boolean, default to false
+    return false;
   }
 
-  toggleCurrentPeriod = () => {
+  setOptionValue(name, value) {
     const { filter } = this.props;
-    const operator = getOperator(filter);
+    const options = getFilterOptions(filter);
+    this.props.onFilterChange(
+      setFilterOptions(filter, {
+        ...options,
+        [name]: !options[name],
+      }),
+    );
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Filter", "SetOption", name);
+  }
 
-    if (operator && operator.options && operator.options["include-current"]) {
-        const options = getFilterOptions(filter);
-        this.props.onFilterChange(setFilterOptions(filter, {
-          ...options,
-          "include-current": !options["include-current"]
-        }));
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Filter", "ToggleCurrentPeriod", !options["include-current"])
-    }
+  toggleOptionValue(name) {
+    this.setOptionValue(name, !this.getOptionValue(name));
   }
 
   render() {
-    const { filter } = this.props;
-    const operator = getOperator(filter);
-    if (operator && operator.options && operator.options["include-current"]) {
-      return (
-        <div className="flex align-center" onClick={() => this.toggleCurrentPeriod()}>
-            <CheckBox checked={this.hasCurrentPeriod()} />
-            <label className="ml1">
-                {jt`Include ${<b>{getCurrentIntervalName(filter)}</b>}`}
-            </label>
-        </div>
-      )
+    const options = Object.entries(this.getOptions());
+    if (options.length === 0) {
+      return null;
     }
-    return null;
+    return (
+      <div className="flex align-center">
+        {options.map(([name, option]) => (
+          <div
+            key={name}
+            className="flex align-center"
+            onClick={() => this.toggleOptionValue(name)}
+          >
+            <CheckBox checked={this.getOptionValue(name)} />
+            <label className="ml1">{this.getOptionName(name)}</label>
+          </div>
+        ))}
+      </div>
+    );
   }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
index 589838e5a7a06582f18b06b5d0525f514377000a..df9ea37b2bed24ed50413f9f82741de0b7d74dd5 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterPopover.jsx
@@ -1,16 +1,17 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import FieldList from "../FieldList.jsx";
 import OperatorSelector from "./OperatorSelector.jsx";
 import FilterOptions from "./FilterOptions";
-import DatePicker from "./pickers/DatePicker.jsx";
+import DatePicker, { getOperator } from "./pickers/DatePicker.jsx";
 import TimePicker from "./pickers/TimePicker.jsx";
 import NumberPicker from "./pickers/NumberPicker.jsx";
 import SelectPicker from "./pickers/SelectPicker.jsx";
 import TextPicker from "./pickers/TextPicker.jsx";
+import FieldValuesWidget from "metabase/components/FieldValuesWidget.jsx";
 
 import Icon from "metabase/components/Icon.jsx";
 
@@ -21,296 +22,369 @@ import { formatField, singularize } from "metabase/lib/formatting";
 import cx from "classnames";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
-import type { Filter, FieldFilter, ConcreteField } from "metabase/meta/types/Query";
-import type { FieldMetadata, Operator } from "metabase/meta/types/Metadata";
+import type {
+  Filter,
+  FieldFilter,
+  ConcreteField,
+} from "metabase/meta/types/Query";
+import type { Operator } from "metabase/meta/types/Metadata";
+
+import Field from "metabase-lib/lib/metadata/Field";
 
 type Props = {
-    maxHeight?: number,
-    query: StructuredQuery,
-    filter?: Filter,
-    onCommitFilter: (filter: Filter) => void,
-    onClose: () => void
-}
+  maxHeight?: number,
+  query: StructuredQuery,
+  filter?: Filter,
+  onCommitFilter: (filter: Filter) => void,
+  onClose: () => void,
+};
 
 type State = {
-    filter: FieldFilter
-}
+  filter: FieldFilter,
+};
 
 export default class FilterPopover extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            // $FlowFixMe
-            filter: props.filter || []
-        };
-    }
-
-    componentWillMount() {
-        window.addEventListener('keydown', this.commitOnEnter);
-    }
-
-    componentWillUnmount() {
-        window.removeEventListener('keydown', this.commitOnEnter);
+  props: Props;
+  state: State;
+
+  constructor(props: Props) {
+    super(props);
+
+    const filter = props.filter || [];
+    this.state = {
+      // $FlowFixMe
+      filter: filter,
+    };
+  }
+
+  componentWillMount() {
+    window.addEventListener("keydown", this.commitOnEnter);
+  }
+
+  componentWillUnmount() {
+    window.removeEventListener("keydown", this.commitOnEnter);
+  }
+
+  commitOnEnter = (event: KeyboardEvent) => {
+    if (this.isValid() && event.key === "Enter") {
+      this.commitFilter(this.state.filter);
     }
+  };
 
-    commitOnEnter = (event: KeyboardEvent) => {
-        if(this.isValid() && event.key === "Enter") {
-            this.commitFilter(this.state.filter);
-        }
-    }
-
-    commitFilter = (filter: FieldFilter) => {
-        this.props.onCommitFilter(filter);
-        this.props.onClose();
-    }
+  commitFilter = (filter: FieldFilter) => {
+    this.props.onCommitFilter(filter);
+    this.props.onClose();
+  };
 
-    setField = (fieldId: ConcreteField) => {
-        const { query } = this.props;
-        let { filter } = this.state;
-        if (filter[1] !== fieldId) {
-            // different field, reset the filter
-            filter = [];
+  setField = (fieldId: ConcreteField) => {
+    const { query } = this.props;
+    let { filter } = this.state;
+    if (filter[1] !== fieldId) {
+      // different field, reset the filter
+      filter = [];
 
-            // update the field
-            filter[1] = fieldId;
+      // update the field
+      filter[1] = fieldId;
 
-            // default to the first operator
-            let { field } = Query.getFieldTarget(filter[1], query.table());
+      // default to the first operator
+      let { field } = query.table().fieldTarget(filter[1]);
 
-            // let the DatePicker choose the default operator, otherwise use the first one
-            let operator = isDate(field) ? null : field.operators[0].name;
+      // let the DatePicker choose the default operator, otherwise use the first one
+      let operator = isDate(field) ? null : field.operators[0].name;
 
-            // $FlowFixMe
-            filter = this._updateOperator(filter, operator);
-        }
-        this.setState({ filter });
+      // $FlowFixMe
+      filter = this._updateOperator(filter, operator);
     }
+    this.setState({ filter });
+  };
 
-    setFilter = (filter: FieldFilter) => {
-        this.setState({ filter });
-    }
+  setFilter = (filter: FieldFilter) => {
+    this.setState({ filter });
+  };
 
-    setOperator = (operator: string) => {
-        let { filter } = this.state;
-        if (filter[0] !== operator) {
-            filter = this._updateOperator(filter, operator);
-            this.setState({ filter });
+  setOperator = (operator: string) => {
+    let { filter } = this.state;
+    if (filter[0] !== operator) {
+      filter = this._updateOperator(filter, operator);
+    }
+    this.setState({ filter });
+  };
+
+  setValue(index: number, value: any) {
+    let { filter } = this.state;
+    // $FlowFixMe Flow doesn't like spread operator
+    let newFilter: FieldFilter = [...filter];
+    newFilter[index + 2] = value;
+    this.setState({ filter: newFilter });
+  }
+
+  setValues = (values: any[]) => {
+    let { filter } = this.state;
+    // $FlowFixMe
+    this.setState({ filter: filter.slice(0, 2).concat(values) });
+  };
+
+  _updateOperator(oldFilter: FieldFilter, operatorName: ?string): FieldFilter {
+    const { query } = this.props;
+    let { field } = query.table().fieldTarget(oldFilter[1]);
+    let operator = field.operator(operatorName);
+    let oldOperator = field.operator(oldFilter[0]);
+
+    // update the operator
+    // $FlowFixMe
+    let filter: FieldFilter = [operatorName, oldFilter[1]];
+
+    if (operator) {
+      for (let i = 0; i < operator.fields.length; i++) {
+        if (operator.defaults && operator.defaults[i] !== undefined) {
+          filter.push(operator.defaults[i]);
+        } else {
+          filter.push(undefined);
+        }
+      }
+      if (operator.optionsDefaults) {
+        filter.push(operator.optionsDefaults);
+      }
+      if (oldOperator) {
+        // copy over values of the same type
+        for (let i = 0; i < oldFilter.length - 2; i++) {
+          let field = operator.multi ? operator.fields[0] : operator.fields[i];
+          let oldField = oldOperator.multi
+            ? oldOperator.fields[0]
+            : oldOperator.fields[i];
+          if (
+            field &&
+            oldField &&
+            field.type === oldField.type &&
+            oldFilter[i + 2] !== undefined
+          ) {
+            filter[i + 2] = oldFilter[i + 2];
+          }
         }
+      }
     }
-
-    setValue(index: number, value: any) {
-        let { filter } = this.state;
-        // $FlowFixMe Flow doesn't like spread operator
-        let newFilter: FieldFilter = [...filter]
-        newFilter[index + 2] = value;
-        this.setState({ filter: newFilter });
+    return filter;
+  }
+
+  isValid() {
+    const { query } = this.props;
+    let { filter } = this.state;
+    // has an operator name and field id
+    if (filter[0] == null || !Query.isValidField(filter[1])) {
+      return false;
     }
-
-    setValues = (values: any[]) => {
-        let { filter } = this.state;
-        // $FlowFixMe
-        this.setState({ filter: filter.slice(0,2).concat(values) });
+    // field/operator combo is valid
+    let { field } = query.table().fieldTarget(filter[1]);
+    let operator = field.operators_lookup[filter[0]];
+    if (operator) {
+      // has the mininum number of arguments
+      if (filter.length - 2 < operator.fields.length) {
+        return false;
+      }
     }
-
-    _updateOperator(oldFilter: FieldFilter, operatorName: ?string): FieldFilter {
-        const { query } = this.props;
-        let { field } = Query.getFieldTarget(oldFilter[1], query.table());
-        let operator = field.operators_lookup[operatorName];
-        let oldOperator = field.operators_lookup[oldFilter[0]];
-
-        // update the operator
-        // $FlowFixMe
-        let filter: FieldFilter = [operatorName, oldFilter[1]];
-
-        if (operator) {
-            for (let i = 0; i < operator.fields.length; i++) {
-                if (operator.defaults && operator.defaults[i] !== undefined) {
-                    filter.push(operator.defaults[i]);
-                } else {
-                    filter.push(undefined);
-                }
-            }
-            if (oldOperator) {
-                // copy over values of the same type
-                for (let i = 0; i < oldFilter.length - 2; i++) {
-                    let field = operator.multi ? operator.fields[0] : operator.fields[i];
-                    let oldField = oldOperator.multi ? oldOperator.fields[0] : oldOperator.fields[i];
-                    if (field && oldField && field.type === oldField.type && oldFilter[i + 2] !== undefined) {
-                        filter[i + 2] = oldFilter[i + 2];
-                    }
-                }
-            }
-        }
-        return filter;
+    // arguments are non-null/undefined
+    for (var i = 2; i < filter.length; i++) {
+      if (filter[i] == null) {
+        return false;
+      }
     }
 
-    isValid() {
-        const { query } = this.props;
-        let { filter } = this.state;
-        // has an operator name and field id
-        if (filter[0] == null || !Query.isValidField(filter[1])) {
-            return false;
+    return true;
+  }
+
+  clearField = () => {
+    let { filter } = this.state;
+    // $FlowFixMe
+    this.setState({
+      filter: [...filter.slice(0, 1), null, ...filter.slice(2)],
+    });
+  };
+
+  renderPicker(filter: FieldFilter, field: Field) {
+    let operator: ?Operator = field.operators_lookup[filter[0]];
+    let fieldWidgets =
+      operator &&
+      operator.fields.map((operatorField, index) => {
+        if (!operator) {
+          return null;
         }
-        // field/operator combo is valid
-        let { field } = Query.getFieldTarget(filter[1], query.table());
-        let operator = field.operators_lookup[filter[0]];
-        if (operator) {
-            // has the mininum number of arguments
-            if (filter.length - 2 < operator.fields.length) {
-                return false;
-            }
+        let values, onValuesChange;
+        let placeholder =
+          (operator && operator.placeholders && operator.placeholders[index]) ||
+          undefined;
+        if (operator.multi) {
+          values = this.state.filter.slice(2);
+          onValuesChange = values => this.setValues(values);
+        } else {
+          // $FlowFixMe
+          values = [this.state.filter[2 + index]];
+          onValuesChange = values => this.setValue(index, values[0]);
         }
-        // arguments are non-null/undefined
-        for (var i = 2; i < filter.length; i++) {
-            if (filter[i] == null) {
-                return false;
-            }
+        if (operatorField.type === "select") {
+          return (
+            <SelectPicker
+              key={index}
+              options={operatorField.values}
+              // $FlowFixMe
+              values={(values: Array<string>)}
+              onValuesChange={onValuesChange}
+              placeholder={placeholder}
+              multi={operator.multi}
+              onCommit={this.onCommit}
+            />
+          );
+        } else if (field) {
+          return (
+            <FieldValuesWidget
+              value={(values: Array<string>)}
+              onChange={onValuesChange}
+              multi={operator.multi}
+              placeholder={placeholder}
+              field={field}
+              searchField={field.filterSearchField()}
+              autoFocus={index === 0}
+              alwaysShowOptions={operator.fields.length === 1}
+              minWidth={440}
+              maxWidth={440}
+            />
+          );
+        } else if (operatorField.type === "text") {
+          return (
+            <TextPicker
+              key={index}
+              // $FlowFixMe
+              values={(values: Array<string>)}
+              onValuesChange={onValuesChange}
+              placeholder={placeholder}
+              multi={operator.multi}
+              onCommit={this.onCommit}
+            />
+          );
+        } else if (operatorField.type === "number") {
+          return (
+            <NumberPicker
+              key={index}
+              // $FlowFixMe
+              values={(values: Array<number | null>)}
+              onValuesChange={onValuesChange}
+              placeholder={placeholder}
+              multi={operator.multi}
+              onCommit={this.onCommit}
+            />
+          );
         }
-
-        return true;
+        return (
+          <span key={index}>
+            {t`not implemented ${operatorField.type}`}{" "}
+            {operator.multi ? t`true` : t`false`}
+          </span>
+        );
+      });
+    if (fieldWidgets && fieldWidgets.filter(f => f).length > 0) {
+      return fieldWidgets;
+    } else {
+      return <div className="mb1" />;
     }
+  }
 
-    clearField = () => {
-        let { filter } = this.state;
-        // $FlowFixMe
-        this.setState({ filter: [...filter.slice(0, 1), null, ...filter.slice(2)] });
+  onCommit = () => {
+    if (this.isValid()) {
+      this.commitFilter(this.state.filter);
     }
-
-    renderPicker(filter: FieldFilter, field: FieldMetadata) {
-        let operator: ?Operator = field.operators_lookup[filter[0]];
-        return operator && operator.fields.map((operatorField, index) => {
-            if (!operator) {
-                return;
-            }
-            let values, onValuesChange;
-            let placeholder = operator && operator.placeholders && operator.placeholders[index] || undefined;
-            if (operator.multi) {
-                values = this.state.filter.slice(2);
-                onValuesChange = (values) => this.setValues(values);
-            } else {
-                values = [this.state.filter[2 + index]];
-                onValuesChange = (values) => this.setValue(index, values[0]);
-            }
-            if (operatorField.type === "select") {
-                return (
-                    <SelectPicker
-                        options={operatorField.values}
-                        // $FlowFixMe
-                        values={(values: Array<string>)}
-                        onValuesChange={onValuesChange}
-                        placeholder={placeholder}
-                        multi={operator.multi}
-                        onCommit={this.onCommit}
-                    />
-                );
-            } else if (operatorField.type === "text") {
-                return (
-                    <TextPicker
-                        // $FlowFixMe
-                        values={(values: Array<string>)}
-                        onValuesChange={onValuesChange}
-                        placeholder={placeholder}
-                        multi={operator.multi}
-                        onCommit={this.onCommit}
-                    />
-                );
-            } else if (operatorField.type === "number") {
-                return (
-                    <NumberPicker
-                        // $FlowFixMe
-                        values={(values: Array<number|null>)}
-                        onValuesChange={onValuesChange}
-                        placeholder={placeholder}
-                        multi={operator.multi}
-                        onCommit={this.onCommit}
-                    />
-                );
-            }
-            return <span>{t`not implemented ${operatorField.type}`} {operator.multi ? t`true` : t`false`}</span>;
-        });
-    }
-
-    onCommit = () => {
-        if (this.isValid()) {
-            this.commitFilter(this.state.filter)
-        }
-    }
-
-    render() {
-        const { query } = this.props;
-        const { filter } = this.state;
-        const [operator, fieldRef] = filter;
-
-        if (operator === "SEGMENT" || fieldRef == undefined) {
-            return (
-                <div className="FilterPopover">
-                    <FieldList
-                        className="text-purple"
-                        maxHeight={this.props.maxHeight}
-                        field={fieldRef}
-                        fieldOptions={query.filterFieldOptions(filter)}
-                        segmentOptions={query.filterSegmentOptions(filter)}
-                        tableMetadata={query.table()}
-                        onFieldChange={this.setField}
-                        onFilterChange={this.commitFilter}
-                    />
-                </div>
-            );
-        } else {
-            let { table, field } = Query.getFieldTarget(fieldRef, query.table());
-            const dimension = query.parseFieldReference(fieldRef);
-            return (
-                <div style={{
-                    minWidth: 300,
-                    // $FlowFixMe
-                    maxWidth: dimension.field().isDate() ? null : 500
-                }}>
-                    <div className="FilterPopover-header text-grey-3 p1 mt1 flex align-center">
-                        <a className="cursor-pointer text-purple-hover transition-color flex align-center" onClick={this.clearField}>
-                            <Icon name="chevronleft" size={18}/>
-                            <h3 className="inline-block">{singularize(table.display_name)}</h3>
-                        </a>
-                        <h3 className="mx1">-</h3>
-                        <h3 className="text-default">{formatField(field)}</h3>
-                    </div>
-                    { isTime(field) ?
-                        <TimePicker
-                            className="mt1 border-top"
-                            filter={filter}
-                            onFilterChange={this.setFilter}
-                        />
-                    : isDate(field) ?
-                        <DatePicker
-                            className="mt1 border-top"
-                            filter={filter}
-                            onFilterChange={this.setFilter}
-                        />
-                    :
-                        <div>
-                            <OperatorSelector
-                                operator={filter[0]}
-                                operators={field.operators}
-                                onOperatorChange={this.setOperator}
-                            />
-                            { this.renderPicker(filter, field) }
-                        </div>
-                    }
-                    <div className="FilterPopover-footer border-top flex align-center p2">
-                        <FilterOptions filter={filter} onFilterChange={this.setFilter} />
-                        <button
-                            data-ui-tag="add-filter"
-                            className={cx("Button Button--purple ml-auto", { "disabled": !this.isValid() })}
-                            onClick={() => this.commitFilter(this.state.filter)}
-                        >
-                            {!this.props.filter ? t`Add filter` : t`Update filter`}
-                        </button>
-                    </div>
-                </div>
-            );
-        }
+  };
+
+  render() {
+    const { query } = this.props;
+    const { filter } = this.state;
+    const [operatorName, fieldRef] = filter;
+
+    if (operatorName === "SEGMENT" || fieldRef == undefined) {
+      return (
+        <div className="FilterPopover">
+          <FieldList
+            className="text-purple"
+            maxHeight={this.props.maxHeight}
+            field={fieldRef}
+            fieldOptions={query.filterFieldOptions(filter)}
+            segmentOptions={query.filterSegmentOptions(filter)}
+            tableMetadata={query.table()}
+            onFieldChange={this.setField}
+            onFilterChange={this.commitFilter}
+          />
+        </div>
+      );
+    } else {
+      let { table, field } = query.table().fieldTarget(fieldRef);
+      const dimension = query.parseFieldReference(fieldRef);
+      return (
+        <div
+          style={{
+            minWidth: 300,
+            // $FlowFixMe
+            maxWidth: dimension.field().isDate() ? null : 500,
+          }}
+        >
+          <div className="FilterPopover-header border-bottom text-grey-3 p1 mt1 flex align-center">
+            <a
+              className="cursor-pointer text-purple-hover transition-color flex align-center"
+              onClick={this.clearField}
+            >
+              <Icon name="chevronleft" size={18} />
+              <h3 className="inline-block">
+                {singularize(table.display_name)}
+              </h3>
+            </a>
+            <h3 className="mx1">-</h3>
+            <h3 className="text-default">{formatField(field)}</h3>
+          </div>
+          {isTime(field) ? (
+            <TimePicker
+              className="mt1 border-top"
+              filter={filter}
+              onFilterChange={this.setFilter}
+            />
+          ) : isDate(field) ? (
+            <DatePicker
+              className="mt1 border-top"
+              filter={filter}
+              onFilterChange={this.setFilter}
+            />
+          ) : (
+            <div>
+              <div className="inline-block px1 pt1">
+                <OperatorSelector
+                  operator={operatorName}
+                  operators={field.operators}
+                  onOperatorChange={this.setOperator}
+                />
+              </div>
+              {this.renderPicker(filter, field)}
+            </div>
+          )}
+          <div className="FilterPopover-footer border-top flex align-center p1 pl2">
+            <FilterOptions
+              filter={filter}
+              onFilterChange={this.setFilter}
+              operator={
+                isDate(field)
+                  ? // DatePicker uses a different set of operator objects
+                    getOperator(filter)
+                  : // Normal operators defined in schema_metadata
+                    field.operator && field.operator(operatorName)
+              }
+            />
+            <button
+              data-ui-tag="add-filter"
+              className={cx("Button Button--purple ml-auto", {
+                disabled: !this.isValid(),
+              })}
+              onClick={() => this.commitFilter(this.state.filter)}
+            >
+              {!this.props.filter ? t`Add filter` : t`Update filter`}
+            </button>
+          </div>
+        </div>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
index b61a60123e4df9aefc290a57ab4e47d6b0b0fda9..006016213876d2f554cf1b65d3d60411914173b7 100644
--- a/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/FilterWidget.jsx
@@ -1,171 +1,202 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
-import FieldName from '../FieldName.jsx';
+import FieldName from "../FieldName.jsx";
 import Popover from "metabase/components/Popover.jsx";
 import FilterPopover from "./FilterPopover.jsx";
+import Value from "metabase/components/Value";
 
 import { generateTimeFilterValuesDescriptions } from "metabase/lib/query_time";
-import { formatValue } from "metabase/lib/formatting";
+import { hasFilterOptions } from "metabase/lib/query/filter";
 
 import cx from "classnames";
 import _ from "underscore";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import type { Filter } from "metabase/meta/types/Query";
+import type { Value as ValueType } from "metabase/meta/types/Dataset";
 
 type Props = {
-    query: StructuredQuery,
-    filter: Filter,
-    index: number,
-    updateFilter?: (index: number, field: Filter) => void,
-    removeFilter?: (index: number) => void,
-    maxDisplayValues?: number
-}
+  query: StructuredQuery,
+  filter: Filter,
+  index: number,
+  updateFilter?: (index: number, field: Filter) => void,
+  removeFilter?: (index: number) => void,
+  maxDisplayValues?: number,
+};
 type State = {
-    isOpen: bool
-}
+  isOpen: boolean,
+};
 
 export default class FilterWidget extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
+  props: Props;
+  state: State;
 
-        this.state = {
-            isOpen: this.props.filter[0] == undefined
-        };
-    }
+  constructor(props: Props) {
+    super(props);
 
-    static defaultProps = {
-        maxDisplayValues: 1
+    this.state = {
+      isOpen: this.props.filter[0] == undefined,
     };
-
-    open = () => {
-        this.setState({ isOpen: true });
-    }
-
-    close = () => {
-        this.setState({ isOpen: false });
-    }
-
-    renderOperatorFilter() {
-        const { query, filter, maxDisplayValues } = this.props;
-        let [op, field, ...values] = filter;
-
-        const dimension = query.parseFieldReference(field);
-        if (!dimension) {
-            return null;
-        }
-
-        const operator = dimension.operator(op);
-
-        let formattedValues;
-        // $FlowFixMe: not understanding maxDisplayValues is provided by defaultProps
-        if (operator && operator.multi && values.length > maxDisplayValues) {
-            formattedValues = [values.length + " selections"];
-        } else if (dimension.field().isDate() && !dimension.field().isTime()) {
-            formattedValues = generateTimeFilterValuesDescriptions(filter);
-        } else {
-            // TODO Atte Keinänen 7/16/17: Move formatValue to metabase-lib
-            formattedValues = values.filter(value => value !== undefined).map(value =>
-                formatValue(value, { column: dimension.field() })
-            )
-        }
-
-        return (
-            <div
-                className="flex flex-column justify-center"
-                onClick={this.open}
-            >
-                <div className="flex align-center" style={{padding: "0.5em", paddingTop: "0.3em", paddingBottom: "0.3em", paddingLeft: 0}}>
-                    <FieldName
-                        className="Filter-section Filter-section-field"
-                        field={field}
-                        tableMetadata={query.table()}
-                    />
-                    <div className="Filter-section Filter-section-operator">
-                        &nbsp;
-                        <a className="QueryOption flex align-center">{operator && operator.moreVerboseName}</a>
-                    </div>
-                </div>
-                { formattedValues.length > 0 && (
-                    <div className="flex align-center flex-wrap">
-                        {formattedValues.map((formattedValue, valueIndex) =>
-                            <div key={valueIndex} className="Filter-section Filter-section-value">
-                                <span className="QueryOption">{formattedValue}</span>
-                            </div>
-                        )}
-                    </div>
-                )}
-            </div>
-        )
-    }
-
-    renderSegmentFilter() {
-        const { query, filter } = this.props;
-        const segment = _.find(query.table().segments, (s) => s.id === filter[1]);
-        return (
-            <div onClick={this.open}>
-                <div className="flex align-center" style={{padding: "0.5em", paddingTop: "0.3em", paddingBottom: "0.3em", paddingLeft: 0}}>
-                    <div className="Filter-section Filter-section-field">
-                        <span className="QueryOption">{t`Matches`}</span>
-                    </div>
-                </div>
-                <div className="flex align-center flex-wrap">
-                    <div className="Filter-section Filter-section-value">
-                        <span className="QueryOption">{segment && segment.name}</span>
-                    </div>
-                </div>
-            </div>
-        )
+  }
+
+  static defaultProps = {
+    maxDisplayValues: 1,
+  };
+
+  open = () => {
+    this.setState({ isOpen: true });
+  };
+
+  close = () => {
+    this.setState({ isOpen: false });
+  };
+
+  renderOperatorFilter() {
+    const { query, filter, maxDisplayValues } = this.props;
+    let [op, field] = filter;
+    // $FlowFixMe
+    let values: ValueType[] = hasFilterOptions(filter)
+      ? filter.slice(2, -1)
+      : filter.slice(2);
+
+    const dimension = query.parseFieldReference(field);
+    if (!dimension) {
+      return null;
     }
 
-    renderPopover() {
-        if (this.state.isOpen) {
-            const { query, filter } = this.props;
-            return (
-                <Popover
-                    id="FilterPopover"
-                    ref="filterPopover"
-                    className="FilterPopover"
-                    isInitiallyOpen={this.props.filter[1] === null}
-                    onClose={this.close}
-                    horizontalAttachments={["left"]}
-                    autoWidth
-                >
-                    <FilterPopover
-                        query={query}
-                        filter={filter}
-                        onCommitFilter={(filter) => this.props.updateFilter && this.props.updateFilter(this.props.index, filter)}
-                        onClose={this.close}
-                    />
-                </Popover>
-            );
-        }
+    const operator = dimension.operator(op);
+
+    let formattedValues;
+    // $FlowFixMe: not understanding maxDisplayValues is provided by defaultProps
+    if (operator && operator.multi && values.length > maxDisplayValues) {
+      formattedValues = [values.length + " selections"];
+    } else if (dimension.field().isDate() && !dimension.field().isTime()) {
+      formattedValues = generateTimeFilterValuesDescriptions(filter);
+    } else {
+      formattedValues = values
+        .filter(value => value !== undefined)
+        .map((value, index) => (
+          <Value key={index} value={value} column={dimension.field()} remap />
+        ));
     }
 
-    render() {
-        const { filter, index, removeFilter } = this.props;
-        return (
-            <div className={cx("Query-filter p1 pl2", { "selected": this.state.isOpen })}>
-                <div className="flex justify-center">
-                    {filter[0] === "SEGMENT" ?
-                        this.renderSegmentFilter()
-                    :
-                        this.renderOperatorFilter()
-                    }
-                    {this.renderPopover()}
-                </div>
-                { removeFilter &&
-                    <a className="text-grey-2 no-decoration px1 flex align-center" onClick={() => removeFilter(index)}>
-                        <Icon name='close' size={14} />
-                    </a>
-                }
-            </div>
-        );
+    return (
+      <div className="flex flex-column justify-center" onClick={this.open}>
+        <div
+          className="flex align-center"
+          style={{
+            padding: "0.5em",
+            paddingTop: "0.3em",
+            paddingBottom: "0.3em",
+            paddingLeft: 0,
+          }}
+        >
+          <FieldName
+            className="Filter-section Filter-section-field"
+            field={field}
+            tableMetadata={query.table()}
+          />
+          <div className="Filter-section Filter-section-operator">
+            &nbsp;
+            <a className="QueryOption flex align-center">
+              {operator && operator.moreVerboseName}
+            </a>
+          </div>
+        </div>
+        {formattedValues.length > 0 && (
+          <div className="flex align-center flex-wrap">
+            {formattedValues.map((formattedValue, valueIndex) => (
+              <div
+                key={valueIndex}
+                className="Filter-section Filter-section-value"
+              >
+                <span className="QueryOption">{formattedValue}</span>
+              </div>
+            ))}
+          </div>
+        )}
+      </div>
+    );
+  }
+
+  renderSegmentFilter() {
+    const { query, filter } = this.props;
+    const segment = _.find(query.table().segments, s => s.id === filter[1]);
+    return (
+      <div onClick={this.open}>
+        <div
+          className="flex align-center"
+          style={{
+            padding: "0.5em",
+            paddingTop: "0.3em",
+            paddingBottom: "0.3em",
+            paddingLeft: 0,
+          }}
+        >
+          <div className="Filter-section Filter-section-field">
+            <span className="QueryOption">{t`Matches`}</span>
+          </div>
+        </div>
+        <div className="flex align-center flex-wrap">
+          <div className="Filter-section Filter-section-value">
+            <span className="QueryOption">{segment && segment.name}</span>
+          </div>
+        </div>
+      </div>
+    );
+  }
+
+  renderPopover() {
+    if (this.state.isOpen) {
+      const { query, filter } = this.props;
+      return (
+        <Popover
+          id="FilterPopover"
+          ref="filterPopover"
+          className="FilterPopover"
+          isInitiallyOpen={this.props.filter[1] === null}
+          onClose={this.close}
+          horizontalAttachments={["left", "center"]}
+          autoWidth
+        >
+          <FilterPopover
+            query={query}
+            filter={filter}
+            onCommitFilter={filter =>
+              this.props.updateFilter &&
+              this.props.updateFilter(this.props.index, filter)
+            }
+            onClose={this.close}
+          />
+        </Popover>
+      );
     }
+  }
+
+  render() {
+    const { filter, index, removeFilter } = this.props;
+    return (
+      <div
+        className={cx("Query-filter p1 pl2", { selected: this.state.isOpen })}
+      >
+        <div className="flex justify-center">
+          {filter[0] === "SEGMENT"
+            ? this.renderSegmentFilter()
+            : this.renderOperatorFilter()}
+          {this.renderPopover()}
+        </div>
+        {removeFilter && (
+          <a
+            className="text-grey-2 no-decoration px1 flex align-center"
+            onClick={() => removeFilter(index)}
+          >
+            <Icon name="close" size={14} />
+          </a>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
index 4470dd5c902eed3c79db32f89f4ec54178fb86e8..3636cbd9fd0c040c1996f506ade39f59dc1e8af1 100644
--- a/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/OperatorSelector.jsx
@@ -1,85 +1,41 @@
 /* @flow */
 
 import React, { Component } from "react";
-import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
-import cx from "classnames";
-import _ from "underscore";
-import { t } from 'c-3po';
-import {forceRedraw} from "metabase/lib/dom";
+import Select, { Option } from "metabase/components/Select";
 
-import Icon from "metabase/components/Icon.jsx";
-
-import type { Operator, OperatorName } from "metabase/meta/types/Metadata"
+import type { Operator, OperatorName } from "metabase/meta/types/Metadata";
 
 type Props = {
-    operator: string,
-    operators: Operator[],
-    onOperatorChange: (name: OperatorName) => void
-};
-
-type State = {
-    expanded: bool
+  operator: string,
+  operators: Operator[],
+  onOperatorChange: (name: OperatorName) => void,
 };
 
 export default class OperatorSelector extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-        // if the initial operator is "advanced" expand the list
-        let operator = _.find(props.operators, o => o.name === props.operator);
-        this.state = {
-            expanded: !!(operator && operator.advanced)
-        };
-    }
-
-    static propTypes = {
-        operator: PropTypes.string,
-        operators: PropTypes.array.isRequired,
-        onOperatorChange: PropTypes.func.isRequired
-    };
-
-    expandOperators = () => {
-        this.setState({ expanded: true }, () => {
-            // HACK: Address Safari rendering bug which causes https://github.com/metabase/metabase/issues/5075
-            forceRedraw(ReactDOM.findDOMNode(this));
-        });
-    };
-
-    render() {
-        let { operator, operators } = this.props;
-        let { expanded } = this.state;
-
-        let defaultOperators = operators.filter(o => !o.advanced);
-        let expandedOperators = operators.filter(o => o.advanced);
-
-        let visibleOperators = defaultOperators;
-        if (expanded) {
-            visibleOperators = visibleOperators.concat(expandedOperators);
-        }
-
-        return (
-            <div id="OperatorSelector" className="border-bottom p1" style={{
-                maxWidth: 300
-            }}>
-                { visibleOperators.map(o =>
-                    <button
-                        key={o.name}
-                        className={cx("Button Button-normal Button--medium mr1 mb1", { "Button--purple": o.name === operator })}
-                        onClick={() => this.props.onOperatorChange(o.name)}
-                    >
-                        {o.verboseName}
-                    </button>
-                )}
-                { !expanded && expandedOperators.length > 0 ?
-                    <div className="text-grey-3 text-purple-hover transition-color cursor-pointer" onClick={this.expandOperators}>
-                        <Icon className="px1" name="chevrondown" size={14} />
-                        {t`More Options`}
-                    </div>
-                : null }
-            </div>
-        );
-    }
+  props: Props;
+
+  static propTypes = {
+    operator: PropTypes.string,
+    operators: PropTypes.array.isRequired,
+    onOperatorChange: PropTypes.func.isRequired,
+  };
+
+  render() {
+    let { operator, operators, onOperatorChange } = this.props;
+
+    return (
+      <Select
+        value={operator}
+        onChange={e => onOperatorChange(e.target.value)}
+        className="border-medium"
+      >
+        {operators.map(o => (
+          <Option key={o.name} value={o.name}>
+            {o.verboseName}
+          </Option>
+        ))}
+      </Select>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
index ee916150524267e31fbb079dde1b435e4e57854a..4a176248b493ed368e900b4e8d9005e24d432832 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/DatePicker.jsx
@@ -2,8 +2,8 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
-import cx from 'classnames';
+import { t } from "c-3po";
+import cx from "classnames";
 import moment from "moment";
 import _ from "underscore";
 
@@ -16,317 +16,388 @@ import Calendar from "metabase/components/Calendar";
 import Query from "metabase/lib/query";
 import { mbqlEq } from "metabase/lib/query/util";
 
-
 import type {
-    FieldFilter, TimeIntervalFilter,
-    DatetimeUnit,
-    ConcreteField,
-    LocalFieldReference, ForeignFieldReference, ExpressionReference
+  FieldFilter,
+  TimeIntervalFilter,
+  DatetimeUnit,
+  ConcreteField,
+  LocalFieldReference,
+  ForeignFieldReference,
+  ExpressionReference,
 } from "metabase/meta/types/Query";
 
-const SingleDatePicker = ({ filter: [op, field, value], onFilterChange, hideTimeSelectors }) =>
-    <div className="mx2">
+const SingleDatePicker = ({
+  filter: [op, field, value],
+  onFilterChange,
+  hideTimeSelectors,
+}) => (
+  <div className="mx2">
+    <SpecificDatePicker
+      value={value}
+      onChange={value => onFilterChange([op, field, value])}
+      hideTimeSelectors={hideTimeSelectors}
+      calendar
+    />
+  </div>
+);
+
+const MultiDatePicker = ({
+  filter: [op, field, startValue, endValue],
+  onFilterChange,
+  hideTimeSelectors,
+}) => (
+  <div className="mx2 mb1">
+    <div className="Grid Grid--1of2 Grid--gutters">
+      <div className="Grid-cell">
+        <SpecificDatePicker
+          value={startValue}
+          hideTimeSelectors={hideTimeSelectors}
+          onChange={value => onFilterChange([op, field, value, endValue])}
+        />
+      </div>
+      <div className="Grid-cell">
         <SpecificDatePicker
-            value={value}
-            onChange={(value) => onFilterChange([op, field, value])}
-            hideTimeSelectors={hideTimeSelectors}
-            calendar
+          value={endValue}
+          hideTimeSelectors={hideTimeSelectors}
+          onChange={value => onFilterChange([op, field, startValue, value])}
         />
+      </div>
     </div>
-
-const MultiDatePicker = ({ filter: [op, field, startValue, endValue], onFilterChange , hideTimeSelectors}) =>
-    <div className="mx2 mb1">
-        <div className="Grid Grid--1of2 Grid--gutters">
-            <div className="Grid-cell">
-                <SpecificDatePicker
-                    value={startValue}
-                    hideTimeSelectors={hideTimeSelectors}
-                    onChange={(value) => onFilterChange([op, field, value, endValue])}
-                />
-            </div>
-            <div className="Grid-cell">
-                <SpecificDatePicker
-                    value={endValue}
-                    hideTimeSelectors={hideTimeSelectors}
-                    onChange={(value) => onFilterChange([op, field, startValue, value])}
-                />
-            </div>
-        </div>
-        <div className="Calendar--noContext">
-            <Calendar
-                initial={startValue ? moment(startValue) : moment()}
-                selected={startValue && moment(startValue)}
-                selectedEnd={endValue && moment(endValue)}
-                onChange={(startValue, endValue) => onFilterChange([op, field, startValue, endValue])}
-                isDual
-            />
-        </div>
+    <div className="Calendar--noContext">
+      <Calendar
+        initial={startValue ? moment(startValue) : moment()}
+        selected={startValue && moment(startValue)}
+        selectedEnd={endValue && moment(endValue)}
+        onChange={(startValue, endValue) =>
+          onFilterChange([op, field, startValue, endValue])
+        }
+        isDual
+      />
     </div>
+  </div>
+);
 
-const PreviousPicker =  (props) =>
-    <RelativeDatePicker {...props} formatter={(value) => value * -1} />
+const PreviousPicker = props => (
+  <RelativeDatePicker {...props} formatter={value => value * -1} />
+);
 
 PreviousPicker.horizontalLayout = true;
 
-const NextPicker = (props) =>
-    <RelativeDatePicker {...props} />
+const NextPicker = props => <RelativeDatePicker {...props} />;
 
 NextPicker.horizontalLayout = true;
 
 type CurrentPickerProps = {
-    filter: TimeIntervalFilter,
-    onFilterChange: (filter: TimeIntervalFilter) => void
+  filter: TimeIntervalFilter,
+  onFilterChange: (filter: TimeIntervalFilter) => void,
 };
 
 type CurrentPickerState = {
-    showUnits: boolean
+  showUnits: boolean,
 };
 
 class CurrentPicker extends Component {
-    props: CurrentPickerProps;
-    state: CurrentPickerState;
-
-    state = {
-        showUnits: false
-    };
-
-    static horizontalLayout = true;
-
-    render() {
-        const { filter: [operator, field, intervals, unit], onFilterChange } = this.props
-        return (
-            <div className="flex-full mr2 mb2">
-                <DateUnitSelector
-                    value={unit}
-                    open={this.state.showUnits}
-                    onChange={(value) => {
-                        onFilterChange([operator, field, intervals, value]);
-                        this.setState({ showUnits: false });
-                    }}
-                    togglePicker={() => this.setState({ showUnits: !this.state.showUnits })}
-                    formatter={(val) => val}
-                    periods={DATE_PERIODS}
-                />
-            </div>
-        )
-    }
+  props: CurrentPickerProps;
+  state: CurrentPickerState;
+
+  state = {
+    showUnits: false,
+  };
+
+  static horizontalLayout = true;
+
+  render() {
+    const {
+      filter: [operator, field, intervals, unit],
+      onFilterChange,
+    } = this.props;
+    return (
+      <div className="flex-full mr2 mb2">
+        <DateUnitSelector
+          value={unit}
+          open={this.state.showUnits}
+          onChange={value => {
+            onFilterChange([operator, field, intervals, value]);
+            this.setState({ showUnits: false });
+          }}
+          togglePicker={() =>
+            this.setState({ showUnits: !this.state.showUnits })
+          }
+          formatter={val => val}
+          periods={DATE_PERIODS}
+        />
+      </div>
+    );
+  }
 }
 
-const getIntervals = ([op, field, value, unit]) => mbqlEq(op, "time-interval") && typeof value === "number" ? Math.abs(value) : 30;
-const getUnit      = ([op, field, value, unit]) => mbqlEq(op, "time-interval") && unit ? unit : "day";
-const getOptions   = ([op, field, value, unit, options]) => mbqlEq(op, "time-interval") && options || {};
-
-const getDate = (value) => {
-    if (typeof value !== "string" || !moment(value).isValid()) {
-        value = moment().format("YYYY-MM-DD");
-    }
-    return value;
-}
+const getIntervals = ([op, field, value, unit]) =>
+  mbqlEq(op, "time-interval") && typeof value === "number"
+    ? Math.abs(value)
+    : 30;
+const getUnit = ([op, field, value, unit]) =>
+  mbqlEq(op, "time-interval") && unit ? unit : "day";
+const getOptions = ([op, field, value, unit, options]) =>
+  (mbqlEq(op, "time-interval") && options) || {};
+
+const getDate = value => {
+  if (typeof value !== "string" || !moment(value).isValid()) {
+    value = moment().format("YYYY-MM-DD");
+  }
+  return value;
+};
 
-const hasTime = (value) => typeof value === "string" && /T\d{2}:\d{2}:\d{2}$/.test(value);
+const hasTime = value =>
+  typeof value === "string" && /T\d{2}:\d{2}:\d{2}$/.test(value);
 
-function getDateTimeField(field: ConcreteField, bucketing: ?DatetimeUnit): ConcreteField {
-    let target = getDateTimeFieldTarget(field);
-    if (bucketing) {
-        // $FlowFixMe
-        return ["datetime-field", target, bucketing];
-    } else {
-        return target;
-    }
+function getDateTimeField(
+  field: ConcreteField,
+  bucketing: ?DatetimeUnit,
+): ConcreteField {
+  let target = getDateTimeFieldTarget(field);
+  if (bucketing) {
+    // $FlowFixMe
+    return ["datetime-field", target, bucketing];
+  } else {
+    return target;
+  }
 }
 
-export function getDateTimeFieldTarget(field: ConcreteField): LocalFieldReference|ForeignFieldReference|ExpressionReference {
-    if (Query.isDatetimeField(field)) {
-        // $FlowFixMe:
-        return (field[1]: LocalFieldReference|ForeignFieldReference|ExpressionReference);
-    } else {
-        // $FlowFixMe
-        return field;
-    }
+export function getDateTimeFieldTarget(
+  field: ConcreteField,
+): LocalFieldReference | ForeignFieldReference | ExpressionReference {
+  if (Query.isDatetimeField(field)) {
+    // $FlowFixMe:
+    return (field[1]: // $FlowFixMe:
+    LocalFieldReference | ForeignFieldReference | ExpressionReference);
+  } else {
+    // $FlowFixMe
+    return field;
+  }
 }
 
 // wraps values in "datetime-field" is any of them have a time component
-function getDateTimeFieldAndValues(filter: FieldFilter, count: number): [ConcreteField, any] {
-    const values = filter.slice(2, 2 + count).map(value => value && getDate(value));
-    const bucketing = _.any(values, hasTime) ? "minute" : null;
-    const field = getDateTimeField(filter[1], bucketing);
-    // $FlowFixMe
-    return [field, ...values];
+function getDateTimeFieldAndValues(
+  filter: FieldFilter,
+  count: number,
+): [ConcreteField, any] {
+  const values = filter
+    .slice(2, 2 + count)
+    .map(value => value && getDate(value));
+  const bucketing = _.any(values, hasTime) ? "minute" : null;
+  const field = getDateTimeField(filter[1], bucketing);
+  // $FlowFixMe
+  return [field, ...values];
 }
 
-
-export type OperatorName = "all"|"previous"|"next"|"current"|"before"|"after"|"on"|"between"|"empty"|"not-empty";
+export type OperatorName =
+  | "all"
+  | "previous"
+  | "next"
+  | "current"
+  | "before"
+  | "after"
+  | "on"
+  | "between"
+  | "empty"
+  | "not-empty";
 
 export type Operator = {
-    name: OperatorName,
-    displayName: string,
-    widget?: any,
-    init: (filter: FieldFilter) => any,
-    test: (filter: FieldFilter) => boolean,
-    options?: { [key: string]: any }
-}
+  name: OperatorName,
+  displayName: string,
+  widget?: any,
+  init: (filter: FieldFilter) => any,
+  test: (filter: FieldFilter) => boolean,
+  options?: { [key: string]: any },
+};
 
 const ALL_TIME_OPERATOR = {
-    name: "all",
-    displayName: t`All Time`,
-    init: () => null,
-    test: (op) => op === null
-}
+  name: "all",
+  displayName: t`All Time`,
+  init: () => null,
+  test: op => op === null,
+};
 
 export const DATE_OPERATORS: Operator[] = [
-    {
-        name: "previous",
-        displayName: t`Previous`,
-        init: (filter) => ["time-interval", getDateTimeField(filter[1]), -getIntervals(filter), getUnit(filter), getOptions(filter)],
-        // $FlowFixMe
-        test: ([op, field, value]) => mbqlEq(op, "time-interval") && value < 0 || Object.is(value, -0),
-        widget: PreviousPicker,
-        options: { "include-current": true },
-    },
-    {
-        name: "next",
-        displayName: t`Next`,
-        init: (filter) => ["time-interval", getDateTimeField(filter[1]), getIntervals(filter), getUnit(filter), getOptions(filter)],
-        // $FlowFixMe
-        test: ([op, field, value]) => mbqlEq(op, "time-interval") && value >= 0,
-        widget: NextPicker,
-        options: { "include-current": true },
-    },
-    {
-        name: "current",
-        displayName: t`Current`,
-        init: (filter) => ["time-interval", getDateTimeField(filter[1]), "current", getUnit(filter)],
-        test: ([op, field, value]) => mbqlEq(op, "time-interval") && value === "current",
-        widget: CurrentPicker,
-    },
-    {
-        name: "before",
-        displayName: t`Before`,
-        init: (filter) =>  ["<", ...getDateTimeFieldAndValues(filter, 1)],
-        test: ([op]) => op === "<",
-        widget: SingleDatePicker,
-    },
-    {
-        name: "after",
-        displayName: t`After`,
-        init: (filter) => [">", ...getDateTimeFieldAndValues(filter, 1)],
-        test: ([op]) => op === ">",
-        widget: SingleDatePicker,
-    },
-    {
-        name: "on",
-        displayName: t`On`,
-        init: (filter) => ["=", ...getDateTimeFieldAndValues(filter, 1)],
-        test: ([op]) => op === "=",
-        widget: SingleDatePicker,
-    },
-    {
-        name: "between",
-        displayName: t`Between`,
-        init: (filter) => ["BETWEEN", ...getDateTimeFieldAndValues(filter, 2)],
-        test: ([op]) => mbqlEq(op, "between"),
-        widget: MultiDatePicker,
-    },
+  {
+    name: "previous",
+    displayName: t`Previous`,
+    init: filter => [
+      "time-interval",
+      getDateTimeField(filter[1]),
+      -getIntervals(filter),
+      getUnit(filter),
+      getOptions(filter),
+    ],
+    test: ([op, field, value]) =>
+      // $FlowFixMe
+      (mbqlEq(op, "time-interval") && value < 0) || Object.is(value, -0),
+    widget: PreviousPicker,
+    options: { "include-current": true },
+  },
+  {
+    name: "next",
+    displayName: t`Next`,
+    init: filter => [
+      "time-interval",
+      getDateTimeField(filter[1]),
+      getIntervals(filter),
+      getUnit(filter),
+      getOptions(filter),
+    ],
+    // $FlowFixMe
+    test: ([op, field, value]) => mbqlEq(op, "time-interval") && value >= 0,
+    widget: NextPicker,
+    options: { "include-current": true },
+  },
+  {
+    name: "current",
+    displayName: t`Current`,
+    init: filter => [
+      "time-interval",
+      getDateTimeField(filter[1]),
+      "current",
+      getUnit(filter),
+    ],
+    test: ([op, field, value]) =>
+      mbqlEq(op, "time-interval") && value === "current",
+    widget: CurrentPicker,
+  },
+  {
+    name: "before",
+    displayName: t`Before`,
+    init: filter => ["<", ...getDateTimeFieldAndValues(filter, 1)],
+    test: ([op]) => op === "<",
+    widget: SingleDatePicker,
+  },
+  {
+    name: "after",
+    displayName: t`After`,
+    init: filter => [">", ...getDateTimeFieldAndValues(filter, 1)],
+    test: ([op]) => op === ">",
+    widget: SingleDatePicker,
+  },
+  {
+    name: "on",
+    displayName: t`On`,
+    init: filter => ["=", ...getDateTimeFieldAndValues(filter, 1)],
+    test: ([op]) => op === "=",
+    widget: SingleDatePicker,
+  },
+  {
+    name: "between",
+    displayName: t`Between`,
+    init: filter => ["BETWEEN", ...getDateTimeFieldAndValues(filter, 2)],
+    test: ([op]) => mbqlEq(op, "between"),
+    widget: MultiDatePicker,
+  },
 ];
 
 export const EMPTINESS_OPERATORS: Operator[] = [
-    {
-        name: "empty",
-        displayName: t`Is Empty`,
-        init: (filter) => ["IS_NULL", getDateTimeField(filter[1])],
-        test: ([op]) => op === "IS_NULL"
-    },
-    {
-        name: "not-empty",
-        displayName: t`Not Empty`,
-        init: (filter) => ["NOT_NULL", getDateTimeField(filter[1])],
-        test: ([op]) => op === "NOT_NULL"
-    }
+  {
+    name: "empty",
+    displayName: t`Is Empty`,
+    init: filter => ["IS_NULL", getDateTimeField(filter[1])],
+    test: ([op]) => op === "IS_NULL",
+  },
+  {
+    name: "not-empty",
+    displayName: t`Not Empty`,
+    init: filter => ["NOT_NULL", getDateTimeField(filter[1])],
+    test: ([op]) => op === "NOT_NULL",
+  },
 ];
 
-export const ALL_OPERATORS: Operator[] = DATE_OPERATORS.concat(EMPTINESS_OPERATORS);
+export const ALL_OPERATORS: Operator[] = DATE_OPERATORS.concat(
+  EMPTINESS_OPERATORS,
+);
 
-export function getOperator(filter: FieldFilter, operators?: Operator[] = ALL_OPERATORS) {
-    return _.find(operators, (o) => o.test(filter));
+export function getOperator(
+  filter: FieldFilter,
+  operators?: Operator[] = ALL_OPERATORS,
+) {
+  return _.find(operators, o => o.test(filter));
 }
 
 type Props = {
-    className?: string,
-    filter: FieldFilter,
-    onFilterChange: (filter: FieldFilter) => void,
-    hideEmptinessOperators?: boolean, // Don't show is empty / not empty dialog
-    hideTimeSelectors?: boolean,
-    includeAllTime?: boolean,
-    operators?: Operator[],
-}
+  className?: string,
+  filter: FieldFilter,
+  onFilterChange: (filter: FieldFilter) => void,
+  hideEmptinessOperators?: boolean, // Don't show is empty / not empty dialog
+  hideTimeSelectors?: boolean,
+  includeAllTime?: boolean,
+  operators?: Operator[],
+};
 
 type State = {
-    operators: Operator[]
-}
+  operators: Operator[],
+};
 
 export default class DatePicker extends Component {
-    props: Props;
-    state: State = {
-        operators: []
-    };
-
-    static propTypes = {
-        filter: PropTypes.array.isRequired,
-        onFilterChange: PropTypes.func.isRequired,
-        className: PropTypes.string,
-        hideEmptinessOperators: PropTypes.bool,
-        hideTimeSelectors: PropTypes.bool,
-        operators: PropTypes.array,
-    };
-
-    componentWillMount() {
-        let operators = this.props.operators || DATE_OPERATORS;
-        if (!this.props.hideEmptinessOperators) {
-            operators = operators.concat(EMPTINESS_OPERATORS);
-        }
-
-        const operator = getOperator(this.props.filter, operators) || operators[0];
-        this.props.onFilterChange(operator.init(this.props.filter));
-
-        this.setState({ operators })
+  props: Props;
+  state: State = {
+    operators: [],
+  };
+
+  static propTypes = {
+    filter: PropTypes.array.isRequired,
+    onFilterChange: PropTypes.func.isRequired,
+    className: PropTypes.string,
+    hideEmptinessOperators: PropTypes.bool,
+    hideTimeSelectors: PropTypes.bool,
+    operators: PropTypes.array,
+  };
+
+  componentWillMount() {
+    let operators = this.props.operators || DATE_OPERATORS;
+    if (!this.props.hideEmptinessOperators) {
+      operators = operators.concat(EMPTINESS_OPERATORS);
     }
 
-    render() {
-        const { filter, onFilterChange, includeAllTime } = this.props;
-        let { operators } = this.state;
-        if (includeAllTime) {
-            operators = [ALL_TIME_OPERATOR, ...operators];
-        }
+    const operator = getOperator(this.props.filter, operators) || operators[0];
+    this.props.onFilterChange(operator.init(this.props.filter));
+
+    this.setState({ operators });
+  }
 
-        const operator = getOperator(this.props.filter, operators);
-        const Widget = operator && operator.widget;
-
-        return (
-            <div
-              // apply flex to align the operator selector and the "Widget" if necessary
-              className={cx("border-top pt2", { "flex align-center": Widget && Widget.horizontalLayout })}
-              style={{ minWidth: 380 }}
-            >
-                <DateOperatorSelector
-                    operator={operator && operator.name}
-                    operators={operators}
-                    onOperatorChange={operator => onFilterChange(operator.init(filter))}
-                />
-                { Widget &&
-                    <Widget
-                        {...this.props}
-                        filter={filter}
-                        hideHoursAndMinutes={this.props.hideTimeSelectors}
-                        onFilterChange={filter => {
-                            if (operator && operator.init) {
-                                onFilterChange(operator.init(filter));
-                            } else {
-                                onFilterChange(filter);
-                            }
-                        }}
-                    />
-                }
-            </div>
-        )
+  render() {
+    const { filter, onFilterChange, includeAllTime } = this.props;
+    let { operators } = this.state;
+    if (includeAllTime) {
+      operators = [ALL_TIME_OPERATOR, ...operators];
     }
+
+    const operator = getOperator(this.props.filter, operators);
+    const Widget = operator && operator.widget;
+
+    return (
+      <div
+        // apply flex to align the operator selector and the "Widget" if necessary
+        className={cx("border-top pt2", {
+          "flex align-center": Widget && Widget.horizontalLayout,
+        })}
+        style={{ minWidth: 380 }}
+      >
+        <DateOperatorSelector
+          operator={operator && operator.name}
+          operators={operators}
+          onOperatorChange={operator => onFilterChange(operator.init(filter))}
+        />
+        {Widget && (
+          <Widget
+            {...this.props}
+            filter={filter}
+            hideHoursAndMinutes={this.props.hideTimeSelectors}
+            onFilterChange={filter => {
+              if (operator && operator.init) {
+                onFilterChange(operator.init(filter));
+              } else {
+                onFilterChange(filter);
+              }
+            }}
+          />
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx
index 0472b54747a78cf720c826262b2ec7e96f234030..073fe23793a4fe3b58634428938a0ac747686232 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/HoursMinutesInput.jsx
@@ -1,40 +1,63 @@
 import React from "react";
 
-import NumericInput from "./NumericInput";
+import NumericInput from "metabase/components/NumericInput.jsx";
 import Icon from "metabase/components/Icon";
 
 import cx from "classnames";
 
-const HoursMinutesInput = ({ hours, minutes, onChangeHours, onChangeMinutes, onClear }) =>
-    <div className="flex align-center">
-        <NumericInput
-            className="input"
-            style={{ height: 36 }}
-            size={2}
-            maxLength={2}
-            value={(hours % 12) === 0 ? "12" : String(hours % 12)}
-            onChange={(value) => onChangeHours((hours >= 12 ? 12 : 0) + value) }
-        />
-        <span className="px1">:</span>
-        <NumericInput
-            className="input"
-            style={{ height: 36 }}
-            size={2}
-            maxLength={2}
-            value={(minutes < 10 ? "0" : "") + minutes}
-            onChange={(value) => onChangeMinutes(value) }
-        />
-        <div className="flex align-center pl1">
-            <span className={cx("text-purple-hover mr1", { "text-purple": hours < 12, "cursor-pointer": hours >= 12 })} onClick={hours >= 12 ? () => onChangeHours(hours - 12) : null}>AM</span>
-            <span className={cx("text-purple-hover mr1", { "text-purple": hours >= 12, "cursor-pointer": hours < 12 })} onClick={hours < 12 ? () => onChangeHours(hours + 12) : null}>PM</span>
-        </div>
-        { onClear &&
-            <Icon
-                className="text-grey-2 cursor-pointer text-grey-4-hover ml-auto"
-                name="close"
-                onClick={onClear}
-            />
-        }
+const HoursMinutesInput = ({
+  hours,
+  minutes,
+  onChangeHours,
+  onChangeMinutes,
+  onClear,
+}) => (
+  <div className="flex align-center">
+    <NumericInput
+      className="input"
+      style={{ height: 36 }}
+      size={2}
+      maxLength={2}
+      value={hours % 12 === 0 ? "12" : String(hours % 12)}
+      onChange={value => onChangeHours((hours >= 12 ? 12 : 0) + value)}
+    />
+    <span className="px1">:</span>
+    <NumericInput
+      className="input"
+      style={{ height: 36 }}
+      size={2}
+      maxLength={2}
+      value={(minutes < 10 ? "0" : "") + minutes}
+      onChange={value => onChangeMinutes(value)}
+    />
+    <div className="flex align-center pl1">
+      <span
+        className={cx("text-purple-hover mr1", {
+          "text-purple": hours < 12,
+          "cursor-pointer": hours >= 12,
+        })}
+        onClick={hours >= 12 ? () => onChangeHours(hours - 12) : null}
+      >
+        AM
+      </span>
+      <span
+        className={cx("text-purple-hover mr1", {
+          "text-purple": hours >= 12,
+          "cursor-pointer": hours < 12,
+        })}
+        onClick={hours < 12 ? () => onChangeHours(hours + 12) : null}
+      >
+        PM
+      </span>
     </div>
+    {onClear && (
+      <Icon
+        className="text-grey-2 cursor-pointer text-grey-4-hover ml-auto"
+        name="close"
+        onClick={onClear}
+      />
+    )}
+  </div>
+);
 
 export default HoursMinutesInput;
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
index 808ae9da9557ed78e30a2da8a172b3bc42ba3c6e..c26013ca9c0a98765cfaba8d80f8fa456e992aeb 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/NumberPicker.jsx
@@ -2,74 +2,77 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import TextPicker from "./TextPicker.jsx";
 
 type Props = {
-    values: Array<number|null>,
-    onValuesChange: (values: any[]) => void,
-    placeholder?: string,
-    multi?: bool,
-    onCommit: () => void,
-}
+  values: Array<number | null>,
+  onValuesChange: (values: any[]) => void,
+  placeholder?: string,
+  multi?: boolean,
+  onCommit: () => void,
+};
 
 type State = {
-    stringValues: Array<string>,
-    validations: bool[]
-}
+  stringValues: Array<string>,
+  validations: boolean[],
+};
 
 export default class NumberPicker extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            stringValues: props.values.map(v => {
-                if(typeof v === 'number') {
-                    return String(v)
-                } else {
-                    return String(v || "")
-                }
-            }),
-            validations: this._validate(props.values)
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      stringValues: props.values.map(v => {
+        if (typeof v === "number") {
+          return String(v);
+        } else {
+          return String(v || "");
         }
-    }
-
-    static propTypes = {
-        values: PropTypes.array.isRequired,
-        onValuesChange: PropTypes.func.isRequired,
-        placeholder: PropTypes.string,
-        multi: PropTypes.bool
+      }),
+      validations: this._validate(props.values),
     };
+  }
 
-    static defaultProps = {
-        placeholder: t`Enter desired number`
-    };
+  static propTypes = {
+    values: PropTypes.array.isRequired,
+    onValuesChange: PropTypes.func.isRequired,
+    placeholder: PropTypes.string,
+    multi: PropTypes.bool,
+  };
+
+  static defaultProps = {
+    placeholder: t`Enter desired number`,
+  };
 
-    _validate(values: Array<number|null>) {
-        return values.map(v => v === undefined || !isNaN(v));
-    }
+  _validate(values: Array<number | null>) {
+    return values.map(v => v === undefined || !isNaN(v));
+  }
 
-    onValuesChange(stringValues: string[]) {
-        let values = stringValues.map(v => parseFloat(v))
-        this.props.onValuesChange(values.map(v => isNaN(v) ? null : v));
-        this.setState({
-            stringValues: stringValues,
-            validations: this._validate(values)
-        });
-    }
+  onValuesChange(stringValues: string[]) {
+    let values = stringValues.map(v => parseFloat(v));
+    this.props.onValuesChange(values.map(v => (isNaN(v) ? null : v)));
+    this.setState({
+      stringValues: stringValues,
+      validations: this._validate(values),
+    });
+  }
 
-    render() {
-        // $FlowFixMe
-        const values: Array<string|null> = this.state.stringValues.slice(0, this.props.values.length);
-        return (
-            <TextPicker
-                {...this.props}
-                values={values}
-                validations={this.state.validations}
-                onValuesChange={(values) => this.onValuesChange(values)}
-            />
-        );
-    }
+  render() {
+    // $FlowFixMe
+    const values: Array<string | null> = this.state.stringValues.slice(
+      0,
+      this.props.values.length,
+    );
+    return (
+      <TextPicker
+        {...this.props}
+        values={values}
+        validations={this.state.validations}
+        onValuesChange={values => this.onValuesChange(values)}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/NumericInput.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/NumericInput.jsx
deleted file mode 100644
index 2f619674039093b8c8a5a5f9a3dbeac56796f3a9..0000000000000000000000000000000000000000
--- a/frontend/src/metabase/query_builder/components/filters/pickers/NumericInput.jsx
+++ /dev/null
@@ -1,24 +0,0 @@
-/* @flow */
-
-import React from "react";
-
-import Input from "metabase/components/Input.jsx";
-
-type Props = {
-    value: ?(number|string);
-    onChange: (value: ?number) => void
-}
-
-const NumericInput = ({ value, onChange, ...props }: Props) =>
-    <Input
-        value={value == null ? "" : String(value)}
-        onBlurChange={({ target: { value }}) => {
-            value = value ? parseInt(value, 10) : null;
-            if (!isNaN(value)) {
-                onChange(value);
-            }
-        }}
-        {...props}
-    />
-
-export default NumericInput;
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
index 51e9cd476c579d2797c98ac98cff0c62f097ec06..4be244e2e891d895fa2ef587c1f439efb31eb597 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/RelativeDatePicker.jsx
@@ -2,83 +2,91 @@
 
 import React, { Component } from "react";
 
-import NumericInput from "./NumericInput.jsx";
+import NumericInput from "metabase/components/NumericInput.jsx";
 import DateUnitSelector from "../DateUnitSelector";
 
-import type { TimeIntervalFilter, RelativeDatetimeUnit } from "metabase/meta/types/Query";
+import type {
+  TimeIntervalFilter,
+  RelativeDatetimeUnit,
+} from "metabase/meta/types/Query";
 
 export const DATE_PERIODS: RelativeDatetimeUnit[] = [
-    "day",
-    "week",
-    "month",
-    "year"
+  "day",
+  "week",
+  "month",
+  "year",
 ];
 
-const TIME_PERIODS: RelativeDatetimeUnit[] = [
-    "minute",
-    "hour",
-];
+const TIME_PERIODS: RelativeDatetimeUnit[] = ["minute", "hour"];
 
 const ALL_PERIODS = DATE_PERIODS.concat(TIME_PERIODS);
 
 type Props = {
-    filter: TimeIntervalFilter,
-    onFilterChange: (filter: TimeIntervalFilter) => void,
-    formatter: (value: any) => any,
-    hideTimeSelectors?: boolean
-}
+  filter: TimeIntervalFilter,
+  onFilterChange: (filter: TimeIntervalFilter) => void,
+  formatter: (value: any) => any,
+  hideTimeSelectors?: boolean,
+};
 
 type State = {
-    showUnits: bool
-}
+  showUnits: boolean,
+};
 
 export default class RelativeDatePicker extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    state = {
-        showUnits: false
-    };
+  state = {
+    showUnits: false,
+  };
 
-    static defaultProps = {
-        formatter: (value) => value
-    }
+  static defaultProps = {
+    formatter: value => value,
+  };
 
-    render() {
-        const { filter: [op, field, intervals, unit], onFilterChange, formatter } = this.props;
-        return (
-            <div className="flex-full mb2 flex align-center">
-                <NumericInput
-                    className="mr2 input border-purple text-right"
-                    style={{
-                      width: 65,
-                      // needed to match Select's AdminSelect classes :-/
-                      fontSize: 14,
-                      fontWeight: 700,
-                      padding: 8,
-                    }}
-                    data-ui-tag="relative-date-input"
-                    value={typeof intervals === "number" ? Math.abs(intervals) : intervals}
-                    onChange={(value) =>
-                        onFilterChange([op, field, formatter(value), unit])
-                    }
-                    placeholder="30"
-                />
-                <div className="flex-full mr2">
-                    <DateUnitSelector
-                        open={this.state.showUnits}
-                        value={unit}
-                        onChange={(value) => {
-                            onFilterChange([op, field, intervals, value]);
-                            this.setState({ showUnits: false });
-                        }}
-                        togglePicker={() => this.setState({ showUnits: !this.state.showUnits})}
-                        intervals={intervals}
-                        formatter={formatter}
-                        periods={this.props.hideTimeSelectors ? DATE_PERIODS : ALL_PERIODS}
-                    />
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const {
+      filter: [op, field, intervals, unit],
+      onFilterChange,
+      formatter,
+    } = this.props;
+    return (
+      <div className="flex-full mb2 flex align-center">
+        <NumericInput
+          className="mr2 input border-purple text-right"
+          style={{
+            width: 65,
+            // needed to match Select's AdminSelect classes :-/
+            fontSize: 14,
+            fontWeight: 700,
+            padding: 8,
+          }}
+          data-ui-tag="relative-date-input"
+          value={
+            typeof intervals === "number" ? Math.abs(intervals) : intervals
+          }
+          onChange={value =>
+            onFilterChange([op, field, formatter(value), unit])
+          }
+          placeholder="30"
+        />
+        <div className="flex-full mr2">
+          <DateUnitSelector
+            open={this.state.showUnits}
+            value={unit}
+            onChange={value => {
+              onFilterChange([op, field, intervals, value]);
+              this.setState({ showUnits: false });
+            }}
+            togglePicker={() =>
+              this.setState({ showUnits: !this.state.showUnits })
+            }
+            intervals={intervals}
+            formatter={formatter}
+            periods={this.props.hideTimeSelectors ? DATE_PERIODS : ALL_PERIODS}
+          />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
index 3db48551f4ed9c1d6399a6736b33ae30af57b660..6999267de716870e1af7a1ffc5628e112d708220 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SelectPicker.jsx
@@ -2,8 +2,8 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
-import CheckBox from 'metabase/components/CheckBox.jsx';
+import { t } from "c-3po";
+import CheckBox from "metabase/components/CheckBox.jsx";
 import ListSearchField from "metabase/components/ListSearchField.jsx";
 
 import { capitalize } from "metabase/lib/formatting";
@@ -12,151 +12,159 @@ import { createMultiwordSearchRegex } from "metabase/lib/string";
 import cx from "classnames";
 
 type SelectOption = {
-    name: string,
-    key: string
+  name: string,
+  key: string,
 };
 
 type Props = {
-    options: Array<SelectOption>,
-    values: Array<string>,
-    onValuesChange: (values: any[]) => void,
-    placeholder?: string,
-    multi?: bool
-}
+  options: Array<SelectOption>,
+  values: Array<string>,
+  onValuesChange: (values: any[]) => void,
+  placeholder?: string,
+  multi?: boolean,
+};
 
 type State = {
-    searchText: string,
-    searchRegex: ?RegExp,
-}
+  searchText: string,
+  searchRegex: ?RegExp,
+};
 
 export default class SelectPicker extends Component {
-    state: State;
-    props: Props;
+  state: State;
+  props: Props;
 
-    constructor(props: Props) {
-        super(props);
+  constructor(props: Props) {
+    super(props);
 
-        this.state = {
-            searchText: "",
-            searchRegex: null
-        };
-    }
-
-    static propTypes = {
-        options: PropTypes.array.isRequired,
-        values: PropTypes.array.isRequired,
-        onValuesChange: PropTypes.func.isRequired,
-        placeholder: PropTypes.string,
-        multi: PropTypes.bool
+    this.state = {
+      searchText: "",
+      searchRegex: null,
     };
+  }
 
-    updateSearchText = (value: string) => {
-        let regex = null;
+  static propTypes = {
+    options: PropTypes.array.isRequired,
+    values: PropTypes.array.isRequired,
+    onValuesChange: PropTypes.func.isRequired,
+    placeholder: PropTypes.string,
+    multi: PropTypes.bool,
+  };
 
-        if (value) {
-            regex = createMultiwordSearchRegex(value);
-        }
+  updateSearchText = (value: string) => {
+    let regex = null;
 
-        this.setState({
-            searchText: value,
-            searchRegex: regex
-        });
+    if (value) {
+      regex = createMultiwordSearchRegex(value);
     }
 
-    selectValue(key: string, selected: bool) {
-        let values;
-        if (this.props.multi) {
-            values = this.props.values.slice().filter(v => v != null);
-        } else {
-            values = []
-        }
-        if (selected) {
-            values.push(key);
-        } else {
-            values = values.filter(v => v !== key);
-        }
-        this.props.onValuesChange(values);
+    this.setState({
+      searchText: value,
+      searchRegex: regex,
+    });
+  };
+
+  selectValue(key: string, selected: boolean) {
+    let values;
+    if (this.props.multi) {
+      values = this.props.values.slice().filter(v => v != null);
+    } else {
+      values = [];
     }
-
-    nameForOption(option: SelectOption) {
-        if (option.name === "") {
-            return t`Empty`;
-        } else if (typeof option.name === "string") {
-            return option.name;
-        } else {
-            return capitalize(String(option.name));
-        }
+    if (selected) {
+      values.push(key);
+    } else {
+      values = values.filter(v => v !== key);
     }
+    this.props.onValuesChange(values);
+  }
+
+  nameForOption(option: SelectOption) {
+    if (option.name === "") {
+      return t`Empty`;
+    } else if (typeof option.name === "string") {
+      return option.name;
+    } else {
+      return capitalize(String(option.name));
+    }
+  }
 
-    render() {
-        let { values, options, placeholder, multi } = this.props;
+  render() {
+    let { values, options, placeholder, multi } = this.props;
 
-        let checked = new Set(values);
+    let checked = new Set(values);
 
-        let validOptions = [];
-        let regex = this.state.searchRegex;
+    let validOptions = [];
+    let regex = this.state.searchRegex;
 
-        if (regex){
-            for (const option of options) {
-                if (regex.test(option.key) || regex.test(option.name)) {
-                    validOptions.push(option);
-                }
-            }
-        } else {
-            validOptions = options.slice();
+    if (regex) {
+      for (const option of options) {
+        if (regex.test(option.key) || regex.test(option.name)) {
+          validOptions.push(option);
         }
+      }
+    } else {
+      validOptions = options.slice();
+    }
 
-        return (
-            <div>
-                { validOptions.length <= 10 && !regex ?
-                  null :
-                  <div className="px1 pt1">
-                      <ListSearchField
-                          onChange={this.updateSearchText}
-                          searchText={this.state.searchText}
-                          placeholder={t`Find a value`}
-                          autoFocus={true}
-                      />
-                  </div>
-                }
-                <div className="px1 pt1" style={{maxHeight: '400px', overflowY: 'scroll'}}>
-                    { placeholder ?
-                      <h5>{placeholder}</h5>
-                      : null }
-                     { multi ?
-                       <ul>
-                           {validOptions.map((option, index) =>
-                               <li key={index}>
-                                   <label className="flex align-center cursor-pointer p1" onClick={() => this.selectValue(option.key, !checked.has(option.key))}>
-                                       <CheckBox
-                                           checked={checked.has(option.key)}
-                                           color='purple'
-                                       />
-                                       <h4 className="ml1">{this.nameForOption(option)}</h4>
-                                   </label>
-                               </li>
-                            )}
-                       </ul>
-                       :
-                       <div className="flex flex-wrap py1">
-                           {validOptions.map((option, index) =>
-                               <div className="half" style={{ padding: "0.15em" }}>
-                                   <button
-                                       style={{ height: "95px" }}
-                                       className={cx("full rounded bordered border-purple text-centered text-bold", {
-                                               "text-purple bg-white": values[0] !== option.key,
-                                               "text-white bg-purple": values[0] === option.key
-                                           })}
-                                       onClick={() => this.selectValue(option.key, true)}
-                                   >
-                                       {this.nameForOption(option)}
-                                   </button>
-                               </div>
-                            )}
-                       </div>
-                     }
+    return (
+      <div>
+        {validOptions.length <= 10 && !regex ? null : (
+          <div className="px1 pt1">
+            <ListSearchField
+              onChange={this.updateSearchText}
+              searchText={this.state.searchText}
+              placeholder={t`Find a value`}
+              autoFocus={true}
+            />
+          </div>
+        )}
+        <div
+          className="px1 pt1"
+          style={{ maxHeight: "400px", overflowY: "scroll" }}
+        >
+          {placeholder ? <h5>{placeholder}</h5> : null}
+          {multi ? (
+            <ul>
+              {validOptions.map((option, index) => (
+                <li key={index}>
+                  <label
+                    className="flex align-center cursor-pointer p1"
+                    onClick={() =>
+                      this.selectValue(option.key, !checked.has(option.key))
+                    }
+                  >
+                    <CheckBox
+                      checked={checked.has(option.key)}
+                      color="purple"
+                    />
+                    <h4 className="ml1">{this.nameForOption(option)}</h4>
+                  </label>
+                </li>
+              ))}
+            </ul>
+          ) : (
+            <div className="flex flex-wrap py1">
+              {validOptions.map((option, index) => (
+                <div className="half" style={{ padding: "0.15em" }}>
+                  <button
+                    style={{ height: "95px" }}
+                    className={cx(
+                      "full rounded bordered border-purple text-centered text-bold",
+                      {
+                        "text-purple bg-white": values[0] !== option.key,
+                        "text-white bg-purple": values[0] === option.key,
+                      },
+                    )}
+                    onClick={() => this.selectValue(option.key, true)}
+                  >
+                    {this.nameForOption(option)}
+                  </button>
                 </div>
+              ))}
             </div>
-        );
-    }
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
index b9e677b064405a27357e800eb33dff8772b48a6a..6a01457a7acdd9deff4a836d704db1fcf13a62c8 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/SpecificDatePicker.jsx
@@ -1,8 +1,8 @@
 /* @flow */
 
-import React, { Component } from 'react';
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Calendar from "metabase/components/Calendar";
 import Input from "metabase/components/Input";
 import Icon from "metabase/components/Icon";
@@ -17,146 +17,147 @@ const DATE_FORMAT = "YYYY-MM-DD";
 const DATE_TIME_FORMAT = "YYYY-MM-DDTHH:mm:ss";
 
 type Props = {
-    value: ?string,
-    onChange: (value: ?string) => void,
-    calendar?: bool,
-    hideTimeSelectors?: bool
-}
+  value: ?string,
+  onChange: (value: ?string) => void,
+  calendar?: boolean,
+  hideTimeSelectors?: boolean,
+};
 
 type State = {
-    showCalendar: bool
-}
+  showCalendar: boolean,
+};
 
 export default class SpecificDatePicker extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    constructor(props: Props) {
-        super(props);
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      showCalendar: true,
+    };
+  }
 
-        this.state = {
-            showCalendar: true
-        }
+  static propTypes = {
+    value: PropTypes.string,
+    onChange: PropTypes.func.isRequired,
+  };
+
+  onChange = (date: ?string, hours: ?number, minutes: ?number) => {
+    let m = moment(date);
+    if (!m.isValid()) {
+      this.props.onChange(null);
     }
 
-    static propTypes = {
-        value: PropTypes.string,
-        onChange: PropTypes.func.isRequired,
-    };
+    let hasTime = false;
+    if (hours != null) {
+      m.hours(hours);
+      hasTime = true;
+    }
+    if (minutes != null) {
+      m.minutes(minutes);
+      hasTime = true;
+    }
 
-    onChange = (date: ?string, hours: ?number, minutes: ?number) => {
-        let m = moment(date);
-        if (!m.isValid()) {
-            this.props.onChange(null);
-        }
-
-        let hasTime = false;
-        if (hours != null) {
-            m.hours(hours);
-            hasTime = true;
-        }
-        if (minutes != null) {
-            m.minutes(minutes);
-            hasTime = true;
-        }
-
-        if (hasTime) {
-            this.props.onChange(m.format(DATE_TIME_FORMAT));
-        } else {
-            this.props.onChange(m.format(DATE_FORMAT));
-        }
+    if (hasTime) {
+      this.props.onChange(m.format(DATE_TIME_FORMAT));
+    } else {
+      this.props.onChange(m.format(DATE_FORMAT));
+    }
+  };
+
+  render() {
+    const { value, calendar, hideTimeSelectors } = this.props;
+    const { showCalendar } = this.state;
+
+    let date, hours, minutes;
+    if (moment(value, DATE_TIME_FORMAT, true).isValid()) {
+      date = moment(value, DATE_TIME_FORMAT, true);
+      hours = date.hours();
+      minutes = date.minutes();
+      date.startOf("day");
+    } else if (moment(value, DATE_FORMAT, true).isValid()) {
+      date = moment(value, DATE_FORMAT, true);
     }
 
-    render() {
-        const { value, calendar, hideTimeSelectors } = this.props;
-        const { showCalendar } = this.state;
-
-        let date, hours, minutes;
-        if (moment(value, DATE_TIME_FORMAT, true).isValid()) {
-            date = moment(value, DATE_TIME_FORMAT, true);
-            hours = date.hours();
-            minutes = date.minutes();
-            date.startOf("day");
-        } else if (moment(value, DATE_FORMAT, true).isValid()) {
-            date = moment(value, DATE_FORMAT, true);
-        }
-
-        return (
-            <div>
-                <div className="flex align-center mb1">
-                    <div className={cx('border-top border-bottom full border-left', { 'border-right': !calendar })}>
-                        <Input
-                            placeholder={moment().format("MM/DD/YYYY")}
-                            className="borderless full p2 h3"
-                            style={{
-                                outline: 'none'
-                            }}
-                            value={date ? date.format("MM/DD/YYYY") : ""}
-                            onBlurChange={({ target: { value } }) => {
-                                let date = moment(value, "MM/DD/YYYY");
-                                if (date.isValid()) {
-                                    this.onChange(date, hours, minutes)
-                                } else {
-                                    this.onChange(null)
-                                }
-                            }}
-                            ref="value"
-                        />
-                    </div>
-                    { calendar &&
-                        <div className="border-right border-bottom border-top p2">
-                            <Tooltip
-                                tooltip={
-                                    showCalendar ? t`Hide calendar` : t`Show calendar`
-                                }
-                                children={
-                                    <Icon
-                                        className="text-purple-hover cursor-pointer"
-                                        name='calendar'
-                                        onClick={() => this.setState({ showCalendar: !this.state.showCalendar })}
-                                    />
-                                }
-                            />
-                        </div>
-                    }
-                </div>
-
-                { calendar &&
-                    <ExpandingContent open={showCalendar}>
-                        <Calendar
-                            selected={date}
-                            initial={date || moment()}
-                            onChange={(value) => this.onChange(value, hours, minutes)}
-                            isRangePicker={false}
-                        />
-                    </ExpandingContent>
+    return (
+      <div>
+        <div className="flex align-center mb1">
+          <div
+            className={cx("border-top border-bottom full border-left", {
+              "border-right": !calendar,
+            })}
+          >
+            <Input
+              placeholder={moment().format("MM/DD/YYYY")}
+              className="borderless full p2 h3"
+              style={{
+                outline: "none",
+              }}
+              value={date ? date.format("MM/DD/YYYY") : ""}
+              onBlurChange={({ target: { value } }) => {
+                let date = moment(value, "MM/DD/YYYY");
+                if (date.isValid()) {
+                  this.onChange(date, hours, minutes);
+                } else {
+                  this.onChange(null);
                 }
-
-                { !hideTimeSelectors &&
-                    <div className={cx({'py2': calendar}, {'mb3': !calendar})}>
-                        { hours == null || minutes == null ?
-                            <div
-                                className="text-purple-hover cursor-pointer flex align-center"
-                                onClick={() => this.onChange(date, 12, 30) }
-                            >
-                                <Icon
-                                    className="mr1"
-                                    name='clock'
-                                />
-                                Add a time
-                            </div>
-                            :
-                            <HoursMinutesInput
-                                onClear={() => this.onChange(date, null, null)}
-                                hours={hours}
-                                minutes={minutes}
-                                onChangeHours={hours => this.onChange(date, hours, minutes)}
-                                onChangeMinutes={minutes => this.onChange(date, hours, minutes)}
-                            />
-                        }
-                    </div>
+              }}
+              ref="value"
+            />
+          </div>
+          {calendar && (
+            <div className="border-right border-bottom border-top p2">
+              <Tooltip
+                tooltip={showCalendar ? t`Hide calendar` : t`Show calendar`}
+                children={
+                  <Icon
+                    className="text-purple-hover cursor-pointer"
+                    name="calendar"
+                    onClick={() =>
+                      this.setState({ showCalendar: !this.state.showCalendar })
+                    }
+                  />
                 }
+              />
             </div>
-        )
-    }
+          )}
+        </div>
+
+        {calendar && (
+          <ExpandingContent open={showCalendar}>
+            <Calendar
+              selected={date}
+              initial={date || moment()}
+              onChange={value => this.onChange(value, hours, minutes)}
+              isRangePicker={false}
+            />
+          </ExpandingContent>
+        )}
+
+        {!hideTimeSelectors && (
+          <div className={cx({ py2: calendar }, { mb3: !calendar })}>
+            {hours == null || minutes == null ? (
+              <div
+                className="text-purple-hover cursor-pointer flex align-center"
+                onClick={() => this.onChange(date, 12, 30)}
+              >
+                <Icon className="mr1" name="clock" />
+                Add a time
+              </div>
+            ) : (
+              <HoursMinutesInput
+                onClear={() => this.onChange(date, null, null)}
+                hours={hours}
+                minutes={minutes}
+                onChangeHours={hours => this.onChange(date, hours, minutes)}
+                onChangeMinutes={minutes => this.onChange(date, hours, minutes)}
+              />
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
index e4362ed15928f076dda246f0d0ec04ad096a92f5..941d7e232468aeabd54974894a11b748d6936a15 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/TextPicker.jsx
@@ -1,98 +1,103 @@
 /* @flow */
 
-import React, {Component} from "react";
+import React, { Component } from "react";
 import PropTypes from "prop-types";
-import AutosizeTextarea from 'react-textarea-autosize';
-import { t } from 'c-3po';
+import AutosizeTextarea from "react-textarea-autosize";
+import { t } from "c-3po";
 import cx from "classnames";
 import _ from "underscore";
 
 type Props = {
-    values: Array<string|null>,
-    onValuesChange: (values: any[]) => void,
-    validations: bool[],
-    placeholder?: string,
-    multi?: bool,
-    onCommit: () => void,
+  values: Array<string | null>,
+  onValuesChange: (values: any[]) => void,
+  validations: boolean[],
+  placeholder?: string,
+  multi?: boolean,
+  onCommit: () => void,
 };
 
 type State = {
-    fieldString: string,
-}
+  fieldString: string,
+};
 
 export default class TextPicker extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    static propTypes = {
-        values: PropTypes.array.isRequired,
-        onValuesChange: PropTypes.func.isRequired,
-        placeholder: PropTypes.string,
-        validations: PropTypes.array,
-        multi: PropTypes.bool,
-        onCommit: PropTypes.func,
-    };
+  static propTypes = {
+    values: PropTypes.array.isRequired,
+    onValuesChange: PropTypes.func.isRequired,
+    placeholder: PropTypes.string,
+    validations: PropTypes.array,
+    multi: PropTypes.bool,
+    onCommit: PropTypes.func,
+  };
 
-    static defaultProps = {
-        validations: [],
-        placeholder: t`Enter desired text`
-    };
+  static defaultProps = {
+    validations: [],
+    placeholder: t`Enter desired text`,
+  };
 
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            fieldString: props.values.join(', ')
-        };
-    }
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      fieldString: props.values.join(", "),
+    };
+  }
 
-    setValue(fieldString: ?string) {
-        if (fieldString != null) {
-            // Only strip newlines from field string to not interfere with copy-pasting
-            const newLineRegex = /\r?\n|\r/g;
-            const newFieldString = fieldString.replace(newLineRegex,'');
-            this.setState({fieldString: newFieldString});
+  setValue(fieldString: ?string) {
+    if (fieldString != null) {
+      // Only strip newlines from field string to not interfere with copy-pasting
+      const newLineRegex = /\r?\n|\r/g;
+      const newFieldString = fieldString.replace(newLineRegex, "");
+      this.setState({ fieldString: newFieldString });
 
-            // Construct the values array for real-time validation
-            // Trim values to prevent confusing problems with leading/trailing whitespaces
-            const newValues = newFieldString.split(',').map((v) => v.trim()).filter((v) => v !== "");
-            this.props.onValuesChange(newValues);
-        } else {
-            this.props.onValuesChange([]);
-        }
+      // Construct the values array for real-time validation
+      // Trim values to prevent confusing problems with leading/trailing whitespaces
+      const newValues = newFieldString
+        .split(",")
+        .map(v => v.trim())
+        .filter(v => v !== "");
+      this.props.onValuesChange(newValues);
+    } else {
+      this.props.onValuesChange([]);
     }
+  }
 
-    render() {
-        let {validations, multi, onCommit} = this.props;
-        const hasInvalidValues = _.some(validations, (v) => v === false);
+  render() {
+    let { validations, multi, onCommit } = this.props;
+    const hasInvalidValues = _.some(validations, v => v === false);
 
-        const commitOnEnter = (e) => {
-            if (e.key === "Enter" && onCommit) {
-                onCommit();
-            }
-        };
+    const commitOnEnter = e => {
+      if (e.key === "Enter" && onCommit) {
+        onCommit();
+      }
+    };
 
-        return (
-            <div>
-                <div className="FilterInput px1 pt1 relative">
-                    <AutosizeTextarea
-                        className={cx("input block full border-purple", { "border-error": hasInvalidValues })}
-                        type="text"
-                        value={this.state.fieldString}
-                        onChange={(e) => this.setValue(e.target.value)}
-                        onKeyPress={commitOnEnter}
-                        placeholder={this.props.placeholder}
-                        autoFocus={true}
-                        style={{resize: "none"}}
-                        maxRows={8}
-                    />
-                </div>
+    return (
+      <div>
+        <div className="FilterInput px1 pt1 relative">
+          <AutosizeTextarea
+            className={cx("input block full border-purple", {
+              "border-error": hasInvalidValues,
+            })}
+            type="text"
+            value={this.state.fieldString}
+            onChange={e => this.setValue(e.target.value)}
+            onKeyPress={commitOnEnter}
+            placeholder={this.props.placeholder}
+            autoFocus={true}
+            style={{ resize: "none" }}
+            maxRows={8}
+          />
+        </div>
 
-                { multi ?
-                    <div className="p1 text-small">
-                        {t`You can enter multiple values separated by commas`}
-                    </div>
-                    : null }
-            </div>
-        );
-    }
+        {multi ? (
+          <div className="p1 text-small">
+            {t`You can enter multiple values separated by commas`}
+          </div>
+        ) : null}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx b/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx
index f8ee5c0719db4be0933858f4e138038b68c9da63..28e1b68639c73b2a69edfe7ea3ef611fc5826593 100644
--- a/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx
+++ b/frontend/src/metabase/query_builder/components/filters/pickers/TimePicker.jsx
@@ -1,5 +1,5 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import DatePicker, { getDateTimeFieldTarget } from "./DatePicker";
 import HoursMinutesInput from "./HoursMinutesInput";
@@ -7,69 +7,106 @@ import { mbqlEq } from "metabase/lib/query/util";
 import { parseTime } from "metabase/lib/time";
 
 const TimeInput = ({ value, onChange }) => {
-    const time = parseTime(value);
-    return (
-      <HoursMinutesInput
-        hours={time.hour()}
-        minutes={time.minute()}
-        onChangeHours={(hours) => onChange(time.hour(hours).format("HH:mm:00.000"))}
-        onChangeMinutes={(minutes) => onChange(time.minute(minutes).format("HH:mm:00.000"))}
-      />
-    );
-}
+  const time = parseTime(value);
+  return (
+    <HoursMinutesInput
+      hours={time.hour()}
+      minutes={time.minute()}
+      onChangeHours={hours => onChange(time.hour(hours).format("HH:mm:00.000"))}
+      onChangeMinutes={minutes =>
+        onChange(time.minute(minutes).format("HH:mm:00.000"))
+      }
+    />
+  );
+};
 
-const SingleTimePicker = ({ filter, onFilterChange }) =>
+const SingleTimePicker = ({ filter, onFilterChange }) => (
   <div className="mx2 mb2">
-    <TimeInput value={getTime(filter[2])} onChange={(time) => onFilterChange([filter[0], filter[1], time])} />
+    <TimeInput
+      value={getTime(filter[2])}
+      onChange={time => onFilterChange([filter[0], filter[1], time])}
+    />
   </div>
+);
 
 SingleTimePicker.horizontalLayout = true;
 
-const MultiTimePicker = ({ filter, onFilterChange }) =>
-  <div className="flex align-center justify-between mx2 mb1" style={{ minWidth: 480 }}>
-    <TimeInput value={getTime(filter[2])} onChange={(time) => onFilterChange([filter[0], filter[1], ...sortTimes(time, filter[3])])} />
+const MultiTimePicker = ({ filter, onFilterChange }) => (
+  <div
+    className="flex align-center justify-between mx2 mb1"
+    style={{ minWidth: 480 }}
+  >
+    <TimeInput
+      value={getTime(filter[2])}
+      onChange={time =>
+        onFilterChange([filter[0], filter[1], ...sortTimes(time, filter[3])])
+      }
+    />
     <span className="h3">and</span>
-    <TimeInput value={getTime(filter[3])} onChange={(time) => onFilterChange([filter[0], filter[1], ...sortTimes(filter[2], time)])} />
+    <TimeInput
+      value={getTime(filter[3])}
+      onChange={time =>
+        onFilterChange([filter[0], filter[1], ...sortTimes(filter[2], time)])
+      }
+    />
   </div>
+);
 
 const sortTimes = (a, b) => {
-    console.log(parseTime(a).isAfter(parseTime(b)))
-    return parseTime(a).isAfter(parseTime(b)) ? [b, a] : [a, b];
-}
+  console.log(parseTime(a).isAfter(parseTime(b)));
+  return parseTime(a).isAfter(parseTime(b)) ? [b, a] : [a, b];
+};
 
-const getTime = (value) => {
-    if (typeof value === "string" && /^\d+:\d+(:\d+(.\d+(\+\d+:\d+)?)?)?$/.test(value)) {
-      return value;
-    } else {
-      return "00:00:00.000+00:00"
-    }
-}
+const getTime = value => {
+  if (
+    typeof value === "string" &&
+    /^\d+:\d+(:\d+(.\d+(\+\d+:\d+)?)?)?$/.test(value)
+  ) {
+    return value;
+  } else {
+    return "00:00:00.000+00:00";
+  }
+};
 
 export const TIME_OPERATORS: Operator[] = [
   {
-      name: "before",
-      displayName: t`Before`,
-      init: (filter) =>  ["<", getDateTimeFieldTarget(filter[1]), getTime(filter[2])],
-      test: ([op]) => op === "<",
-      widget: SingleTimePicker,
+    name: "before",
+    displayName: t`Before`,
+    init: filter => [
+      "<",
+      getDateTimeFieldTarget(filter[1]),
+      getTime(filter[2]),
+    ],
+    test: ([op]) => op === "<",
+    widget: SingleTimePicker,
   },
   {
-      name: "after",
-      displayName: t`After`,
-      init: (filter) => [">", getDateTimeFieldTarget(filter[1]), getTime(filter[2])],
-      test: ([op]) => op === ">",
-      widget: SingleTimePicker,
+    name: "after",
+    displayName: t`After`,
+    init: filter => [
+      ">",
+      getDateTimeFieldTarget(filter[1]),
+      getTime(filter[2]),
+    ],
+    test: ([op]) => op === ">",
+    widget: SingleTimePicker,
   },
   {
-      name: "between",
-      displayName: t`Between`,
-      init: (filter) => ["BETWEEN", getDateTimeFieldTarget(filter[1]), getTime(filter[2]), getTime(filter[3])],
-      test: ([op]) => mbqlEq(op, "between"),
-      widget: MultiTimePicker,
+    name: "between",
+    displayName: t`Between`,
+    init: filter => [
+      "BETWEEN",
+      getDateTimeFieldTarget(filter[1]),
+      getTime(filter[2]),
+      getTime(filter[3]),
+    ],
+    test: ([op]) => mbqlEq(op, "between"),
+    widget: MultiTimePicker,
   },
-]
+];
 
-const TimePicker = (props) =>
+const TimePicker = props => (
   <DatePicker {...props} operators={TIME_OPERATORS} />
+);
 
 export default TimePicker;
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
index 66abc7d4a0bf23316911d8730e722d97b696cae7..7e67c9af79422b3ede3362bfa4d939baeba466df 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorHelp.jsx
@@ -1,118 +1,160 @@
 import React from "react";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import Code from "metabase/components/Code.jsx";
 
 const EXAMPLES = {
-    variable: {
-        database: null,
-        type: "native",
-        native: {
-            query: "SELECT count(*)\nFROM products\nWHERE category = {{category}}",
-            template_tags: {
-                "category": { name: "category", display_name: "Category", type: "text", required: true, default: "Widget" }
-            }
+  variable: {
+    database: null,
+    type: "native",
+    native: {
+      query: "SELECT count(*)\nFROM products\nWHERE category = {{category}}",
+      template_tags: {
+        category: {
+          name: "category",
+          display_name: "Category",
+          type: "text",
+          required: true,
+          default: "Widget",
         },
-
+      },
     },
-    dimension: {
-        database: null,
-        type: "native",
-        native: {
-            query: "SELECT count(*)\nFROM products\nWHERE {{created_at}}",
-            template_tags: {
-                "created_at": { name: "created_at", display_name: "Created At", type: "dimension", dimension: null }
-            }
-        }
+  },
+  dimension: {
+    database: null,
+    type: "native",
+    native: {
+      query: "SELECT count(*)\nFROM products\nWHERE {{created_at}}",
+      template_tags: {
+        created_at: {
+          name: "created_at",
+          display_name: "Created At",
+          type: "dimension",
+          dimension: null,
+        },
+      },
     },
-    optional: {
-        database: null,
-        type: "native",
-        native: {
-            query: "SELECT count(*)\nFROM products\n[[WHERE category = {{category}}]]",
-            template_tags: {
-                "category": { name: "category", display_name: "Category", type: "text", required: false }
-            }
-        }
+  },
+  optional: {
+    database: null,
+    type: "native",
+    native: {
+      query:
+        "SELECT count(*)\nFROM products\n[[WHERE category = {{category}}]]",
+      template_tags: {
+        category: {
+          name: "category",
+          display_name: "Category",
+          type: "text",
+          required: false,
+        },
+      },
     },
-    multipleOptional: {
-        database: null,
-        type: "native",
-        native: {
-            query: "SELECT count(*)\nFROM products\nWHERE 1=1\n  [[AND id = {{id}}]]\n  [[AND category = {{category}}]]",
-            template_tags: {
-                "id": { name: "id", display_name: "ID", type: "number", required: false },
-                "category": { name: "category", display_name:"Category", type: "text", required: false }
-            }
-        }
+  },
+  multipleOptional: {
+    database: null,
+    type: "native",
+    native: {
+      query:
+        "SELECT count(*)\nFROM products\nWHERE 1=1\n  [[AND id = {{id}}]]\n  [[AND category = {{category}}]]",
+      template_tags: {
+        id: { name: "id", display_name: "ID", type: "number", required: false },
+        category: {
+          name: "category",
+          display_name: "Category",
+          type: "text",
+          required: false,
+        },
+      },
     },
-}
-
+  },
+};
 
-const TagExample = ({ datasetQuery, setDatasetQuery }) =>
-    <div>
-        <h5>Example:</h5>
-        <p>
-            <Code>{datasetQuery.native.query}</Code>
-            { setDatasetQuery && (
-                <div
-                    className="Button Button--small mt1"
-                    data-metabase-event="QueryBuilder;Template Tag Example Query Used"
-                    onClick={() => setDatasetQuery(datasetQuery, true) }
-                >
-                    {t`Try it`}
-                </div>
-            )}
-        </p>
-    </div>
+const TagExample = ({ datasetQuery, setDatasetQuery }) => (
+  <div>
+    <h5>Example:</h5>
+    <p>
+      <Code>{datasetQuery.native.query}</Code>
+      {setDatasetQuery && (
+        <div
+          className="Button Button--small mt1"
+          data-metabase-event="QueryBuilder;Template Tag Example Query Used"
+          onClick={() => setDatasetQuery(datasetQuery, true)}
+        >
+          {t`Try it`}
+        </div>
+      )}
+    </p>
+  </div>
+);
 
 const TagEditorHelp = ({ setDatasetQuery, sampleDatasetId }) => {
-    let setQueryWithSampleDatasetId = null;
-    if (sampleDatasetId != null) {
-        setQueryWithSampleDatasetId = (dataset_query, run) => {
-            setDatasetQuery({
-                ...dataset_query,
-                database: sampleDatasetId
-            }, run);
-        }
-    }
-    return (
-        <div>
-            <h4>{t`What's this for?`}</h4>
-            <p>
-                {t`Variables in native queries let you dynamically replace values in your queries using filter widgets or through the URL.`}
-            </p>
+  let setQueryWithSampleDatasetId = null;
+  if (sampleDatasetId != null) {
+    setQueryWithSampleDatasetId = (dataset_query, run) => {
+      setDatasetQuery(
+        {
+          ...dataset_query,
+          database: sampleDatasetId,
+        },
+        run,
+      );
+    };
+  }
+  return (
+    <div>
+      <h4>{t`What's this for?`}</h4>
+      <p>
+        {t`Variables in native queries let you dynamically replace values in your queries using filter widgets or through the URL.`}
+      </p>
 
-            <h4 className="pt2">{t`Variables`}</h4>
-            <p>
-                {jt`${<Code>{"{{variable_name}}"}</Code>} creates a variable in this SQL template called "variable_name". Variables can be given types in the side panel, which changes their behavior. All variable types other than "Field Filter" will automatically cause a filter widget to be placed on this question; with Field Filters, this is optional. When this filter widget is filled in, that value replaces the variable in the SQL template.`}
-            </p>
-            <TagExample datasetQuery={EXAMPLES.variable} setDatasetQuery={setQueryWithSampleDatasetId} />
+      <h4 className="pt2">{t`Variables`}</h4>
+      <p>
+        {jt`${(
+          <Code>{"{{variable_name}}"}</Code>
+        )} creates a variable in this SQL template called "variable_name". Variables can be given types in the side panel, which changes their behavior. All variable types other than "Field Filter" will automatically cause a filter widget to be placed on this question; with Field Filters, this is optional. When this filter widget is filled in, that value replaces the variable in the SQL template.`}
+      </p>
+      <TagExample
+        datasetQuery={EXAMPLES.variable}
+        setDatasetQuery={setQueryWithSampleDatasetId}
+      />
 
-            <h4 className="pt2">{t`Field Filters`}</h4>
-            <p>
-                {t`Giving a variable the "Field Filter" type allows you to link SQL cards to dashboard filter widgets or use more types of filter widgets on your SQL question. A Field Filter variable inserts SQL similar to that generated by the GUI query builder when adding filters on existing columns.`}
-            </p>
-            <p>
-                {t`When adding a Field Filter variable, you'll need to map it to a specific field. You can then choose to display a filter widget on your question, but even if you don't, you can now map your Field Filter variable to a dashboard filter when adding this question to a dashboard. Field Filters should be used inside of a "WHERE" clause.`}
-            </p>
-            <TagExample datasetQuery={EXAMPLES.dimension} />
+      <h4 className="pt2">{t`Field Filters`}</h4>
+      <p>
+        {t`Giving a variable the "Field Filter" type allows you to link SQL cards to dashboard filter widgets or use more types of filter widgets on your SQL question. A Field Filter variable inserts SQL similar to that generated by the GUI query builder when adding filters on existing columns.`}
+      </p>
+      <p>
+        {t`When adding a Field Filter variable, you'll need to map it to a specific field. You can then choose to display a filter widget on your question, but even if you don't, you can now map your Field Filter variable to a dashboard filter when adding this question to a dashboard. Field Filters should be used inside of a "WHERE" clause.`}
+      </p>
+      <TagExample datasetQuery={EXAMPLES.dimension} />
 
-            <h4 className="pt2">{t`Optional Clauses`}</h4>
-            <p>
-                {jt`brackets around a ${<Code>{"[[{{variable}}]]"}</Code>} create an optional clause in the template. If "variable" is set, then the entire clause is placed into the template. If not, then the entire clause is ignored.`}
-            </p>
-            <TagExample datasetQuery={EXAMPLES.optional} setDatasetQuery={setQueryWithSampleDatasetId} />
+      <h4 className="pt2">{t`Optional Clauses`}</h4>
+      <p>
+        {jt`brackets around a ${(
+          <Code>{"[[{{variable}}]]"}</Code>
+        )} create an optional clause in the template. If "variable" is set, then the entire clause is placed into the template. If not, then the entire clause is ignored.`}
+      </p>
+      <TagExample
+        datasetQuery={EXAMPLES.optional}
+        setDatasetQuery={setQueryWithSampleDatasetId}
+      />
 
-            <p>
-                {t`To use multiple optional clauses you can include at least one non-optional WHERE clause followed by optional clauses starting with "AND".`}
-            </p>
-            <TagExample datasetQuery={EXAMPLES.multipleOptional} setDatasetQuery={setQueryWithSampleDatasetId} />
+      <p>
+        {t`To use multiple optional clauses you can include at least one non-optional WHERE clause followed by optional clauses starting with "AND".`}
+      </p>
+      <TagExample
+        datasetQuery={EXAMPLES.multipleOptional}
+        setDatasetQuery={setQueryWithSampleDatasetId}
+      />
 
-            <p className="pt2 link">
-                <a href="http://www.metabase.com/docs/latest/users-guide/start" target="_blank" data-metabase-event="QueryBuilder;Template Tag Documentation Click">{t`Read the full documentation`}</a>
-            </p>
-        </div>
-    )
-}
+      <p className="pt2 link">
+        <a
+          href="https://www.metabase.com/docs/latest/users-guide/13-sql-parameters.html"
+          target="_blank"
+          data-metabase-event="QueryBuilder;Template Tag Documentation Click"
+        >{t`Read the full documentation`}</a>
+      </p>
+    </div>
+  );
+};
 
 export default TagEditorHelp;
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 002a479d5fcadc7693c6a64138274ce36617a3bd..f0be7a5cf48c6cb96d52e1749f7d3b504021878d 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorParam.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import _ from "underscore";
 import { connect } from "react-redux";
 
@@ -10,7 +10,7 @@ import ParameterValueWidget from "metabase/parameters/components/ParameterValueW
 
 import { parameterOptionsForField } from "metabase/meta/Dashboard";
 import type { TemplateTag } from "metabase/meta/types/Query";
-import type { Database } from "metabase/meta/types/Database"
+import type { Database } from "metabase/meta/types/Database";
 
 import Field from "metabase-lib/lib/metadata/Field";
 import { fetchField } from "metabase/redux/metadata";
@@ -20,190 +20,209 @@ import Metadata from "metabase-lib/lib/metadata/Metadata";
 import type { FieldId } from "metabase/meta/types/Field";
 
 type Props = {
-    tag: TemplateTag,
-    onUpdate: (tag: TemplateTag) => void,
-    databaseFields: Field[],
-    database: Database,
-    databases: Database[],
-    metadata: Metadata,
-    fetchField: (FieldId) => void
+  tag: TemplateTag,
+  onUpdate: (tag: TemplateTag) => void,
+  databaseFields: Field[],
+  database: Database,
+  databases: Database[],
+  metadata: Metadata,
+  fetchField: FieldId => void,
 };
 
-@connect((state) => ({ metadata: getMetadata(state) }),{ fetchField })
+@connect(state => ({ metadata: getMetadata(state) }), { fetchField })
 export default class TagEditorParam extends Component {
-    props: Props;
+  props: Props;
 
-    componentWillMount() {
-        const { tag, fetchField } = this.props
+  componentWillMount() {
+    const { tag, fetchField } = this.props;
 
-        if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
-            const fieldId = tag.dimension[1]
-            // Field values might already have been loaded so force the load of other field information too
-            fetchField(fieldId, true)
-        }
+    if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
+      const fieldId = tag.dimension[1];
+      // Field values might already have been loaded so force the load of other field information too
+      fetchField(fieldId, true);
     }
-
-    setParameterAttribute(attr, val) {
-        // only register an update if the value actually changes
-        if (this.props.tag[attr] !== val) {
-            this.props.onUpdate({
-                ...this.props.tag,
-                [attr]: val
-            });
-        }
+  }
+
+  setParameterAttribute(attr, val) {
+    // only register an update if the value actually changes
+    if (this.props.tag[attr] !== val) {
+      this.props.onUpdate({
+        ...this.props.tag,
+        [attr]: val,
+      });
     }
-
-    setRequired(required) {
-        if (this.props.tag.required !== required) {
-            this.props.onUpdate({
-                ...this.props.tag,
-                required: required,
-                default: undefined
-            });
-        }
+  }
+
+  setRequired(required) {
+    if (this.props.tag.required !== required) {
+      this.props.onUpdate({
+        ...this.props.tag,
+        required: required,
+        default: undefined,
+      });
     }
-
-    setType(type) {
-        if (this.props.tag.type !== type) {
-            this.props.onUpdate({
-                ...this.props.tag,
-                type: type,
-                dimension: undefined,
-                widget_type: undefined
-            });
-        }
+  }
+
+  setType(type) {
+    if (this.props.tag.type !== type) {
+      this.props.onUpdate({
+        ...this.props.tag,
+        type: type,
+        dimension: undefined,
+        widget_type: undefined,
+      });
     }
-
-    setDimension(fieldId) {
-        const { tag, onUpdate, metadata } = this.props;
-        const dimension = ["field-id", fieldId];
-        if (!_.isEqual(tag.dimension !== dimension)) {
-            const field = metadata.fields[dimension[1]]
-            if (!field) {
-                return;
-            }
-            const options = parameterOptionsForField(field);
-            let widget_type;
-            if (tag.widget_type && _.findWhere(options, { type: tag.widget_type })) {
-                widget_type = tag.widget_type;
-            } else if (options.length > 0) {
-                widget_type = options[0].type;
-            }
-            onUpdate({
-                ...tag,
-                dimension,
-                widget_type
-            });
-        }
+  }
+
+  setDimension(fieldId) {
+    const { tag, onUpdate, metadata } = this.props;
+    const dimension = ["field-id", fieldId];
+    if (!_.isEqual(tag.dimension !== dimension)) {
+      const field = metadata.fields[dimension[1]];
+      if (!field) {
+        return;
+      }
+      const options = parameterOptionsForField(field);
+      let widget_type;
+      if (tag.widget_type && _.findWhere(options, { type: tag.widget_type })) {
+        widget_type = tag.widget_type;
+      } else if (options.length > 0) {
+        widget_type = options[0].type;
+      }
+      onUpdate({
+        ...tag,
+        dimension,
+        widget_type,
+      });
+    }
+  }
+
+  render() {
+    const { tag, database, databases, metadata } = this.props;
+
+    let widgetOptions,
+      table,
+      fieldMetadataLoaded = false;
+    if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
+      const field = metadata.fields[tag.dimension[1]];
+
+      if (field) {
+        widgetOptions = parameterOptionsForField(field);
+        table = field.table;
+        fieldMetadataLoaded = true;
+      }
     }
 
-    render() {
-        const { tag, database, databases, metadata } = this.props;
-
-        let widgetOptions, table, fieldMetadataLoaded = false;
-        if (tag.type === "dimension" && Array.isArray(tag.dimension)) {
-            const field = metadata.fields[tag.dimension[1]]
-
-            if (field) {
-                widgetOptions = parameterOptionsForField(field);
-                table = field.table
-                fieldMetadataLoaded = true
+    const isDimension = tag.type === "dimension";
+    const hasSelectedDimensionField =
+      isDimension && Array.isArray(tag.dimension);
+    return (
+      <div className="pb2 mb2 border-bottom border-dark">
+        <h3 className="pb2">{tag.name}</h3>
+
+        <div className="pb1">
+          <h5 className="pb1 text-normal">{t`Filter label`}</h5>
+          <Input
+            type="text"
+            value={tag.display_name}
+            className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded full"
+            onBlurChange={e =>
+              this.setParameterAttribute("display_name", e.target.value)
             }
-        }
-
-        const isDimension = tag.type === "dimension"
-        const hasSelectedDimensionField = isDimension && Array.isArray(tag.dimension)
-        return (
-            <div className="pb2 mb2 border-bottom border-dark">
-                <h3 className="pb2">{tag.name}</h3>
-
-                <div className="pb1">
-                    <h5 className="pb1 text-normal">{t`Filter label`}</h5>
-                    <Input
-                        type="text"
-                        value={tag.display_name}
-                        className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded full"
-                        onBlurChange={(e) => this.setParameterAttribute("display_name", e.target.value)}
-                    />
-                </div>
-
-                <div className="pb1">
-                    <h5 className="pb1 text-normal">{t`Variable type`}</h5>
-                    <Select
-                        className="border-med bg-white block"
-                        value={tag.type}
-                        onChange={(e) => this.setType(e.target.value)}
-                        isInitiallyOpen={!tag.type}
-                        placeholder={t`Select…`}
-                        height={300}
-                    >
-                        <Option value="text">{t`Text`}</Option>
-                        <Option value="number">{t`Number`}</Option>
-                        <Option value="date">{t`Date`}</Option>
-                        <Option value="dimension">{t`Field Filter`}</Option>
-                    </Select>
-                </div>
-
-                { tag.type === "dimension" &&
-                    <div className="pb1">
-                        <h5 className="pb1 text-normal">{t`Field to map to`}</h5>
-
-                        { (!hasSelectedDimensionField || (hasSelectedDimensionField && fieldMetadataLoaded)) &&
-                            <SchemaTableAndFieldDataSelector
-                                databases={databases}
-                                selectedDatabaseId={database.id}
-                                selectedTableId={table ? table.id : null}
-                                selectedFieldId={hasSelectedDimensionField ? tag.dimension[1] : null}
-                                setFieldFn={(fieldId) => this.setDimension(fieldId)}
-                                className="AdminSelect flex align-center"
-                                isInitiallyOpen={!tag.dimension}
-                            />
-                        }
-                    </div>
-                }
-
-                { widgetOptions && widgetOptions.length > 0 &&
-                    <div className="pb1">
-                        <h5 className="pb1 text-normal">{t`Filter widget type`}</h5>
-                        <Select
-                            className="border-med bg-white block"
-                            value={tag.widget_type}
-                            onChange={(e) => this.setParameterAttribute("widget_type", e.target.value)}
-                            isInitiallyOpen={!tag.widget_type}
-                            placeholder={t`Select…`}
-                        >
-                            {[{ name: "None", type: undefined }].concat(widgetOptions).map(widgetOption =>
-                                <Option key={widgetOption.type} value={widgetOption.type}>
-                                    {widgetOption.name}
-                                </Option>
-                            )}
-                        </Select>
-                    </div>
+          />
+        </div>
+
+        <div className="pb1">
+          <h5 className="pb1 text-normal">{t`Variable type`}</h5>
+          <Select
+            className="border-med bg-white block"
+            value={tag.type}
+            onChange={e => this.setType(e.target.value)}
+            isInitiallyOpen={!tag.type}
+            placeholder={t`Select…`}
+            height={300}
+          >
+            <Option value="text">{t`Text`}</Option>
+            <Option value="number">{t`Number`}</Option>
+            <Option value="date">{t`Date`}</Option>
+            <Option value="dimension">{t`Field Filter`}</Option>
+          </Select>
+        </div>
+
+        {tag.type === "dimension" && (
+          <div className="pb1">
+            <h5 className="pb1 text-normal">{t`Field to map to`}</h5>
+
+            {(!hasSelectedDimensionField ||
+              (hasSelectedDimensionField && fieldMetadataLoaded)) && (
+              <SchemaTableAndFieldDataSelector
+                databases={databases}
+                selectedDatabaseId={database.id}
+                selectedTableId={table ? table.id : null}
+                selectedFieldId={
+                  hasSelectedDimensionField ? tag.dimension[1] : null
                 }
-
-                { tag.type !== "dimension" &&
-                    <div className="flex align-center pb1">
-                        <h5 className="text-normal mr1">{t`Required?`}</h5>
-                        <Toggle value={tag.required} onChange={(value) => this.setRequired(value)} />
-                    </div>
-                }
-
-                { ((tag.type !== "dimension" && tag.required) || (tag.type === "dimension" || tag.widget_type)) &&
-                    <div className="pb1">
-                        <h5 className="pb1 text-normal">{t`Default filter widget value`}</h5>
-                        <ParameterValueWidget
-                            parameter={{
-                                type: tag.widget_type || (tag.type === "date" ? "date/single" : null)
-                            }}
-                            value={tag.default}
-                            setValue={(value) => this.setParameterAttribute("default", value)}
-                            className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded bg-white"
-                            isEditing
-                            commitImmediately
-                        />
-                    </div>
+                setFieldFn={fieldId => this.setDimension(fieldId)}
+                className="AdminSelect flex align-center"
+                isInitiallyOpen={!tag.dimension}
+              />
+            )}
+          </div>
+        )}
+
+        {widgetOptions &&
+          widgetOptions.length > 0 && (
+            <div className="pb1">
+              <h5 className="pb1 text-normal">{t`Filter widget type`}</h5>
+              <Select
+                className="border-med bg-white block"
+                value={tag.widget_type}
+                onChange={e =>
+                  this.setParameterAttribute("widget_type", e.target.value)
                 }
+                isInitiallyOpen={!tag.widget_type}
+                placeholder={t`Select…`}
+              >
+                {[{ name: "None", type: undefined }]
+                  .concat(widgetOptions)
+                  .map(widgetOption => (
+                    <Option key={widgetOption.type} value={widgetOption.type}>
+                      {widgetOption.name}
+                    </Option>
+                  ))}
+              </Select>
             </div>
-        );
-    }
+          )}
+
+        {tag.type !== "dimension" && (
+          <div className="flex align-center pb1">
+            <h5 className="text-normal mr1">{t`Required?`}</h5>
+            <Toggle
+              value={tag.required}
+              onChange={value => this.setRequired(value)}
+            />
+          </div>
+        )}
+
+        {((tag.type !== "dimension" && tag.required) ||
+          (tag.type === "dimension" || tag.widget_type)) && (
+          <div className="pb1">
+            <h5 className="pb1 text-normal">{t`Default filter widget value`}</h5>
+            <ParameterValueWidget
+              parameter={{
+                type:
+                  tag.widget_type ||
+                  (tag.type === "date" ? "date/single" : null),
+              }}
+              value={tag.default}
+              setValue={value => this.setParameterAttribute("default", value)}
+              className="AdminSelect p1 text-bold text-grey-4 bordered border-med rounded bg-white"
+              isEditing
+              commitImmediately
+            />
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
index 0d6b4e75208c9810ac3cf4c113ade2d7fe788d53..5f9c81fc20ea0481f41db61b03f84cd31edc76c8 100644
--- a/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
+++ b/frontend/src/metabase/query_builder/components/template_tags/TagEditorSidebar.jsx
@@ -6,117 +6,148 @@ import Icon from "metabase/components/Icon.jsx";
 import TagEditorParam from "./TagEditorParam.jsx";
 import TagEditorHelp from "./TagEditorHelp.jsx";
 import MetabaseAnalytics from "metabase/lib/analytics";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
-import type { DatasetQuery } from "metabase/meta/types/Card"
-import type { TableId } from "metabase/meta/types/Table"
-import type { Database } from "metabase/meta/types/Database"
-import type { TemplateTag } from "metabase/meta/types/Query"
-import type { Field as FieldObject } from "metabase/meta/types/Field"
+import type { DatasetQuery } from "metabase/meta/types/Card";
+import type { TableId } from "metabase/meta/types/Table";
+import type { Database } from "metabase/meta/types/Database";
+import type { TemplateTag } from "metabase/meta/types/Query";
+import type { Field as FieldObject } from "metabase/meta/types/Field";
 
 type Props = {
-    query: NativeQuery,
+  query: NativeQuery,
 
-    setDatasetQuery: (datasetQuery: DatasetQuery) => void,
-    updateTemplateTag: (tag: TemplateTag) => void,
+  setDatasetQuery: (datasetQuery: DatasetQuery) => void,
+  updateTemplateTag: (tag: TemplateTag) => void,
 
-    databaseFields: FieldObject[],
-    databases: Database[],
-    sampleDatasetId: TableId,
+  databaseFields: FieldObject[],
+  databases: Database[],
+  sampleDatasetId: TableId,
 
-    onClose: () => void,
-}
+  onClose: () => void,
+};
 type State = {
-    section: "help"|"settings";
-}
+  section: "help" | "settings",
+};
 
 export default class TagEditorSidebar extends Component {
-    props: Props;
-    state: State = {
-        section: "settings"
-    }
-
-    static propTypes = {
-        card: PropTypes.object.isRequired,
-        onClose: PropTypes.func.isRequired,
-        updateTemplateTag: PropTypes.func.isRequired,
-        databaseFields: PropTypes.array,
-        setDatasetQuery: PropTypes.func.isRequired,
-        sampleDatasetId: PropTypes.number,
-    };
+  props: Props;
+  state: State = {
+    section: "settings",
+  };
 
-    setSection (section) {
-        this.setState({ section: section });
-        MetabaseAnalytics.trackEvent("QueryBuilder", "Template Tag Editor Section Change", section)
-    }
+  static propTypes = {
+    card: PropTypes.object.isRequired,
+    onClose: PropTypes.func.isRequired,
+    updateTemplateTag: PropTypes.func.isRequired,
+    databaseFields: PropTypes.array,
+    setDatasetQuery: PropTypes.func.isRequired,
+    sampleDatasetId: PropTypes.number,
+  };
 
-    render() {
-        const { databases, databaseFields, sampleDatasetId, setDatasetQuery, query, updateTemplateTag, onClose } = this.props;
-        const tags = query.templateTags();
-        const databaseId = query.datasetQuery().database;
-        const database = databases.find(db => db.id === databaseId);
+  setSection(section) {
+    this.setState({ section: section });
+    MetabaseAnalytics.trackEvent(
+      "QueryBuilder",
+      "Template Tag Editor Section Change",
+      section,
+    );
+  }
 
-        let section;
-        if (tags.length === 0) {
-            section = "help";
-        } else {
-            section = this.state.section;
-        }
+  render() {
+    const {
+      databases,
+      databaseFields,
+      sampleDatasetId,
+      setDatasetQuery,
+      query,
+      updateTemplateTag,
+      onClose,
+    } = this.props;
+    const tags = query.templateTags();
+    const databaseId = query.datasetQuery().database;
+    const database = databases.find(db => db.id === databaseId);
 
-        return (
-            <div className="DataReference-container p3 full-height scroll-y">
-                <div className="DataReference-header flex align-center mb2">
-                    <h2 className="text-default">
-                        {t`Variables`}
-                    </h2>
-                    <a className="flex-align-right text-default text-brand-hover no-decoration" onClick={() => onClose()}>
-                        <Icon name="close" size={18} />
-                    </a>
-                </div>
-                <div className="DataReference-content">
-                    <div className="Button-group Button-group--brand text-uppercase mb2">
-                        <a className={cx("Button Button--small", { "Button--active": section === "settings" , "disabled": tags.length === 0 })} onClick={() => this.setSection("settings")}>{t`Settings`}</a>
-                        <a className={cx("Button Button--small", { "Button--active": section === "help" })} onClick={() => this.setSection("help")}>{t`Help`}</a>
-                    </div>
-                    { section === "settings" ?
-                        <SettingsPane
-                            tags={tags}
-                            onUpdate={updateTemplateTag}
-                            databaseFields={databaseFields}
-                            database={database}
-                            databases={databases}
-                        />
-                    :
-                        <TagEditorHelp
-                            sampleDatasetId={sampleDatasetId}
-                            setDatasetQuery={setDatasetQuery}
-                        />
-                    }
-                </div>
-            </div>
-        );
+    let section;
+    if (tags.length === 0) {
+      section = "help";
+    } else {
+      section = this.state.section;
     }
+
+    return (
+      <div className="DataReference-container p3 full-height scroll-y">
+        <div className="DataReference-header flex align-center mb2">
+          <h2 className="text-default">{t`Variables`}</h2>
+          <a
+            className="flex-align-right text-default text-brand-hover no-decoration"
+            onClick={() => onClose()}
+          >
+            <Icon name="close" size={18} />
+          </a>
+        </div>
+        <div className="DataReference-content">
+          <div className="Button-group Button-group--brand text-uppercase mb2">
+            <a
+              className={cx("Button Button--small", {
+                "Button--active": section === "settings",
+                disabled: tags.length === 0,
+              })}
+              onClick={() => this.setSection("settings")}
+            >{t`Settings`}</a>
+            <a
+              className={cx("Button Button--small", {
+                "Button--active": section === "help",
+              })}
+              onClick={() => this.setSection("help")}
+            >{t`Help`}</a>
+          </div>
+          {section === "settings" ? (
+            <SettingsPane
+              tags={tags}
+              onUpdate={updateTemplateTag}
+              databaseFields={databaseFields}
+              database={database}
+              databases={databases}
+            />
+          ) : (
+            <TagEditorHelp
+              sampleDatasetId={sampleDatasetId}
+              setDatasetQuery={setDatasetQuery}
+            />
+          )}
+        </div>
+      </div>
+    );
+  }
 }
 
-const SettingsPane = ({ tags, onUpdate, databaseFields, database, databases }) =>
-    <div>
-        { tags.map(tag =>
-            <div key={tags.name}>
-                <TagEditorParam
-                    tag={tag}
-                    onUpdate={onUpdate}
-                    databaseFields={databaseFields}
-                    database={database}
-                    databases={databases}
-                />
-            </div>
-        ) }
-    </div>
+const SettingsPane = ({
+  tags,
+  onUpdate,
+  databaseFields,
+  database,
+  databases,
+}) => (
+  <div>
+    {tags.map(tag => (
+      <div key={tags.name}>
+        <TagEditorParam
+          tag={tag}
+          onUpdate={onUpdate}
+          databaseFields={databaseFields}
+          database={database}
+          databases={databases}
+        />
+      </div>
+    ))}
+  </div>
+);
 
 SettingsPane.propTypes = {
-    tags: PropTypes.object.isRequired,
-    onUpdate: PropTypes.func.isRequired,
-    databaseFields: PropTypes.array
+  tags: PropTypes.object.isRequired,
+  onUpdate: PropTypes.func.isRequired,
+  databaseFields: PropTypes.array,
 };
diff --git a/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx b/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx
index f3cbfd8effd9a4036f9f31ce9cf8e5b2d8aa6212..668ee6a45c833c77bfd4da73f54509288717002b 100644
--- a/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx
+++ b/frontend/src/metabase/query_builder/containers/ArchiveQuestionModal.jsx
@@ -1,58 +1,62 @@
-import React, { Component } from "react"
-import { connect } from "react-redux"
-import { t } from 'c-3po';
-import Button from "metabase/components/Button"
-import Icon from "metabase/components/Icon"
-import ModalWithTrigger from "metabase/components/ModalWithTrigger"
-import Tooltip from "metabase/components/Tooltip"
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import { t } from "c-3po";
+import Button from "metabase/components/Button";
+import Icon from "metabase/components/Icon";
+import ModalWithTrigger from "metabase/components/ModalWithTrigger";
+import Tooltip from "metabase/components/Tooltip";
 
-import { archiveQuestion } from "metabase/query_builder/actions"
+import { archiveQuestion } from "metabase/query_builder/actions";
 
-const mapStateToProps = () => ({})
+const mapStateToProps = () => ({});
 
 const mapDispatchToProps = {
-    archiveQuestion
-}
+  archiveQuestion,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 class ArchiveQuestionModal extends Component {
-    onArchive = async () => {
-        try {
-            await this.props.archiveQuestion()
-            this.onClose();
-        } catch (error) {
-            console.error(error)
-            this.setState({ error })
-        }
+  onArchive = async () => {
+    try {
+      await this.props.archiveQuestion();
+      this.onClose();
+    } catch (error) {
+      console.error(error);
+      this.setState({ error });
     }
+  };
 
-    onClose = () => {
-        if (this.refs.archiveModal) {
-            this.refs.archiveModal.close();
-        }
+  onClose = () => {
+    if (this.refs.archiveModal) {
+      this.refs.archiveModal.close();
     }
+  };
 
-    render () {
-        return (
-            <ModalWithTrigger
-                ref="archiveModal"
-                triggerElement={
-                    <Tooltip key="archive" tooltip={t`Archive`}>
-                        <span className="text-brand-hover">
-                            <Icon name="archive" size={16} />
-                        </span>
-                    </Tooltip>
-                }
-                title={t`Archive this question?`}
-                footer={[
-                    <Button key='cancel' onClick={this.onClose}>{t`Cancel`}</Button>,
-                    <Button key='archive' warning onClick={this.onArchive}>{t`Archive`}</Button>
-                ]}
-            >
-                <div className="px4 pb4">{t`This question will be removed from any dashboards or pulses using it.`}</div>
-            </ModalWithTrigger>
-        )
-    }
+  render() {
+    return (
+      <ModalWithTrigger
+        ref="archiveModal"
+        triggerElement={
+          <Tooltip key="archive" tooltip={t`Archive`}>
+            <span className="text-brand-hover">
+              <Icon name="archive" size={16} />
+            </span>
+          </Tooltip>
+        }
+        title={t`Archive this question?`}
+        footer={[
+          <Button key="cancel" onClick={this.onClose}>{t`Cancel`}</Button>,
+          <Button
+            key="archive"
+            warning
+            onClick={this.onArchive}
+          >{t`Archive`}</Button>,
+        ]}
+      >
+        <div className="px4 pb4">{t`This question will be removed from any dashboards or pulses using it.`}</div>
+      </ModalWithTrigger>
+    );
+  }
 }
 
-export default ArchiveQuestionModal
+export default ArchiveQuestionModal;
diff --git a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
index 5435be343fb6e1e6642b193e524d2094ab07160b..d39970e2c4adf4eed81611ca7549803e92b0f521 100644
--- a/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
+++ b/frontend/src/metabase/query_builder/containers/QueryBuilder.jsx
@@ -3,7 +3,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 import _ from "underscore";
 
@@ -23,32 +23,32 @@ import ActionsWidget from "../components/ActionsWidget.jsx";
 import title from "metabase/hoc/Title";
 
 import {
-    getCard,
-    getOriginalCard,
-    getLastRunCard,
-    getQueryResult,
-    getQueryResults,
-    getParameterValues,
-    getIsDirty,
-    getIsNew,
-    getIsObjectDetail,
-    getTables,
-    getTableMetadata,
-    getTableForeignKeys,
-    getTableForeignKeyReferences,
-    getUiControls,
-    getParameters,
-    getDatabaseFields,
-    getSampleDatasetId,
-    getNativeDatabases,
-    getIsRunnable,
-    getIsResultDirty,
-    getMode,
-    getQuery,
-    getQuestion,
-    getOriginalQuestion,
-    getSettings,
-    getRawSeries
+  getCard,
+  getOriginalCard,
+  getLastRunCard,
+  getQueryResult,
+  getQueryResults,
+  getParameterValues,
+  getIsDirty,
+  getIsNew,
+  getIsObjectDetail,
+  getTables,
+  getTableMetadata,
+  getTableForeignKeys,
+  getTableForeignKeyReferences,
+  getUiControls,
+  getParameters,
+  getDatabaseFields,
+  getSampleDatasetId,
+  getNativeDatabases,
+  getIsRunnable,
+  getIsResultDirty,
+  getMode,
+  getQuery,
+  getQuestion,
+  getOriginalQuestion,
+  getSettings,
+  getRawSeries,
 } from "../selectors";
 
 import { getMetadata, getDatabasesList } from "metabase/selectors/metadata";
@@ -63,215 +63,248 @@ import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 function autocompleteResults(card, prefix) {
-    let databaseId = card && card.dataset_query && card.dataset_query.database;
-    let apiCall = MetabaseApi.db_autocomplete_suggestions({
-       dbId: databaseId,
-       prefix: prefix
-    });
-    return apiCall;
+  let databaseId = card && card.dataset_query && card.dataset_query.database;
+  let apiCall = MetabaseApi.db_autocomplete_suggestions({
+    dbId: databaseId,
+    prefix: prefix,
+  });
+  return apiCall;
 }
 
 const mapStateToProps = (state, props) => {
-    return {
-        isAdmin:                   getUserIsAdmin(state, props),
-        fromUrl:                   props.location.query.from,
-
-        question:                  getQuestion(state),
-        query:                     getQuery(state),
-
-        mode:                      getMode(state),
-
-        card:                      getCard(state),
-        originalCard:              getOriginalCard(state),
-        originalQuestion:          getOriginalQuestion(state),
-        lastRunCard:               getLastRunCard(state),
-
-        parameterValues:           getParameterValues(state),
-
-        databases:                 getDatabasesList(state),
-        nativeDatabases:           getNativeDatabases(state),
-        tables:                    getTables(state),
-        tableMetadata:             getTableMetadata(state),
-        metadata:                  getMetadata(state),
-
-        tableForeignKeys:          getTableForeignKeys(state),
-        tableForeignKeyReferences: getTableForeignKeyReferences(state),
-
-        result:                    getQueryResult(state),
-        results:                   getQueryResults(state),
-        rawSeries:                 getRawSeries(state),
-
-        isDirty:                   getIsDirty(state),
-        isNew:                     getIsNew(state),
-        isObjectDetail:            getIsObjectDetail(state),
-
-        uiControls:                getUiControls(state),
-        parameters:                getParameters(state),
-        databaseFields:            getDatabaseFields(state),
-        sampleDatasetId:           getSampleDatasetId(state),
-
-        isShowingDataReference:    state.qb.uiControls.isShowingDataReference,
-        isShowingTutorial:         state.qb.uiControls.isShowingTutorial,
-        isEditing:                 state.qb.uiControls.isEditing,
-        isRunning:                 state.qb.uiControls.isRunning,
-        isRunnable:                getIsRunnable(state),
-        isResultDirty:             getIsResultDirty(state),
-
-        loadTableAndForeignKeysFn: loadTableAndForeignKeys,
-        autocompleteResultsFn:     (prefix) => autocompleteResults(state.qb.card, prefix),
-        instanceSettings:          getSettings(state)
-    }
-}
-
-const getURL = (location) =>
-    location.pathname + location.search + location.hash;
+  return {
+    isAdmin: getUserIsAdmin(state, props),
+    fromUrl: props.location.query.from,
+
+    question: getQuestion(state),
+    query: getQuery(state),
+
+    mode: getMode(state),
+
+    card: getCard(state),
+    originalCard: getOriginalCard(state),
+    originalQuestion: getOriginalQuestion(state),
+    lastRunCard: getLastRunCard(state),
+
+    parameterValues: getParameterValues(state),
+
+    databases: getDatabasesList(state),
+    nativeDatabases: getNativeDatabases(state),
+    tables: getTables(state),
+    tableMetadata: getTableMetadata(state),
+    metadata: getMetadata(state),
+
+    tableForeignKeys: getTableForeignKeys(state),
+    tableForeignKeyReferences: getTableForeignKeyReferences(state),
+
+    result: getQueryResult(state),
+    results: getQueryResults(state),
+    rawSeries: getRawSeries(state),
+
+    isDirty: getIsDirty(state),
+    isNew: getIsNew(state),
+    isObjectDetail: getIsObjectDetail(state),
+
+    uiControls: getUiControls(state),
+    parameters: getParameters(state),
+    databaseFields: getDatabaseFields(state),
+    sampleDatasetId: getSampleDatasetId(state),
+
+    isShowingDataReference: state.qb.uiControls.isShowingDataReference,
+    isShowingTutorial: state.qb.uiControls.isShowingTutorial,
+    isEditing: state.qb.uiControls.isEditing,
+    isRunning: state.qb.uiControls.isRunning,
+    isRunnable: getIsRunnable(state),
+    isResultDirty: getIsResultDirty(state),
+
+    loadTableAndForeignKeysFn: loadTableAndForeignKeys,
+    autocompleteResultsFn: prefix => autocompleteResults(state.qb.card, prefix),
+    instanceSettings: getSettings(state),
+  };
+};
 
+const getURL = location => location.pathname + location.search + location.hash;
 
 const mapDispatchToProps = {
-    ...actions,
-    onChangeLocation: push
+  ...actions,
+  onChangeLocation: push,
 };
 
-
-
 @connect(mapStateToProps, mapDispatchToProps)
 @title(({ card }) => (card && card.name) || t`Question`)
 export default class QueryBuilder extends Component {
-    forceUpdateDebounced: () => void;
-
-    constructor(props, context) {
-        super(props, context);
-
-        // TODO: React tells us that forceUpdate() is not the best thing to use, so ideally we can find a different way to trigger this
-        this.forceUpdateDebounced = _.debounce(this.forceUpdate.bind(this), 400);
-    }
-
-    componentWillMount() {
-        this.props.initializeQB(this.props.location, this.props.params);
+  forceUpdateDebounced: () => void;
+
+  constructor(props, context) {
+    super(props, context);
+
+    // TODO: React tells us that forceUpdate() is not the best thing to use, so ideally we can find a different way to trigger this
+    this.forceUpdateDebounced = _.debounce(this.forceUpdate.bind(this), 400);
+  }
+
+  componentWillMount() {
+    this.props.initializeQB(this.props.location, this.props.params);
+  }
+
+  componentDidMount() {
+    window.addEventListener("resize", this.handleResize);
+  }
+
+  componentWillReceiveProps(nextProps) {
+    if (
+      nextProps.uiControls.isShowingDataReference !==
+        this.props.uiControls.isShowingDataReference ||
+      nextProps.uiControls.isShowingTemplateTagsEditor !==
+        this.props.uiControls.isShowingTemplateTagsEditor
+    ) {
+      // when the data reference is toggled we need to trigger a rerender after a short delay in order to
+      // ensure that some components are updated after the animation completes (e.g. card visualization)
+      window.setTimeout(this.forceUpdateDebounced, 300);
     }
 
-    componentDidMount() {
-        window.addEventListener("resize", this.handleResize);
+    if (
+      nextProps.location.action === "POP" &&
+      getURL(nextProps.location) !== getURL(this.props.location)
+    ) {
+      this.props.popState(nextProps.location);
+    } else if (
+      this.props.location.hash !== "#?tutorial" &&
+      nextProps.location.hash === "#?tutorial"
+    ) {
+      this.props.initializeQB(nextProps.location, nextProps.params);
+    } else if (
+      getURL(nextProps.location) === "/question" &&
+      getURL(this.props.location) !== "/question"
+    ) {
+      this.props.initializeQB(nextProps.location, nextProps.params);
     }
+  }
 
-    componentWillReceiveProps(nextProps) {
-        if (nextProps.uiControls.isShowingDataReference !== this.props.uiControls.isShowingDataReference ||
-            nextProps.uiControls.isShowingTemplateTagsEditor !== this.props.uiControls.isShowingTemplateTagsEditor) {
-            // when the data reference is toggled we need to trigger a rerender after a short delay in order to
-            // ensure that some components are updated after the animation completes (e.g. card visualization)
-            window.setTimeout(this.forceUpdateDebounced, 300);
-        }
-
-        if (nextProps.location.action === "POP" && getURL(nextProps.location) !== getURL(this.props.location)) {
-            this.props.popState(nextProps.location);
-        } else if (this.props.location.hash !== "#?tutorial" && nextProps.location.hash === "#?tutorial") {
-            this.props.initializeQB(nextProps.location, nextProps.params);
-        } else if (getURL(nextProps.location) === "/question" && getURL(this.props.location) !== "/question") {
-            this.props.initializeQB(nextProps.location, nextProps.params);
-        }
+  componentDidUpdate() {
+    let viz = ReactDOM.findDOMNode(this.refs.viz);
+    if (viz) {
+      viz.style.opacity = 1.0;
     }
-
-    componentDidUpdate() {
-        let viz = ReactDOM.findDOMNode(this.refs.viz);
-        if (viz) {
-            viz.style.opacity = 1.0;
-        }
-    }
-
-    componentWillUnmount() {
-        // cancel the query if one is running
-        this.props.cancelQuery();
-
-        window.removeEventListener("resize", this.handleResize);
-    }
-
-    // When the window is resized we need to re-render, mainly so that our visualization pane updates
-    // Debounce the function to improve resizing performance.
-    handleResize = (e) => {
-        this.forceUpdateDebounced();
-        let viz = ReactDOM.findDOMNode(this.refs.viz);
-        if (viz) {
-            viz.style.opacity = 0.2;
-        }
-    }
-
-    render() {
-        return (
-            <div className="flex-full flex relative">
-                <LegacyQueryBuilder {...this.props} />
-            </div>
-        )
+  }
+
+  componentWillUnmount() {
+    // cancel the query if one is running
+    this.props.cancelQuery();
+
+    window.removeEventListener("resize", this.handleResize);
+  }
+
+  // When the window is resized we need to re-render, mainly so that our visualization pane updates
+  // Debounce the function to improve resizing performance.
+  handleResize = e => {
+    this.forceUpdateDebounced();
+    let viz = ReactDOM.findDOMNode(this.refs.viz);
+    if (viz) {
+      viz.style.opacity = 0.2;
     }
+  };
+
+  render() {
+    return (
+      <div className="flex-full flex relative">
+        <LegacyQueryBuilder {...this.props} />
+      </div>
+    );
+  }
 }
 
 class LegacyQueryBuilder extends Component {
-    render() {
-        const { query, card, isDirty, databases, uiControls, mode, } = this.props;
-
-        // if we don't have a card at all or no databases then we are initializing, so keep it simple
-        if (!card || !databases) {
-            return (
-                <div></div>
-            );
-        }
-
-        const showDrawer = uiControls.isShowingDataReference || uiControls.isShowingTemplateTagsEditor;
-        const ModeFooter = mode && mode.ModeFooter;
-
-        return (
-            <div className="flex-full relative">
-                <div className={cx("QueryBuilder flex flex-column bg-white spread", {"QueryBuilder--showSideDrawer": showDrawer})}>
-                    <div id="react_qb_header">
-                        <QueryHeader {...this.props}/>
-                    </div>
-
-                    <div id="react_qb_editor" className="z2 hide sm-show">
-                        { query instanceof NativeQuery ?
-                            <NativeQueryEditor
-                                {...this.props}
-                                isOpen={!card.dataset_query.native.query || isDirty}
-                                datasetQuery={card && card.dataset_query}
-                            />
-                        : (query instanceof StructuredQuery) ?
-                            <div className="wrapper">
-                                <GuiQueryEditor
-                                    {...this.props}
-                                    datasetQuery={card && card.dataset_query}
-                                />
-                            </div>
-                        : null }
-                    </div>
-
-                    <div ref="viz" id="react_qb_viz" className="flex z1"
-                         style={{"transition": "opacity 0.25s ease-in-out"}}>
-                        <QueryVisualization {...this.props} className="full wrapper mb2 z1"/>
-                    </div>
-
-                    { ModeFooter &&
-                        <ModeFooter {...this.props} className="flex-no-shrink" />
-                    }
-                </div>
-
-                <div className={cx("SideDrawer hide sm-show", { "SideDrawer--show": showDrawer })}>
-                    { uiControls.isShowingDataReference &&
-                        <DataReference {...this.props} onClose={() => this.props.toggleDataReference()} />
-                    }
-
-                    { uiControls.isShowingTemplateTagsEditor && query instanceof NativeQuery &&
-                        <TagEditorSidebar {...this.props} onClose={() => this.props.toggleTemplateTagsEditor()} />
-                    }
-                </div>
-
-                { uiControls.isShowingTutorial &&
-                    <QueryBuilderTutorial onClose={() => this.props.closeQbTutorial()} />
-                }
-
-                { uiControls.isShowingNewbModal &&
-                    <SavedQuestionIntroModal onClose={() => this.props.closeQbNewbModal()} />
-                }
-
-                <ActionsWidget {...this.props} className="z2 absolute bottom right" />
-            </div>
-        );
+  render() {
+    const { query, card, isDirty, databases, uiControls, mode } = this.props;
+
+    // if we don't have a card at all or no databases then we are initializing, so keep it simple
+    if (!card || !databases) {
+      return <div />;
     }
+
+    const showDrawer =
+      uiControls.isShowingDataReference ||
+      uiControls.isShowingTemplateTagsEditor;
+    const ModeFooter = mode && mode.ModeFooter;
+
+    return (
+      <div className="flex-full relative">
+        <div
+          className={cx("QueryBuilder flex flex-column bg-white spread", {
+            "QueryBuilder--showSideDrawer": showDrawer,
+          })}
+        >
+          <div id="react_qb_header">
+            <QueryHeader {...this.props} />
+          </div>
+
+          <div id="react_qb_editor" className="z2 hide sm-show">
+            {query instanceof NativeQuery ? (
+              <NativeQueryEditor
+                {...this.props}
+                isOpen={!card.dataset_query.native.query || isDirty}
+                datasetQuery={card && card.dataset_query}
+              />
+            ) : query instanceof StructuredQuery ? (
+              <div className="wrapper">
+                <GuiQueryEditor
+                  {...this.props}
+                  datasetQuery={card && card.dataset_query}
+                />
+              </div>
+            ) : null}
+          </div>
+
+          <div
+            ref="viz"
+            id="react_qb_viz"
+            className="flex z1"
+            style={{ transition: "opacity 0.25s ease-in-out" }}
+          >
+            <QueryVisualization
+              {...this.props}
+              className="full wrapper mb2 z1"
+            />
+          </div>
+
+          {ModeFooter && (
+            <ModeFooter {...this.props} className="flex-no-shrink" />
+          )}
+        </div>
+
+        <div
+          className={cx("SideDrawer hide sm-show", {
+            "SideDrawer--show": showDrawer,
+          })}
+        >
+          {uiControls.isShowingDataReference && (
+            <DataReference
+              {...this.props}
+              onClose={() => this.props.toggleDataReference()}
+            />
+          )}
+
+          {uiControls.isShowingTemplateTagsEditor &&
+            query instanceof NativeQuery && (
+              <TagEditorSidebar
+                {...this.props}
+                onClose={() => this.props.toggleTemplateTagsEditor()}
+              />
+            )}
+        </div>
+
+        {uiControls.isShowingTutorial && (
+          <QueryBuilderTutorial onClose={() => this.props.closeQbTutorial()} />
+        )}
+
+        {uiControls.isShowingNewbModal && (
+          <SavedQuestionIntroModal
+            onClose={() => this.props.closeQbNewbModal()}
+          />
+        )}
+
+        <ActionsWidget {...this.props} className="z2 absolute bottom right" />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
index a96a68b57e4481a3d00510dfa422304e0e2f1470..8cad882d28b119e1fadfef91d763387d2b434229 100644
--- a/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
+++ b/frontend/src/metabase/query_builder/containers/QuestionEmbedWidget.jsx
@@ -8,33 +8,52 @@ import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 import * as Urls from "metabase/lib/urls";
 
 import { getParameters } from "metabase/meta/Card";
-import { createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams, } from "../actions";
+import {
+  createPublicLink,
+  deletePublicLink,
+  updateEnableEmbedding,
+  updateEmbeddingParams,
+} from "../actions";
 
 const mapDispatchToProps = {
-    createPublicLink,
-    deletePublicLink,
-    updateEnableEmbedding,
-    updateEmbeddingParams,
-}
+  createPublicLink,
+  deletePublicLink,
+  updateEnableEmbedding,
+  updateEmbeddingParams,
+};
 
 @connect(null, mapDispatchToProps)
 export default class QuestionEmbedWidget extends Component {
-    render() {
-        const { className, card, createPublicLink, deletePublicLink, updateEnableEmbedding, updateEmbeddingParams, ...props } = this.props;
-        return (
-            <EmbedWidget
-                {...props}
-                className={className}
-                resource={card}
-                resourceType="question"
-                resourceParameters={getParameters(card)}
-                onCreatePublicLink={() => createPublicLink(card)}
-                onDisablePublicLink={() => deletePublicLink(card)}
-                onUpdateEnableEmbedding={(enableEmbedding) => updateEnableEmbedding(card, enableEmbedding)}
-                onUpdateEmbeddingParams={(embeddingParams) => updateEmbeddingParams(card, embeddingParams)}
-                getPublicUrl={({ public_uuid }, extension) => Urls.publicCard(public_uuid, extension)}
-                extensions={["csv", "xlsx", "json"]}
-            />
-        );
-    }
+  render() {
+    const {
+      className,
+      card,
+      createPublicLink,
+      deletePublicLink,
+      updateEnableEmbedding,
+      updateEmbeddingParams,
+      ...props
+    } = this.props;
+    return (
+      <EmbedWidget
+        {...props}
+        className={className}
+        resource={card}
+        resourceType="question"
+        resourceParameters={getParameters(card)}
+        onCreatePublicLink={() => createPublicLink(card)}
+        onDisablePublicLink={() => deletePublicLink(card)}
+        onUpdateEnableEmbedding={enableEmbedding =>
+          updateEnableEmbedding(card, enableEmbedding)
+        }
+        onUpdateEmbeddingParams={embeddingParams =>
+          updateEmbeddingParams(card, embeddingParams)
+        }
+        getPublicUrl={({ public_uuid }, extension) =>
+          Urls.publicCard(public_uuid, extension)
+        }
+        extensions={["csv", "xlsx", "json"]}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/query_builder/reducers.js b/frontend/src/metabase/query_builder/reducers.js
index ee66cb5c1957faaaf0c525ee86940181c0ebe633..4401441b950e6325eacbb50f25fb5876898ed55d 100644
--- a/frontend/src/metabase/query_builder/reducers.js
+++ b/frontend/src/metabase/query_builder/reducers.js
@@ -3,92 +3,145 @@ import { handleActions } from "redux-actions";
 import { assoc, dissoc } from "icepick";
 
 import {
-    RESET_QB,
-    INITIALIZE_QB,
-    TOGGLE_DATA_REFERENCE,
-    TOGGLE_TEMPLATE_TAGS_EDITOR,
-    SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
-    CLOSE_QB_TUTORIAL,
-    CLOSE_QB_NEWB_MODAL,
-    BEGIN_EDITING,
-    CANCEL_EDITING,
-
-    LOAD_TABLE_METADATA,
-    LOAD_DATABASE_FIELDS,
-    RELOAD_CARD,
-    API_CREATE_QUESTION,
-    API_UPDATE_QUESTION,
-    SET_CARD_AND_RUN,
-    SET_CARD_ATTRIBUTE,
-    SET_CARD_VISUALIZATION,
-    UPDATE_CARD_VISUALIZATION_SETTINGS,
-    REPLACE_ALL_CARD_VISUALIZATION_SETTINGS,
-    UPDATE_TEMPLATE_TAG,
-    SET_PARAMETER_VALUE,
-
-    SET_QUERY_DATABASE,
-    SET_QUERY_SOURCE_TABLE,
-    SET_QUERY_MODE,
-    UPDATE_QUESTION,
-    SET_DATASET_QUERY,
-    RUN_QUERY,
-    CANCEL_QUERY,
-    QUERY_COMPLETED,
-    QUERY_ERRORED,
-    LOAD_OBJECT_DETAIL_FK_REFERENCES,
-
-    SET_CURRENT_STATE,
-
-    CREATE_PUBLIC_LINK,
-    DELETE_PUBLIC_LINK,
-    UPDATE_ENABLE_EMBEDDING,
-    UPDATE_EMBEDDING_PARAMS
+  RESET_QB,
+  INITIALIZE_QB,
+  TOGGLE_DATA_REFERENCE,
+  TOGGLE_TEMPLATE_TAGS_EDITOR,
+  SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR,
+  CLOSE_QB_TUTORIAL,
+  CLOSE_QB_NEWB_MODAL,
+  BEGIN_EDITING,
+  CANCEL_EDITING,
+  LOAD_TABLE_METADATA,
+  LOAD_DATABASE_FIELDS,
+  RELOAD_CARD,
+  API_CREATE_QUESTION,
+  API_UPDATE_QUESTION,
+  SET_CARD_AND_RUN,
+  SET_CARD_ATTRIBUTE,
+  SET_CARD_VISUALIZATION,
+  UPDATE_CARD_VISUALIZATION_SETTINGS,
+  REPLACE_ALL_CARD_VISUALIZATION_SETTINGS,
+  UPDATE_TEMPLATE_TAG,
+  SET_PARAMETER_VALUE,
+  SET_QUERY_DATABASE,
+  SET_QUERY_SOURCE_TABLE,
+  SET_QUERY_MODE,
+  UPDATE_QUESTION,
+  SET_DATASET_QUERY,
+  RUN_QUERY,
+  CANCEL_QUERY,
+  QUERY_COMPLETED,
+  QUERY_ERRORED,
+  LOAD_OBJECT_DETAIL_FK_REFERENCES,
+  SET_CURRENT_STATE,
+  CREATE_PUBLIC_LINK,
+  DELETE_PUBLIC_LINK,
+  UPDATE_ENABLE_EMBEDDING,
+  UPDATE_EMBEDDING_PARAMS,
 } from "./actions";
 
 // various ui state options
-export const uiControls = handleActions({
-    [INITIALIZE_QB]: { next: (state, { payload }) => ({ ...state, ...payload.uiControls }) },
-
-    [TOGGLE_DATA_REFERENCE]: { next: (state, { payload }) => ({ ...state, isShowingDataReference: !state.isShowingDataReference, isShowingTemplateTagsEditor: false }) },
-    [TOGGLE_TEMPLATE_TAGS_EDITOR]: { next: (state, { payload }) => ({ ...state, isShowingTemplateTagsEditor: !state.isShowingTemplateTagsEditor, isShowingDataReference: false }) },
-    [SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR]: { next: (state, { isShowingTemplateTagsEditor }) => ({ ...state, isShowingTemplateTagsEditor, isShowingDataReference: false }) },
-    [SET_DATASET_QUERY]: { next: (state, { payload }) => ({ ...state, isShowingTemplateTagsEditor: payload.openTemplateTagsEditor }) },
-    [CLOSE_QB_TUTORIAL]: { next: (state, { payload }) => ({ ...state, isShowingTutorial: false }) },
-    [CLOSE_QB_NEWB_MODAL]: { next: (state, { payload }) => ({ ...state, isShowingNewbModal: false }) },
-
-    [BEGIN_EDITING]: { next: (state, { payload }) => ({ ...state, isEditing: true }) },
-    [CANCEL_EDITING]: { next: (state, { payload }) => ({ ...state, isEditing: false }) },
-    [API_UPDATE_QUESTION]: { next: (state, { payload }) => ({ ...state, isEditing: false }) },
-    [RELOAD_CARD]: { next: (state, { payload }) => ({ ...state, isEditing: false })},
-
-    [RUN_QUERY]: (state) => ({ ...state, isRunning: true }),
-    [CANCEL_QUERY]: { next: (state, { payload }) => ({ ...state, isRunning: false }) },
-    [QUERY_COMPLETED]: { next: (state, { payload }) => ({ ...state, isRunning: false }) },
-    [QUERY_ERRORED]: { next: (state, { payload }) => ({ ...state, isRunning: false }) },
-}, {
+export const uiControls = handleActions(
+  {
+    [INITIALIZE_QB]: {
+      next: (state, { payload }) => ({ ...state, ...payload.uiControls }),
+    },
+
+    [TOGGLE_DATA_REFERENCE]: {
+      next: (state, { payload }) => ({
+        ...state,
+        isShowingDataReference: !state.isShowingDataReference,
+        isShowingTemplateTagsEditor: false,
+      }),
+    },
+    [TOGGLE_TEMPLATE_TAGS_EDITOR]: {
+      next: (state, { payload }) => ({
+        ...state,
+        isShowingTemplateTagsEditor: !state.isShowingTemplateTagsEditor,
+        isShowingDataReference: false,
+      }),
+    },
+    [SET_IS_SHOWING_TEMPLATE_TAGS_EDITOR]: {
+      next: (state, { isShowingTemplateTagsEditor }) => ({
+        ...state,
+        isShowingTemplateTagsEditor,
+        isShowingDataReference: false,
+      }),
+    },
+    [SET_DATASET_QUERY]: {
+      next: (state, { payload }) => ({
+        ...state,
+        isShowingTemplateTagsEditor: payload.openTemplateTagsEditor,
+      }),
+    },
+    [CLOSE_QB_TUTORIAL]: {
+      next: (state, { payload }) => ({ ...state, isShowingTutorial: false }),
+    },
+    [CLOSE_QB_NEWB_MODAL]: {
+      next: (state, { payload }) => ({ ...state, isShowingNewbModal: false }),
+    },
+
+    [BEGIN_EDITING]: {
+      next: (state, { payload }) => ({ ...state, isEditing: true }),
+    },
+    [CANCEL_EDITING]: {
+      next: (state, { payload }) => ({ ...state, isEditing: false }),
+    },
+    [API_UPDATE_QUESTION]: {
+      next: (state, { payload }) => ({ ...state, isEditing: false }),
+    },
+    [RELOAD_CARD]: {
+      next: (state, { payload }) => ({ ...state, isEditing: false }),
+    },
+
+    [RUN_QUERY]: state => ({ ...state, isRunning: true }),
+    [CANCEL_QUERY]: {
+      next: (state, { payload }) => ({ ...state, isRunning: false }),
+    },
+    [QUERY_COMPLETED]: {
+      next: (state, { payload }) => ({ ...state, isRunning: false }),
+    },
+    [QUERY_ERRORED]: {
+      next: (state, { payload }) => ({ ...state, isRunning: false }),
+    },
+  },
+  {
     isShowingDataReference: false,
     isShowingTemplateTagsEditor: false,
     isShowingTutorial: false,
     isShowingNewbModal: false,
     isEditing: false,
     isRunning: false,
-});
-
+  },
+);
 
 // the card that is actively being worked on
-export const card = handleActions({
+export const card = handleActions(
+  {
     [RESET_QB]: { next: (state, { payload }) => null },
-    [INITIALIZE_QB]: { next: (state, { payload }) => payload ? payload.card : null },
+    [INITIALIZE_QB]: {
+      next: (state, { payload }) => (payload ? payload.card : null),
+    },
     [RELOAD_CARD]: { next: (state, { payload }) => payload },
     [CANCEL_EDITING]: { next: (state, { payload }) => payload },
     [SET_CARD_AND_RUN]: { next: (state, { payload }) => payload.card },
     [API_CREATE_QUESTION]: { next: (state, { payload }) => payload },
     [API_UPDATE_QUESTION]: { next: (state, { payload }) => payload },
 
-    [SET_CARD_ATTRIBUTE]: { next: (state, { payload }) => ({...state, [payload.attr]: payload.value }) },
+    [SET_CARD_ATTRIBUTE]: {
+      next: (state, { payload }) => ({
+        ...state,
+        [payload.attr]: payload.value,
+      }),
+    },
     [SET_CARD_VISUALIZATION]: { next: (state, { payload }) => payload },
-    [UPDATE_CARD_VISUALIZATION_SETTINGS]: { next: (state, { payload }) => payload },
-    [REPLACE_ALL_CARD_VISUALIZATION_SETTINGS]: { next: (state, { payload }) => payload },
+    [UPDATE_CARD_VISUALIZATION_SETTINGS]: {
+      next: (state, { payload }) => payload,
+    },
+    [REPLACE_ALL_CARD_VISUALIZATION_SETTINGS]: {
+      next: (state, { payload }) => payload,
+    },
 
     [UPDATE_TEMPLATE_TAG]: { next: (state, { payload }) => payload },
 
@@ -96,76 +149,159 @@ export const card = handleActions({
     [SET_QUERY_DATABASE]: { next: (state, { payload }) => payload },
     [SET_QUERY_SOURCE_TABLE]: { next: (state, { payload }) => payload },
     [SET_DATASET_QUERY]: { next: (state, { payload }) => payload.card },
-    [UPDATE_QUESTION]: (state, { payload: {card} }) => card,
+    [UPDATE_QUESTION]: (state, { payload: { card } }) => card,
 
-    [QUERY_COMPLETED]: { next: (state, { payload }) => ({ ...state, display: payload.cardDisplay }) },
+    [QUERY_COMPLETED]: {
+      next: (state, { payload }) => ({
+        ...state,
+        display: payload.cardDisplay,
+      }),
+    },
 
-    [CREATE_PUBLIC_LINK]: { next: (state, { payload }) => ({ ...state, public_uuid: payload.uuid })},
-    [DELETE_PUBLIC_LINK]: { next: (state, { payload }) => ({ ...state, public_uuid: null })},
-    [UPDATE_ENABLE_EMBEDDING]: { next: (state, { payload }) => ({ ...state, enable_embedding: payload.enable_embedding })},
-    [UPDATE_EMBEDDING_PARAMS]: { next: (state, { payload }) => ({ ...state, embedding_params: payload.embedding_params })},
-}, null);
+    [CREATE_PUBLIC_LINK]: {
+      next: (state, { payload }) => ({ ...state, public_uuid: payload.uuid }),
+    },
+    [DELETE_PUBLIC_LINK]: {
+      next: (state, { payload }) => ({ ...state, public_uuid: null }),
+    },
+    [UPDATE_ENABLE_EMBEDDING]: {
+      next: (state, { payload }) => ({
+        ...state,
+        enable_embedding: payload.enable_embedding,
+      }),
+    },
+    [UPDATE_EMBEDDING_PARAMS]: {
+      next: (state, { payload }) => ({
+        ...state,
+        embedding_params: payload.embedding_params,
+      }),
+    },
+  },
+  null,
+);
 
 // a copy of the card being worked on at it's last known saved state.  if the card is NEW then this should be null.
 // NOTE: we use JSON serialization/deserialization to ensure a deep clone of the object which is required
 //       because we can't have any links between the active card being modified and the "originalCard" for testing dirtiness
 // ALSO: we consistently check for payload.id because an unsaved card has no "originalCard"
-export const originalCard = handleActions({
-    [INITIALIZE_QB]: { next: (state, { payload }) => payload.originalCard ? Utils.copy(payload.originalCard) : null },
-    [RELOAD_CARD]: { next: (state, { payload }) => payload.id ? Utils.copy(payload) : null },
-    [CANCEL_EDITING]: { next: (state, { payload }) => payload.id ? Utils.copy(payload) : null },
-    [SET_CARD_AND_RUN]: { next: (state, { payload }) => payload.originalCard ? Utils.copy(payload.originalCard) : null },
-    [API_CREATE_QUESTION]: { next: (state, { payload }) => Utils.copy(payload) },
-    [API_UPDATE_QUESTION]: { next: (state, { payload }) => Utils.copy(payload) },
-}, null);
-
-export const tableForeignKeys = handleActions({
+export const originalCard = handleActions(
+  {
+    [INITIALIZE_QB]: {
+      next: (state, { payload }) =>
+        payload.originalCard ? Utils.copy(payload.originalCard) : null,
+    },
+    [RELOAD_CARD]: {
+      next: (state, { payload }) => (payload.id ? Utils.copy(payload) : null),
+    },
+    [CANCEL_EDITING]: {
+      next: (state, { payload }) => (payload.id ? Utils.copy(payload) : null),
+    },
+    [SET_CARD_AND_RUN]: {
+      next: (state, { payload }) =>
+        payload.originalCard ? Utils.copy(payload.originalCard) : null,
+    },
+    [API_CREATE_QUESTION]: {
+      next: (state, { payload }) => Utils.copy(payload),
+    },
+    [API_UPDATE_QUESTION]: {
+      next: (state, { payload }) => Utils.copy(payload),
+    },
+  },
+  null,
+);
+
+export const tableForeignKeys = handleActions(
+  {
     [RESET_QB]: { next: (state, { payload }) => null },
-    [LOAD_TABLE_METADATA]: { next: (state, { payload }) => payload && payload.foreignKeys ? payload.foreignKeys : state }
-}, null);
+    [LOAD_TABLE_METADATA]: {
+      next: (state, { payload }) =>
+        payload && payload.foreignKeys ? payload.foreignKeys : state,
+    },
+  },
+  null,
+);
 
-export const databaseFields = handleActions({
-    [LOAD_DATABASE_FIELDS]: { next: (state, { payload }) => ({ [payload.id]: payload.fields }) }
-}, {});
+export const databaseFields = handleActions(
+  {
+    [LOAD_DATABASE_FIELDS]: {
+      next: (state, { payload }) => ({ [payload.id]: payload.fields }),
+    },
+  },
+  {},
+);
 
 // references to FK tables specifically used on the ObjectDetail page.
-export const tableForeignKeyReferences = handleActions({
-    [LOAD_OBJECT_DETAIL_FK_REFERENCES]: { next: (state, { payload }) => payload}
-}, null);
+export const tableForeignKeyReferences = handleActions(
+  {
+    [LOAD_OBJECT_DETAIL_FK_REFERENCES]: {
+      next: (state, { payload }) => payload,
+    },
+  },
+  null,
+);
 
-export const lastRunCard = handleActions({
+export const lastRunCard = handleActions(
+  {
     [RESET_QB]: { next: (state, { payload }) => null },
     [QUERY_COMPLETED]: { next: (state, { payload }) => payload.card },
     [QUERY_ERRORED]: { next: (state, { payload }) => null },
-}, null);
+  },
+  null,
+);
 
 // NOTE Atte Keinänen 6/1/17: DEPRECATED, you should use queryResults instead
-export const queryResult = handleActions({
+export const queryResult = handleActions(
+  {
     [RESET_QB]: { next: (state, { payload }) => null },
-    [QUERY_COMPLETED]: { next: (state, { payload }) => payload.queryResults[0] },
-    [QUERY_ERRORED]: { next: (state, { payload }) => payload ? payload : state }
-}, null);
+    [QUERY_COMPLETED]: {
+      next: (state, { payload }) => payload.queryResults[0],
+    },
+    [QUERY_ERRORED]: {
+      next: (state, { payload }) => (payload ? payload : state),
+    },
+  },
+  null,
+);
 
 // The results of a query execution.  optionally an error if the query fails to complete successfully.
-export const queryResults = handleActions({
+export const queryResults = handleActions(
+  {
     [RESET_QB]: { next: (state, { payload }) => null },
     [QUERY_COMPLETED]: { next: (state, { payload }) => payload.queryResults },
-    [QUERY_ERRORED]: { next: (state, { payload }) => payload ? payload : state }
-}, null);
+    [QUERY_ERRORED]: {
+      next: (state, { payload }) => (payload ? payload : state),
+    },
+  },
+  null,
+);
 
 // promise used for tracking a query execution in progress.  when a query is started we capture this.
-export const cancelQueryDeferred = handleActions({
-    [RUN_QUERY]: { next: (state, { payload: { cancelQueryDeferred } }) => cancelQueryDeferred},
-    [CANCEL_QUERY]: { next: (state, { payload }) => null},
-    [QUERY_COMPLETED]: { next: (state, { payload }) => null},
-    [QUERY_ERRORED]: { next: (state, { payload }) => null},
-}, null);
-
-export const parameterValues = handleActions({
-    [SET_PARAMETER_VALUE]: { next: (state, { payload: { id, value }}) => value == null ? dissoc(state, id) : assoc(state, id, value) }
-}, {});
+export const cancelQueryDeferred = handleActions(
+  {
+    [RUN_QUERY]: {
+      next: (state, { payload: { cancelQueryDeferred } }) =>
+        cancelQueryDeferred,
+    },
+    [CANCEL_QUERY]: { next: (state, { payload }) => null },
+    [QUERY_COMPLETED]: { next: (state, { payload }) => null },
+    [QUERY_ERRORED]: { next: (state, { payload }) => null },
+  },
+  null,
+);
 
-export const currentState = handleActions({
-    [SET_CURRENT_STATE]: { next: (state, { payload }) => payload }
-}, null);
+export const parameterValues = handleActions(
+  {
+    [SET_PARAMETER_VALUE]: {
+      next: (state, { payload: { id, value } }) =>
+        value == null ? dissoc(state, id) : assoc(state, id, value),
+    },
+  },
+  {},
+);
 
+export const currentState = handleActions(
+  {
+    [SET_CURRENT_STATE]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
diff --git a/frontend/src/metabase/query_builder/selectors.js b/frontend/src/metabase/query_builder/selectors.js
index af6786662abcce7064c54958105d489b02c0537d..56b2c9c0ced40bf26acc91cca52c7b5778664279 100644
--- a/frontend/src/metabase/query_builder/selectors.js
+++ b/frontend/src/metabase/query_builder/selectors.js
@@ -1,4 +1,3 @@
-
 import { createSelector } from "reselect";
 import _ from "underscore";
 
@@ -21,194 +20,235 @@ import { getMetadata, getDatabasesList } from "metabase/selectors/metadata";
 
 export const getUiControls = state => state.qb.uiControls;
 
-export const getIsShowingTemplateTagsEditor = state => getUiControls(state).isShowingTemplateTagsEditor;
-export const getIsShowingDataReference = state => getUiControls(state).isShowingDataReference;
-export const getIsShowingTutorial = state => getUiControls(state).isShowingTutorial;
+export const getIsShowingTemplateTagsEditor = state =>
+  getUiControls(state).isShowingTemplateTagsEditor;
+export const getIsShowingDataReference = state =>
+  getUiControls(state).isShowingDataReference;
+export const getIsShowingTutorial = state =>
+  getUiControls(state).isShowingTutorial;
 export const getIsEditing = state => getUiControls(state).isEditing;
 export const getIsRunning = state => getUiControls(state).isRunning;
 
-export const getCard            = state => state.qb.card;
-export const getOriginalCard    = state => state.qb.originalCard;
-export const getLastRunCard     = state => state.qb.lastRunCard;
+export const getCard = state => state.qb.card;
+export const getOriginalCard = state => state.qb.originalCard;
+export const getLastRunCard = state => state.qb.lastRunCard;
 
 export const getParameterValues = state => state.qb.parameterValues;
-export const getQueryResult     = state => state.qb.queryResult;
-export const getQueryResults    = state => state.qb.queryResults;
+export const getQueryResult = state => state.qb.queryResult;
+export const getQueryResults = state => state.qb.queryResults;
 
 // get instance settings, used for determining whether to display certain actions,
 // currently used only for xrays
-export const getSettings        = state => state.settings.values
+export const getSettings = state => state.settings.values;
 
 export const getIsDirty = createSelector(
-    [getCard, getOriginalCard],
-    (card, originalCard) => {
-        return isCardDirty(card, originalCard);
-    }
+  [getCard, getOriginalCard],
+  (card, originalCard) => {
+    return isCardDirty(card, originalCard);
+  },
 );
 
-export const getIsNew = (state) => state.qb.card && !state.qb.card.id;
+export const getIsNew = state => state.qb.card && !state.qb.card.id;
 
 export const getDatabaseId = createSelector(
-    [getCard],
-    (card) => card && card.dataset_query && card.dataset_query.database
+  [getCard],
+  card => card && card.dataset_query && card.dataset_query.database,
 );
 
-export const getTableId = createSelector(
-    [getCard],
-    (card) => getIn(card, ["dataset_query", "query", "source_table"])
+export const getTableId = createSelector([getCard], card =>
+  getIn(card, ["dataset_query", "query", "source_table"]),
 );
 
-export const getTableForeignKeys          = state => state.qb.tableForeignKeys;
-export const getTableForeignKeyReferences = state => state.qb.tableForeignKeyReferences;
+export const getTableForeignKeys = state => state.qb.tableForeignKeys;
+export const getTableForeignKeyReferences = state =>
+  state.qb.tableForeignKeyReferences;
 
 export const getTables = createSelector(
-    [getDatabaseId, getDatabasesList],
-    (databaseId, databases) => {
-        if (databaseId != null && databases && databases.length > 0) {
-            let db = _.findWhere(databases, { id: databaseId });
-            if (db && db.tables) {
-                return db.tables;
-            }
-        }
-
-        return [];
+  [getDatabaseId, getDatabasesList],
+  (databaseId, databases) => {
+    if (databaseId != null && databases && databases.length > 0) {
+      let db = _.findWhere(databases, { id: databaseId });
+      if (db && db.tables) {
+        return db.tables;
+      }
     }
+
+    return [];
+  },
 );
 
 export const getNativeDatabases = createSelector(
-    [getDatabasesList],
-    (databases) =>
-        databases && databases.filter(db => db.native_permissions === "write")
-)
+  [getDatabasesList],
+  databases =>
+    databases && databases.filter(db => db.native_permissions === "write"),
+);
 
 export const getTableMetadata = createSelector(
-    [getTableId, getMetadata],
-    (tableId, metadata) => metadata.tables[tableId]
-)
+  [getTableId, getMetadata],
+  (tableId, metadata) => metadata.tables[tableId],
+);
 
 export const getSampleDatasetId = createSelector(
-    [getDatabasesList],
-    (databases) => {
-        const sampleDataset = _.findWhere(databases, { is_sample: true });
-        return sampleDataset && sampleDataset.id;
-    }
-)
+  [getDatabasesList],
+  databases => {
+    const sampleDataset = _.findWhere(databases, { is_sample: true });
+    return sampleDataset && sampleDataset.id;
+  },
+);
 
 export const getDatabaseFields = createSelector(
-    [getDatabaseId, state => state.qb.databaseFields],
-    (databaseId, databaseFields) => databaseFields[databaseId]
+  [getDatabaseId, state => state.qb.databaseFields],
+  (databaseId, databaseFields) => databaseFields[databaseId],
 );
 
-
-
 import { getMode as getMode_ } from "metabase/qb/lib/modes";
 import { getAlerts } from "metabase/alert/selectors";
-import { extractRemappings, getVisualizationTransformed } from "metabase/visualizations";
+import {
+  extractRemappings,
+  getVisualizationTransformed,
+} from "metabase/visualizations";
 
 export const getMode = createSelector(
-    [getLastRunCard, getTableMetadata],
-    (card, tableMetadata) => getMode_(card, tableMetadata)
-)
+  [getLastRunCard, getTableMetadata],
+  (card, tableMetadata) => getMode_(card, tableMetadata),
+);
 
 export const getIsObjectDetail = createSelector(
-    [getMode],
-    (mode) => mode && mode.name === "object"
+  [getMode],
+  mode => mode && mode.name === "object",
 );
 
 export const getParameters = createSelector(
-    [getCard, getParameterValues],
-    (card, parameterValues) => getParametersWithExtras(card, parameterValues)
+  [getCard, getParameterValues],
+  (card, parameterValues) => getParametersWithExtras(card, parameterValues),
 );
 
-const getLastRunDatasetQuery = createSelector([getLastRunCard], (card) => card && card.dataset_query);
-const getNextRunDatasetQuery = createSelector([getCard], (card) => card && card.dataset_query);
+const getLastRunDatasetQuery = createSelector(
+  [getLastRunCard],
+  card => card && card.dataset_query,
+);
+const getNextRunDatasetQuery = createSelector(
+  [getCard],
+  card => card && card.dataset_query,
+);
 
-const getLastRunParameters = createSelector([getQueryResult], (queryResult) => queryResult && queryResult.json_query && queryResult.json_query.parameters || []);
-const getLastRunParameterValues = createSelector([getLastRunParameters], (parameters) => parameters.map(parameter => parameter.value));
-const getNextRunParameterValues = createSelector([getParameters], (parameters) =>
-    parameters.map(parameter => parameter.value).filter(p => p !== undefined)
+const getLastRunParameters = createSelector(
+  [getQueryResult],
+  queryResult =>
+    (queryResult &&
+      queryResult.json_query &&
+      queryResult.json_query.parameters) ||
+    [],
+);
+const getLastRunParameterValues = createSelector(
+  [getLastRunParameters],
+  parameters => parameters.map(parameter => parameter.value),
+);
+const getNextRunParameterValues = createSelector([getParameters], parameters =>
+  parameters.map(parameter => parameter.value).filter(p => p !== undefined),
 );
 
 export const getIsResultDirty = createSelector(
-    [getLastRunDatasetQuery, getNextRunDatasetQuery, getLastRunParameterValues, getNextRunParameterValues],
-    (lastDatasetQuery, nextDatasetQuery, lastParameters, nextParameters) => {
-        return !Utils.equals(lastDatasetQuery, nextDatasetQuery) || !Utils.equals(lastParameters, nextParameters);
-    }
-)
+  [
+    getLastRunDatasetQuery,
+    getNextRunDatasetQuery,
+    getLastRunParameterValues,
+    getNextRunParameterValues,
+  ],
+  (lastDatasetQuery, nextDatasetQuery, lastParameters, nextParameters) => {
+    return (
+      !Utils.equals(lastDatasetQuery, nextDatasetQuery) ||
+      !Utils.equals(lastParameters, nextParameters)
+    );
+  },
+);
 
 export const getQuestion = createSelector(
-    [getMetadata, getCard, getParameterValues],
-    (metadata, card, parameterValues) => {
-        return metadata && card && new Question(metadata, card, parameterValues)
-    }
-)
+  [getMetadata, getCard, getParameterValues],
+  (metadata, card, parameterValues) => {
+    return metadata && card && new Question(metadata, card, parameterValues);
+  },
+);
 
 export const getLastRunQuestion = createSelector(
-    [getMetadata, getLastRunCard, getParameterValues],
-    (metadata, getLastRunCard, parameterValues) => {
-        return metadata && getLastRunCard && new Question(metadata, getLastRunCard, parameterValues)
-    }
-)
+  [getMetadata, getLastRunCard, getParameterValues],
+  (metadata, getLastRunCard, parameterValues) => {
+    return (
+      metadata &&
+      getLastRunCard &&
+      new Question(metadata, getLastRunCard, parameterValues)
+    );
+  },
+);
 
 export const getOriginalQuestion = createSelector(
-    [getMetadata, getOriginalCard],
-    (metadata, card) => {
-        // NOTE Atte Keinänen 5/31/17 Should the originalQuestion object take parameterValues or not? (currently not)
-        return metadata && card && new Question(metadata, card)
-    }
-)
+  [getMetadata, getOriginalCard],
+  (metadata, card) => {
+    // NOTE Atte Keinänen 5/31/17 Should the originalQuestion object take parameterValues or not? (currently not)
+    return metadata && card && new Question(metadata, card);
+  },
+);
 
 export const getQuery = createSelector(
-    [getQuestion],
-    (question) => question && question.query()
-)
+  [getQuestion],
+  question => question && question.query(),
+);
 
-export const getIsRunnable = createSelector([getQuestion], (question) => question && question.canRun())
+export const getIsRunnable = createSelector(
+  [getQuestion],
+  question => question && question.canRun(),
+);
 
 export const getQuestionAlerts = createSelector(
-    [getAlerts, getCard],
-    (alerts, card) => card && card.id && _.pick(alerts, (alert) => alert.card.id === card.id) || {}
-)
+  [getAlerts, getCard],
+  (alerts, card) =>
+    (card && card.id && _.pick(alerts, alert => alert.card.id === card.id)) ||
+    {},
+);
 
 export const getResultsMetadata = createSelector(
-    [getQueryResult],
-    (result) => result && result.data && result.data.results_metadata
-)
+  [getQueryResult],
+  result => result && result.data && result.data.results_metadata,
+);
 
 /**
  * Returns the card and query results data in a format that `Visualization.jsx` expects
  */
 export const getRawSeries = createSelector(
-    [getQuestion, getQueryResults, getIsObjectDetail, getLastRunDatasetQuery],
-    (question, results, isObjectDetail, lastRunDatasetQuery) => {
-        // we want to provide the visualization with a card containing the latest
-        // "display", "visualization_settings", etc, (to ensure the correct visualization is shown)
-        // BUT the last executed "dataset_query" (to ensure data matches the query)
-        return results && question.atomicQueries().map((metricQuery, index) => ({
-            card: {
-                ...question.card(),
-                display: isObjectDetail ? "object" : question.card().display,
-                dataset_query: lastRunDatasetQuery
-            },
-            data: results[index] && results[index].data
-        }))
-    }
-)
+  [getQuestion, getQueryResults, getIsObjectDetail, getLastRunDatasetQuery],
+  (question, results, isObjectDetail, lastRunDatasetQuery) => {
+    // we want to provide the visualization with a card containing the latest
+    // "display", "visualization_settings", etc, (to ensure the correct visualization is shown)
+    // BUT the last executed "dataset_query" (to ensure data matches the query)
+    return (
+      results &&
+      question.atomicQueries().map((metricQuery, index) => ({
+        card: {
+          ...question.card(),
+          display: isObjectDetail ? "object" : question.card().display,
+          dataset_query: lastRunDatasetQuery,
+        },
+        data: results[index] && results[index].data,
+      }))
+    );
+  },
+);
 
 /**
  * Returns the final series data that all visualization (starting from the root-level
  * `Visualization.jsx` component) code uses for rendering visualizations.
  */
 export const getTransformedSeries = createSelector(
-    [getRawSeries],
-    (rawSeries) => rawSeries && getVisualizationTransformed(extractRemappings(rawSeries)).series
-)
+  [getRawSeries],
+  rawSeries =>
+    rawSeries &&
+    getVisualizationTransformed(extractRemappings(rawSeries)).series,
+);
 
 /**
  * Returns complete visualization settings (including default values for those settings which aren't explicitly set)
  */
 export const getVisualizationSettings = createSelector(
-    [getTransformedSeries],
-    (series) => series && _getVisualizationSettings(series)
-)
-
+  [getTransformedSeries],
+  series => series && _getVisualizationSettings(series),
+);
diff --git a/frontend/src/metabase/questions/Questions.css b/frontend/src/metabase/questions/Questions.css
index ba659584e89e03675958b74b795e8226bf622644..3403e5da1cbee3a35d40a05cd978a90820f4bbad 100644
--- a/frontend/src/metabase/questions/Questions.css
+++ b/frontend/src/metabase/questions/Questions.css
@@ -1,12 +1,12 @@
 :root {
-    --title-color: #606E7B;
-    --subtitle-color: #AAB7C3;
-    --muted-color: #DEEAF1;
-    --blue-color: #2D86D4;
+  --title-color: #606e7b;
+  --subtitle-color: #aab7c3;
+  --muted-color: #deeaf1;
+  --blue-color: #2d86d4;
 }
 
 :local(.header) {
-    composes: mt4 mb2 from "style";
-    color: var(--title-color);
-    font-size: 24px;
+  composes: mt4 mb2 from "style";
+  color: var(--title-color);
+  font-size: 24px;
 }
diff --git a/frontend/src/metabase/questions/collections.js b/frontend/src/metabase/questions/collections.js
index faa4142886464e036789768f8888776325b583b6..c07c892932acc3c0f12a87cc56691e0b8322b29b 100644
--- a/frontend/src/metabase/questions/collections.js
+++ b/frontend/src/metabase/questions/collections.js
@@ -1,6 +1,10 @@
-
-import { createAction, createThunkAction, handleActions, combineReducers } from "metabase/lib/redux";
-import { reset } from 'redux-form';
+import {
+  createAction,
+  createThunkAction,
+  handleActions,
+  combineReducers,
+} from "metabase/lib/redux";
+import { reset } from "redux-form";
 import { replace } from "react-router-redux";
 
 import _ from "underscore";
@@ -9,96 +13,123 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 
 import { CollectionsApi } from "metabase/services";
 
-export const LOAD_COLLECTION = 'metabase/collections/LOAD_COLLECTION';
-export const LOAD_COLLECTIONS = 'metabase/collections/LOAD_COLLECTIONS';
-export const SAVE_COLLECTION = 'metabase/collections/SAVE_COLLECTION';
-export const DELETE_COLLECTION = 'metabase/collections/DELETE_COLLECTION';
-export const SET_COLLECTION_ARCHIVED = 'metabase/collections/SET_COLLECTION_ARCHIVED';
+export const LOAD_COLLECTION = "metabase/collections/LOAD_COLLECTION";
+export const LOAD_COLLECTIONS = "metabase/collections/LOAD_COLLECTIONS";
+export const SAVE_COLLECTION = "metabase/collections/SAVE_COLLECTION";
+export const DELETE_COLLECTION = "metabase/collections/DELETE_COLLECTION";
+export const SET_COLLECTION_ARCHIVED =
+  "metabase/collections/SET_COLLECTION_ARCHIVED";
 
-export const loadCollection = createAction(LOAD_COLLECTION, (id) => CollectionsApi.get({ id }));
-export const loadCollections = createAction(LOAD_COLLECTIONS, CollectionsApi.list);
+export const loadCollection = createAction(LOAD_COLLECTION, id =>
+  CollectionsApi.get({ id }),
+);
+export const loadCollections = createAction(
+  LOAD_COLLECTIONS,
+  CollectionsApi.list,
+);
 
-export const saveCollection = createThunkAction(SAVE_COLLECTION, (collection) => {
-    return async (dispatch, getState) => {
-        try {
-            if (!collection.description) {
-                // description must be nil or non empty string
-                collection = { ...collection, description: null }
-            }
-            let response;
-            if (collection.id == null) {
-                MetabaseAnalytics.trackEvent("Collections", "Create");
-                response = await CollectionsApi.create(collection);
-            } else {
-                MetabaseAnalytics.trackEvent("Collections", "Update");
-                response = await CollectionsApi.update(collection);
-            }
-            if (response.id != null) {
-                dispatch(reset("collection"));
-                // use `replace` so form url doesn't appear in history
-                dispatch(replace('/questions/'));
-            }
-            return response;
-        } catch (e) {
-            // redux-form expects an object with either { field: error } or { _error: error }
-            if (e.data && e.data.errors) {
-                throw e.data.errors;
-            } else if (e.data && e.data.message) {
-                throw { _error: e.data.message };
-            } else {
-                throw { _error: "An unknown error occured" };
-            }
-        }
+export const saveCollection = createThunkAction(SAVE_COLLECTION, collection => {
+  return async (dispatch, getState) => {
+    try {
+      if (!collection.description) {
+        // description must be nil or non empty string
+        collection = { ...collection, description: null };
+      }
+      let response;
+      if (collection.id == null) {
+        MetabaseAnalytics.trackEvent("Collections", "Create");
+        response = await CollectionsApi.create(collection);
+      } else {
+        MetabaseAnalytics.trackEvent("Collections", "Update");
+        response = await CollectionsApi.update(collection);
+      }
+      if (response.id != null) {
+        dispatch(reset("collection"));
+        // use `replace` so form url doesn't appear in history
+        dispatch(replace("/questions/"));
+      }
+      return response;
+    } catch (e) {
+      // redux-form expects an object with either { field: error } or { _error: error }
+      if (e.data && e.data.errors) {
+        throw e.data.errors;
+      } else if (e.data && e.data.message) {
+        throw { _error: e.data.message };
+      } else {
+        throw { _error: "An unknown error occured" };
+      }
     }
+  };
 });
 
-export const setCollectionArchived = createThunkAction(SET_COLLECTION_ARCHIVED, (id, archived) =>
-    async (dispatch, getState) => {
-        MetabaseAnalytics.trackEvent("Collections", "Set Archived", archived);
-        // HACK: currently the only way to archive/unarchive a collection is to PUT it along with name/description/color, so grab it from the list
-        const collection = _.findWhere(await CollectionsApi.list({ archived: !archived }), { id });
-        return await CollectionsApi.update({ ...collection, archived: archived });
-    }
+export const setCollectionArchived = createThunkAction(
+  SET_COLLECTION_ARCHIVED,
+  (id, archived) => async (dispatch, getState) => {
+    MetabaseAnalytics.trackEvent("Collections", "Set Archived", archived);
+    // HACK: currently the only way to archive/unarchive a collection is to PUT it along with name/description/color, so grab it from the list
+    const collection = _.findWhere(
+      await CollectionsApi.list({ archived: !archived }),
+      { id },
+    );
+    return await CollectionsApi.update({ ...collection, archived: archived });
+  },
 );
 
-export const deleteCollection = createThunkAction(DELETE_COLLECTION, (id) =>
-    async (dispatch, getState) => {
-        try {
-            MetabaseAnalytics.trackEvent("Collections", "Delete");
-            await CollectionsApi.delete({ id });
-            return id;
-        } catch (e) {
-            // TODO: handle error
-            return null;
-        }
+export const deleteCollection = createThunkAction(
+  DELETE_COLLECTION,
+  id => async (dispatch, getState) => {
+    try {
+      MetabaseAnalytics.trackEvent("Collections", "Delete");
+      await CollectionsApi.delete({ id });
+      return id;
+    } catch (e) {
+      // TODO: handle error
+      return null;
     }
+  },
 );
 
-const collections = handleActions({
-    [LOAD_COLLECTIONS]:  { next: (state, { payload }) => payload },
-    [SAVE_COLLECTION]:   { next: (state, { payload }) => state.filter(c => c.id !== payload.id).concat(payload) },
-    [DELETE_COLLECTION]: { next: (state, { payload }) => state.filter(c => c.id !== payload) },
-    [SET_COLLECTION_ARCHIVED]: { next: (state, { payload }) => state.filter(c => c.id !== payload.id) }
-}, []);
+const collections = handleActions(
+  {
+    [LOAD_COLLECTIONS]: { next: (state, { payload }) => payload },
+    [SAVE_COLLECTION]: {
+      next: (state, { payload }) =>
+        state.filter(c => c.id !== payload.id).concat(payload),
+    },
+    [DELETE_COLLECTION]: {
+      next: (state, { payload }) => state.filter(c => c.id !== payload),
+    },
+    [SET_COLLECTION_ARCHIVED]: {
+      next: (state, { payload }) => state.filter(c => c.id !== payload.id),
+    },
+  },
+  [],
+);
 
-const error = handleActions({
+const error = handleActions(
+  {
     [SAVE_COLLECTION]: {
-        next: (state) => null,
-        throw: (state, { error }) => error
-    }
-}, null);
+      next: state => null,
+      throw: (state, { error }) => error,
+    },
+  },
+  null,
+);
 
-const collection = handleActions({
+const collection = handleActions(
+  {
     [LOAD_COLLECTION]: {
-        next: (state, { payload }) => payload
+      next: (state, { payload }) => payload,
     },
     [SAVE_COLLECTION]: {
-        next: (state, { payload }) => payload
-    }
-}, null);
+      next: (state, { payload }) => payload,
+    },
+  },
+  null,
+);
 
 export default combineReducers({
-    collection,
-    collections,
-    error
+  collection,
+  collections,
+  error,
 });
diff --git a/frontend/src/metabase/questions/components/ActionHeader.css b/frontend/src/metabase/questions/components/ActionHeader.css
index ab0909c39fce44aee7506b975a50a5db4daf18f4..67592dcc9039895494b91019ffa60317b369de26 100644
--- a/frontend/src/metabase/questions/components/ActionHeader.css
+++ b/frontend/src/metabase/questions/components/ActionHeader.css
@@ -1,36 +1,36 @@
-@import '../Questions.css';
+@import "../Questions.css";
 
 :local(.actionHeader) {
-    composes: full flex align-center from "style";
-    line-height: 1;
-    font-size: 14px;
-    color: var(--brand-color);
+  composes: full flex align-center from "style";
+  line-height: 1;
+  font-size: 14px;
+  color: var(--brand-color);
 }
 
 :local(.selectedCount) {
-    composes: mx4 from "style";
+  composes: mx4 from "style";
 }
 
 /* ALL CHECKBOX */
 :local(.allCheckbox) {
-    composes: icon from "./List.css";
-    margin-left: 10px;
-    visibility: visible !important;
+  composes: icon from "./List.css";
+  margin-left: 10px;
+  visibility: visible !important;
 }
 
 :local(.selected) {
-    color: var(--blue-color) !important;
+  color: var(--blue-color) !important;
 }
 
 :local(.actionButton) {
-    composes: px1 cursor-pointer text-bold flex align-center from "style";
+  composes: px1 cursor-pointer text-bold flex align-center from "style";
 }
 
 :local(.actionButton) .Icon {
-    padding-left: 0.25em;
-    padding-right: 0.25em;
+  padding-left: 0.25em;
+  padding-right: 0.25em;
 }
 
 :local(.actionButton) .Icon-chevrondown {
-    color: #DEEAF1;
+  color: #deeaf1;
 }
diff --git a/frontend/src/metabase/questions/components/ActionHeader.jsx b/frontend/src/metabase/questions/components/ActionHeader.jsx
index 7f550909ac44d2cb734c62c3e772c0af00d85f11..b66b252e404ba615758151b546d91afcfc9d14a2 100644
--- a/frontend/src/metabase/questions/components/ActionHeader.jsx
+++ b/frontend/src/metabase/questions/components/ActionHeader.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "./ActionHeader.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import StackedCheckBox from "metabase/components/StackedCheckBox.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
@@ -11,61 +11,77 @@ import MoveToCollection from "../containers/MoveToCollection.jsx";
 
 import LabelPopover from "../containers/LabelPopover.jsx";
 
-const ActionHeader = ({ visibleCount, selectedCount, allAreSelected, sectionIsArchive, setAllSelected, setArchived, labels }) =>
-    <div className={S.actionHeader}>
-        <Tooltip tooltip={t`Select all ${visibleCount}`} isEnabled={!allAreSelected}>
-            <span className="ml1">
-                <StackedCheckBox
-                    checked={allAreSelected}
-                    onChange={e => setAllSelected(e.target.checked)}
-                    size={20}
-                    padding={3}
-                />
+const ActionHeader = ({
+  visibleCount,
+  selectedCount,
+  allAreSelected,
+  sectionIsArchive,
+  setAllSelected,
+  setArchived,
+  labels,
+}) => (
+  <div className={S.actionHeader}>
+    <Tooltip
+      tooltip={t`Select all ${visibleCount}`}
+      isEnabled={!allAreSelected}
+    >
+      <span className="ml1">
+        <StackedCheckBox
+          checked={allAreSelected}
+          onChange={e => setAllSelected(e.target.checked)}
+          size={20}
+          padding={3}
+        />
+      </span>
+    </Tooltip>
+    <span className={S.selectedCount}>{t`${selectedCount} selected`}</span>
+    <span className="flex align-center flex-align-right">
+      {!sectionIsArchive && labels.length > 0 ? (
+        <LabelPopover
+          triggerElement={
+            <span className={S.actionButton}>
+              <Icon name="label" />
+              {t`Labels`}
+              <Icon name="chevrondown" size={12} />
             </span>
-        </Tooltip>
-        <span className={S.selectedCount}>
-            {t`${selectedCount} selected`}
-        </span>
-        <span className="flex align-center flex-align-right">
-            { !sectionIsArchive && labels.length > 0 ?
-                <LabelPopover
-                    triggerElement={
-                        <span className={S.actionButton}>
-                            <Icon name="label" />
-                            {t`Labels`}
-                            <Icon name="chevrondown" size={12} />
-                        </span>
-                    }
-                    labels={labels}
-                    count={selectedCount}
-                />
-            : null }
-            <ModalWithTrigger
-                full
-                triggerElement={
-                    <span className={S.actionButton} >
-                        <Icon name="move" className="mr1" />
-                        {t`Move`}
-                    </span>
-                }
-            >
-                <MoveToCollection />
-            </ModalWithTrigger>
-            <span className={S.actionButton} onClick={() => setArchived(undefined, !sectionIsArchive, true)}>
-                <Icon name={ sectionIsArchive ? "unarchive" : "archive" }  className="mr1" />
-                { sectionIsArchive ? t`Unarchive` : t`Archive` }
-            </span>
-        </span>
-    </div>
+          }
+          labels={labels}
+          count={selectedCount}
+        />
+      ) : null}
+      <ModalWithTrigger
+        full
+        triggerElement={
+          <span className={S.actionButton}>
+            <Icon name="move" className="mr1" />
+            {t`Move`}
+          </span>
+        }
+      >
+        <MoveToCollection />
+      </ModalWithTrigger>
+      <span
+        className={S.actionButton}
+        onClick={() => setArchived(undefined, !sectionIsArchive, true)}
+      >
+        <Icon
+          name={sectionIsArchive ? "unarchive" : "archive"}
+          className="mr1"
+        />
+        {sectionIsArchive ? t`Unarchive` : t`Archive`}
+      </span>
+    </span>
+  </div>
+);
 
 ActionHeader.propTypes = {
-    labels:             PropTypes.array.isRequired,
-    visibleCount:       PropTypes.number.isRequired,
-    selectedCount:      PropTypes.number.isRequired,
-    allAreSelected:     PropTypes.bool.isRequired,
-    sectionIsArchive:   PropTypes.bool.isRequired,
-    setAllSelected:     PropTypes.func.isRequired,
-    setArchived:        PropTypes.func.isRequired,
+  labels: PropTypes.array.isRequired,
+  visibleCount: PropTypes.number.isRequired,
+  selectedCount: PropTypes.number.isRequired,
+  allAreSelected: PropTypes.bool.isRequired,
+  sectionIsArchive: PropTypes.bool.isRequired,
+  setAllSelected: PropTypes.func.isRequired,
+  setArchived: PropTypes.func.isRequired,
 };
 
 export default ActionHeader;
diff --git a/frontend/src/metabase/questions/components/CollectionActions.jsx b/frontend/src/metabase/questions/components/CollectionActions.jsx
index 27ff146af85c162cd3507931510ff938b7872f27..0a05aff400a3b55105faf132e4fad15afd455199 100644
--- a/frontend/src/metabase/questions/components/CollectionActions.jsx
+++ b/frontend/src/metabase/questions/components/CollectionActions.jsx
@@ -1,12 +1,19 @@
 import React from "react";
 
-const CollectionActions = ({ children }) =>
-    <div className="flex align-center" onClick={(e) => { e.stopPropagation(); e.preventDefault() }}>
-        {React.Children.map(children, (child, index) =>
-            <div key={index} className="cursor-pointer text-brand-hover mx1">
-                {child}
-            </div>
-        )}
-    </div>
+const CollectionActions = ({ children }) => (
+  <div
+    className="flex align-center"
+    onClick={e => {
+      e.stopPropagation();
+      e.preventDefault();
+    }}
+  >
+    {React.Children.map(children, (child, index) => (
+      <div key={index} className="cursor-pointer text-brand-hover mx1">
+        {child}
+      </div>
+    ))}
+  </div>
+);
 
 export default CollectionActions;
diff --git a/frontend/src/metabase/questions/components/CollectionBadge.jsx b/frontend/src/metabase/questions/components/CollectionBadge.jsx
index 42711f9f18a02d4dde90ea27c39e2d8c22467fbe..b124cadc51001c9df33a69c801eabd03630dfc48 100644
--- a/frontend/src/metabase/questions/components/CollectionBadge.jsx
+++ b/frontend/src/metabase/questions/components/CollectionBadge.jsx
@@ -6,17 +6,22 @@ import * as Urls from "metabase/lib/urls";
 import Color from "color";
 import cx from "classnames";
 
-const CollectionBadge = ({ className, collection }) =>
-    <Link
-        to={Urls.collection(collection)}
-        className={cx(className, "flex align-center px1 rounded mx1")}
-        style={{
-            fontSize: 14,
-            color: Color(collection.color).darken(0.1).hex(),
-            backgroundColor: Color(collection.color).lighten(0.4).hex()
-        }}
-    >
-        {collection.name}
-    </Link>
+const CollectionBadge = ({ className, collection }) => (
+  <Link
+    to={Urls.collection(collection)}
+    className={cx(className, "flex align-center px1 rounded mx1")}
+    style={{
+      fontSize: 14,
+      color: Color(collection.color)
+        .darken(0.1)
+        .hex(),
+      backgroundColor: Color(collection.color)
+        .lighten(0.4)
+        .hex(),
+    }}
+  >
+    {collection.name}
+  </Link>
+);
 
 export default CollectionBadge;
diff --git a/frontend/src/metabase/questions/components/CollectionButtons.jsx b/frontend/src/metabase/questions/components/CollectionButtons.jsx
index a445baf6e600d1844e84e0c24b89554fe79ea637..2bc192b60de2e78ec1be7ad57e4dac08fd210d12 100644
--- a/frontend/src/metabase/questions/components/CollectionButtons.jsx
+++ b/frontend/src/metabase/questions/components/CollectionButtons.jsx
@@ -1,96 +1,108 @@
 import React, { Component } from "react";
 import { Link } from "react-router";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon";
 import ArchiveCollectionWidget from "../containers/ArchiveCollectionWidget";
 
 const COLLECTION_ICON_SIZE = 64;
 
-const COLLECTION_BOX_CLASSES = "relative block p4 hover-parent hover--visibility cursor-pointer text-centered transition-background";
+const COLLECTION_BOX_CLASSES =
+  "relative block p4 hover-parent hover--visibility cursor-pointer text-centered transition-background";
 
-const CollectionButtons = ({ collections, isAdmin, push }) =>
-    <ol className="Grid Grid--gutters Grid--fit small-Grid--1of3 md-Grid--1of4 large-Grid--guttersLg">
-        { collections
-            .map(collection => <CollectionButton {...collection} push={push} isAdmin={isAdmin} />)
-            .concat(isAdmin ? [<NewCollectionButton push={push} />] : [])
-            .map((element, index) =>
-                <li key={index} className="Grid-cell">
-                    {element}
-                </li>
-            )
-        }
-    </ol>
+const CollectionButtons = ({ collections, isAdmin, push }) => (
+  <ol className="Grid Grid--gutters Grid--fit small-Grid--1of3 md-Grid--1of4 large-Grid--guttersLg">
+    {collections
+      .map(collection => (
+        <CollectionButton {...collection} push={push} isAdmin={isAdmin} />
+      ))
+      .concat(isAdmin ? [<NewCollectionButton push={push} />] : [])
+      .map((element, index) => (
+        <li key={index} className="Grid-cell">
+          {element}
+        </li>
+      ))}
+  </ol>
+);
 
 class CollectionButton extends Component {
-    constructor() {
-        super();
-        this.state = { hovered: false };
-    }
+  constructor() {
+    super();
+    this.state = { hovered: false };
+  }
 
-    render () {
-        const { id, name, color, slug, isAdmin } = this.props;
-        return (
-            <Link
-                to={`/questions/collections/${slug}`}
-                className="no-decoration"
-                onMouseEnter={() => this.setState({ hovered: true })}
-                onMouseLeave={() => this.setState({ hovered: false })}
-            >
-                <div
-                    className={cx(COLLECTION_BOX_CLASSES, 'text-white-hover')}
-                    style={{
-                        borderRadius: 10,
-                        backgroundColor: this.state.hovered ? color : '#fafafa'
-                    }}
-                >
-                    { isAdmin &&
-                        <div className="absolute top right mt2 mr2 hover-child">
-                            <Link to={"/collections/permissions?collectionId=" + id} className="mr1">
-                                <Icon name="lockoutline" tooltip={t`Set collection permissions`} />
-                            </Link>
-                            <ArchiveCollectionWidget collectionId={id} />
-                        </div>
-                    }
-                    <Icon
-                        className="mb2 mt2"
-                        name="collection"
-                        size={COLLECTION_ICON_SIZE}
-                        style={{ color: this.state.hovered ? '#fff' : color }}
-                    />
-                    <h3>{ name }</h3>
-                </div>
-            </Link>
-        )
-    }
-}
-
-const NewCollectionButton = ({ push }) =>
-    <div
-        className={cx(COLLECTION_BOX_CLASSES, 'bg-brand-hover', 'text-brand', 'text-white-hover', 'bg-grey-0')}
-        style={{
-            borderRadius: 10
-        }}
-        onClick={() => push(`/collections/create`)}
-    >
-        <div>
-            <div
-                className="flex align-center justify-center ml-auto mr-auto mb2 mt2"
-                style={{
-                    border: '2px solid #D8E8F5',
-                    borderRadius: COLLECTION_ICON_SIZE,
-                    height: COLLECTION_ICON_SIZE,
-                    width: COLLECTION_ICON_SIZE,
-                }}
-            >
+  render() {
+    const { id, name, color, slug, isAdmin } = this.props;
+    return (
+      <Link
+        to={`/questions/collections/${slug}`}
+        className="no-decoration"
+        onMouseEnter={() => this.setState({ hovered: true })}
+        onMouseLeave={() => this.setState({ hovered: false })}
+      >
+        <div
+          className={cx(COLLECTION_BOX_CLASSES, "text-white-hover")}
+          style={{
+            borderRadius: 10,
+            backgroundColor: this.state.hovered ? color : "#fafafa",
+          }}
+        >
+          {isAdmin && (
+            <div className="absolute top right mt2 mr2 hover-child">
+              <Link
+                to={"/collections/permissions?collectionId=" + id}
+                className="mr1"
+              >
                 <Icon
-                    name="add"
-                    width="32"
-                    height="32"
+                  name="lockoutline"
+                  tooltip={t`Set collection permissions`}
                 />
+              </Link>
+              <ArchiveCollectionWidget collectionId={id} />
             </div>
+          )}
+          <Icon
+            className="mb2 mt2"
+            name="collection"
+            size={COLLECTION_ICON_SIZE}
+            style={{ color: this.state.hovered ? "#fff" : color }}
+          />
+          <h3>{name}</h3>
         </div>
-        <h3>{t`New collection`}</h3>
+      </Link>
+    );
+  }
+}
+
+const NewCollectionButton = ({ push }) => (
+  <div
+    className={cx(
+      COLLECTION_BOX_CLASSES,
+      "bg-brand-hover",
+      "text-brand",
+      "text-white-hover",
+      "bg-grey-0",
+    )}
+    style={{
+      borderRadius: 10,
+    }}
+    onClick={() => push(`/collections/create`)}
+  >
+    <div>
+      <div
+        className="flex align-center justify-center ml-auto mr-auto mb2 mt2"
+        style={{
+          border: "2px solid #D8E8F5",
+          borderRadius: COLLECTION_ICON_SIZE,
+          height: COLLECTION_ICON_SIZE,
+          width: COLLECTION_ICON_SIZE,
+        }}
+      >
+        <Icon name="add" width="32" height="32" />
+      </div>
     </div>
+    <h3>{t`New collection`}</h3>
+  </div>
+);
 
 export default CollectionButtons;
diff --git a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
index 5ea409e4f3ef52365f8843142cbd2efba05eaa80..ef351a4bf3e7b8e59ab19c5eab601b84183616b7 100644
--- a/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
+++ b/frontend/src/metabase/questions/components/ExpandingSearchField.jsx
@@ -3,101 +3,103 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 import { Motion, spring } from "react-motion";
 
 import Icon from "metabase/components/Icon";
 
-import { KEYCODE_FORWARD_SLASH, KEYCODE_ENTER, KEYCODE_ESCAPE } from "metabase/lib/keyboard";
+import {
+  KEYCODE_FORWARD_SLASH,
+  KEYCODE_ENTER,
+  KEYCODE_ESCAPE,
+} from "metabase/lib/keyboard";
 
 export default class ExpandingSearchField extends Component {
-    constructor (props, context) {
-        super(props, context);
-        this.state = {
-            active: false
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      active: false,
+    };
+  }
 
-    static propTypes = {
-        onSearch: PropTypes.func.isRequired,
-        className: PropTypes.string,
-        defaultValue: PropTypes.string,
-    }
+  static propTypes = {
+    onSearch: PropTypes.func.isRequired,
+    className: PropTypes.string,
+    defaultValue: PropTypes.string,
+  };
 
-    componentDidMount () {
-        this.listenToSearchKeyDown();
-    }
+  componentDidMount() {
+    this.listenToSearchKeyDown();
+  }
 
-    componentWillUnMount () {
-        this.stopListenToSearchKeyDown();
-    }
+  componentWillUnMount() {
+    this.stopListenToSearchKeyDown();
+  }
 
-    handleSearchKeydown = (e) => {
-        if (!this.state.active && e.keyCode === KEYCODE_FORWARD_SLASH) {
-            this.setActive();
-            e.preventDefault();
-        }
+  handleSearchKeydown = e => {
+    if (!this.state.active && e.keyCode === KEYCODE_FORWARD_SLASH) {
+      this.setActive();
+      e.preventDefault();
     }
+  };
 
-    onKeyPress = (e) => {
-        if (e.keyCode === KEYCODE_ENTER) {
-            this.props.onSearch(e.target.value)
-        } else if (e.keyCode === KEYCODE_ESCAPE) {
-            this.setInactive();
-        }
+  onKeyPress = e => {
+    if (e.keyCode === KEYCODE_ENTER) {
+      this.props.onSearch(e.target.value);
+    } else if (e.keyCode === KEYCODE_ESCAPE) {
+      this.setInactive();
     }
+  };
 
-    setActive = () => {
-        ReactDOM.findDOMNode(this.searchInput).focus();
-    }
+  setActive = () => {
+    ReactDOM.findDOMNode(this.searchInput).focus();
+  };
 
-    setInactive = () => {
-        ReactDOM.findDOMNode(this.searchInput).blur();
-    }
+  setInactive = () => {
+    ReactDOM.findDOMNode(this.searchInput).blur();
+  };
 
-    listenToSearchKeyDown () {
-        window.addEventListener('keydown', this.handleSearchKeydown);
-    }
+  listenToSearchKeyDown() {
+    window.addEventListener("keydown", this.handleSearchKeydown);
+  }
 
-    stopListenToSearchKeyDown() {
-        window.removeEventListener('keydown', this.handleSearchKeydown);
-    }
+  stopListenToSearchKeyDown() {
+    window.removeEventListener("keydown", this.handleSearchKeydown);
+  }
 
-    render () {
-        const { className } = this.props;
-        const { active } = this.state;
-        return (
-            <div
-                className={cx(
-                    className,
-                    'bordered border-grey-1 flex align-center pr2 transition-border',
-                    { 'border-brand' : active }
-                )}
-                onClick={this.setActive}
-                style={{borderRadius: 99}}
-            >
-                <Icon
-                    className={cx('ml2 text-grey-3', { 'text-brand': active })}
-                    name="search"
-                />
-                <Motion
-                    style={{width: active ? spring(400) : spring(200) }}
-                >
-                    { interpolatingStyle =>
-                        <input
-                            ref={(search) => this.searchInput = search}
-                            className="input borderless text-bold"
-                            placeholder={t`Search for a question`}
-                            style={Object.assign({}, interpolatingStyle, { fontSize: '1em'})}
-                            onFocus={() => this.setState({ active: true })}
-                            onBlur={() => this.setState({ active: false })}
-                            onKeyUp={this.onKeyPress}
-                            defaultValue={this.props.defaultValue}
-                        />
-                    }
-                </Motion>
-            </div>
-        );
-    }
+  render() {
+    const { className } = this.props;
+    const { active } = this.state;
+    return (
+      <div
+        className={cx(
+          className,
+          "bordered border-grey-1 flex align-center pr2 transition-border",
+          { "border-brand": active },
+        )}
+        onClick={this.setActive}
+        style={{ borderRadius: 99 }}
+      >
+        <Icon
+          className={cx("ml2 text-grey-3", { "text-brand": active })}
+          name="search"
+        />
+        <Motion style={{ width: active ? spring(400) : spring(200) }}>
+          {interpolatingStyle => (
+            <input
+              ref={search => (this.searchInput = search)}
+              className="input borderless text-bold"
+              placeholder={t`Search for a question`}
+              style={Object.assign({}, interpolatingStyle, { fontSize: "1em" })}
+              onFocus={() => this.setState({ active: true })}
+              onBlur={() => this.setState({ active: false })}
+              onKeyUp={this.onKeyPress}
+              defaultValue={this.props.defaultValue}
+            />
+          )}
+        </Motion>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/components/Item.jsx b/frontend/src/metabase/questions/components/Item.jsx
index 4c5b88a31142b2f43ce3aad70f7f8b0c2af7f2f6..a938ed4a3f67914cdc8034ccc876853abf89c081 100644
--- a/frontend/src/metabase/questions/components/Item.jsx
+++ b/frontend/src/metabase/questions/components/Item.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { Link } from "react-router";
 import cx from "classnames";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./List.css";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -20,156 +20,200 @@ import * as Urls from "metabase/lib/urls";
 const ITEM_ICON_SIZE = 20;
 
 const Item = ({
-    entity,
-    id, name, description, labels, created, by, favorite, collection, archived,
-    icon, selected, setItemSelected, setFavorited, setArchived, showCollectionName,
-    onEntityClick
-}) =>
-    <div className={cx('hover-parent hover--visibility', S.item)}>
-        <div className="flex flex-full align-center">
-            <div className="relative flex ml1 mr2" style={{ width: ITEM_ICON_SIZE, height: ITEM_ICON_SIZE }}>
-                { icon &&
-                    <Icon
-                        className={cx("text-light-blue absolute top left visible", { "hover-child--hidden": !!setItemSelected })}
-                        name={icon}
-                        size={ITEM_ICON_SIZE}
-                    />
-                }
-                { setItemSelected &&
-                    <span className={cx(
-                        "absolute top left",
-                        { "visible": selected },
-                        { "hover-child": !selected }
-                    )}>
-                        <CheckBox
-                            checked={selected}
-                            onChange={e => setItemSelected({ [id]: e.target.checked })}
-                            size={ITEM_ICON_SIZE}
-                            padding={3}
-                        />
-                    </span>
-                }
-            </div>
-            <ItemBody
-                entity={entity}
-                id={id}
-                name={name}
-                description={description}
-                labels={labels}
-                favorite={favorite}
-                collection={showCollectionName && collection}
-                setFavorited={setFavorited}
-                onEntityClick={onEntityClick}
-            />
-        </div>
-        <div className="flex flex-column ml-auto">
-            <ItemCreated
-                by={by}
-                created={created}
+  entity,
+  id,
+  name,
+  description,
+  labels,
+  created,
+  by,
+  favorite,
+  collection,
+  archived,
+  icon,
+  selected,
+  setItemSelected,
+  setFavorited,
+  setArchived,
+  showCollectionName,
+  onEntityClick,
+}) => (
+  <div className={cx("hover-parent hover--visibility", S.item)}>
+    <div className="flex flex-full align-center">
+      <div
+        className="relative flex ml1 mr2"
+        style={{ width: ITEM_ICON_SIZE, height: ITEM_ICON_SIZE }}
+      >
+        {icon && (
+          <Icon
+            className={cx("text-light-blue absolute top left visible", {
+              "hover-child--hidden": !!setItemSelected,
+            })}
+            name={icon}
+            size={ITEM_ICON_SIZE}
+          />
+        )}
+        {setItemSelected && (
+          <span
+            className={cx(
+              "absolute top left",
+              { visible: selected },
+              { "hover-child": !selected },
+            )}
+          >
+            <CheckBox
+              checked={selected}
+              onChange={e => setItemSelected({ [id]: e.target.checked })}
+              size={ITEM_ICON_SIZE}
+              padding={3}
             />
-            { setArchived &&
-                <div className="hover-child mt1 ml-auto">
-                    <ModalWithTrigger
-                        full
-                        triggerElement={
-                            <Tooltip tooltip={t`Move to a collection`}>
-                                <Icon
-                                    className="text-light-blue cursor-pointer text-brand-hover transition-color mx2"
-                                    name="move"
-                                    size={18}
-                                />
-                            </Tooltip>
-                        }
-                    >
-                        <MoveToCollection
-                            questionId={id}
-                            initialCollectionId={collection && collection.id}
-                        />
-                    </ModalWithTrigger>
-                    <Tooltip tooltip={archived ? t`Unarchive` : t`Archive`}>
-                        <Icon
-                            className="text-light-blue cursor-pointer text-brand-hover transition-color"
-                            name={ archived ? "unarchive" : "archive"}
-                            onClick={() => setArchived(id, !archived, true)}
-                            size={18}
-                        />
-                    </Tooltip>
-                </div>
+          </span>
+        )}
+      </div>
+      <ItemBody
+        entity={entity}
+        id={id}
+        name={name}
+        description={description}
+        labels={labels}
+        favorite={favorite}
+        collection={showCollectionName && collection}
+        setFavorited={setFavorited}
+        onEntityClick={onEntityClick}
+      />
+    </div>
+    <div className="flex flex-column ml-auto">
+      <ItemCreated by={by} created={created} />
+      {setArchived && (
+        <div className="hover-child mt1 ml-auto">
+          <ModalWithTrigger
+            full
+            triggerElement={
+              <Tooltip tooltip={t`Move to a collection`}>
+                <Icon
+                  className="text-light-blue cursor-pointer text-brand-hover transition-color mx2"
+                  name="move"
+                  size={18}
+                />
+              </Tooltip>
             }
+          >
+            <MoveToCollection
+              questionId={id}
+              initialCollectionId={collection && collection.id}
+            />
+          </ModalWithTrigger>
+          <Tooltip tooltip={archived ? t`Unarchive` : t`Archive`}>
+            <Icon
+              className="text-light-blue cursor-pointer text-brand-hover transition-color"
+              name={archived ? "unarchive" : "archive"}
+              onClick={() => setArchived(id, !archived, true)}
+              size={18}
+            />
+          </Tooltip>
         </div>
+      )}
     </div>
+  </div>
+);
 
 Item.propTypes = {
-    entity:             PropTypes.object.isRequired,
-    id:                 PropTypes.number.isRequired,
-    name:               PropTypes.string.isRequired,
-    created:            PropTypes.string.isRequired,
-    description:        PropTypes.string,
-    by:                 PropTypes.string.isRequired,
-    labels:             PropTypes.array.isRequired,
-    collection:         PropTypes.object,
-    selected:           PropTypes.bool.isRequired,
-    favorite:           PropTypes.bool.isRequired,
-    archived:           PropTypes.bool.isRequired,
-    icon:               PropTypes.string.isRequired,
-    setItemSelected:    PropTypes.func,
-    setFavorited:       PropTypes.func,
-    setArchived:        PropTypes.func,
-    onEntityClick:      PropTypes.func,
-    showCollectionName: PropTypes.bool,
+  entity: PropTypes.object.isRequired,
+  id: PropTypes.number.isRequired,
+  name: PropTypes.string.isRequired,
+  created: PropTypes.string.isRequired,
+  description: PropTypes.string,
+  by: PropTypes.string.isRequired,
+  labels: PropTypes.array.isRequired,
+  collection: PropTypes.object,
+  selected: PropTypes.bool.isRequired,
+  favorite: PropTypes.bool.isRequired,
+  archived: PropTypes.bool.isRequired,
+  icon: PropTypes.string.isRequired,
+  setItemSelected: PropTypes.func,
+  setFavorited: PropTypes.func,
+  setArchived: PropTypes.func,
+  onEntityClick: PropTypes.func,
+  showCollectionName: PropTypes.bool,
 };
 
-const ItemBody = pure(({ entity, id, name, description, labels, favorite, collection, setFavorited, onEntityClick }) =>
+const ItemBody = pure(
+  ({
+    entity,
+    id,
+    name,
+    description,
+    labels,
+    favorite,
+    collection,
+    setFavorited,
+    onEntityClick,
+  }) => (
     <div className={S.itemBody}>
-        <div className={cx('flex', S.itemTitle)}>
-            <Link to={Urls.question(id)} className={cx(S.itemName)} onClick={onEntityClick && ((e) => { e.preventDefault(); onEntityClick(entity); })}>
-                {name}
-            </Link>
-            { collection &&
-                <CollectionBadge collection={collection} />
-            }
-            { favorite != null && setFavorited &&
-                <Tooltip tooltip={favorite ? t`Unfavorite` : t`Favorite`}>
-                    <Icon
-                        className={cx(
-                            "flex cursor-pointer",
-                            {"hover-child text-light-blue text-brand-hover": !favorite},
-                            {"visible text-gold": favorite}
-                        )}
-                        name={favorite ? "star" : "staroutline"}
-                        size={ITEM_ICON_SIZE}
-                        onClick={() => setFavorited(id, !favorite) }
-                    />
-                </Tooltip>
-            }
-            <Labels labels={labels} />
-        </div>
-        <div className={cx({ 'text-slate': description }, { 'text-light-blue': !description })}>
-            {description ? description : t`No description yet`}
-        </div>
+      <div className={cx("flex", S.itemTitle)}>
+        <Link
+          to={Urls.question(id)}
+          className={cx(S.itemName)}
+          onClick={
+            onEntityClick &&
+            (e => {
+              e.preventDefault();
+              onEntityClick(entity);
+            })
+          }
+        >
+          {name}
+        </Link>
+        {collection && <CollectionBadge collection={collection} />}
+        {favorite != null &&
+          setFavorited && (
+            <Tooltip tooltip={favorite ? t`Unfavorite` : t`Favorite`}>
+              <Icon
+                className={cx(
+                  "flex cursor-pointer",
+                  { "hover-child text-light-blue text-brand-hover": !favorite },
+                  { "visible text-gold": favorite },
+                )}
+                name={favorite ? "star" : "staroutline"}
+                size={ITEM_ICON_SIZE}
+                onClick={() => setFavorited(id, !favorite)}
+              />
+            </Tooltip>
+          )}
+        <Labels labels={labels} />
+      </div>
+      <div
+        className={cx(
+          { "text-slate": description },
+          { "text-light-blue": !description },
+        )}
+      >
+        {description ? description : t`No description yet`}
+      </div>
     </div>
+  ),
 );
 
 ItemBody.propTypes = {
-    description:        PropTypes.string,
-    favorite:           PropTypes.bool.isRequired,
-    id:                 PropTypes.number.isRequired,
-    name:               PropTypes.string.isRequired,
-    setFavorited:       PropTypes.func,
+  description: PropTypes.string,
+  favorite: PropTypes.bool.isRequired,
+  id: PropTypes.number.isRequired,
+  name: PropTypes.string.isRequired,
+  setFavorited: PropTypes.func,
 };
 
-const ItemCreated = pure(({ created, by }) =>
-    (created || by) ?
-        <div className={S.itemSubtitle}>
-            {t`Created` + (created ? ` ${created}` : ``) + (by ? t` by ${by}` : ``)}
-        </div>
-    :
-        null
+const ItemCreated = pure(
+  ({ created, by }) =>
+    created || by ? (
+      <div className={S.itemSubtitle}>
+        {t`Created` + (created ? ` ${created}` : ``) + (by ? t` by ${by}` : ``)}
+      </div>
+    ) : null,
 );
 
 ItemCreated.propTypes = {
-    created:            PropTypes.string.isRequired,
-    by:                 PropTypes.string.isRequired,
+  created: PropTypes.string.isRequired,
+  by: PropTypes.string.isRequired,
 };
 
 export default pure(Item);
diff --git a/frontend/src/metabase/questions/components/LabelIconPicker.css b/frontend/src/metabase/questions/components/LabelIconPicker.css
index 61a7703c11358915d6ab7151e04ef9868a91eac6..61c3418d733664105f125b24bb5520493a2813f4 100644
--- a/frontend/src/metabase/questions/components/LabelIconPicker.css
+++ b/frontend/src/metabase/questions/components/LabelIconPicker.css
@@ -1,63 +1,63 @@
-@import '../Questions.css';
+@import "../Questions.css";
 
 :local(.sectionHeader) {
-    composes: p1 px3 from "style";
-    composes: flex align-center from "style";
-    display: flex;
-    align-items: center;
-    position: absolute;
-    bottom: 0px;
+  composes: p1 px3 from "style";
+  composes: flex align-center from "style";
+  display: flex;
+  align-items: center;
+  position: absolute;
+  bottom: 0px;
 }
 
 :local(.list) {
-    composes: flex align-center from "style";
-    composes: px2 from "style";
+  composes: flex align-center from "style";
+  composes: px2 from "style";
 }
 
 :local(.sectionList) {
-    composes: list;
-    composes: border-top from "style";
-    composes: py1 from "style";
-    justify-content: space-around;
+  composes: list;
+  composes: border-top from "style";
+  composes: py1 from "style";
+  justify-content: space-around;
 }
 
 :local(.option) {
-    composes: p1 from "style";
-    composes: flex layout-centered from "style";
-    composes: cursor-pointer from "style";
-    border: 1px solid transparent;
-    width: 50px;
-    height: 50px;
+  composes: p1 from "style";
+  composes: flex layout-centered from "style";
+  composes: cursor-pointer from "style";
+  border: 1px solid transparent;
+  width: 50px;
+  height: 50px;
 }
 
 :local(.option):hover {
-    border: 1px solid rgb(235,235,235);
-    border-radius: 3px;
-    background-color: rgb(248,248,248);
-    cursor: pointer;
+  border: 1px solid rgb(235, 235, 235);
+  border-radius: 3px;
+  background-color: rgb(248, 248, 248);
+  cursor: pointer;
 }
 
 :local(.dropdownButton) {
-    composes: flex rounded px1 layout-centered from "style";
-    border: 1px solid #d9d9d9;
-    padding: 10px;
+  composes: flex rounded px1 layout-centered from "style";
+  border: 1px solid #d9d9d9;
+  padding: 10px;
 }
 
 :local(.dropdownButton):hover {
-    border-color: #aaa;
+  border-color: #aaa;
 }
 
 :local(.chevron) {
-    composes: ml1 from "style";
-    color: #d9d9d9;
+  composes: ml1 from "style";
+  color: #d9d9d9;
 }
 
 :local(.category) {
-    justify-content: space-between;
-    composes: p1 cursor-pointer transition-color from "style";
-    color: #CFE4F5;
+  justify-content: space-between;
+  composes: p1 cursor-pointer transition-color from "style";
+  color: #cfe4f5;
 }
 
 :local(.category):hover {
-    color: var(--blue-color);
+  color: var(--blue-color);
 }
diff --git a/frontend/src/metabase/questions/components/LabelIconPicker.jsx b/frontend/src/metabase/questions/components/LabelIconPicker.jsx
index 67494cbc8ae454303d6574c5d312dca2cd4660a7..e7e9ec7c55871f86521d85d1859744358fee243d 100644
--- a/frontend/src/metabase/questions/components/LabelIconPicker.jsx
+++ b/frontend/src/metabase/questions/components/LabelIconPicker.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./LabelIconPicker.css";
 
 import Icon from "metabase/components/Icon.jsx";
@@ -25,99 +25,123 @@ const ROWS = [];
 const CATEGORY_ROW_MAP = {};
 
 function pushHeader(title) {
-    ROWS.push({ type: "header", title: title });
+  ROWS.push({ type: "header", title: title });
 }
 function pushIcons(icons) {
-    for (let icon of icons) {
-        let current = ROWS[ROWS.length - 1];
-        if (current.type !== "icons" || current.icons.length === ICONS_PER_ROW) {
-            current = { type: "icons", icons: [] };
-            ROWS.push(current);
-        }
-        current.icons.push(icon);
+  for (let icon of icons) {
+    let current = ROWS[ROWS.length - 1];
+    if (current.type !== "icons" || current.icons.length === ICONS_PER_ROW) {
+      current = { type: "icons", icons: [] };
+      ROWS.push(current);
     }
+    current.icons.push(icon);
+  }
 }
 
 // Colors
-const ALL_COLORS = [].concat(...[colors.saturated, colors.normal, colors.desaturated].map(o => Object.values(o)));
+const ALL_COLORS = [].concat(
+  ...[colors.saturated, colors.normal, colors.desaturated].map(o =>
+    Object.values(o),
+  ),
+);
 pushHeader(t`Colors`);
 pushIcons(ALL_COLORS);
 
 // Emoji
 categories.map(category => {
-    CATEGORY_ROW_MAP[category.id] = ROWS.length;
-    pushHeader(category.name);
-    pushIcons(category.emoji);
+  CATEGORY_ROW_MAP[category.id] = ROWS.length;
+  pushHeader(category.name);
+  pushIcons(category.emoji);
 });
 
 export default class LabelIconPicker extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            topIndex: 0,
-            scrollToIndex: 0
-        };
-    }
-
-    static propTypes = {
-        value:      PropTypes.string,
-        onChange:   PropTypes.func.isRequired,
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      topIndex: 0,
+      scrollToIndex: 0,
     };
+  }
 
-    scrollToCategory(id) {
-        let categoryIndex = CATEGORY_ROW_MAP[id];
-        if (categoryIndex > this.state.topIndex) {
-            this.setState({ scrollToIndex: categoryIndex + VISIBLE_ROWS - 1 });
-        } else {
-            this.setState({ scrollToIndex: categoryIndex });
-        }
+  static propTypes = {
+    value: PropTypes.string,
+    onChange: PropTypes.func.isRequired,
+  };
+
+  scrollToCategory(id) {
+    let categoryIndex = CATEGORY_ROW_MAP[id];
+    if (categoryIndex > this.state.topIndex) {
+      this.setState({ scrollToIndex: categoryIndex + VISIBLE_ROWS - 1 });
+    } else {
+      this.setState({ scrollToIndex: categoryIndex });
     }
+  }
 
-    render() {
-        const { value, onChange } = this.props;
-        return (
-            <PopoverWithTrigger
-                triggerElement={<LabelIconButton value={value} />}
-                ref="popover"
+  render() {
+    const { value, onChange } = this.props;
+    return (
+      <PopoverWithTrigger
+        triggerElement={<LabelIconButton value={value} />}
+        ref="popover"
+      >
+        <List
+          width={WIDTH}
+          height={HEIGHT}
+          rowCount={ROWS.length}
+          rowHeight={ROW_HEIGHT}
+          rowRenderer={({ index, key, style }) =>
+            ROWS[index].type === "header" ? (
+              <div key={key} style={style} className={S.sectionHeader}>
+                {ROWS[index].title}
+              </div>
+            ) : (
+              <ul key={key} style={style} className={S.list}>
+                {ROWS[index].icons.map(icon => (
+                  <li
+                    key={icon}
+                    className={S.option}
+                    onClick={() => {
+                      onChange(icon);
+                      this.refs.popover.close();
+                    }}
+                  >
+                    <LabelIcon icon={icon} size={28} />
+                  </li>
+                ))}
+              </ul>
+            )
+          }
+          scrollToIndex={this.state.scrollToIndex}
+          onRowsRendered={({
+            overscanStartIndex,
+            overscanStopIndex,
+            startIndex,
+            stopIndex,
+          }) => this.setState({ topIndex: startIndex })}
+        />
+        <ul className={S.sectionList} style={{ width: WIDTH }}>
+          {categories.map(category => (
+            <li
+              key={category.id}
+              className={S.category}
+              onClick={() => this.scrollToCategory(category.id)}
             >
-                <List
-                  width={WIDTH}
-                  height={HEIGHT}
-                  rowCount={ROWS.length}
-                  rowHeight={ROW_HEIGHT}
-                  rowRenderer={ ({ index, key, style }) =>
-                      ROWS[index].type === "header" ?
-                          <div key={key} style={style} className={S.sectionHeader}>{ROWS[index].title}</div>
-                      :
-                          <ul key={key} style={style} className={S.list}>
-                              { ROWS[index].icons.map(icon =>
-                                  <li key={icon} className={S.option} onClick={() => { onChange(icon); this.refs.popover.close() }}>
-                                      <LabelIcon icon={icon} size={28} />
-                                  </li>
-                              )}
-                          </ul>
-                  }
-                  scrollToIndex={this.state.scrollToIndex}
-                  onRowsRendered={({ overscanStartIndex, overscanStopIndex, startIndex, stopIndex }) => this.setState({ topIndex: startIndex })}
-                />
-                <ul className={S.sectionList} style={{ width: WIDTH }}>
-                    { categories.map(category =>
-                        <li key={category.id} className={S.category} onClick={() => this.scrollToCategory(category.id) }>
-                          <Icon name={`emoji${category.id}`} />
-                        </li>
-                    )}
-                </ul>
-            </PopoverWithTrigger>
-        );
-    }
+              <Icon name={`emoji${category.id}`} />
+            </li>
+          ))}
+        </ul>
+      </PopoverWithTrigger>
+    );
+  }
 }
 
-const LabelIconButton = ({ value = "#eee" }) =>
-    <span className={S.dropdownButton}>
-        <LabelIcon icon={value} size={28} />
-        <Icon className={S.chevron} name="chevrondown" size={14} />
-    </span>
+const LabelIconButton = ({ value = "#eee" }) => (
+  <span className={S.dropdownButton}>
+    <LabelIcon icon={value} size={28} />
+    <Icon className={S.chevron} name="chevrondown" size={14} />
+  </span>
+);
 
 LabelIconButton.propTypes = {
-    value:      PropTypes.string
+  value: PropTypes.string,
 };
diff --git a/frontend/src/metabase/questions/components/LabelPicker.css b/frontend/src/metabase/questions/components/LabelPicker.css
index 97aa2b4a856748441d43f159017d22c368f6f0f2..38ca712f296657f4901466fea62295ecf8775f9a 100644
--- a/frontend/src/metabase/questions/components/LabelPicker.css
+++ b/frontend/src/metabase/questions/components/LabelPicker.css
@@ -1,67 +1,67 @@
 @import "../Questions.css";
 
 :local(.picker) {
-    width: 290px;
-    color: var(--title-color);
-    overflow: hidden;
+  width: 290px;
+  color: var(--title-color);
+  overflow: hidden;
 }
 
 :local(.heading) {
-    composes: text-bold p2 border-bottom from "style";
-    font-size: 14px;
+  composes: text-bold p2 border-bottom from "style";
+  font-size: 14px;
 }
 
 :local(.footer) {
-    composes: text-bold p2 border-top from "style";
-    font-size: 14px;
+  composes: text-bold p2 border-top from "style";
+  font-size: 14px;
 }
 
 :local(.options) {
-    composes: full-height py1 scroll-y from "style";
-    max-height: 300px;
+  composes: full-height py1 scroll-y from "style";
+  max-height: 300px;
 }
 
 :local(.option) {
-    composes: cursor-pointer relative from "style";
+  composes: cursor-pointer relative from "style";
 }
 
 :local(.optionContent) {
-    composes: px3 py1 flex align-center text-bold from "style";
-    font-size: 1em;
-    color: var(--title-color);
+  composes: px3 py1 flex align-center text-bold from "style";
+  font-size: 1em;
+  color: var(--title-color);
 }
 
 :local(.optionBackground) {
-    composes: spread from "style";
-    opacity: 0.1;
-    background-color: currentColor;
-    visibility: hidden;
+  composes: spread from "style";
+  opacity: 0.1;
+  background-color: currentColor;
+  visibility: hidden;
 }
 
 :local(.option.selected) :local(.optionContent),
 :local(.option):hover :local(.optionContent) {
-    color: inherit;
+  color: inherit;
 }
 
 :local(.option.selected) :local(.optionBackground),
 :local(.option):hover :local(.optionBackground) {
-    visibility: visible;
+  visibility: visible;
 }
 
 :local(.name) {
-    text-overflow: ellipsis;
-    white-space: nowrap;
-    overflow-x: hidden;
+  text-overflow: ellipsis;
+  white-space: nowrap;
+  overflow-x: hidden;
 }
 
 :local(.mainIcon) {
-    composes: mr1 flex-no-shrink from "style";
+  composes: mr1 flex-no-shrink from "style";
 }
 
 :local(.removeIcon) {
-    composes: flex-align-right flex-no-shrink from "style";
-    visibility: hidden;
+  composes: flex-align-right flex-no-shrink from "style";
+  visibility: hidden;
 }
 :local(.option):hover :local(.removeIcon) {
-    visibility: visible;
+  visibility: visible;
 }
diff --git a/frontend/src/metabase/questions/components/LabelPicker.jsx b/frontend/src/metabase/questions/components/LabelPicker.jsx
index c71f84f34bd0bc4f76e916d795e274b405e97733..6babfc61297a0f9f8b6562b4020c1096b96c75fa 100644
--- a/frontend/src/metabase/questions/components/LabelPicker.jsx
+++ b/frontend/src/metabase/questions/components/LabelPicker.jsx
@@ -2,7 +2,7 @@
 import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./LabelPicker.css";
 
 import LabelIcon from "metabase/components/LabelIcon.jsx";
@@ -11,56 +11,58 @@ import Tooltip from "metabase/components/Tooltip.jsx";
 
 import cx from "classnames";
 
-const LabelPicker = ({ labels, count, item, setLabeled }) =>
-    <div className={S.picker}>
-        <div className={S.heading}>
-        { count > 1 ?
-            t`Apply labels to ${count} questions`
-        :
-            t`Label as`
-        }
-        </div>
-        <ul className={S.options}>
-            { labels.map(label => {
-                let color = label.icon.charAt(0) === "#" ? label.icon : undefined;
-                let selected = item && item.labels.indexOf(label) >= 0 || label.selected === true;
-                let partiallySelected = !item && label.selected === null;
-                return (
-                    <li
-                        key={label.id}
-                        onClick={() => setLabeled(item && item.id, label.id, !selected, !item)}
-                        className={cx(S.option, { [S.selected]: selected })}
-                        style={{ color: color }}
-                    >
-                        <div className={S.optionContent}>
-                            { selected ?
-                                <Icon className={S.mainIcon} name="check" />
-                            : partiallySelected ?
-                                <Icon className={S.mainIcon} name="close" />
-                            :
-                                <LabelIcon className={S.mainIcon} icon={label.icon} />
-                            }
-                            <span className={S.name}>{label.name}</span>
-                            { selected && <Icon className={S.removeIcon} name="close" /> }
-                        </div>
-                        <div className={S.optionBackground}></div>
-                    </li>
-                )
-            }) }
-        </ul>
-        <div className={S.footer}>
-            <Link className="link" to="/labels">{t`Add and edit labels`}</Link>
-            <Tooltip tooltip={t`In an upcoming release, Labels will be removed in favor of Collections.`}>
-                <Icon name="warning2" className="text-error float-right" />
-            </Tooltip>
-        </div>
+const LabelPicker = ({ labels, count, item, setLabeled }) => (
+  <div className={S.picker}>
+    <div className={S.heading}>
+      {count > 1 ? t`Apply labels to ${count} questions` : t`Label as`}
     </div>
+    <ul className={S.options}>
+      {labels.map(label => {
+        let color = label.icon.charAt(0) === "#" ? label.icon : undefined;
+        let selected =
+          (item && item.labels.indexOf(label) >= 0) || label.selected === true;
+        let partiallySelected = !item && label.selected === null;
+        return (
+          <li
+            key={label.id}
+            onClick={() =>
+              setLabeled(item && item.id, label.id, !selected, !item)
+            }
+            className={cx(S.option, { [S.selected]: selected })}
+            style={{ color: color }}
+          >
+            <div className={S.optionContent}>
+              {selected ? (
+                <Icon className={S.mainIcon} name="check" />
+              ) : partiallySelected ? (
+                <Icon className={S.mainIcon} name="close" />
+              ) : (
+                <LabelIcon className={S.mainIcon} icon={label.icon} />
+              )}
+              <span className={S.name}>{label.name}</span>
+              {selected && <Icon className={S.removeIcon} name="close" />}
+            </div>
+            <div className={S.optionBackground} />
+          </li>
+        );
+      })}
+    </ul>
+    <div className={S.footer}>
+      <Link className="link" to="/labels">{t`Add and edit labels`}</Link>
+      <Tooltip
+        tooltip={t`In an upcoming release, Labels will be removed in favor of Collections.`}
+      >
+        <Icon name="warning2" className="text-error float-right" />
+      </Tooltip>
+    </div>
+  </div>
+);
 
 LabelPicker.propTypes = {
-    labels:     PropTypes.array.isRequired,
-    count:      PropTypes.number,
-    item:       PropTypes.object,
-    setLabeled: PropTypes.func.isRequired,
+  labels: PropTypes.array.isRequired,
+  count: PropTypes.number,
+  item: PropTypes.object,
+  setLabeled: PropTypes.func.isRequired,
 };
 
 export default LabelPicker;
diff --git a/frontend/src/metabase/questions/components/Labels.css b/frontend/src/metabase/questions/components/Labels.css
index 3256b398711a7649e83c357d6fbd4cfd05bae856..49dd79656ea5b78ef153911e48ff4ec2bcb0774e 100644
--- a/frontend/src/metabase/questions/components/Labels.css
+++ b/frontend/src/metabase/questions/components/Labels.css
@@ -3,29 +3,29 @@
 }
 
 :local(.listItem) {
-    composes: inline-block from "style";
-    margin-bottom: 8px;
+  composes: inline-block from "style";
+  margin-bottom: 8px;
 }
 
 :local(.label) {
-    composes: inline from "style";
-    composes: text-white from "style";
-    composes: text-bold from "style";
-    font-size: 14px;
-    border-radius: 2px;
-    padding: 0.25em 0.5em;
-    margin: 0 0.25em;
-    line-height: 1;
-    white-space: nowrap;
+  composes: inline from "style";
+  composes: text-white from "style";
+  composes: text-bold from "style";
+  font-size: 14px;
+  border-radius: 2px;
+  padding: 0.25em 0.5em;
+  margin: 0 0.25em;
+  line-height: 1;
+  white-space: nowrap;
 }
 
 :local(.emojiLabel) {
-    composes: bg-white from "style";
-    color: rgba(0,0,0,0.54);
-    border: 1px solid #DEEAF1;
-    box-shadow: 1px 1px 0 0 #BFD5E1;
+  composes: bg-white from "style";
+  color: rgba(0, 0, 0, 0.54);
+  border: 1px solid #deeaf1;
+  box-shadow: 1px 1px 0 0 #bfd5e1;
 }
 
 :local(.emojiIcon) {
-    margin-right: 0.5em;
+  margin-right: 0.5em;
 }
diff --git a/frontend/src/metabase/questions/components/Labels.jsx b/frontend/src/metabase/questions/components/Labels.jsx
index 87f822aa96cbe4833aa66ce63519a43dd93568df..b56b84708df5e755ac43a729e0a4b26b67a2b5b8 100644
--- a/frontend/src/metabase/questions/components/Labels.jsx
+++ b/frontend/src/metabase/questions/components/Labels.jsx
@@ -3,70 +3,77 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./Labels.css";
-import color from 'color'
+import color from "color";
 import * as Urls from "metabase/lib/urls";
 
-import EmojiIcon from "metabase/components/EmojiIcon.jsx"
+import EmojiIcon from "metabase/components/EmojiIcon.jsx";
 
 import cx from "classnames";
 
-const Labels = ({ labels }) =>
-    <ul className={S.list}>
-        { labels.map(label =>
-            <li className={S.listItem} key={label.id}>
-                <Label {...label} />
-            </li>
-        )}
-    </ul>
+const Labels = ({ labels }) => (
+  <ul className={S.list}>
+    {labels.map(label => (
+      <li className={S.listItem} key={label.id}>
+        <Label {...label} />
+      </li>
+    ))}
+  </ul>
+);
 
 Labels.propTypes = {
-    labels:  PropTypes.array.isRequired,
+  labels: PropTypes.array.isRequired,
 };
 
 class Label extends Component {
   constructor(props) {
-    super(props)
+    super(props);
     this.state = {
-      hovered: false
-    }
+      hovered: false,
+    };
   }
-  render () {
-    const { name, icon, slug } = this.props
-    const { hovered } = this.state
+  render() {
+    const { name, icon, slug } = this.props;
+    const { hovered } = this.state;
     return (
       <Link
         to={Urls.label({ slug })}
         onMouseEnter={() => this.setState({ hovered: true })}
         onMouseLeave={() => this.setState({ hovered: false })}
       >
-          { icon.charAt(0) === ":" ?
-              <span className={cx(S.label, S.emojiLabel)}>
-                  <EmojiIcon name={icon} className={S.emojiIcon} />
-                  <span>{name}</span>
-              </span>
-          : icon.charAt(0) === "#" ?
-              <span
-                className={S.label}
-                style={{
-                  backgroundColor: hovered ? color(icon).darken(0.1).hex() : icon,
-                  boxShadow: `1px 1px 0 ${color(icon).darken(hovered ? 0.1 : 0.2).hex()}`,
-                  transition: 'background .3s ease-in-out'
-                }}
-              >
-                {name}
-              </span>
-          :
-              <span className={S.label}>{name}</span>
-          }
+        {icon.charAt(0) === ":" ? (
+          <span className={cx(S.label, S.emojiLabel)}>
+            <EmojiIcon name={icon} className={S.emojiIcon} />
+            <span>{name}</span>
+          </span>
+        ) : icon.charAt(0) === "#" ? (
+          <span
+            className={S.label}
+            style={{
+              backgroundColor: hovered
+                ? color(icon)
+                    .darken(0.1)
+                    .hex()
+                : icon,
+              boxShadow: `1px 1px 0 ${color(icon)
+                .darken(hovered ? 0.1 : 0.2)
+                .hex()}`,
+              transition: "background .3s ease-in-out",
+            }}
+          >
+            {name}
+          </span>
+        ) : (
+          <span className={S.label}>{name}</span>
+        )}
       </Link>
-    )
+    );
   }
 }
 
 Label.propTypes = {
-    name:   PropTypes.string.isRequired,
-    icon:   PropTypes.string.isRequired,
-    slug:   PropTypes.string.isRequired,
+  name: PropTypes.string.isRequired,
+  icon: PropTypes.string.isRequired,
+  slug: PropTypes.string.isRequired,
 };
 
 export default Labels;
diff --git a/frontend/src/metabase/questions/components/List.css b/frontend/src/metabase/questions/components/List.css
index 70663604962afcd0fa67767ee1599b45bbcc870b..b43f1a2708fa34a69f8e209411b1afe70dda415e 100644
--- a/frontend/src/metabase/questions/components/List.css
+++ b/frontend/src/metabase/questions/components/List.css
@@ -1,4 +1,4 @@
-@import '../Questions.css';
+@import "../Questions.css";
 
 :local(.list) {
   composes: ml-auto mr-auto from "style";
@@ -11,58 +11,57 @@
 }
 
 :local(.list) a {
-    text-decoration: none;
+  text-decoration: none;
 }
 
 :local(.header) {
-    composes: header from "../Questions.css";
+  composes: header from "../Questions.css";
 }
 
 :local(.empty) {
-    composes:  flex align-center justify-center from "style";
-    composes: full from "style";
+  composes: flex align-center justify-center from "style";
+  composes: full from "style";
 }
 
 :local(.item) {
-    composes: border-top from "style";
-    composes: flex align-center from "style";
-    composes: relative from "style";
-    padding-top: 20px;
-    padding-bottom: 20px;
-    border-color: #EDF5FB;
+  composes: border-top from "style";
+  composes: flex align-center from "style";
+  composes: relative from "style";
+  padding-top: 20px;
+  padding-bottom: 20px;
+  border-color: #edf5fb;
 }
 
 :local(.itemBody) {
-    composes: flex-full from "style";
+  composes: flex-full from "style";
 }
 
 :local(.itemTitle) {
-    composes: text-bold from "style";
-    color: var(--title-color);
-    font-size: 18px;
+  composes: text-bold from "style";
+  color: var(--title-color);
+  font-size: 18px;
 }
 
 :local(.itemName) {
-    composes: mr1 from "style";
-    composes: inline-block from "style";
+  composes: mr1 from "style";
+  composes: inline-block from "style";
 }
 
 :local(.itemName):hover {
-    color: var(--blue-color);
+  color: var(--blue-color);
 }
 
 :local(.itemSubtitle) {
-    color: var(--subtitle-color);
-    font-size: 14px;
+  color: var(--subtitle-color);
+  font-size: 14px;
 }
 
 :local(.itemSubtitleBold) {
-    color: var(--title-color);
+  color: var(--title-color);
 }
 
 /* TAG */
 :local(.open) :local(.tagIcon) {
-    visibility: visible;
-    color: var(--blue-color);
+  visibility: visible;
+  color: var(--blue-color);
 }
-
diff --git a/frontend/src/metabase/questions/components/List.jsx b/frontend/src/metabase/questions/components/List.jsx
index cc49ac1cf0ab94d1ff5773371f55b8ae442c9533..33eececdce502d3309f1d9b8a64dc1a2a93080d2 100644
--- a/frontend/src/metabase/questions/components/List.jsx
+++ b/frontend/src/metabase/questions/components/List.jsx
@@ -7,15 +7,16 @@ import pure from "recompose/pure";
 
 import EntityItem from "../containers/EntityItem.jsx";
 
-const List = ({ entityIds, ...props }) =>
-    <ul className={S.list}>
-        { entityIds.map(entityId =>
-            <EntityItem key={entityId} entityId={entityId} {...props} />
-        )}
-    </ul>
+const List = ({ entityIds, ...props }) => (
+  <ul className={S.list}>
+    {entityIds.map(entityId => (
+      <EntityItem key={entityId} entityId={entityId} {...props} />
+    ))}
+  </ul>
+);
 
 List.propTypes = {
-    entityIds:          PropTypes.array.isRequired,
+  entityIds: PropTypes.array.isRequired,
 };
 
 export default pure(List);
diff --git a/frontend/src/metabase/questions/containers/AddToDashboard.jsx b/frontend/src/metabase/questions/containers/AddToDashboard.jsx
index 7e480e79e447168d22e2c1b75b6d89025d1a0172..9260875d7406660e9e84523b98c4669f26f995dd 100644
--- a/frontend/src/metabase/questions/containers/AddToDashboard.jsx
+++ b/frontend/src/metabase/questions/containers/AddToDashboard.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Button from "metabase/components/Button.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
 import Icon from "metabase/components/Icon.jsx";
@@ -10,129 +10,118 @@ import EntityList from "./EntityList";
 import ExpandingSearchField from "../components/ExpandingSearchField.jsx";
 
 export default class AddToDashboard extends Component {
+  state = {
+    collection: null,
+    query: null,
+  };
 
-    state = {
-        collection: null,
-        query: null
-    }
+  renderQuestionList = () => {
+    return (
+      <EntityList
+        entityType="cards"
+        entityQuery={this.state.query}
+        editable={false}
+        showSearchWidget={false}
+        onEntityClick={this.props.onAdd}
+      />
+    );
+  };
 
-    renderQuestionList = () => {
-        return (
-            <EntityList
-                entityType="cards"
-                entityQuery={this.state.query}
-                editable={false}
-                showSearchWidget={false}
-                onEntityClick={this.props.onAdd}
-            />
-        )
-    }
-
-    renderCollections = () => {
-        return (
-            <Collections>
-                { collections =>
-                    <div>
-                        {
-                        /*
+  renderCollections = () => {
+    return (
+      <Collections>
+        {collections => (
+          <div>
+            {/*
                             only show the collections list if there are actually collections
                             fixes #4668
                         */
-                        collections.length > 0
-                            ? (
-                                <ol>
-                                    { collections.map((collection, index) =>
-                                        <li
-                                            className="text-brand-hover flex align-center border-bottom cursor-pointer py1 md-py2"
-                                            key={index}
-                                            onClick={() => this.setState({
-                                                collection: collection,
-                                                query: { collection: collection.slug }
-                                            })}
-                                        >
-                                            <Icon
-                                                className="mr2"
-                                                name="all"
-                                                style={{ color: collection.color }}
-                                            />
-                                            <h3>{collection.name}</h3>
-                                            <Icon
-                                                className="ml-auto"
-                                                name="chevronright"
-                                            />
-                                        </li>
-                                    )}
-                                    <li
-                                        className="text-brand-hover flex align-center border-bottom cursor-pointer py1 md-py2"
-                                        onClick={() => this.setState({
-                                            collection: { name: t`Everything else` },
-                                            query: { collection: "" }
-                                        })}
-                                    >
-                                        <Icon
-                                            className="mr2"
-                                            name="everything"
-                                        />
-                                        <h3>Everything else</h3>
-                                        <Icon
-                                            className="ml-auto"
-                                            name="chevronright"
-                                        />
-                                    </li>
-                                </ol>
-                            )
-                            : this.renderQuestionList()
-                        }
-                    </div>
-                }
-            </Collections>
-        )
-    }
-
-    render() {
-        const { query, collection } = this.state;
-        return (
-            <ModalContent
-                title={t`Pick a question to add`}
-                className="px4 mb4 scroll-y"
-                onClose={() => this.props.onClose()}
-            >
-                <div className="py1">
-                    <div className="flex align-center">
-                    { !query ?
-                        <ExpandingSearchField
-                            defaultValue={query && query.q}
-                            onSearch={(value) => this.setState({
-                                collection: null,
-                                query: { q: value }
-                            })}
-                        />
-                    :
-                        <HeaderWithBack
-                            name={collection && collection.name}
-                            onBack={() => this.setState({ collection: null, query: null })}
-                        />
+            collections.length > 0 ? (
+              <ol>
+                {collections.map((collection, index) => (
+                  <li
+                    className="text-brand-hover flex align-center border-bottom cursor-pointer py1 md-py2"
+                    key={index}
+                    onClick={() =>
+                      this.setState({
+                        collection: collection,
+                        query: { collection: collection.slug },
+                      })
                     }
-                    { query &&
-                        <div className="ml-auto flex align-center">
-                            <h5>Sort by</h5>
-                            <Button borderless>
-                                {t`Last modified`}
-                            </Button>
-                            <Button borderless>
-                                {t`Alphabetical order`}
-                            </Button>
-                        </div>
-                    }
-                    </div>
-                </div>
-                { query
-                    // a search term has been entered so show the questions list
-                    ? this.renderQuestionList()
-                    // show the collections list
-                    : this.renderCollections()
+                  >
+                    <Icon
+                      className="mr2"
+                      name="all"
+                      style={{ color: collection.color }}
+                    />
+                    <h3>{collection.name}</h3>
+                    <Icon className="ml-auto" name="chevronright" />
+                  </li>
+                ))}
+                <li
+                  className="text-brand-hover flex align-center border-bottom cursor-pointer py1 md-py2"
+                  onClick={() =>
+                    this.setState({
+                      collection: { name: t`Everything else` },
+                      query: { collection: "" },
+                    })
+                  }
+                >
+                  <Icon className="mr2" name="everything" />
+                  <h3>Everything else</h3>
+                  <Icon className="ml-auto" name="chevronright" />
+                </li>
+              </ol>
+            ) : (
+              this.renderQuestionList()
+            )}
+          </div>
+        )}
+      </Collections>
+    );
+  };
+
+  render() {
+    const { query, collection } = this.state;
+    return (
+      <ModalContent
+        title={t`Pick a question to add`}
+        className="px4 mb4 scroll-y"
+        onClose={() => this.props.onClose()}
+      >
+        <div className="py1">
+          <div className="flex align-center">
+            {!query ? (
+              <ExpandingSearchField
+                defaultValue={query && query.q}
+                onSearch={value =>
+                  this.setState({
+                    collection: null,
+                    query: { q: value },
+                  })
                 }
-            </ModalContent>
-        );
-    }
+              />
+            ) : (
+              <HeaderWithBack
+                name={collection && collection.name}
+                onBack={() => this.setState({ collection: null, query: null })}
+              />
+            )}
+            {query && (
+              <div className="ml-auto flex align-center">
+                <h5>Sort by</h5>
+                <Button borderless>{t`Last modified`}</Button>
+                <Button borderless>{t`Alphabetical order`}</Button>
+              </div>
+            )}
+          </div>
+        </div>
+        {query
+          ? // a search term has been entered so show the questions list
+            this.renderQuestionList()
+          : // show the collections list
+            this.renderCollections()}
+      </ModalContent>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/Archive.jsx b/frontend/src/metabase/questions/containers/Archive.jsx
index c8ed91fcfe56424b8a1c7696ab32fa37b501bd94..23d84d3756decf8757ab0366737eb511aa469ac7 100644
--- a/frontend/src/metabase/questions/containers/Archive.jsx
+++ b/frontend/src/metabase/questions/containers/Archive.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import HeaderWithBack from "metabase/components/HeaderWithBack";
 import SearchHeader from "metabase/components/SearchHeader";
@@ -14,59 +14,88 @@ import { getUserIsAdmin } from "metabase/selectors/user";
 import visualizations from "metabase/visualizations";
 
 const mapStateToProps = (state, props) => ({
-    searchText:             getSearchText(state, props),
-    archivedCards:          getVisibleEntities(state, { entityType: "cards", entityQuery: { f: "archived" }}) || [],
-    archivedCollections:    getVisibleEntities(state, { entityType: "collections", entityQuery: { archived: true }}) || [],
+  searchText: getSearchText(state, props),
+  archivedCards:
+    getVisibleEntities(state, {
+      entityType: "cards",
+      entityQuery: { f: "archived" },
+    }) || [],
+  archivedCollections:
+    getVisibleEntities(state, {
+      entityType: "collections",
+      entityQuery: { archived: true },
+    }) || [],
 
-    isAdmin:                getUserIsAdmin(state, props)
-})
+  isAdmin: getUserIsAdmin(state, props),
+});
 
 const mapDispatchToProps = {
-    loadEntities,
-    setSearchText,
-    setArchived,
-    setCollectionArchived
-}
+  loadEntities,
+  setSearchText,
+  setArchived,
+  setCollectionArchived,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class Archive extends Component {
-    componentWillMount() {
-        this.loadEntities();
-    }
-    loadEntities() {
-        this.props.loadEntities("cards", { f: "archived" });
-        this.props.loadEntities("collections", { archived: true });
-    }
-    render () {
-        const { archivedCards, archivedCollections, isAdmin } = this.props;
-        const items = [
-            ...archivedCollections.map(collection => ({ type: "collection", ...collection })),
-            ...archivedCards.map(card => ({ type: "card", ...card }))
-        ]//.sort((a,b) => a.updated_at.valueOf() - b.updated_at.valueOf()))
+  componentWillMount() {
+    this.loadEntities();
+  }
+  loadEntities() {
+    this.props.loadEntities("cards", { f: "archived" });
+    this.props.loadEntities("collections", { archived: true });
+  }
+  render() {
+    const { archivedCards, archivedCollections, isAdmin } = this.props;
+    const items = [
+      ...archivedCollections.map(collection => ({
+        type: "collection",
+        ...collection,
+      })),
+      ...archivedCards.map(card => ({ type: "card", ...card })),
+    ]; //.sort((a,b) => a.updated_at.valueOf() - b.updated_at.valueOf()))
 
-        return (
-            <div className="px4 pt3">
-                <div className="flex align-center mb2">
-                    <HeaderWithBack name={t`Archive`} />
-                </div>
-                <SearchHeader searchText={this.props.searchText} setSearchText={this.props.setSearchText} />
-                <div>
-                    { items.map(item =>
-                        item.type === "collection" ?
-                            <ArchivedItem key={item.type + item.id} name={item.name} type="collection" icon="collection" color={item.color} isAdmin={isAdmin} onUnarchive={async () => {
-                                await this.props.setCollectionArchived(item.id, false);
-                                this.loadEntities()
-                            }} />
-                        : item.type === "card" ?
-                            <ArchivedItem key={item.type + item.id} name={item.name} type="card" icon={visualizations.get(item.display).iconName} isAdmin={isAdmin} onUnarchive={async () => {
-                                await this.props.setArchived(item.id, false, true);
-                                this.loadEntities();
-                            }} />
-                        :
-                            null
-                    )}
-                </div>
-            </div>
-        );
-    }
+    return (
+      <div className="px4 pt3">
+        <div className="flex align-center mb2">
+          <HeaderWithBack name={t`Archive`} />
+        </div>
+        <SearchHeader
+          searchText={this.props.searchText}
+          setSearchText={this.props.setSearchText}
+        />
+        <div>
+          {items.map(
+            item =>
+              item.type === "collection" ? (
+                <ArchivedItem
+                  key={item.type + item.id}
+                  name={item.name}
+                  type="collection"
+                  icon="collection"
+                  color={item.color}
+                  isAdmin={isAdmin}
+                  onUnarchive={async () => {
+                    await this.props.setCollectionArchived(item.id, false);
+                    this.loadEntities();
+                  }}
+                />
+              ) : item.type === "card" ? (
+                <ArchivedItem
+                  key={item.type + item.id}
+                  name={item.name}
+                  type="card"
+                  icon={visualizations.get(item.display).iconName}
+                  isAdmin={isAdmin}
+                  onUnarchive={async () => {
+                    await this.props.setArchived(item.id, false, true);
+                    this.loadEntities();
+                  }}
+                />
+              ) : null,
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
index 96c59f30f62c9cf1b626829996e07d338ba7982c..f3d7ea8feb79e39221df14b1bfb67ec36cf2dad1 100644
--- a/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
+++ b/frontend/src/metabase/questions/containers/ArchiveCollectionWidget.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger";
 import Button from "metabase/components/Button";
 import Icon from "metabase/components/Icon";
@@ -8,52 +8,51 @@ import Tooltip from "metabase/components/Tooltip";
 
 import { setCollectionArchived } from "../collections";
 
-const mapStateToProps = (state, props) => ({
-})
+const mapStateToProps = (state, props) => ({});
 
 const mapDispatchToProps = {
-    setCollectionArchived
-}
+  setCollectionArchived,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class ArchiveCollectionWidget extends Component {
-    _onArchive = async () => {
-        try {
-            await this.props.setCollectionArchived(this.props.collectionId, true);
-            this._onClose();
-            if (this.props.onArchived) {
-                this.props.onArchived();
-            }
-        } catch (error) {
-            console.error(error)
-            this.setState({ error })
-        }
+  _onArchive = async () => {
+    try {
+      await this.props.setCollectionArchived(this.props.collectionId, true);
+      this._onClose();
+      if (this.props.onArchived) {
+        this.props.onArchived();
+      }
+    } catch (error) {
+      console.error(error);
+      this.setState({ error });
     }
+  };
 
-    _onClose = () => {
-        if (this.refs.modal) {
-            this.refs.modal.close();
-        }
+  _onClose = () => {
+    if (this.refs.modal) {
+      this.refs.modal.close();
     }
+  };
 
-    render() {
-        return (
-            <ModalWithTrigger
-                {...this.props}
-                ref="modal"
-                triggerElement={
-                    <Tooltip tooltip={t`Archive collection`}>
-                        <Icon size={18} name="archive" />
-                    </Tooltip>
-                }
-                title={t`Archive this collection?`}
-                footer={[
-                    <Button onClick={this._onClose}>{t`Cancel`}</Button>,
-                    <Button warning onClick={this._onArchive}>{t`Archive`}</Button>
-                ]}
-            >
-                <div className="px4 pb4">{t`The saved questions in this collection will also be archived.`}</div>
-            </ModalWithTrigger>
-        );
-    }
+  render() {
+    return (
+      <ModalWithTrigger
+        {...this.props}
+        ref="modal"
+        triggerElement={
+          <Tooltip tooltip={t`Archive collection`}>
+            <Icon size={18} name="archive" />
+          </Tooltip>
+        }
+        title={t`Archive this collection?`}
+        footer={[
+          <Button onClick={this._onClose}>{t`Cancel`}</Button>,
+          <Button warning onClick={this._onArchive}>{t`Archive`}</Button>,
+        ]}
+      >
+        <div className="px4 pb4">{t`The saved questions in this collection will also be archived.`}</div>
+      </ModalWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/CollectionCreate.jsx b/frontend/src/metabase/questions/containers/CollectionCreate.jsx
index edb1e6a93591ac30b71f849e12987d92034fa286..99a6814fbbd6510269ef41df55e83572fe1df1a9 100644
--- a/frontend/src/metabase/questions/containers/CollectionCreate.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionCreate.jsx
@@ -8,23 +8,23 @@ import CollectionEditorForm from "./CollectionEditorForm.jsx";
 import { saveCollection } from "../collections";
 
 const mapStateToProps = (state, props) => ({
-    error: state.collections.error,
-    collection: state.collections.collection,
+  error: state.collections.error,
+  collection: state.collections.collection,
 });
 
 const mapDispatchToProps = {
-    saveCollection,
-    onClose: () => push("/questions")
-}
+  saveCollection,
+  onClose: () => push("/questions"),
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class CollectionEdit extends Component {
-    render() {
-        return (
-            <CollectionEditorForm
-                {...this.props}
-                onSubmit={this.props.saveCollection}
-            />
-        );
-    }
+  render() {
+    return (
+      <CollectionEditorForm
+        {...this.props}
+        onSubmit={this.props.saveCollection}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/CollectionEdit.jsx b/frontend/src/metabase/questions/containers/CollectionEdit.jsx
index b297607024bd6d59d9fe4f7aaa00af56314bc42d..8b30faf534548f0ed41f1930ae408c636c07f978 100644
--- a/frontend/src/metabase/questions/containers/CollectionEdit.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionEdit.jsx
@@ -8,28 +8,28 @@ import CollectionEditorForm from "./CollectionEditorForm.jsx";
 import { saveCollection, loadCollection } from "../collections";
 
 const mapStateToProps = (state, props) => ({
-    error: state.collections.error,
-    collection: state.collections.collection,
+  error: state.collections.error,
+  collection: state.collections.collection,
 });
 
 const mapDispatchToProps = {
-    loadCollection,
-    saveCollection,
-    onClose: goBack
-}
+  loadCollection,
+  saveCollection,
+  onClose: goBack,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class CollectionEdit extends Component {
-    componentWillMount() {
-        this.props.loadCollection(this.props.params.collectionId);
-    }
-    render() {
-        return (
-            <CollectionEditorForm
-                {...this.props}
-                onSubmit={this.props.saveCollection}
-                initialValues={this.props.collection}
-            />
-        );
-    }
+  componentWillMount() {
+    this.props.loadCollection(this.props.params.collectionId);
+  }
+  render() {
+    return (
+      <CollectionEditorForm
+        {...this.props}
+        onSubmit={this.props.saveCollection}
+        initialValues={this.props.collection}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx b/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
index a915c4fae5ce1dc8d25d4357801b038e9abf150e..95ffa5e0a4b76f7216c65cfd6b92be1e5e6316d1 100644
--- a/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionEditorForm.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Button from "metabase/components/Button";
 import ColorPicker from "metabase/components/ColorPicker";
 import FormField from "metabase/components/FormField";
@@ -11,95 +11,90 @@ import { reduxForm } from "redux-form";
 import { normal, getRandomColor } from "metabase/lib/colors";
 
 const formConfig = {
-    form: 'collection',
-    fields: ['id', 'name', 'description', 'color'],
-    validate: (values) => {
-        const errors = {};
-        if (!values.name) {
-            errors.name = t`Name is required`;
-        } else if (values.name.length > 100) {
-            errors.name = t`Name must be 100 characters or less`;
-        }
-        if (!values.color) {
-            errors.color = t`Color is required`;
-        }
-        return errors;
-    },
-    initialValues: {
-        name: "",
-        description: "",
-        // pick a random color to start so everything isn't blue all the time
-        color: getRandomColor(normal)
+  form: "collection",
+  fields: ["id", "name", "description", "color"],
+  validate: values => {
+    const errors = {};
+    if (!values.name) {
+      errors.name = t`Name is required`;
+    } else if (values.name.length > 100) {
+      errors.name = t`Name must be 100 characters or less`;
     }
-}
+    if (!values.color) {
+      errors.color = t`Color is required`;
+    }
+    return errors;
+  },
+  initialValues: {
+    name: "",
+    description: "",
+    // pick a random color to start so everything isn't blue all the time
+    color: getRandomColor(normal),
+  },
+};
 
 export const getFormTitle = ({ id, name }) =>
-    id.value ? name.value : t`New collection`
-
-export const getActionText = ({ id }) =>
-    id.value ? t`Update`: t`Create`
+  id.value ? name.value : t`New collection`;
 
+export const getActionText = ({ id }) => (id.value ? t`Update` : t`Create`);
 
-export const CollectionEditorFormActions = ({ handleSubmit, invalid, onClose, fields}) =>
-    <div>
-        <Button className="mr1" onClick={onClose}>
-            {t`Cancel`}
-        </Button>
-        <Button primary disabled={invalid} onClick={handleSubmit}>
-            { getActionText(fields) }
-        </Button>
-    </div>
+export const CollectionEditorFormActions = ({
+  handleSubmit,
+  invalid,
+  onClose,
+  fields,
+}) => (
+  <div>
+    <Button className="mr1" onClick={onClose}>
+      {t`Cancel`}
+    </Button>
+    <Button primary disabled={invalid} onClick={handleSubmit}>
+      {getActionText(fields)}
+    </Button>
+  </div>
+);
 
 export class CollectionEditorForm extends Component {
-    props: {
-        fields: Object,
-        onClose: Function,
-        invalid: Boolean,
-        handleSubmit: Function,
-    }
+  props: {
+    fields: Object,
+    onClose: Function,
+    invalid: Boolean,
+    handleSubmit: Function,
+  };
 
-    render() {
-        const { fields, onClose } = this.props;
-        return (
-            <Modal
-                inline
-                form
-                title={getFormTitle(fields)}
-                footer={<CollectionEditorFormActions {...this.props} />}
-                onClose={onClose}
-            >
-                <div className="NewForm ml-auto mr-auto mt4 pt2" style={{ width: 540 }}>
-                    <FormField
-                        displayName={t`Name`}
-                        {...fields.name}
-                    >
-                        <Input
-                            className="Form-input full"
-                            placeholder={t`My new fantastic collection`}
-                            autoFocus
-                            {...fields.name}
-                        />
-                    </FormField>
-                    <FormField
-                        displayName={t`Description`}
-                        {...fields.description}
-                    >
-                        <textarea
-                            className="Form-input full"
-                            placeholder={t`It's optional but oh, so helpful`}
-                            {...fields.description}
-                        />
-                    </FormField>
-                    <FormField
-                        displayName={t`Color`}
-                        {...fields.color}
-                    >
-                        <ColorPicker {...fields.color} />
-                    </FormField>
-                </div>
-            </Modal>
-        )
-    }
+  render() {
+    const { fields, onClose } = this.props;
+    return (
+      <Modal
+        inline
+        form
+        title={getFormTitle(fields)}
+        footer={<CollectionEditorFormActions {...this.props} />}
+        onClose={onClose}
+      >
+        <div className="NewForm ml-auto mr-auto mt4 pt2" style={{ width: 540 }}>
+          <FormField displayName={t`Name`} {...fields.name}>
+            <Input
+              className="Form-input full"
+              placeholder={t`My new fantastic collection`}
+              autoFocus
+              {...fields.name}
+            />
+          </FormField>
+          <FormField displayName={t`Description`} {...fields.description}>
+            <textarea
+              className="Form-input full"
+              placeholder={t`It's optional but oh, so helpful`}
+              {...fields.description}
+            />
+          </FormField>
+          <FormField displayName={t`Color`} {...fields.color}>
+            <ColorPicker {...fields.color} />
+          </FormField>
+        </div>
+      </Modal>
+    );
+  }
 }
 
-export default reduxForm(formConfig)(CollectionEditorForm)
+export default reduxForm(formConfig)(CollectionEditorForm);
diff --git a/frontend/src/metabase/questions/containers/CollectionList.jsx b/frontend/src/metabase/questions/containers/CollectionList.jsx
index 649ae5051536a7b33df1c0b8497401ff9f73f341..b454d93bb2fb59c016c8a449ff63989c9c411c78 100644
--- a/frontend/src/metabase/questions/containers/CollectionList.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionList.jsx
@@ -5,22 +5,24 @@ import { getAllCollections, getWritableCollections } from "../selectors";
 import { loadCollections } from "../collections";
 
 const mapStateToProps = (state, props) => ({
-    collections: props.writable ? getWritableCollections(state, props) : getAllCollections(state, props)
-})
+  collections: props.writable
+    ? getWritableCollections(state, props)
+    : getAllCollections(state, props),
+});
 
 const mapDispatchToProps = {
-    loadCollections
-}
+  loadCollections,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 class Collections extends Component {
-    componentWillMount() {
-        this.props.loadCollections();
-    }
-    render () {
-        const collectionList = this.props.children(this.props.collections)
-        return collectionList && React.Children.only(collectionList);
-    }
+  componentWillMount() {
+    this.props.loadCollections();
+  }
+  render() {
+    const collectionList = this.props.children(this.props.collections);
+    return collectionList && React.Children.only(collectionList);
+  }
 }
 
 export default Collections;
diff --git a/frontend/src/metabase/questions/containers/CollectionPage.jsx b/frontend/src/metabase/questions/containers/CollectionPage.jsx
index 7620c7c10bde4ce252da3e1bcbb6c3af985dbebc..5474f7849e14af2f174164c0411f79f6edcdf81e 100644
--- a/frontend/src/metabase/questions/containers/CollectionPage.jsx
+++ b/frontend/src/metabase/questions/containers/CollectionPage.jsx
@@ -2,7 +2,7 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { push, replace, goBack } from "react-router-redux";
 import title from "metabase/hoc/Title";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon";
 import HeaderWithBack from "metabase/components/HeaderWithBack";
 
@@ -14,62 +14,94 @@ import { loadCollections } from "../collections";
 import _ from "underscore";
 
 const mapStateToProps = (state, props) => ({
-    collection: _.findWhere(state.collections.collections, { slug: props.params.collectionSlug })
-})
+  collection: _.findWhere(state.collections.collections, {
+    slug: props.params.collectionSlug,
+  }),
+});
 
-const mapDispatchToProps = ({
-    push,
-    replace,
-    goBack,
-    goToQuestions: () => push(`/questions`),
-    editCollection: (id) => push(`/collections/${id}`),
-    editPermissions: (id) => push(`/collections/permissions?collectionId=${id}`),
-    loadCollections,
-})
+const mapDispatchToProps = {
+  push,
+  replace,
+  goBack,
+  goToQuestions: () => push(`/questions`),
+  editCollection: id => push(`/collections/${id}`),
+  editPermissions: id => push(`/collections/permissions?collectionId=${id}`),
+  loadCollections,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @title(({ collection }) => collection && collection.name)
 export default class CollectionPage extends Component {
-    componentWillMount () {
-        this.props.loadCollections();
-    }
-    render () {
-        const { collection, params, location, push, replace, goBack } = this.props;
-        const canEdit = collection && collection.can_write;
-        return (
-            <div className="mx4 mt4">
-                <div className="flex align-center">
-                    <HeaderWithBack
-                        name={collection && collection.name}
-                        description={collection && collection.description}
-                        onBack={window.history.length === 1 ?
-                            () => push("/questions") :
-                            () => goBack()
-                        }
-                    />
-                    <div className="ml-auto">
-                        <CollectionActions>
-                            { canEdit && <ArchiveCollectionWidget collectionId={this.props.collection.id} onArchived={this.props.goToQuestions}/> }
-                            { canEdit && <Icon size={18} name="pencil" tooltip={t`Edit collection`} onClick={() => this.props.editCollection(this.props.collection.id)} /> }
-                            { canEdit && <Icon size={18} name="lock" tooltip={t`Set permissions`} onClick={() => this.props.editPermissions(this.props.collection.id)} /> }
-                        </CollectionActions>
-                    </div>
-                </div>
-                <div className="mt4">
-                    <EntityList
-                        defaultEmptyState={t`No questions have been added to this collection yet.`}
-                        entityType="cards"
-                        entityQuery={{ f: "all", collection: params.collectionSlug, ...location.query }}
-                        // use replace when changing sections so back button still takes you back to collections page
-                        onChangeSection={(section) => replace({
-                            ...location,
-                            query: { ...location.query, f: section }
-                        })}
-                        showCollectionName={false}
-                        editable={canEdit}
-                    />
-                </div>
-            </div>
-        );
-    }
+  componentWillMount() {
+    this.props.loadCollections();
+  }
+  render() {
+    const { collection, params, location, push, replace, goBack } = this.props;
+    const canEdit = collection && collection.can_write;
+    return (
+      <div className="mx4 mt4">
+        <div className="flex align-center">
+          <HeaderWithBack
+            name={collection && collection.name}
+            description={collection && collection.description}
+            onBack={
+              window.history.length === 1
+                ? () => push("/questions")
+                : () => goBack()
+            }
+          />
+          <div className="ml-auto">
+            <CollectionActions>
+              {canEdit && (
+                <ArchiveCollectionWidget
+                  collectionId={this.props.collection.id}
+                  onArchived={this.props.goToQuestions}
+                />
+              )}
+              {canEdit && (
+                <Icon
+                  size={18}
+                  name="pencil"
+                  tooltip={t`Edit collection`}
+                  onClick={() =>
+                    this.props.editCollection(this.props.collection.id)
+                  }
+                />
+              )}
+              {canEdit && (
+                <Icon
+                  size={18}
+                  name="lock"
+                  tooltip={t`Set permissions`}
+                  onClick={() =>
+                    this.props.editPermissions(this.props.collection.id)
+                  }
+                />
+              )}
+            </CollectionActions>
+          </div>
+        </div>
+        <div className="mt4">
+          <EntityList
+            defaultEmptyState={t`No questions have been added to this collection yet.`}
+            entityType="cards"
+            entityQuery={{
+              f: "all",
+              collection: params.collectionSlug,
+              ...location.query,
+            }}
+            // use replace when changing sections so back button still takes you back to collections page
+            onChangeSection={section =>
+              replace({
+                ...location,
+                query: { ...location.query, f: section },
+              })
+            }
+            showCollectionName={false}
+            editable={canEdit}
+          />
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/EditLabels.css b/frontend/src/metabase/questions/containers/EditLabels.css
index 25e00fa35022b434612b23f31ecd58aecec54ff2..9cd0efd8f6dc24b299a2ac89bb6439a25ca190aa 100644
--- a/frontend/src/metabase/questions/containers/EditLabels.css
+++ b/frontend/src/metabase/questions/containers/EditLabels.css
@@ -1,51 +1,51 @@
-@import '../Questions.css';
+@import "../Questions.css";
 
 :local(.header) {
-    composes: header from "../Questions.css";
+  composes: header from "../Questions.css";
 }
 
 :local(.editor) {
-  composes: flex-full from "style"
+  composes: flex-full from "style";
 }
 
 :local(.list) {
-    composes: pt2 from "style";
+  composes: pt2 from "style";
 }
 
 :local(.label),
 :local(.labelEditing) {
-    composes: flex align-center from "style";
-    composes: border-top from "style";
-    composes: py2 from "style";
+  composes: flex align-center from "style";
+  composes: border-top from "style";
+  composes: py2 from "style";
 }
 
 :local(.label) {
-    composes: pl1 from "style";
+  composes: pl1 from "style";
 }
 
 :local(.name) {
-    composes: flex-full from "style";
-    composes: ml4 from "style";
-    font-size: 18px;
-    color: var(--title-color);
+  composes: flex-full from "style";
+  composes: ml4 from "style";
+  font-size: 18px;
+  color: var(--title-color);
 }
 
 :local(.edit) {
-    font-weight: bold;
-    composes: pr2 from "style";
-    color: var(--brand-color);
+  font-weight: bold;
+  composes: pr2 from "style";
+  color: var(--brand-color);
 }
 
 :local(.delete) {
-    composes: cursor-pointer from "style";
+  composes: cursor-pointer from "style";
 }
 
 :local(.delete),
 :local(.edit) {
-    visibility: hidden;
+  visibility: hidden;
 }
 
 :local(.label):hover :local(.delete),
 :local(.label):hover :local(.edit) {
-    visibility: visible;;
+  visibility: visible;
 }
diff --git a/frontend/src/metabase/questions/containers/EditLabels.jsx b/frontend/src/metabase/questions/containers/EditLabels.jsx
index fc56745bfce92748d0ac68f363daea0e62e4cc39..56ab31cc5d43b037ab456d4aa9b0142d04d58710 100644
--- a/frontend/src/metabase/questions/containers/EditLabels.jsx
+++ b/frontend/src/metabase/questions/containers/EditLabels.jsx
@@ -2,28 +2,33 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./EditLabels.css";
 
 import Confirm from "metabase/components/Confirm.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
 import * as labelsActions from "../labels";
-import { getLabels, getLabelsLoading, getLabelsError, getEditingLabelId } from "../selectors";
+import {
+  getLabels,
+  getLabelsLoading,
+  getLabelsError,
+  getEditingLabelId,
+} from "../selectors";
 
 import * as colors from "metabase/lib/colors";
 
 const mapStateToProps = (state, props) => {
   return {
-      labels:           getLabels(state, props),
-      labelsLoading:    getLabelsLoading(state, props),
-      labelsError:      getLabelsError(state, props),
-      editingLabelId:   getEditingLabelId(state, props)
-  }
-}
+    labels: getLabels(state, props),
+    labelsLoading: getLabelsLoading(state, props),
+    labelsError: getLabelsError(state, props),
+    editingLabelId: getEditingLabelId(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    ...labelsActions
+  ...labelsActions,
 };
 
 import Icon from "metabase/components/Icon.jsx";
@@ -35,63 +40,111 @@ import EmptyState from "metabase/components/EmptyState.jsx";
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class EditLabels extends Component {
-    static propTypes = {
-        style:          PropTypes.object,
-        labels:         PropTypes.array.isRequired,
-        labelsLoading:  PropTypes.bool.isRequired,
-        labelsError:    PropTypes.any,
-        editingLabelId: PropTypes.number,
-        saveLabel:      PropTypes.func.isRequired,
-        editLabel:      PropTypes.func.isRequired,
-        deleteLabel:    PropTypes.func.isRequired,
-        loadLabels:     PropTypes.func.isRequired
-    };
+  static propTypes = {
+    style: PropTypes.object,
+    labels: PropTypes.array.isRequired,
+    labelsLoading: PropTypes.bool.isRequired,
+    labelsError: PropTypes.any,
+    editingLabelId: PropTypes.number,
+    saveLabel: PropTypes.func.isRequired,
+    editLabel: PropTypes.func.isRequired,
+    deleteLabel: PropTypes.func.isRequired,
+    loadLabels: PropTypes.func.isRequired,
+  };
 
-    componentWillMount() {
-        this.props.loadLabels();
-    }
+  componentWillMount() {
+    this.props.loadLabels();
+  }
 
-    render() {
-        const { style, labels, labelsLoading, labelsError, editingLabelId, saveLabel, editLabel, deleteLabel } = this.props;
-        return (
-            <div className={S.editor} style={style}>
-                <div className="wrapper wrapper--trim">
-                    <div className={S.header}>{t`Add and edit labels`}</div>
-                    <div className="bordered border-error rounded p2 mb2">
-                        <h3 className="text-error mb1">{t`Heads up!`}</h3>
-                        <div>{t`In an upcoming release, Labels will be removed in favor of Collections.`}</div>
-                    </div>
-                </div>
-                <LabelEditorForm onSubmit={saveLabel} initialValues={{ icon: colors.normal.blue, name: "" }} submitButtonText={t`Create Label`} className="wrapper wrapper--trim"/>
-                <LoadingAndErrorWrapper loading={labelsLoading} error={labelsError} noBackground noWrapper>
-                { () => labels.length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <ul className={S.list}>
-                        { labels.map(label =>
-                            editingLabelId === label.id ?
-                                <li key={label.id} className={S.labelEditing}>
-                                    <LabelEditorForm formKey={String(label.id)} className="flex-full" onSubmit={saveLabel} initialValues={label} submitButtonText={t`Update Label`}/>
-                                    <a className={" text-grey-1 text-grey-4-hover ml2"} onClick={() => editLabel(null)}>{t`Cancel`}</a>
-                                </li>
-                            :
-                                <li key={label.id} className={S.label}>
-                                    <LabelIcon icon={label.icon} size={28} />
-                                    <span className={S.name}>{label.name}</span>
-                                    <a className={S.edit} onClick={() => editLabel(label.id)}>{t`Edit`}</a>
-                                    <Confirm title={t`Delete label "${label.name}"`} action={() => deleteLabel(label.id)}>
-                                        <Icon className={S.delete + " text-grey-1 text-grey-4-hover"} name="close" size={14} />
-                                    </Confirm>
-                                </li>
-                        )}
-                        </ul>
-                    </div>
-                :
-                    <div className="full-height full flex-full flex align-center justify-center">
-                        <EmptyState message={t`Create labels to group and manage questions.`} icon="label" />
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        );
-    }
+  render() {
+    const {
+      style,
+      labels,
+      labelsLoading,
+      labelsError,
+      editingLabelId,
+      saveLabel,
+      editLabel,
+      deleteLabel,
+    } = this.props;
+    return (
+      <div className={S.editor} style={style}>
+        <div className="wrapper wrapper--trim">
+          <div className={S.header}>{t`Add and edit labels`}</div>
+          <div className="bordered border-error rounded p2 mb2">
+            <h3 className="text-error mb1">{t`Heads up!`}</h3>
+            <div
+            >{t`In an upcoming release, Labels will be removed in favor of Collections.`}</div>
+          </div>
+        </div>
+        <LabelEditorForm
+          onSubmit={saveLabel}
+          initialValues={{ icon: colors.normal.blue, name: "" }}
+          submitButtonText={t`Create Label`}
+          className="wrapper wrapper--trim"
+        />
+        <LoadingAndErrorWrapper
+          loading={labelsLoading}
+          error={labelsError}
+          noBackground
+          noWrapper
+        >
+          {() =>
+            labels.length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <ul className={S.list}>
+                  {labels.map(
+                    label =>
+                      editingLabelId === label.id ? (
+                        <li key={label.id} className={S.labelEditing}>
+                          <LabelEditorForm
+                            formKey={String(label.id)}
+                            className="flex-full"
+                            onSubmit={saveLabel}
+                            initialValues={label}
+                            submitButtonText={t`Update Label`}
+                          />
+                          <a
+                            className={" text-grey-1 text-grey-4-hover ml2"}
+                            onClick={() => editLabel(null)}
+                          >{t`Cancel`}</a>
+                        </li>
+                      ) : (
+                        <li key={label.id} className={S.label}>
+                          <LabelIcon icon={label.icon} size={28} />
+                          <span className={S.name}>{label.name}</span>
+                          <a
+                            className={S.edit}
+                            onClick={() => editLabel(label.id)}
+                          >{t`Edit`}</a>
+                          <Confirm
+                            title={t`Delete label "${label.name}"`}
+                            action={() => deleteLabel(label.id)}
+                          >
+                            <Icon
+                              className={
+                                S.delete + " text-grey-1 text-grey-4-hover"
+                              }
+                              name="close"
+                              size={14}
+                            />
+                          </Confirm>
+                        </li>
+                      ),
+                  )}
+                </ul>
+              </div>
+            ) : (
+              <div className="full-height full flex-full flex align-center justify-center">
+                <EmptyState
+                  message={t`Create labels to group and manage questions.`}
+                  icon="label"
+                />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/EntityItem.jsx b/frontend/src/metabase/questions/containers/EntityItem.jsx
index 12b839966c9e02d383383c0620486ac611535880..902e87e0eeb5a79fe43cf445903c0f39c1b67fc1 100644
--- a/frontend/src/metabase/questions/containers/EntityItem.jsx
+++ b/frontend/src/metabase/questions/containers/EntityItem.jsx
@@ -9,49 +9,61 @@ import { setItemSelected, setFavorited, setArchived } from "../questions";
 import { makeGetItem } from "../selectors";
 
 const makeMapStateToProps = () => {
-    const getItem = makeGetItem()
-    const mapStateToProps = (state, props) => {
-        return {
-            item: getItem(state, props)
-        };
+  const getItem = makeGetItem();
+  const mapStateToProps = (state, props) => {
+    return {
+      item: getItem(state, props),
     };
-    return mapStateToProps;
-}
+  };
+  return mapStateToProps;
+};
 
 const mapDispatchToProps = {
-    setItemSelected,
-    setFavorited,
-    setArchived
+  setItemSelected,
+  setFavorited,
+  setArchived,
 };
 
 @connect(makeMapStateToProps, mapDispatchToProps)
 export default class EntityItem extends Component {
-    static propTypes = {
-        item:               PropTypes.object.isRequired,
-        setItemSelected:    PropTypes.func.isRequired,
-        setFavorited:       PropTypes.func.isRequired,
-        setArchived:        PropTypes.func.isRequired,
-        editable:           PropTypes.bool,
-        showCollectionName: PropTypes.bool,
-        onEntityClick:      PropTypes.func,
-        onMove:             PropTypes.func,
-    };
+  static propTypes = {
+    item: PropTypes.object.isRequired,
+    setItemSelected: PropTypes.func.isRequired,
+    setFavorited: PropTypes.func.isRequired,
+    setArchived: PropTypes.func.isRequired,
+    editable: PropTypes.bool,
+    showCollectionName: PropTypes.bool,
+    onEntityClick: PropTypes.func,
+    onMove: PropTypes.func,
+  };
 
-    render() {
-        let { item, editable, setItemSelected, setFavorited, setArchived, onMove, onEntityClick, showCollectionName } = this.props;
-        return (
-            <li className="relative" style={{ display: item.visible ? undefined : "none" }}>
-                <Item
-                    setItemSelected={editable ? setItemSelected : null}
-                    setFavorited={editable ? setFavorited : null}
-                    setArchived={editable ? setArchived : null}
-                    onMove={editable ? onMove : null}
-                    onEntityClick={onEntityClick}
-                    showCollectionName={showCollectionName}
-                    entity={item}
-                    {...item}
-                />
-            </li>
-        )
-    }
+  render() {
+    let {
+      item,
+      editable,
+      setItemSelected,
+      setFavorited,
+      setArchived,
+      onMove,
+      onEntityClick,
+      showCollectionName,
+    } = this.props;
+    return (
+      <li
+        className="relative"
+        style={{ display: item.visible ? undefined : "none" }}
+      >
+        <Item
+          setItemSelected={editable ? setItemSelected : null}
+          setFavorited={editable ? setFavorited : null}
+          setArchived={editable ? setArchived : null}
+          onMove={editable ? onMove : null}
+          onEntityClick={onEntityClick}
+          showCollectionName={showCollectionName}
+          entity={item}
+          {...item}
+        />
+      </li>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/EntityList.jsx b/frontend/src/metabase/questions/containers/EntityList.jsx
index f42ce4b667bebe67fb1ad5a4b1039b9cbfa4c229..9242e6a9c51af43d2c82609c0c67801d895d3450 100644
--- a/frontend/src/metabase/questions/containers/EntityList.jsx
+++ b/frontend/src/metabase/questions/containers/EntityList.jsx
@@ -3,10 +3,10 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import EmptyState from "metabase/components/EmptyState";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
-import ListFilterWidget from "metabase/components/ListFilterWidget"
+import ListFilterWidget from "metabase/components/ListFilterWidget";
 
 import S from "../components/List.css";
 
@@ -16,223 +16,263 @@ import ActionHeader from "../components/ActionHeader";
 
 import _ from "underscore";
 
-import { loadEntities, setSearchText, setItemSelected, setAllSelected, setArchived } from "../questions";
+import {
+  loadEntities,
+  setSearchText,
+  setItemSelected,
+  setAllSelected,
+  setArchived,
+} from "../questions";
 import { loadLabels } from "../labels";
 import {
-    getSection, getEntityIds,
-    getSectionLoading, getSectionError,
-    getSearchText,
-    getVisibleCount, getSelectedCount, getAllAreSelected, getSectionIsArchive,
-    getLabelsWithSelectedState
+  getSection,
+  getEntityIds,
+  getSectionLoading,
+  getSectionError,
+  getSearchText,
+  getVisibleCount,
+  getSelectedCount,
+  getAllAreSelected,
+  getSectionIsArchive,
+  getLabelsWithSelectedState,
 } from "../selectors";
 
-
 const mapStateToProps = (state, props) => {
   return {
-      section:          getSection(state, props),
-      entityIds:        getEntityIds(state, props),
-      loading:          getSectionLoading(state, props),
-      error:            getSectionError(state, props),
+    section: getSection(state, props),
+    entityIds: getEntityIds(state, props),
+    loading: getSectionLoading(state, props),
+    error: getSectionError(state, props),
 
-      searchText:       getSearchText(state, props),
+    searchText: getSearchText(state, props),
 
-      visibleCount:     getVisibleCount(state, props),
-      selectedCount:    getSelectedCount(state, props),
-      allAreSelected:   getAllAreSelected(state, props),
-      sectionIsArchive: getSectionIsArchive(state, props),
+    visibleCount: getVisibleCount(state, props),
+    selectedCount: getSelectedCount(state, props),
+    allAreSelected: getAllAreSelected(state, props),
+    sectionIsArchive: getSectionIsArchive(state, props),
 
-      labels:           getLabelsWithSelectedState(state, props),
-  }
-}
+    labels: getLabelsWithSelectedState(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    setItemSelected,
-    setAllSelected,
-    setSearchText,
-    setArchived,
-    loadEntities,
-    loadLabels
-}
+  setItemSelected,
+  setAllSelected,
+  setSearchText,
+  setArchived,
+  loadEntities,
+  loadLabels,
+};
 
 const SECTIONS = [
-    {
-        id: 'all',
-        name: t`All questions`,
-        icon: 'all',
-        empty: t`No questions have been saved yet.`,
-    },
-    {
-        id: 'fav',
-        name: t`Favorites`,
-        icon: 'star',
-        empty: t`You haven't favorited any questions yet.`,
-    },
-    {
-        id: 'recent',
-        name: t`Recently viewed`,
-        icon: 'recents',
-        empty: t`You haven't viewed any questions recently.`,
-    },
-    {
-        id: 'mine',
-        name: t`Saved by me`,
-        icon: 'mine',
-        empty:  t`You haven't saved any questions yet.`
-    },
-    {
-        id: 'popular',
-        name: t`Most popular`,
-        icon: 'popular',
-        empty: t`The most-viewed questions across your company will show up here.`,
-    },
-    {
-        id: 'archived',
-        name: t`Archive`,
-        icon: 'archive',
-        empty: t`If you no longer need a question, you can archive it.`
-    }
+  {
+    id: "all",
+    name: t`All questions`,
+    icon: "all",
+    empty: t`No questions have been saved yet.`,
+  },
+  {
+    id: "fav",
+    name: t`Favorites`,
+    icon: "star",
+    empty: t`You haven't favorited any questions yet.`,
+  },
+  {
+    id: "recent",
+    name: t`Recently viewed`,
+    icon: "recents",
+    empty: t`You haven't viewed any questions recently.`,
+  },
+  {
+    id: "mine",
+    name: t`Saved by me`,
+    icon: "mine",
+    empty: t`You haven't saved any questions yet.`,
+  },
+  {
+    id: "popular",
+    name: t`Most popular`,
+    icon: "popular",
+    empty: t`The most-viewed questions across your company will show up here.`,
+  },
+  {
+    id: "archived",
+    name: t`Archive`,
+    icon: "archive",
+    empty: t`If you no longer need a question, you can archive it.`,
+  },
 ];
 
 const DEFAULT_SECTION = {
-    icon: 'all',
-    empty: t`There aren't any questions matching that criteria.`
-}
+  icon: "all",
+  empty: t`There aren't any questions matching that criteria.`,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class EntityList extends Component {
-    static propTypes = {
-        style:              PropTypes.object,
-
-        entityQuery:        PropTypes.object.isRequired,
-        entityType:         PropTypes.string.isRequired,
-
-        section:            PropTypes.string,
-        loading:            PropTypes.bool.isRequired,
-        error:              PropTypes.any,
-        entityIds:          PropTypes.array.isRequired,
-        searchText:         PropTypes.string.isRequired,
-        setSearchText:      PropTypes.func.isRequired,
-        visibleCount:       PropTypes.number.isRequired,
-        selectedCount:      PropTypes.number.isRequired,
-        allAreSelected:     PropTypes.bool.isRequired,
-        sectionIsArchive:   PropTypes.bool.isRequired,
-        labels:             PropTypes.array.isRequired,
-        setItemSelected:    PropTypes.func.isRequired,
-        setAllSelected:     PropTypes.func.isRequired,
-        setArchived:        PropTypes.func.isRequired,
-
-        loadEntities:       PropTypes.func.isRequired,
-        loadLabels:         PropTypes.func.isRequired,
-
-        onEntityClick:      PropTypes.func,
-        onChangeSection:    PropTypes.func,
-        showSearchWidget:   PropTypes.bool.isRequired,
-        showCollectionName: PropTypes.bool.isRequired,
-        editable:           PropTypes.bool.isRequired,
-
-        defaultEmptyState:  PropTypes.string
-    };
-
-    static defaultProps = {
-        showSearchWidget: true,
-        showCollectionName: true,
-        editable: true,
-    }
+  static propTypes = {
+    style: PropTypes.object,
 
-    componentDidUpdate(prevProps) {
-        // Scroll to the top of the list if the section changed
-        // A little hacky, something like https://github.com/taion/scroll-behavior might be better
-        if (this.props.section !== prevProps.section) {
-            ReactDOM.findDOMNode(this).scrollTop = 0;
-        }
-    }
+    entityQuery: PropTypes.object.isRequired,
+    entityType: PropTypes.string.isRequired,
 
-    componentWillMount() {
-        this.props.loadLabels();
-        this.props.loadEntities(this.props.entityType, this.props.entityQuery);
-    }
-    componentWillReceiveProps(nextProps) {
-        if (!_.isEqual(this.props.entityQuery, nextProps.entityQuery) || nextProps.entityType !== this.props.entityType) {
-            this.props.loadEntities(nextProps.entityType, nextProps.entityQuery);
-        }
-    }
+    section: PropTypes.string,
+    loading: PropTypes.bool.isRequired,
+    error: PropTypes.any,
+    entityIds: PropTypes.array.isRequired,
+    searchText: PropTypes.string.isRequired,
+    setSearchText: PropTypes.func.isRequired,
+    visibleCount: PropTypes.number.isRequired,
+    selectedCount: PropTypes.number.isRequired,
+    allAreSelected: PropTypes.bool.isRequired,
+    sectionIsArchive: PropTypes.bool.isRequired,
+    labels: PropTypes.array.isRequired,
+    setItemSelected: PropTypes.func.isRequired,
+    setAllSelected: PropTypes.func.isRequired,
+    setArchived: PropTypes.func.isRequired,
+
+    loadEntities: PropTypes.func.isRequired,
+    loadLabels: PropTypes.func.isRequired,
+
+    onEntityClick: PropTypes.func,
+    onChangeSection: PropTypes.func,
+    showSearchWidget: PropTypes.bool.isRequired,
+    showCollectionName: PropTypes.bool.isRequired,
+    editable: PropTypes.bool.isRequired,
 
-    getSection () {
-        return _.findWhere(SECTIONS, { id: this.props.entityQuery && this.props.entityQuery.f || "all" }) || DEFAULT_SECTION;
+    defaultEmptyState: PropTypes.string,
+  };
+
+  static defaultProps = {
+    showSearchWidget: true,
+    showCollectionName: true,
+    editable: true,
+  };
+
+  componentDidUpdate(prevProps) {
+    // Scroll to the top of the list if the section changed
+    // A little hacky, something like https://github.com/taion/scroll-behavior might be better
+    if (this.props.section !== prevProps.section) {
+      ReactDOM.findDOMNode(this).scrollTop = 0;
     }
+  }
 
-    render() {
-        const {
-            style,
-            loading, error,
-            entityType, entityIds,
-            searchText, setSearchText, showSearchWidget,
-            visibleCount, selectedCount, allAreSelected, sectionIsArchive, labels,
-            setItemSelected, setAllSelected, setArchived, onChangeSection,
-            showCollectionName,
-            editable, onEntityClick,
-        } = this.props;
-
-        const section = this.getSection();
-
-
-        const hasEntitiesInPlainState = entityIds.length > 0 || section.section !== "all";
-
-        const showActionHeader = (editable && selectedCount > 0);
-        const showSearchHeader = (hasEntitiesInPlainState && showSearchWidget);
-        const showEntityFilterWidget = onChangeSection;
-
-        return (
-            <div style={style}>
-                { (showActionHeader || showSearchHeader || showEntityFilterWidget) &&
-                    <div className="flex align-center my1" style={{height: 40}}>
-                        { showActionHeader ?
-                            <ActionHeader
-                                visibleCount={visibleCount}
-                                selectedCount={selectedCount}
-                                allAreSelected={allAreSelected}
-                                sectionIsArchive={sectionIsArchive}
-                                setAllSelected={setAllSelected}
-                                setArchived={setArchived}
-                                labels={labels}
-                            />
-                        : showSearchHeader ?
-                                <div style={{marginLeft: "10px"}}>
-                                    <SearchHeader
-                                        searchText={searchText}
-                                        setSearchText={setSearchText}
-                                    />
-                                </div>
-                        :
-                            null
-                      }
-                      { showEntityFilterWidget && hasEntitiesInPlainState &&
-                          <ListFilterWidget
-                              items={SECTIONS.filter(item => item.id !== "archived")}
-                              activeItem={section}
-                              onChange={(item) => onChangeSection(item.id)}
-                          />
-                      }
-                    </div>
-                }
-                <LoadingAndErrorWrapper className="full" loading={!error && loading} error={error}>
-                { () =>
-                    entityIds.length > 0 ?
-                        <List
-                            entityType={entityType}
-                            entityIds={entityIds}
-                            editable={editable}
-                            setItemSelected={setItemSelected}
-                            onEntityClick={onEntityClick}
-                            showCollectionName={showCollectionName}
-                        />
-                    :
-                        <div className={S.empty}>
-                            <EmptyState message={section.id === "all" && this.props.defaultEmptyState ? this.props.defaultEmptyState : section.empty} icon={section.icon} />
-                        </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        );
+  componentWillMount() {
+    this.props.loadLabels();
+    this.props.loadEntities(this.props.entityType, this.props.entityQuery);
+  }
+  componentWillReceiveProps(nextProps) {
+    if (
+      !_.isEqual(this.props.entityQuery, nextProps.entityQuery) ||
+      nextProps.entityType !== this.props.entityType
+    ) {
+      this.props.loadEntities(nextProps.entityType, nextProps.entityQuery);
     }
+  }
+
+  getSection() {
+    return (
+      _.findWhere(SECTIONS, {
+        id: (this.props.entityQuery && this.props.entityQuery.f) || "all",
+      }) || DEFAULT_SECTION
+    );
+  }
+
+  render() {
+    const {
+      style,
+      loading,
+      error,
+      entityType,
+      entityIds,
+      searchText,
+      setSearchText,
+      showSearchWidget,
+      visibleCount,
+      selectedCount,
+      allAreSelected,
+      sectionIsArchive,
+      labels,
+      setItemSelected,
+      setAllSelected,
+      setArchived,
+      onChangeSection,
+      showCollectionName,
+      editable,
+      onEntityClick,
+    } = this.props;
+
+    const section = this.getSection();
+
+    const hasEntitiesInPlainState =
+      entityIds.length > 0 || section.section !== "all";
+
+    const showActionHeader = editable && selectedCount > 0;
+    const showSearchHeader = hasEntitiesInPlainState && showSearchWidget;
+    const showEntityFilterWidget = onChangeSection;
+
+    return (
+      <div style={style}>
+        {(showActionHeader || showSearchHeader || showEntityFilterWidget) && (
+          <div className="flex align-center my1" style={{ height: 40 }}>
+            {showActionHeader ? (
+              <ActionHeader
+                visibleCount={visibleCount}
+                selectedCount={selectedCount}
+                allAreSelected={allAreSelected}
+                sectionIsArchive={sectionIsArchive}
+                setAllSelected={setAllSelected}
+                setArchived={setArchived}
+                labels={labels}
+              />
+            ) : showSearchHeader ? (
+              <div style={{ marginLeft: "10px" }}>
+                <SearchHeader
+                  searchText={searchText}
+                  setSearchText={setSearchText}
+                />
+              </div>
+            ) : null}
+            {showEntityFilterWidget &&
+              hasEntitiesInPlainState && (
+                <ListFilterWidget
+                  items={SECTIONS.filter(item => item.id !== "archived")}
+                  activeItem={section}
+                  onChange={item => onChangeSection(item.id)}
+                />
+              )}
+          </div>
+        )}
+        <LoadingAndErrorWrapper
+          className="full"
+          loading={!error && loading}
+          error={error}
+        >
+          {() =>
+            entityIds.length > 0 ? (
+              <List
+                entityType={entityType}
+                entityIds={entityIds}
+                editable={editable}
+                setItemSelected={setItemSelected}
+                onEntityClick={onEntityClick}
+                showCollectionName={showCollectionName}
+              />
+            ) : (
+              <div className={S.empty}>
+                <EmptyState
+                  message={
+                    section.id === "all" && this.props.defaultEmptyState
+                      ? this.props.defaultEmptyState
+                      : section.empty
+                  }
+                  icon={section.icon}
+                />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/LabelEditorForm.css b/frontend/src/metabase/questions/containers/LabelEditorForm.css
index a8e845c316c7fa5d08ab4debcba01ce406e966c9..c79d8f49fbefed0e11b0d7257e3731e5c087f73b 100644
--- a/frontend/src/metabase/questions/containers/LabelEditorForm.css
+++ b/frontend/src/metabase/questions/containers/LabelEditorForm.css
@@ -1,12 +1,12 @@
 :local(.nameInput) {
-    composes: flex-full ml1 from "style";
-    font-size: 18px;
+  composes: flex-full ml1 from "style";
+  font-size: 18px;
 }
 
 :local(.invalid) {
-    composes: border-error from "style";
+  composes: border-error from "style";
 }
 
 :local(.errorMessage) {
-    composes: px1 pt1 text-bold text-error from "style";
+  composes: px1 pt1 text-bold text-error from "style";
 }
diff --git a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
index 9923d0189f9e8d4d1dc599399bf6838366bb9eb1..15a54097f534ecbfa25a7e1436e60230b2fb6164 100644
--- a/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
+++ b/frontend/src/metabase/questions/containers/LabelEditorForm.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import S from "./LabelEditorForm.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LabelIconPicker from "../components/LabelIconPicker.jsx";
 
 import { reduxForm } from "redux-form";
@@ -10,46 +10,73 @@ import { reduxForm } from "redux-form";
 import cx from "classnames";
 
 @reduxForm({
-    form: 'label',
-    fields: ['icon', 'name', 'id'],
-    validate: (values) => {
-        const errors = {};
-        if (!values.name) {
-            errors.name = true;
-        }
-        if (!values.icon) {
-            errors.icon = t`Icon is required`;
-        }
-        return errors;
+  form: "label",
+  fields: ["icon", "name", "id"],
+  validate: values => {
+    const errors = {};
+    if (!values.name) {
+      errors.name = true;
     }
+    if (!values.icon) {
+      errors.icon = t`Icon is required`;
+    }
+    return errors;
+  },
 })
 export default class LabelEditorForm extends Component {
-    static propTypes = {
-        className:          PropTypes.string,
-        fields:             PropTypes.object.isRequired,
-        invalid:            PropTypes.bool.isRequired,
-        error:              PropTypes.any,
-        submitButtonText:   PropTypes.string.isRequired,
-        handleSubmit:       PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    className: PropTypes.string,
+    fields: PropTypes.object.isRequired,
+    invalid: PropTypes.bool.isRequired,
+    error: PropTypes.any,
+    submitButtonText: PropTypes.string.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+  };
 
-    render() {
-        const { fields: { icon, name }, error, handleSubmit, invalid, className, submitButtonText } = this.props;
-        const nameInvalid = name.invalid && ((name.active && name.value) || (!name.active && name.visited));
-        const errorMessage = name.error || error;
-        return (
-            <form className={className} onSubmit={handleSubmit}>
-                <div className="flex">
-                    <LabelIconPicker {...icon} />
-                    <div className="full">
-                        <div className="flex">
-                          <input className={cx(S.nameInput, "input", { [S.invalid]: nameInvalid })} type="text" placeholder={t`Name`} {...name}/>
-                          <button className={cx("Button", "ml1", { "disabled": invalid, "Button--primary": !invalid })} type="submit">{submitButtonText}</button>
-                        </div>
-                        { nameInvalid && errorMessage && <div className={S.errorMessage}>{errorMessage}</div> }
-                    </div>
-                </div>
-            </form>
-        );
-    }
+  render() {
+    const {
+      fields: { icon, name },
+      error,
+      handleSubmit,
+      invalid,
+      className,
+      submitButtonText,
+    } = this.props;
+    const nameInvalid =
+      name.invalid &&
+      ((name.active && name.value) || (!name.active && name.visited));
+    const errorMessage = name.error || error;
+    return (
+      <form className={className} onSubmit={handleSubmit}>
+        <div className="flex">
+          <LabelIconPicker {...icon} />
+          <div className="full">
+            <div className="flex">
+              <input
+                className={cx(S.nameInput, "input", {
+                  [S.invalid]: nameInvalid,
+                })}
+                type="text"
+                placeholder={t`Name`}
+                {...name}
+              />
+              <button
+                className={cx("Button", "ml1", {
+                  disabled: invalid,
+                  "Button--primary": !invalid,
+                })}
+                type="submit"
+              >
+                {submitButtonText}
+              </button>
+            </div>
+            {nameInvalid &&
+              errorMessage && (
+                <div className={S.errorMessage}>{errorMessage}</div>
+              )}
+          </div>
+        </div>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/LabelPopover.jsx b/frontend/src/metabase/questions/containers/LabelPopover.jsx
index 7530929de177fda583887c2bbbf593e44e74c364..492ce24c8d50cdeb7cc5e05426eec974d0544c84 100644
--- a/frontend/src/metabase/questions/containers/LabelPopover.jsx
+++ b/frontend/src/metabase/questions/containers/LabelPopover.jsx
@@ -11,31 +11,36 @@ import { getLabels } from "../selectors";
 
 const mapStateToProps = (state, props) => {
   return {
-      labels: props.labels || getLabels(state, props)
-  }
-}
+    labels: props.labels || getLabels(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    setLabeled
-}
+  setLabeled,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class LabelPopover extends Component {
-    static propTypes = {
-        labels:     PropTypes.array.isRequired,
-        item:       PropTypes.object,
-        count:      PropTypes.number,
-        setLabeled: PropTypes.func.isRequired,
-    };
+  static propTypes = {
+    labels: PropTypes.array.isRequired,
+    item: PropTypes.object,
+    count: PropTypes.number,
+    setLabeled: PropTypes.func.isRequired,
+  };
 
-    render() {
-        const { labels, setLabeled, item, count } = this.props;
-        return (
-            <PopoverWithTrigger {...this.props}>
-                { () =>
-                    <LabelPicker labels={labels} setLabeled={setLabeled} item={item} count={count} />
-                }
-            </PopoverWithTrigger>
-        );
-    }
+  render() {
+    const { labels, setLabeled, item, count } = this.props;
+    return (
+      <PopoverWithTrigger {...this.props}>
+        {() => (
+          <LabelPicker
+            labels={labels}
+            setLabeled={setLabeled}
+            item={item}
+            count={count}
+          />
+        )}
+      </PopoverWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/questions/containers/MoveToCollection.jsx b/frontend/src/metabase/questions/containers/MoveToCollection.jsx
index afdf563105b04f01ef7b7d001e27c500a331cb7e..5b8dc126adf50c1ccf51a7aa9384a420c67c44da 100644
--- a/frontend/src/metabase/questions/containers/MoveToCollection.jsx
+++ b/frontend/src/metabase/questions/containers/MoveToCollection.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Button from "metabase/components/Button";
 import Icon from "metabase/components/Icon";
 import ModalContent from "metabase/components/ModalContent";
@@ -12,87 +12,103 @@ import cx from "classnames";
 import { setCollection } from "../questions";
 import { loadCollections } from "../collections";
 
-const mapStateToProps = (state, props) => ({
-
-})
+const mapStateToProps = (state, props) => ({});
 
 const mapDispatchToProps = {
-    loadCollections,
-    defaultSetCollection: setCollection
-}
+  loadCollections,
+  defaultSetCollection: setCollection,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MoveToCollection extends Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-            currentCollection: { id:  props.initialCollectionId }
-        }
+  constructor(props) {
+    super(props);
+    this.state = {
+      currentCollection: { id: props.initialCollectionId },
+    };
+  }
 
-    }
+  componentWillMount() {
+    this.props.loadCollections();
+  }
 
-    componentWillMount() {
-        this.props.loadCollections()
+  async onMove(collection) {
+    try {
+      this.setState({ error: null });
+      const setCollection =
+        this.props.setCollection || this.props.defaultSetCollection;
+      await setCollection(this.props.questionId, collection, true);
+      this.props.onClose();
+    } catch (error) {
+      this.setState({ error });
     }
+  }
 
-    async onMove(collection) {
-        try {
-            this.setState({ error: null })
-            const setCollection = this.props.setCollection || this.props.defaultSetCollection
-            await setCollection(this.props.questionId, collection, true);
-            this.props.onClose();
-        } catch (error) {
-            this.setState({ error })
+  render() {
+    const { onClose } = this.props;
+    const { currentCollection, error } = this.state;
+    return (
+      <ModalContent
+        title={t`Which collection should this be in?`}
+        footer={
+          <div>
+            {error && (
+              <span className="text-error mr1">
+                {error.data && error.data.message}
+              </span>
+            )}
+            <Button className="mr1" onClick={onClose}>
+              {t`Cancel`}
+            </Button>
+            <Button
+              primary
+              disabled={currentCollection.id === undefined}
+              onClick={() => this.onMove(currentCollection)}
+            >
+              {t`Move`}
+            </Button>
+          </div>
         }
-    }
-
-    render() {
-        const { onClose } = this.props;
-        const { currentCollection, error } = this.state;
-        return (
-            <ModalContent
-                title={t`Which collection should this be in?`}
-                footer={
-                    <div>
-                        { error &&
-                            <span className="text-error mr1">{error.data && error.data.message}</span>
-                        }
-                        <Button className="mr1" onClick={onClose}>
-                            {t`Cancel`}
-                        </Button>
-                        <Button primary disabled={currentCollection.id === undefined} onClick={() => this.onMove(currentCollection)}>
-                            {t`Move`}
-                        </Button>
-                    </div>
-                }
-                fullPageModal={true}
-                onClose={onClose}
+        fullPageModal={true}
+        onClose={onClose}
+      >
+        <CollectionList writable>
+          {collections => (
+            <ol
+              className="List text-brand ml-auto mr-auto"
+              style={{ width: 520 }}
             >
-                <CollectionList writable>
-                    { collections =>
-                        <ol className="List text-brand ml-auto mr-auto" style={{ width: 520 }}>
-                            { [{ name: t`None`, id: null }].concat(collections).map((collection, index) =>
-                                <li
-                                    className={cx("List-item flex align-center cursor-pointer mb1 p1", { "List-item--selected": collection.id === currentCollection.id })}
-                                    key={index}
-                                    onClick={() => this.setState({ currentCollection: collection })}
-                                >
-                                    <Icon
-                                        className="Icon mr2"
-                                        name="all"
-                                        style={{
-                                            color: collection.color,
-                                            visibility: collection.color == null ? "hidden" : null
-                                        }}
-                                    />
-                                    <h3 className="List-item-title">{collection.name}</h3>
-                                </li>
-                            )}
-                        </ol>
+              {[{ name: t`None`, id: null }]
+                .concat(collections)
+                .map((collection, index) => (
+                  <li
+                    className={cx(
+                      "List-item flex align-center cursor-pointer mb1 p1",
+                      {
+                        "List-item--selected":
+                          collection.id === currentCollection.id,
+                      },
+                    )}
+                    key={index}
+                    onClick={() =>
+                      this.setState({ currentCollection: collection })
                     }
-                </CollectionList>
-            </ModalContent>
-        )
-    }
+                  >
+                    <Icon
+                      className="Icon mr2"
+                      name="all"
+                      style={{
+                        color: collection.color,
+                        visibility: collection.color == null ? "hidden" : null,
+                      }}
+                    />
+                    <h3 className="List-item-title">{collection.name}</h3>
+                  </li>
+                ))}
+            </ol>
+          )}
+        </CollectionList>
+      </ModalContent>
+    );
+  }
 }
-
diff --git a/frontend/src/metabase/questions/containers/QuestionIndex.jsx b/frontend/src/metabase/questions/containers/QuestionIndex.jsx
index 12914c5e54a624fbed861e39691bf4bfd39aafc3..dbc3c25970ace20f24329b7aefd6bef4f85bf40d 100644
--- a/frontend/src/metabase/questions/containers/QuestionIndex.jsx
+++ b/frontend/src/metabase/questions/containers/QuestionIndex.jsx
@@ -2,150 +2,192 @@ import React, { Component } from "react";
 import { connect } from "react-redux";
 import { Link } from "react-router";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon";
 import Button from "metabase/components/Button";
 
 import ExpandingSearchField from "../components/ExpandingSearchField";
 import CollectionActions from "../components/CollectionActions";
 
-import CollectionButtons from "../components/CollectionButtons"
+import CollectionButtons from "../components/CollectionButtons";
 
 import EntityList from "./EntityList";
 
 import { search } from "../questions";
 import { loadCollections } from "../collections";
-import { getLoadingInitialEntities, getAllCollections, getAllEntities } from "../selectors";
+import {
+  getLoadingInitialEntities,
+  getAllCollections,
+  getAllEntities,
+} from "../selectors";
 import { getUserIsAdmin } from "metabase/selectors/user";
 
 import { replace, push } from "react-router-redux";
 import EmptyState from "metabase/components/EmptyState";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
 
-export const CollectionEmptyState = () =>
-    <div className="flex align-center p2 mt4 bordered border-med border-brand rounded bg-grey-0 text-brand">
-        <Icon name="collection" size={32} className="mr2"/>
-        <div className="flex-full">
-            <h3>{t`Create collections for your saved questions`}</h3>
-            <div className="mt1">
-                {t`Collections help you organize your questions and allow you to decide who gets to see what.`}
-                {" "}
-                <a href="http://www.metabase.com/docs/latest/administration-guide/06-collections.html" target="_blank">
-                    {t`Learn more`}
-                </a>
-            </div>
-        </div>
-        <Link to="/collections/create">
-            <Button primary>{t`Create a collection`}</Button>
-        </Link>
-    </div>;
-
-export const NoSavedQuestionsState = () =>
-    <div className="flex-full flex align-center justify-center mb4">
-        <EmptyState
-            message={<span>{t`Explore your data, create charts or maps, and save what you find.`}</span>}
-            image="/app/img/questions_illustration"
-            action={t`Ask a question`}
-            link="/question"
-        />
-    </div>;
-
-export const QuestionIndexHeader = ({questions, collections, isAdmin, onSearch}) => {
-    // Some replication of logic for making writing tests easier
-    const hasCollections = collections && collections.length > 0;
-    const hasQuestionsWithoutCollection = questions && questions.length > 0;
-
-    const showSearch = hasCollections || hasQuestionsWithoutCollection;
-    const showSetPermissionsLink = isAdmin && hasCollections;
-
-    return (
-        <div className="flex align-center pt4 pb2">
-
-          { showSearch && hasCollections &&
-          <ExpandingSearchField onSearch={onSearch}/>
-          }
-
-        <div className="flex align-center ml-auto">
-            <CollectionActions>
-                { showSetPermissionsLink &&
-                <Link to="/collections/permissions">
-                    <Icon size={18} name="lock" tooltip={t`Set permissions for collections`}/>
-                </Link>
-                }
-                <Link to="/questions/archive">
-                    <Icon size={20} name="viewArchive" tooltip={t`View the archive`}/>
-                </Link>
-            </CollectionActions>
-        </div>
+export const CollectionEmptyState = () => (
+  <div className="flex align-center p2 mt4 bordered border-med border-brand rounded bg-grey-0 text-brand">
+    <Icon name="collection" size={32} className="mr2" />
+    <div className="flex-full">
+      <h3>{t`Create collections for your saved questions`}</h3>
+      <div className="mt1">
+        {t`Collections help you organize your questions and allow you to decide who gets to see what.`}{" "}
+        <a
+          href="http://www.metabase.com/docs/latest/administration-guide/06-collections.html"
+          target="_blank"
+        >
+          {t`Learn more`}
+        </a>
+      </div>
     </div>
-    );
+    <Link to="/collections/create">
+      <Button primary>{t`Create a collection`}</Button>
+    </Link>
+  </div>
+);
+
+export const NoSavedQuestionsState = () => (
+  <div className="flex-full flex align-center justify-center mb4">
+    <EmptyState
+      message={
+        <span
+        >{t`Explore your data, create charts or maps, and save what you find.`}</span>
+      }
+      image="/app/img/questions_illustration"
+      action={t`Ask a question`}
+      link="/question"
+    />
+  </div>
+);
+
+export const QuestionIndexHeader = ({
+  questions,
+  collections,
+  isAdmin,
+  onSearch,
+}) => {
+  // Some replication of logic for making writing tests easier
+  const hasCollections = collections && collections.length > 0;
+  const hasQuestionsWithoutCollection = questions && questions.length > 0;
+
+  const showSearch = hasCollections || hasQuestionsWithoutCollection;
+  const showSetPermissionsLink = isAdmin && hasCollections;
+
+  return (
+    <div className="flex align-center pt4 pb2">
+      {showSearch &&
+        hasCollections && <ExpandingSearchField onSearch={onSearch} />}
+
+      <div className="flex align-center ml-auto">
+        <CollectionActions>
+          {showSetPermissionsLink && (
+            <Link to="/collections/permissions">
+              <Icon
+                size={18}
+                name="lock"
+                tooltip={t`Set permissions for collections`}
+              />
+            </Link>
+          )}
+          <Link to="/questions/archive">
+            <Icon size={20} name="viewArchive" tooltip={t`View the archive`} />
+          </Link>
+        </CollectionActions>
+      </div>
+    </div>
+  );
 };
 
 const mapStateToProps = (state, props) => ({
-    loading:     getLoadingInitialEntities(state, props),
-    questions:   getAllEntities(state, props),
-    collections: getAllCollections(state, props),
-    isAdmin:     getUserIsAdmin(state, props)
+  loading: getLoadingInitialEntities(state, props),
+  questions: getAllEntities(state, props),
+  collections: getAllCollections(state, props),
+  isAdmin: getUserIsAdmin(state, props),
 });
 
-const mapDispatchToProps = ({
-    search,
-    loadCollections,
-    replace,
-    push,
-});
+const mapDispatchToProps = {
+  search,
+  loadCollections,
+  replace,
+  push,
+};
 
 /* connect() is in the end of this file because of the plain QuestionIndex component is used in Jest tests */
 export class QuestionIndex extends Component {
-    componentWillMount() {
-        this.props.loadCollections();
-    }
-
-    render () {
-        const { loading, questions, collections, replace, push, location, isAdmin } = this.props;
-
-        const hasCollections = collections && collections.length > 0;
-        const hasQuestionsWithoutCollection = questions && questions.length > 0;
-
-        const showNoCollectionsState = !loading && isAdmin && !hasCollections;
-        const showNoSavedQuestionsState = !loading && !hasCollections && !hasQuestionsWithoutCollection;
-
-        const hasEntityListSectionQuery = !!(location.query && location.query.f);
-        const showEntityList = hasQuestionsWithoutCollection || hasEntityListSectionQuery;
-
-        return (
-            <div className={cx("relative px4", {"full-height flex flex-column bg-slate-extra-light": showNoSavedQuestionsState})}>
-                {/* Use loading wrapper only for displaying the loading indicator as EntityList component should always be in DOM */}
-                { loading && <LoadingAndErrorWrapper loading={true} noBackground /> }
-
-                { showNoCollectionsState && <CollectionEmptyState /> }
-
-                { !loading && <QuestionIndexHeader
-                    questions={questions}
-                    collections={collections}
-                    isAdmin={isAdmin}
-                    onSearch={this.props.search}
-                /> }
-
-                { hasCollections && <CollectionButtons collections={collections} isAdmin={isAdmin} push={push} /> }
-
-                { showNoSavedQuestionsState && <NoSavedQuestionsState /> }
-
-                <div className={cx("pt4", { "hide": !showEntityList })}>
-                    {/* EntityList loads `questions` according to the query specified in the url query string */}
-                    <EntityList
-                        entityType="cards"
-                        entityQuery={{f: "all", collection: "", ...location.query}}
-                        // use replace when changing sections so back button still takes you back to collections page
-                        onChangeSection={(section) => replace({
-                            ...location,
-                            query: {...location.query, f: section}
-                        })}
-                    />
-                </div>
-            </div>
-        )
-    }
+  componentWillMount() {
+    this.props.loadCollections();
+  }
+
+  render() {
+    const {
+      loading,
+      questions,
+      collections,
+      replace,
+      push,
+      location,
+      isAdmin,
+    } = this.props;
+
+    const hasCollections = collections && collections.length > 0;
+    const hasQuestionsWithoutCollection = questions && questions.length > 0;
+
+    const showNoCollectionsState = !loading && isAdmin && !hasCollections;
+    const showNoSavedQuestionsState =
+      !loading && !hasCollections && !hasQuestionsWithoutCollection;
+
+    const hasEntityListSectionQuery = !!(location.query && location.query.f);
+    const showEntityList =
+      hasQuestionsWithoutCollection || hasEntityListSectionQuery;
+
+    return (
+      <div
+        className={cx("relative px4", {
+          "full-height flex flex-column bg-slate-extra-light": showNoSavedQuestionsState,
+        })}
+      >
+        {/* Use loading wrapper only for displaying the loading indicator as EntityList component should always be in DOM */}
+        {loading && <LoadingAndErrorWrapper loading={true} noBackground />}
+
+        {showNoCollectionsState && <CollectionEmptyState />}
+
+        {!loading && (
+          <QuestionIndexHeader
+            questions={questions}
+            collections={collections}
+            isAdmin={isAdmin}
+            onSearch={this.props.search}
+          />
+        )}
+
+        {hasCollections && (
+          <CollectionButtons
+            collections={collections}
+            isAdmin={isAdmin}
+            push={push}
+          />
+        )}
+
+        {showNoSavedQuestionsState && <NoSavedQuestionsState />}
+
+        <div className={cx("pt4", { hide: !showEntityList })}>
+          {/* EntityList loads `questions` according to the query specified in the url query string */}
+          <EntityList
+            entityType="cards"
+            entityQuery={{ f: "all", collection: "", ...location.query }}
+            // use replace when changing sections so back button still takes you back to collections page
+            onChangeSection={section =>
+              replace({
+                ...location,
+                query: { ...location.query, f: section },
+              })
+            }
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
 export default connect(mapStateToProps, mapDispatchToProps)(QuestionIndex);
diff --git a/frontend/src/metabase/questions/containers/SearchResults.jsx b/frontend/src/metabase/questions/containers/SearchResults.jsx
index dd8068fbe968d57035ff4301d685b9783bd38bd7..474f288c12b38e93a9c5ea51475125176e2d8e7c 100644
--- a/frontend/src/metabase/questions/containers/SearchResults.jsx
+++ b/frontend/src/metabase/questions/containers/SearchResults.jsx
@@ -1,6 +1,6 @@
 import React, { Component } from "react";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import HeaderWithBack from "metabase/components/HeaderWithBack";
 
 import ExpandingSearchField from "../components/ExpandingSearchField";
@@ -12,46 +12,49 @@ import { getTotalCount } from "../selectors";
 import { search } from "../questions";
 
 const mapStateToProps = (state, props) => ({
-    totalCount: getTotalCount(state, props),
-})
+  totalCount: getTotalCount(state, props),
+});
 
-const mapDispatchToProps = ({
-    // pass "true" as 2nd arg to replace history state so back button still takes you back to index
-    search: (term) => search(term, true)
-})
+const mapDispatchToProps = {
+  // pass "true" as 2nd arg to replace history state so back button still takes you back to index
+  search: term => search(term, true),
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 class SearchResults extends Component {
-    render () {
-        const { totalCount } = this.props;
-        return (
-          <div className="pt4">
-            <div className="flex align-center border-bottom">
-                <div className="pl4 pb4">
-                  <ExpandingSearchField
-                      active
-                      defaultValue={this.props.location.query.q}
-                      onSearch={this.props.search}
-                  />
-                </div>
-            </div>
-            <div className="px4 pt3">
-                <div className="flex align-center mb3">
-                    <HeaderWithBack name={totalCount != null ?
-                        `${totalCount} ${inflect("result", totalCount)}` :
-                        t`Search results`}
-                    />
-                </div>
-                <EntityList
-                    entityType="cards"
-                    entityQuery={this.props.location.query}
-                    showSearchWidget={false}
-                    defaultEmptyState={t`No matching questions found`}
-                />
-            </div>
+  render() {
+    const { totalCount } = this.props;
+    return (
+      <div className="pt4">
+        <div className="flex align-center border-bottom">
+          <div className="pl4 pb4">
+            <ExpandingSearchField
+              active
+              defaultValue={this.props.location.query.q}
+              onSearch={this.props.search}
+            />
           </div>
-        );
-    }
+        </div>
+        <div className="px4 pt3">
+          <div className="flex align-center mb3">
+            <HeaderWithBack
+              name={
+                totalCount != null
+                  ? `${totalCount} ${inflect("result", totalCount)}`
+                  : t`Search results`
+              }
+            />
+          </div>
+          <EntityList
+            entityType="cards"
+            entityQuery={this.props.location.query}
+            showSearchWidget={false}
+            defaultEmptyState={t`No matching questions found`}
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
 export default SearchResults;
diff --git a/frontend/src/metabase/questions/labels.js b/frontend/src/metabase/questions/labels.js
index 776451a0018c00b39f6ac7b1ee3f3d0272692ed1..cce5bfa5787e15cf78f5a177fabb6deacef8b931 100644
--- a/frontend/src/metabase/questions/labels.js
+++ b/frontend/src/metabase/questions/labels.js
@@ -1,120 +1,123 @@
-
-import {createAction, createThunkAction, mergeEntities} from "metabase/lib/redux";
-import { reset } from 'redux-form';
+import {
+  createAction,
+  createThunkAction,
+  mergeEntities,
+} from "metabase/lib/redux";
+import { reset } from "redux-form";
 import { normalize, schema } from "normalizr";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-const label = new schema.Entity('labels');
+const label = new schema.Entity("labels");
 import { LabelApi } from "metabase/services";
 
 import _ from "underscore";
 
-const LOAD_LABELS = 'metabase/labels/LOAD_LABELS';
-const EDIT_LABEL = 'metabase/labels/EDIT_LABEL';
-const SAVE_LABEL = 'metabase/labels/SAVE_LABEL';
-const DELETE_LABEL = 'metabase/labels/DELETE_LABEL';
+const LOAD_LABELS = "metabase/labels/LOAD_LABELS";
+const EDIT_LABEL = "metabase/labels/EDIT_LABEL";
+const SAVE_LABEL = "metabase/labels/SAVE_LABEL";
+const DELETE_LABEL = "metabase/labels/DELETE_LABEL";
 
 export const loadLabels = createThunkAction(LOAD_LABELS, () => {
-    return async (dispatch, getState) => {
-        let response = await LabelApi.list();
-        return normalize(response, [label]);
-    }
+  return async (dispatch, getState) => {
+    let response = await LabelApi.list();
+    return normalize(response, [label]);
+  };
 });
 
-export const saveLabel = createThunkAction(SAVE_LABEL, (values) => {
-    return async (dispatch, getState) => {
-        try {
-            let response;
-            if (values.id == null) {
-                MetabaseAnalytics.trackEvent("Labels", "Create");
-                response = await LabelApi.create(values);
-            } else {
-                MetabaseAnalytics.trackEvent("Labels", "Update");
-                response = await LabelApi.update(values);
-            }
-            if (response.id != null) {
-                dispatch(reset("label"));
-            }
-            return response;
-        } catch (e) {
-            // redux-form expects an object with either { field: error } or { _error: error }
-            if (e.data && e.data.errors) {
-                throw e.data.errors;
-            } else if (e.data && e.data.message) {
-                throw { _error: e.data.message };
-            } else {
-                throw { _error: "An unknown error occured" };
-            }
-        }
+export const saveLabel = createThunkAction(SAVE_LABEL, values => {
+  return async (dispatch, getState) => {
+    try {
+      let response;
+      if (values.id == null) {
+        MetabaseAnalytics.trackEvent("Labels", "Create");
+        response = await LabelApi.create(values);
+      } else {
+        MetabaseAnalytics.trackEvent("Labels", "Update");
+        response = await LabelApi.update(values);
+      }
+      if (response.id != null) {
+        dispatch(reset("label"));
+      }
+      return response;
+    } catch (e) {
+      // redux-form expects an object with either { field: error } or { _error: error }
+      if (e.data && e.data.errors) {
+        throw e.data.errors;
+      } else if (e.data && e.data.message) {
+        throw { _error: e.data.message };
+      } else {
+        throw { _error: "An unknown error occured" };
+      }
     }
+  };
 });
 
-export const deleteLabel = createThunkAction(DELETE_LABEL, (id) => {
-    return async (dispatch, getState) => {
-        try {
-            MetabaseAnalytics.trackEvent("Labels", "Delete");
-            await LabelApi.delete({ id });
-            return id;
-        } catch (e) {
-            // TODO: handle error
-            return null;
-        }
+export const deleteLabel = createThunkAction(DELETE_LABEL, id => {
+  return async (dispatch, getState) => {
+    try {
+      MetabaseAnalytics.trackEvent("Labels", "Delete");
+      await LabelApi.delete({ id });
+      return id;
+    } catch (e) {
+      // TODO: handle error
+      return null;
     }
+  };
 });
 
 export const editLabel = createAction(EDIT_LABEL);
 
 const initialState = {
-    entities: {
-        labels: {}
-    },
-    labelIds: null,
-    error: null,
-    editing: null
+  entities: {
+    labels: {},
+  },
+  labelIds: null,
+  error: null,
+  editing: null,
 };
 
 export default function(state = initialState, { type, payload, error }) {
-    switch (type) {
-        case LOAD_LABELS:
-            if (error) {
-                return { ...state, error: payload };
-            } else {
-                return {
-                    ...state,
-                    entities: mergeEntities(state.entities, payload.entities),
-                    labelIds: payload.result,
-                    error: null
-                };
-            }
-        case SAVE_LABEL:
-            if (error || payload == null) {
-                return state;
-            }
-            return {
-                ...state,
-                entities: {
-                    ...state.entities,
-                    labels: { ...state.entities.labels, [payload.id]: payload }
-                },
-                labelIds: _.uniq([...state.labelIds, payload.id]),
-                editing: state.editing === payload.id ? null : state.editing
-            };
-        case EDIT_LABEL:
-            return { ...state, editing: payload };
-        case DELETE_LABEL:
-            if (payload == null) {
-                return state;
-            }
-            return {
-                ...state,
-                entities: {
-                    ...state.entities,
-                    labels: { ...state.entities.labels, [payload]: undefined }
-                },
-                labelIds: state.labelIds.filter(id => id !== payload)
-            };
-        default:
-            return state;
-    }
+  switch (type) {
+    case LOAD_LABELS:
+      if (error) {
+        return { ...state, error: payload };
+      } else {
+        return {
+          ...state,
+          entities: mergeEntities(state.entities, payload.entities),
+          labelIds: payload.result,
+          error: null,
+        };
+      }
+    case SAVE_LABEL:
+      if (error || payload == null) {
+        return state;
+      }
+      return {
+        ...state,
+        entities: {
+          ...state.entities,
+          labels: { ...state.entities.labels, [payload.id]: payload },
+        },
+        labelIds: _.uniq([...state.labelIds, payload.id]),
+        editing: state.editing === payload.id ? null : state.editing,
+      };
+    case EDIT_LABEL:
+      return { ...state, editing: payload };
+    case DELETE_LABEL:
+      if (payload == null) {
+        return state;
+      }
+      return {
+        ...state,
+        entities: {
+          ...state.entities,
+          labels: { ...state.entities.labels, [payload]: undefined },
+        },
+        labelIds: state.labelIds.filter(id => id !== payload),
+      };
+    default:
+      return state;
+  }
 }
diff --git a/frontend/src/metabase/questions/questions.js b/frontend/src/metabase/questions/questions.js
index 80b1377a57de0b88f4051bff5599eefbad1d3ba5..9724e018a0b9d4df57a1c513ecde472be38855f5 100644
--- a/frontend/src/metabase/questions/questions.js
+++ b/frontend/src/metabase/questions/questions.js
@@ -1,5 +1,9 @@
-
-import {createAction, createThunkAction, mergeEntities, momentifyArraysTimestamps} from "metabase/lib/redux";
+import {
+  createAction,
+  createThunkAction,
+  mergeEntities,
+  momentifyArraysTimestamps,
+} from "metabase/lib/redux";
 
 import { normalize, schema } from "normalizr";
 import { getIn, assocIn, updateIn, chain } from "icepick";
@@ -17,316 +21,536 @@ import { getVisibleEntities, getSelectedEntities } from "./selectors";
 
 import { SET_COLLECTION_ARCHIVED } from "./collections";
 
-const label = new schema.Entity('labels');
-const collection = new schema.Entity('collections');
-const card = new schema.Entity('cards', {
+const label = new schema.Entity("labels");
+const collection = new schema.Entity("collections");
+const card = new schema.Entity("cards", {
   labels: [label],
   // collection: collection
 });
 
 import { CardApi, CollectionsApi } from "metabase/services";
 
-export const LOAD_ENTITIES = 'metabase/questions/LOAD_ENTITIES';
-const SET_SEARCH_TEXT = 'metabase/questions/SET_SEARCH_TEXT';
-const SET_ITEM_SELECTED = 'metabase/questions/SET_ITEM_SELECTED';
-const SET_ALL_SELECTED = 'metabase/questions/SET_ALL_SELECTED';
-const SET_FAVORITED = 'metabase/questions/SET_FAVORITED';
-const SET_ARCHIVED = 'metabase/questions/SET_ARCHIVED';
-const SET_LABELED = 'metabase/questions/SET_LABELED';
-const SET_COLLECTION = 'metabase/collections/SET_COLLECTION';
+export const LOAD_ENTITIES = "metabase/questions/LOAD_ENTITIES";
+const SET_SEARCH_TEXT = "metabase/questions/SET_SEARCH_TEXT";
+const SET_ITEM_SELECTED = "metabase/questions/SET_ITEM_SELECTED";
+const SET_ALL_SELECTED = "metabase/questions/SET_ALL_SELECTED";
+const SET_FAVORITED = "metabase/questions/SET_FAVORITED";
+const SET_ARCHIVED = "metabase/questions/SET_ARCHIVED";
+const SET_LABELED = "metabase/questions/SET_LABELED";
+const SET_COLLECTION = "metabase/collections/SET_COLLECTION";
 
-export const loadEntities = createThunkAction(LOAD_ENTITIES, (entityType, entityQueryObject) => {
+export const loadEntities = createThunkAction(
+  LOAD_ENTITIES,
+  (entityType, entityQueryObject) => {
     return async (dispatch, getState) => {
-        let entityQuery = JSON.stringify(entityQueryObject);
-        try {
-            let result;
-            dispatch(setRequestState({ statePath: ['questions', 'fetch'], state: "LOADING" }));
-            if (entityType === "cards") {
-                result = { entityType, entityQuery, ...normalize(momentifyArraysTimestamps(await CardApi.list(entityQueryObject)), [card]) };
-            } else if (entityType === "collections") {
-                result = { entityType, entityQuery, ...normalize(momentifyArraysTimestamps(await CollectionsApi.list(entityQueryObject)), [collection]) };
-            } else {
-                throw "Unknown entity type " + entityType;
-            }
-            dispatch(setRequestState({ statePath: ['questions', 'fetch'], state: "LOADED" }));
-            return result;
-        } catch (error) {
-            throw { entityType, entityQuery, error };
+      let entityQuery = JSON.stringify(entityQueryObject);
+      try {
+        let result;
+        dispatch(
+          setRequestState({
+            statePath: ["questions", "fetch"],
+            state: "LOADING",
+          }),
+        );
+        if (entityType === "cards") {
+          result = {
+            entityType,
+            entityQuery,
+            ...normalize(
+              momentifyArraysTimestamps(await CardApi.list(entityQueryObject)),
+              [card],
+            ),
+          };
+        } else if (entityType === "collections") {
+          result = {
+            entityType,
+            entityQuery,
+            ...normalize(
+              momentifyArraysTimestamps(
+                await CollectionsApi.list(entityQueryObject),
+              ),
+              [collection],
+            ),
+          };
+        } else {
+          throw "Unknown entity type " + entityType;
         }
-    }
-});
+        dispatch(
+          setRequestState({
+            statePath: ["questions", "fetch"],
+            state: "LOADED",
+          }),
+        );
+        return result;
+      } catch (error) {
+        throw { entityType, entityQuery, error };
+      }
+    };
+  },
+);
 
-export const search = (q, repl) => (repl ? replace : push)("/questions/search?q=" + encodeURIComponent(q))
+export const search = (q, repl) =>
+  (repl ? replace : push)("/questions/search?q=" + encodeURIComponent(q));
 
-export const setFavorited = createThunkAction(SET_FAVORITED, (cardId, favorited) => {
+export const setFavorited = createThunkAction(
+  SET_FAVORITED,
+  (cardId, favorited) => {
     return async (dispatch, getState) => {
-        if (favorited) {
-            await CardApi.favorite({ cardId });
-        } else {
-            await CardApi.unfavorite({ cardId });
-        }
-        MetabaseAnalytics.trackEvent("Questions", favorited ? "Favorite" : "Unfavorite");
-        return { id: cardId, favorite: favorited };
-    }
-});
+      if (favorited) {
+        await CardApi.favorite({ cardId });
+      } else {
+        await CardApi.unfavorite({ cardId });
+      }
+      MetabaseAnalytics.trackEvent(
+        "Questions",
+        favorited ? "Favorite" : "Unfavorite",
+      );
+      return { id: cardId, favorite: favorited };
+    };
+  },
+);
 
 import React from "react";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 function createUndo(type, actions, collection) {
-    return {
-        type: type,
-        count: actions.length,
-        message: (undo) => // eslint-disable-line react/display-name
-            <div className="flex flex-column">
-                <div>
-                    { inflect(null, undo.count, t`${undo.count} question was ${type}`, t`${undo.count} questions were ${type}`) }
-                    { undo.count === 1 && collection &&
-                        <span> {t`to the`} <Link className="link" to={Urls.collection(collection)}>{collection.name}</Link> {t`collection`}.</span>
-                    }
-                </div>
-            </div>,
-        actions: actions
-    };
+  return {
+    type: type,
+    count: actions.length,
+    // eslint-disable-next-line react/display-name
+    message: undo => (
+      <div className="flex flex-column">
+        <div>
+          {inflect(
+            null,
+            undo.count,
+            t`${undo.count} question was ${type}`,
+            t`${undo.count} questions were ${type}`,
+          )}
+          {undo.count === 1 &&
+            collection && (
+              <span>
+                {" "}
+                {t`to the`}{" "}
+                <Link className="link" to={Urls.collection(collection)}>
+                  {collection.name}
+                </Link>{" "}
+                {t`collection`}.
+              </span>
+            )}
+        </div>
+      </div>
+    ),
+    actions: actions,
+  };
 }
 
-export const setArchived = createThunkAction(SET_ARCHIVED, (cardId, archived, undoable = false) => {
+export const setArchived = createThunkAction(
+  SET_ARCHIVED,
+  (cardId, archived, undoable = false) => {
     return async (dispatch, getState) => {
-        if (cardId == null) {
-            // bulk archive
-            let selected = getSelectedEntities(getState()).filter(item => item.archived !== archived);
-            selected.map(item => dispatch(setArchived(item.id, archived)));
-            // TODO: errors
-            if (undoable) {
-                dispatch(addUndo(createUndo(
-                    archived ? "archived" : "unarchived",
-                    selected.map(item => setArchived(item.id, !archived))
-                )));
-                MetabaseAnalytics.trackEvent("Questions", archived ? "Bulk Archive" : "Bulk Unarchive", selected.length);
-            }
-        } else {
-            let card = {
-                ...getState().questions.entities.cards[cardId],
-                archived: archived
-            };
-            let response = await CardApi.update(card);
-            if (undoable) {
-                dispatch(addUndo(createUndo(
-                    archived ? "archived" : "unarchived",
-                    [setArchived(cardId, !archived)],
-                    !archived && card.collection
-                )));
-                MetabaseAnalytics.trackEvent("Questions", archived ? "Archive" : "Unarchive");
-            }
-            return response;
+      if (cardId == null) {
+        // bulk archive
+        let selected = getSelectedEntities(getState()).filter(
+          item => item.archived !== archived,
+        );
+        selected.map(item => dispatch(setArchived(item.id, archived)));
+        // TODO: errors
+        if (undoable) {
+          dispatch(
+            addUndo(
+              createUndo(
+                archived ? "archived" : "unarchived",
+                selected.map(item => setArchived(item.id, !archived)),
+              ),
+            ),
+          );
+          MetabaseAnalytics.trackEvent(
+            "Questions",
+            archived ? "Bulk Archive" : "Bulk Unarchive",
+            selected.length,
+          );
         }
-    }
-});
+      } else {
+        let card = {
+          ...getState().questions.entities.cards[cardId],
+          archived: archived,
+        };
+        let response = await CardApi.update(card);
+        if (undoable) {
+          dispatch(
+            addUndo(
+              createUndo(
+                archived ? "archived" : "unarchived",
+                [setArchived(cardId, !archived)],
+                !archived && card.collection,
+              ),
+            ),
+          );
+          MetabaseAnalytics.trackEvent(
+            "Questions",
+            archived ? "Archive" : "Unarchive",
+          );
+        }
+        return response;
+      }
+    };
+  },
+);
 
-export const setLabeled = createThunkAction(SET_LABELED, (cardId, labelId, labeled, undoable = false) => {
+export const setLabeled = createThunkAction(
+  SET_LABELED,
+  (cardId, labelId, labeled, undoable = false) => {
     return async (dispatch, getState) => {
-        if (cardId == null) {
-            // bulk label
-            let selected = getSelectedEntities(getState());
-            selected.map(item => dispatch(setLabeled(item.id, labelId, labeled)));
-            // TODO: errors
-            if (undoable) {
-                dispatch(addUndo(createUndo(
-                    labeled ? "labeled" : "unlabeled",
-                    selected.map(item => setLabeled(item.id, labelId, !labeled))
-                )));
-                MetabaseAnalytics.trackEvent("Questions", labeled ? "Bulk Apply Label" : "Bulk Remove Label", selected.length);
-            }
-        } else {
-            const state = getState();
-            const labelSlug = getIn(state.questions, ["entities", "labels", labelId, "slug"]);
-            const labels = getIn(state.questions, ["entities", "cards", cardId, "labels"]);
-            const newLabels = labels.filter(id => id !== labelId);
-            if (labeled) {
-                newLabels.push(labelId);
-            }
-            if (labels.length !== newLabels.length) {
-                await CardApi.updateLabels({ cardId, label_ids: newLabels });
-                if (undoable) {
-                    dispatch(addUndo(createUndo(
-                        labeled ? "labeled" : "unlabeled",
-                        [setLabeled(cardId, labelId, !labeled)]
-                    )));
-                    MetabaseAnalytics.trackEvent("Questions", labeled ? "Apply Label" : "Remove Label");
-                }
-                return { id: cardId, labels: newLabels, _changedLabelSlug: labelSlug, _changedLabeled: labeled };
-            }
+      if (cardId == null) {
+        // bulk label
+        let selected = getSelectedEntities(getState());
+        selected.map(item => dispatch(setLabeled(item.id, labelId, labeled)));
+        // TODO: errors
+        if (undoable) {
+          dispatch(
+            addUndo(
+              createUndo(
+                labeled ? "labeled" : "unlabeled",
+                selected.map(item => setLabeled(item.id, labelId, !labeled)),
+              ),
+            ),
+          );
+          MetabaseAnalytics.trackEvent(
+            "Questions",
+            labeled ? "Bulk Apply Label" : "Bulk Remove Label",
+            selected.length,
+          );
         }
-    }
-});
+      } else {
+        const state = getState();
+        const labelSlug = getIn(state.questions, [
+          "entities",
+          "labels",
+          labelId,
+          "slug",
+        ]);
+        const labels = getIn(state.questions, [
+          "entities",
+          "cards",
+          cardId,
+          "labels",
+        ]);
+        const newLabels = labels.filter(id => id !== labelId);
+        if (labeled) {
+          newLabels.push(labelId);
+        }
+        if (labels.length !== newLabels.length) {
+          await CardApi.updateLabels({ cardId, label_ids: newLabels });
+          if (undoable) {
+            dispatch(
+              addUndo(
+                createUndo(labeled ? "labeled" : "unlabeled", [
+                  setLabeled(cardId, labelId, !labeled),
+                ]),
+              ),
+            );
+            MetabaseAnalytics.trackEvent(
+              "Questions",
+              labeled ? "Apply Label" : "Remove Label",
+            );
+          }
+          return {
+            id: cardId,
+            labels: newLabels,
+            _changedLabelSlug: labelSlug,
+            _changedLabeled: labeled,
+          };
+        }
+      }
+    };
+  },
+);
 
-const getCardCollectionId = (state, cardId) => getIn(state, ["questions", "entities", "cards", cardId, "collection_id"])
+const getCardCollectionId = (state, cardId) =>
+  getIn(state, ["questions", "entities", "cards", cardId, "collection_id"]);
 
-export const setCollection = createThunkAction(SET_COLLECTION, (cardId, collection, undoable = false) => {
+export const setCollection = createThunkAction(
+  SET_COLLECTION,
+  (cardId, collection, undoable = false) => {
     return async (dispatch, getState) => {
-        const state = getState();
-        const collectionId = collection.id;
-        if (cardId == null) {
-            // bulk move
-            let selected = getSelectedEntities(getState());
-            if (undoable) {
-                dispatch(addUndo(createUndo(
-                    "moved",
-                    selected.map(item => setCollection(item.id, getCardCollectionId(state, item.id)))
-                )));
-                MetabaseAnalytics.trackEvent("Questions", "Bulk Move to Collection");
-            }
-            selected.map(item => dispatch(setCollection(item.id, { id: collectionId })));
-        } else {
-            const collection = _.findWhere(state.collections.collections, { id: collectionId });
+      const state = getState();
+      const collectionId = collection.id;
+      if (cardId == null) {
+        // bulk move
+        let selected = getSelectedEntities(getState());
+        if (undoable) {
+          dispatch(
+            addUndo(
+              createUndo(
+                "moved",
+                selected.map(item =>
+                  setCollection(item.id, getCardCollectionId(state, item.id)),
+                ),
+              ),
+            ),
+          );
+          MetabaseAnalytics.trackEvent("Questions", "Bulk Move to Collection");
+        }
+        selected.map(item =>
+          dispatch(setCollection(item.id, { id: collectionId })),
+        );
+      } else {
+        const collection = _.findWhere(state.collections.collections, {
+          id: collectionId,
+        });
 
-            if (undoable) {
-                dispatch(addUndo(createUndo(
-                    "moved",
-                    [setCollection(cardId, getCardCollectionId(state, cardId))]
-                )));
-                MetabaseAnalytics.trackEvent("Questions", "Move to Collection");
-            }
-            const card = await CardApi.update({ id: cardId, collection_id: collectionId });
-            return {
-                ...card,
-                _changedSectionSlug: collection && collection.slug
-            }
+        if (undoable) {
+          dispatch(
+            addUndo(
+              createUndo("moved", [
+                setCollection(cardId, getCardCollectionId(state, cardId)),
+              ]),
+            ),
+          );
+          MetabaseAnalytics.trackEvent("Questions", "Move to Collection");
         }
-    }
-});
+        const card = await CardApi.update({
+          id: cardId,
+          collection_id: collectionId,
+        });
+        return {
+          ...card,
+          _changedSectionSlug: collection && collection.slug,
+        };
+      }
+    };
+  },
+);
 
 export const setSearchText = createAction(SET_SEARCH_TEXT);
 export const setItemSelected = createAction(SET_ITEM_SELECTED);
 
-export const setAllSelected = createThunkAction(SET_ALL_SELECTED, (selected) => {
-    return async (dispatch, getState) => {
-        const visibleEntities = getVisibleEntities(getState());
-        let selectedIds = {}
-        if (selected) {
-            for (let entity of visibleEntities) {
-                selectedIds[entity.id] = true;
-            }
-        }
-        MetabaseAnalytics.trackEvent("Questions", selected ? "Select All" : "Unselect All", visibleEntities.length);
-        return selectedIds;
+export const setAllSelected = createThunkAction(SET_ALL_SELECTED, selected => {
+  return async (dispatch, getState) => {
+    const visibleEntities = getVisibleEntities(getState());
+    let selectedIds = {};
+    if (selected) {
+      for (let entity of visibleEntities) {
+        selectedIds[entity.id] = true;
+      }
     }
+    MetabaseAnalytics.trackEvent(
+      "Questions",
+      selected ? "Select All" : "Unselect All",
+      visibleEntities.length,
+    );
+    return selectedIds;
+  };
 });
 
 const initialState = {
-    lastEntityType: null,
-    lastEntityQuery: null,
-    entities: {},
-    loadingInitialEntities: true,
-    itemsBySection: {},
-    searchText: "",
-    selectedIds: {},
-    undos: []
+  lastEntityType: null,
+  lastEntityQuery: null,
+  entities: {},
+  loadingInitialEntities: true,
+  itemsBySection: {},
+  searchText: "",
+  selectedIds: {},
+  undos: [],
 };
 
 export default function(state = initialState, { type, payload, error }) {
-    switch (type) {
-        case SET_SEARCH_TEXT:
-            return { ...state, searchText: payload };
-        case SET_ITEM_SELECTED:
-            return { ...state, selectedIds: { ...state.selectedIds, ...payload } };
-        case SET_ALL_SELECTED:
-            return { ...state, selectedIds: payload };
-        case LOAD_ENTITIES:
-            if (error) {
-                return assocIn(state, ["itemsBySection", payload.entityType, payload.entityQuery, "error"], payload.error);
-            } else {
-                return (chain(state)
-                    .assoc("loadingInitialEntities", false)
-                    .assoc("entities", mergeEntities(state.entities, payload.entities))
-                    .assoc("lastEntityType", payload.entityType)
-                    .assoc("lastEntityQuery", payload.entityQuery)
-                    .assoc("selectedIds", {})
-                    .assoc("searchText", "")
-                    .assocIn(["itemsBySection", payload.entityType, payload.entityQuery, "error"], null)
-                    .assocIn(["itemsBySection", payload.entityType, payload.entityQuery, "items"], payload.result)
-                    // store the initial sort order so if we remove and undo an item it can be put back in it's original location
-                    .assocIn(["itemsBySection", payload.entityType, payload.entityQuery, "sortIndex"], payload.result.reduce((o, id, i) => { o[id] = i; return o; }, {}))
-                    .value());
-            }
-        case SET_FAVORITED:
-            if (error) {
-                return state;
-            } else if (payload && payload.id != null) {
-                state = assocIn(state, ["entities", "cards", payload.id], {
-                    ...getIn(state, ["entities", "cards", payload.id]),
-                    ...payload
-                });
-                // FIXME: incorrectly adds to sections it may not have previously been in, but not a big deal since we reload whens switching sections
-                state = updateSections(state, "cards", payload.id, (s) => s.f === "fav", payload.favorite);
-            }
-            return state;
-        case SET_ARCHIVED:
-            if (error) {
-                return state;
-            } else if (payload && payload.id != null) {
-                state = assocIn(state, ["entities", "cards", payload.id], {
-                    ...getIn(state, ["entities", "cards", payload.id]),
-                    ...payload
-                });
-                // FIXME: incorrectly adds to sections it may not have previously been in, but not a big deal since we reload whens switching sections
-                state = updateSections(state, "cards", payload.id, (s) => s.f === "archived", payload.archived);
-                state = updateSections(state, "cards", payload.id, (s) => s.f !== "archived", !payload.archived);
-            }
-            return state;
-        case SET_LABELED:
-            if (error) {
-                return state;
-            } else if (payload && payload.id != null) {
-                state = assocIn(state, ["entities", "cards", payload.id], {
-                    ...getIn(state, ["entities", "cards", payload.id]),
-                    ...payload
-                });
-                // FIXME: incorrectly adds to sections it may not have previously been in, but not a big deal since we reload whens switching sections
-                state = updateSections(state, "cards", payload.id, (s) => s.label === payload._changedLabelSlug, payload._changedLabeled);
-            }
-            return state;
-        case SET_COLLECTION:
-            if (error) {
-                return state;
-            } else if (payload && payload.id != null) {
-                state = assocIn(state, ["entities", "cards", payload.id], {
-                    ...getIn(state, ["entities", "cards", payload.id]),
-                    ...payload
-                });
-                state = updateSections(state, "cards", payload.id, (s) => s.collection !== payload._changedSectionSlug, false);
-                state = updateSections(state, "cards", payload.id, (s) => s.collection === payload._changedSectionSlug, true);
-            }
-            return state;
-        case SET_COLLECTION_ARCHIVED:
-            if (error) {
-                return state;
-            } else if (payload && payload.id != null) {
-                state = assocIn(state, ["entities", "collections", payload.id], {
-                    ...getIn(state, ["entities", "collections", payload.id]),
-                    ...payload
-                });
-                state = updateSections(state, "collections", payload.id, (s) => s.archived, payload.archived);
-                state = updateSections(state, "collections", payload.id, (s) => !s.archived, !payload.archived);
-            }
-            return state;
-        default:
-            return state;
-    }
+  switch (type) {
+    case SET_SEARCH_TEXT:
+      return { ...state, searchText: payload };
+    case SET_ITEM_SELECTED:
+      return { ...state, selectedIds: { ...state.selectedIds, ...payload } };
+    case SET_ALL_SELECTED:
+      return { ...state, selectedIds: payload };
+    case LOAD_ENTITIES:
+      if (error) {
+        return assocIn(
+          state,
+          ["itemsBySection", payload.entityType, payload.entityQuery, "error"],
+          payload.error,
+        );
+      } else {
+        return (
+          chain(state)
+            .assoc("loadingInitialEntities", false)
+            .assoc("entities", mergeEntities(state.entities, payload.entities))
+            .assoc("lastEntityType", payload.entityType)
+            .assoc("lastEntityQuery", payload.entityQuery)
+            .assoc("selectedIds", {})
+            .assoc("searchText", "")
+            .assocIn(
+              [
+                "itemsBySection",
+                payload.entityType,
+                payload.entityQuery,
+                "error",
+              ],
+              null,
+            )
+            .assocIn(
+              [
+                "itemsBySection",
+                payload.entityType,
+                payload.entityQuery,
+                "items",
+              ],
+              payload.result,
+            )
+            // store the initial sort order so if we remove and undo an item it can be put back in it's original location
+            .assocIn(
+              [
+                "itemsBySection",
+                payload.entityType,
+                payload.entityQuery,
+                "sortIndex",
+              ],
+              payload.result.reduce((o, id, i) => {
+                o[id] = i;
+                return o;
+              }, {}),
+            )
+            .value()
+        );
+      }
+    case SET_FAVORITED:
+      if (error) {
+        return state;
+      } else if (payload && payload.id != null) {
+        state = assocIn(state, ["entities", "cards", payload.id], {
+          ...getIn(state, ["entities", "cards", payload.id]),
+          ...payload,
+        });
+        // FIXME: incorrectly adds to sections it may not have previously been in, but not a big deal since we reload whens switching sections
+        state = updateSections(
+          state,
+          "cards",
+          payload.id,
+          s => s.f === "fav",
+          payload.favorite,
+        );
+      }
+      return state;
+    case SET_ARCHIVED:
+      if (error) {
+        return state;
+      } else if (payload && payload.id != null) {
+        state = assocIn(state, ["entities", "cards", payload.id], {
+          ...getIn(state, ["entities", "cards", payload.id]),
+          ...payload,
+        });
+        // FIXME: incorrectly adds to sections it may not have previously been in, but not a big deal since we reload whens switching sections
+        state = updateSections(
+          state,
+          "cards",
+          payload.id,
+          s => s.f === "archived",
+          payload.archived,
+        );
+        state = updateSections(
+          state,
+          "cards",
+          payload.id,
+          s => s.f !== "archived",
+          !payload.archived,
+        );
+      }
+      return state;
+    case SET_LABELED:
+      if (error) {
+        return state;
+      } else if (payload && payload.id != null) {
+        state = assocIn(state, ["entities", "cards", payload.id], {
+          ...getIn(state, ["entities", "cards", payload.id]),
+          ...payload,
+        });
+        // FIXME: incorrectly adds to sections it may not have previously been in, but not a big deal since we reload whens switching sections
+        state = updateSections(
+          state,
+          "cards",
+          payload.id,
+          s => s.label === payload._changedLabelSlug,
+          payload._changedLabeled,
+        );
+      }
+      return state;
+    case SET_COLLECTION:
+      if (error) {
+        return state;
+      } else if (payload && payload.id != null) {
+        state = assocIn(state, ["entities", "cards", payload.id], {
+          ...getIn(state, ["entities", "cards", payload.id]),
+          ...payload,
+        });
+        state = updateSections(
+          state,
+          "cards",
+          payload.id,
+          s => s.collection !== payload._changedSectionSlug,
+          false,
+        );
+        state = updateSections(
+          state,
+          "cards",
+          payload.id,
+          s => s.collection === payload._changedSectionSlug,
+          true,
+        );
+      }
+      return state;
+    case SET_COLLECTION_ARCHIVED:
+      if (error) {
+        return state;
+      } else if (payload && payload.id != null) {
+        state = assocIn(state, ["entities", "collections", payload.id], {
+          ...getIn(state, ["entities", "collections", payload.id]),
+          ...payload,
+        });
+        state = updateSections(
+          state,
+          "collections",
+          payload.id,
+          s => s.archived,
+          payload.archived,
+        );
+        state = updateSections(
+          state,
+          "collections",
+          payload.id,
+          s => !s.archived,
+          !payload.archived,
+        );
+      }
+      return state;
+    default:
+      return state;
+  }
 }
 
-function updateSections(state, entityType, entityId, sectionPredicate, shouldContain) {
-    return updateIn(state, ["itemsBySection", entityType], (entityQueries) =>
-        _.mapObject(entityQueries, (entityQueryResult, entityQuery) => {
-            if (sectionPredicate(JSON.parse(entityQuery))) {
-                const doesContain = _.contains(entityQueryResult.items, entityId);
-                if (!doesContain && shouldContain) {
-                    return { ...entityQueryResult, items: entityQueryResult.items.concat(entityId) };
-                } else if (doesContain && !shouldContain) {
-                    return { ...entityQueryResult, items: entityQueryResult.items.filter(id => id !== entityId) };
-                }
-            }
-            return entityQueryResult;
-        })
-    );
+function updateSections(
+  state,
+  entityType,
+  entityId,
+  sectionPredicate,
+  shouldContain,
+) {
+  return updateIn(state, ["itemsBySection", entityType], entityQueries =>
+    _.mapObject(entityQueries, (entityQueryResult, entityQuery) => {
+      if (sectionPredicate(JSON.parse(entityQuery))) {
+        const doesContain = _.contains(entityQueryResult.items, entityId);
+        if (!doesContain && shouldContain) {
+          return {
+            ...entityQueryResult,
+            items: entityQueryResult.items.concat(entityId),
+          };
+        } else if (doesContain && !shouldContain) {
+          return {
+            ...entityQueryResult,
+            items: entityQueryResult.items.filter(id => id !== entityId),
+          };
+        }
+      }
+      return entityQueryResult;
+    }),
+  );
 }
diff --git a/frontend/src/metabase/questions/selectors.js b/frontend/src/metabase/questions/selectors.js
index a9a0e0363c7ce876d0b1ca266da88d46b8faefa4..1ecdc1051ceafb82aa6b8d6fd8a7d77bae890789 100644
--- a/frontend/src/metabase/questions/selectors.js
+++ b/frontend/src/metabase/questions/selectors.js
@@ -1,202 +1,228 @@
-
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 import moment from "moment";
 import { getIn } from "icepick";
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import visualizations from "metabase/visualizations";
-import {caseInsensitiveSearch} from "metabase/lib/string"
+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;
+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;
 
-export const getSection                = (state, props) => props.entityQuery && JSON.stringify(props.entityQuery);
-export const getLoadingInitialEntities = (state, props) => state.questions.loadingInitialEntities
-export const getEntities               = (state, props) => state.questions.entities
-export const getItemsBySection         = (state, props) => state.questions.itemsBySection
+export const getSection = (state, props) =>
+  props.entityQuery && JSON.stringify(props.entityQuery);
+export const getLoadingInitialEntities = (state, props) =>
+  state.questions.loadingInitialEntities;
+export const getEntities = (state, props) => state.questions.entities;
+export const getItemsBySection = (state, props) =>
+  state.questions.itemsBySection;
 
-export const getSearchText             = (state, props) => state.questions.searchText;
-export const getSelectedIds            = (state, props) => state.questions.selectedIds;
+export const getSearchText = (state, props) => state.questions.searchText;
+export const getSelectedIds = (state, props) => state.questions.selectedIds;
 
-export const getAllCollections         = (state, props) => state.collections.collections;
+export const getAllCollections = (state, props) =>
+  state.collections.collections;
 
 export const getWritableCollections = createSelector(
-    [getAllCollections],
-    (collections) => _.filter(collections, collection => collection.can_write)
+  [getAllCollections],
+  collections => _.filter(collections, collection => collection.can_write),
 );
 
 export const getQuery = createSelector(
-    [getEntityQuery],
-    (entityQuery) => entityQuery && JSON.parse(entityQuery)
+  [getEntityQuery],
+  entityQuery => entityQuery && JSON.parse(entityQuery),
 );
 
 const getSectionData = createSelector(
-    [getItemsBySection, getEntityType, getEntityQuery],
-    (itemsBySection, entityType, entityQuery) =>
-        getIn(itemsBySection, [entityType, entityQuery])
+  [getItemsBySection, getEntityType, getEntityQuery],
+  (itemsBySection, entityType, entityQuery) =>
+    getIn(itemsBySection, [entityType, entityQuery]),
 );
 
 export const getSectionLoading = createSelector(
-    [getSectionData],
-    (sectionData) =>
-        !(sectionData && sectionData.items)
+  [getSectionData],
+  sectionData => !(sectionData && sectionData.items),
 );
 
 export const getSectionError = createSelector(
-    [getSectionData],
-    (sectionData) =>
-        (sectionData && sectionData.error)
+  [getSectionData],
+  sectionData => sectionData && sectionData.error,
 );
 
 export const getEntityIds = createSelector(
-    [getSectionData],
-    (sectionData) =>
-        sectionData ? _.sortBy(sectionData.items, id => sectionData.sortIndex[id] != null ? sectionData.sortIndex[id] : Infinity) : []
+  [getSectionData],
+  sectionData =>
+    sectionData
+      ? _.sortBy(
+          sectionData.items,
+          id =>
+            sectionData.sortIndex[id] != null
+              ? sectionData.sortIndex[id]
+              : Infinity,
+        )
+      : [],
 );
 
 const getEntity = (state, props) =>
-    getEntities(state, props)[props.entityType][props.entityId];
+  getEntities(state, props)[props.entityType][props.entityId];
 
 const getEntitySelected = (state, props) =>
-    getSelectedIds(state, props)[props.entityId] || false;
+  getSelectedIds(state, props)[props.entityId] || false;
 
 const getEntityVisible = (state, props) =>
-    caseInsensitiveSearch(getEntity(state, props).name, getSearchText(state, props));
+  caseInsensitiveSearch(
+    getEntity(state, props).name,
+    getSearchText(state, props),
+  );
 
-const getLabelEntities = (state, props) => state.labels.entities.labels
+const getLabelEntities = (state, props) => state.labels.entities.labels;
 
 export const makeGetItem = () => {
-    const getItem = createSelector(
-        [getEntity, getEntitySelected, getEntityVisible, getLabelEntities],
-        (entity, selected, visible, labelEntities) => ({
-            name: entity.name,
-            id: entity.id,
-            created: entity.created_at ? moment(entity.created_at).fromNow() : null,
-            by: entity.creator && entity.creator.common_name,
-            icon: visualizations.get(entity.display).iconName,
-            favorite: entity.favorite,
-            archived: entity.archived,
-            collection: entity.collection,
-            labels: entity.labels ? entity.labels.map(labelId => labelEntities[labelId]).filter(l => l) : [],
-            selected,
-            visible,
-            description: entity.description
-        })
-    );
-    return getItem;
-}
+  const getItem = createSelector(
+    [getEntity, getEntitySelected, getEntityVisible, getLabelEntities],
+    (entity, selected, visible, labelEntities) => ({
+      name: entity.name,
+      id: entity.id,
+      created: entity.created_at ? moment(entity.created_at).fromNow() : null,
+      by: entity.creator && entity.creator.common_name,
+      icon: visualizations.get(entity.display).iconName,
+      favorite: entity.favorite,
+      archived: entity.archived,
+      collection: entity.collection,
+      labels: entity.labels
+        ? entity.labels.map(labelId => labelEntities[labelId]).filter(l => l)
+        : [],
+      selected,
+      visible,
+      description: entity.description,
+    }),
+  );
+  return getItem;
+};
 
 export const getAllEntities = createSelector(
-    [getEntityIds, getEntityType, getEntities],
-    (entityIds, entityType, entities) =>
-        entityIds.map(entityId => getIn(entities, [entityType, entityId]))
+  [getEntityIds, getEntityType, getEntities],
+  (entityIds, entityType, entities) =>
+    entityIds.map(entityId => getIn(entities, [entityType, entityId])),
 );
 
 export const getVisibleEntities = createSelector(
-    [getAllEntities, getSearchText],
-    (allEntities, searchText) =>
-        allEntities.filter(entity => caseInsensitiveSearch(entity.name, searchText))
+  [getAllEntities, getSearchText],
+  (allEntities, searchText) =>
+    allEntities.filter(entity =>
+      caseInsensitiveSearch(entity.name, searchText),
+    ),
 );
 
 export const getSelectedEntities = createSelector(
-    [getVisibleEntities, getSelectedIds],
-    (visibleEntities, selectedIds) =>
-        visibleEntities.filter(entity => selectedIds[entity.id])
+  [getVisibleEntities, getSelectedIds],
+  (visibleEntities, selectedIds) =>
+    visibleEntities.filter(entity => selectedIds[entity.id]),
 );
 
 export const getTotalCount = createSelector(
-    [getAllEntities],
-    (entities) => entities.length
+  [getAllEntities],
+  entities => entities.length,
 );
 
 export const getVisibleCount = createSelector(
-    [getVisibleEntities],
-    (visibleEntities) => visibleEntities.length
+  [getVisibleEntities],
+  visibleEntities => visibleEntities.length,
 );
 
 export const getSelectedCount = createSelector(
-    [getSelectedEntities],
-    (selectedEntities) => selectedEntities.length
+  [getSelectedEntities],
+  selectedEntities => selectedEntities.length,
 );
 
 export const getAllAreSelected = createSelector(
-    [getSelectedCount, getVisibleCount],
-    (selectedCount, visibleCount) =>
-        selectedCount === visibleCount && visibleCount > 0
+  [getSelectedCount, getVisibleCount],
+  (selectedCount, visibleCount) =>
+    selectedCount === visibleCount && visibleCount > 0,
 );
 
 export const getSectionIsArchive = createSelector(
-    [getQuery],
-    (query) =>
-        query && query.f === "archived"
+  [getQuery],
+  query => query && query.f === "archived",
 );
 
 const sections = [
-    { id: "all",       name: t`All questions`,   icon: "all" },
-    { id: "fav",       name: t`Favorites`,       icon: "star" },
-    { id: "recent",    name: t`Recently viewed`, icon: "recents" },
-    { id: "mine",      name: t`Saved by me`,     icon: "mine" },
-    { id: "popular",   name: t`Most popular`,    icon: "popular" }
+  { id: "all", name: t`All questions`, icon: "all" },
+  { id: "fav", name: t`Favorites`, icon: "star" },
+  { id: "recent", name: t`Recently viewed`, icon: "recents" },
+  { id: "mine", name: t`Saved by me`, icon: "mine" },
+  { id: "popular", name: t`Most popular`, icon: "popular" },
 ];
 
-export const getSections    = (state, props) => sections;
+export const getSections = (state, props) => sections;
 
 export const getEditingLabelId = (state, props) => state.labels.editing;
 
 export const getLabels = createSelector(
-    [(state, props) => state.labels.entities.labels, (state, props) => state.labels.labelIds],
-    (labelEntities, labelIds) =>
-        labelIds ? labelIds.map(id => labelEntities[id]).sort((a, b) => a.name.localeCompare(b.name)) : []
+  [
+    (state, props) => state.labels.entities.labels,
+    (state, props) => state.labels.labelIds,
+  ],
+  (labelEntities, labelIds) =>
+    labelIds
+      ? labelIds
+          .map(id => labelEntities[id])
+          .sort((a, b) => a.name.localeCompare(b.name))
+      : [],
 );
 
 export const getLabelsLoading = (state, props) => !state.labels.labelIds;
 export const getLabelsError = (state, props) => state.labels.error;
 
 const getLabelCountsForSelectedEntities = createSelector(
-    [getSelectedEntities],
-    (entities) => {
-        let counts = {};
-        for (let entity of entities) {
-            for (let labelId of entity.labels) {
-                counts[labelId] = (counts[labelId] || 0) + 1;
-            }
-        }
-        return counts;
+  [getSelectedEntities],
+  entities => {
+    let counts = {};
+    for (let entity of entities) {
+      for (let labelId of entity.labels) {
+        counts[labelId] = (counts[labelId] || 0) + 1;
+      }
     }
+    return counts;
+  },
 );
 
 export const getLabelsWithSelectedState = createSelector(
-    [getLabels, getSelectedCount, getLabelCountsForSelectedEntities],
-    (labels, selectedCount, counts) =>
-        labels.map(label => ({
-            ...label,
-            count: counts[label.id],
-            selected:
-                counts[label.id] === 0 || counts[label.id] == null ? false :
-                counts[label.id] === selectedCount ? true :
-                null
-        }))
-)
+  [getLabels, getSelectedCount, getLabelCountsForSelectedEntities],
+  (labels, selectedCount, counts) =>
+    labels.map(label => ({
+      ...label,
+      count: counts[label.id],
+      selected:
+        counts[label.id] === 0 || counts[label.id] == null
+          ? false
+          : counts[label.id] === selectedCount ? true : null,
+    })),
+);
 
 export const getSectionName = createSelector(
-    [getSection, getSections, getLabels],
-    (sectionId, sections, labels) => {
-        let match = sectionId && sectionId.match(/^(.*)-(.*)/);
-        if (match) {
-            if (match[1] === "label") {
-                let label = _.findWhere(labels, { slug: match[2] });
-                if (label && label.name) {
-                    return label.name;
-                }
-            }
-        } else {
-            let section = _.findWhere(sections, { id: sectionId });
-            if (section) {
-                return section.name || "";
-            } else if (sectionId === "archived") {
-                return t`Archive`;
-            }
+  [getSection, getSections, getLabels],
+  (sectionId, sections, labels) => {
+    let match = sectionId && sectionId.match(/^(.*)-(.*)/);
+    if (match) {
+      if (match[1] === "label") {
+        let label = _.findWhere(labels, { slug: match[2] });
+        if (label && label.name) {
+          return label.name;
         }
-        return "";
+      }
+    } else {
+      let section = _.findWhere(sections, { id: sectionId });
+      if (section) {
+        return section.name || "";
+      } else if (sectionId === "archived") {
+        return t`Archive`;
+      }
     }
+    return "";
+  },
 );
diff --git a/frontend/src/metabase/reducers-common.js b/frontend/src/metabase/reducers-common.js
index 3d9fe0a4b1de23f56a025334e72f90d547ab82e0..a60bd946f79ff7adebe59293cbe2cc371e174608 100644
--- a/frontend/src/metabase/reducers-common.js
+++ b/frontend/src/metabase/reducers-common.js
@@ -15,12 +15,12 @@ import undo from "metabase/redux/undo";
 import { currentUser } from "metabase/redux/user";
 
 export default {
-    // global reducers
-    app,
-    auth,
-    currentUser,
-    metadata,
-    requests,
-    settings,
-    undo,
+  // global reducers
+  app,
+  auth,
+  currentUser,
+  metadata,
+  requests,
+  settings,
+  undo,
 };
diff --git a/frontend/src/metabase/reducers-main.js b/frontend/src/metabase/reducers-main.js
index a222bd3a05a2d945075b85a811b391affe71a180..c8f0902a2e44ba98bb6e3964ab72416922c1fff6 100644
--- a/frontend/src/metabase/reducers-main.js
+++ b/frontend/src/metabase/reducers-main.js
@@ -2,7 +2,7 @@
 
 // Reducers needed for main application
 
-import { combineReducers } from 'redux';
+import { combineReducers } from "redux";
 
 import commonReducers from "./reducers-common";
 
@@ -39,24 +39,23 @@ import * as pulse from "metabase/pulse/reducers";
 /* xrays */
 import xray from "metabase/xray/xray";
 
-
 export default {
-    ...commonReducers,
-
-    // main app reducers
-    alert,
-    dashboards,
-    dashboard,
-    home: combineReducers(home),
-    new_query,
-    pulse: combineReducers(pulse),
-    qb: combineReducers(qb),
-    questions,
-    collections,
-    labels,
-    reference,
-    xray,
-    setup: combineReducers(setup),
-    user: combineReducers(user),
-    admin,
+  ...commonReducers,
+
+  // main app reducers
+  alert,
+  dashboards,
+  dashboard,
+  home: combineReducers(home),
+  new_query,
+  pulse: combineReducers(pulse),
+  qb: combineReducers(qb),
+  questions,
+  collections,
+  labels,
+  reference,
+  xray,
+  setup: combineReducers(setup),
+  user: combineReducers(user),
+  admin,
 };
diff --git a/frontend/src/metabase/reducers-public.js b/frontend/src/metabase/reducers-public.js
index a40e5f3e01d0c7b63777194d8a160312e3a16e47..a1d754ba37e4bf6d086ae147c04604582e7b1161 100644
--- a/frontend/src/metabase/reducers-public.js
+++ b/frontend/src/metabase/reducers-public.js
@@ -7,6 +7,6 @@ import commonReducers from "./reducers-common";
 import dashboard from "metabase/dashboard/dashboard";
 
 export default {
-    ...commonReducers,
-    dashboard
+  ...commonReducers,
+  dashboard,
 };
diff --git a/frontend/src/metabase/redux/app.js b/frontend/src/metabase/redux/app.js
index 69a88919ee0f6ce0f9307dc9179262e170a51d7f..767b633143951e2ee5ca91c778a9c32492aeb1b5 100644
--- a/frontend/src/metabase/redux/app.js
+++ b/frontend/src/metabase/redux/app.js
@@ -1,19 +1,26 @@
-import { combineReducers, handleActions, createAction } from "metabase/lib/redux";
+import {
+  combineReducers,
+  handleActions,
+  createAction,
+} from "metabase/lib/redux";
 
-import { LOCATION_CHANGE } from "react-router-redux"
+import { LOCATION_CHANGE } from "react-router-redux";
 
 export const SET_ERROR_PAGE = "metabase/app/SET_ERROR_PAGE";
 
-export const setErrorPage = createAction(SET_ERROR_PAGE, (error) => {
-    console.error("Error:", error);
-    return error;
+export const setErrorPage = createAction(SET_ERROR_PAGE, error => {
+  console.error("Error:", error);
+  return error;
 });
 
-const errorPage = handleActions({
+const errorPage = handleActions(
+  {
     [SET_ERROR_PAGE]: (state, { payload }) => payload,
-    [LOCATION_CHANGE]: () => null
-}, null);
+    [LOCATION_CHANGE]: () => null,
+  },
+  null,
+);
 
 export default combineReducers({
-    errorPage
+  errorPage,
 });
diff --git a/frontend/src/metabase/redux/metadata.js b/frontend/src/metabase/redux/metadata.js
index 1d0b4aa677f7d463aceeca45272fca934da00f82..6c7016151658d8629fdd137ccdcb581cd2162f09 100644
--- a/frontend/src/metabase/redux/metadata.js
+++ b/frontend/src/metabase/redux/metadata.js
@@ -1,558 +1,742 @@
 import {
-    handleActions,
-    combineReducers,
-    createAction,
-    createThunkAction,
-    resourceListToMap,
-    fetchData,
-    updateData,
-    handleEntities
+  handleActions,
+  combineReducers,
+  createAction,
+  createThunkAction,
+  resourceListToMap,
+  fetchData,
+  updateData,
+  handleEntities,
 } from "metabase/lib/redux";
 
 import { normalize } from "normalizr";
-import { DatabaseSchema, TableSchema, FieldSchema, SegmentSchema, MetricSchema } from "metabase/schema";
-
-import { getIn, assocIn } from "icepick";
+import {
+  DatabaseSchema,
+  TableSchema,
+  FieldSchema,
+  SegmentSchema,
+  MetricSchema,
+} from "metabase/schema";
+
+import { getIn, assocIn, updateIn } from "icepick";
 import _ from "underscore";
 
-import { MetabaseApi, MetricApi, SegmentApi, RevisionsApi } from "metabase/services";
+import { getMetadata } from "metabase/selectors/metadata";
+
+import {
+  MetabaseApi,
+  MetricApi,
+  SegmentApi,
+  RevisionsApi,
+} from "metabase/services";
 
 export const FETCH_METRICS = "metabase/metadata/FETCH_METRICS";
-export const fetchMetrics = createThunkAction(FETCH_METRICS, (reload = false) => {
+export const fetchMetrics = createThunkAction(
+  FETCH_METRICS,
+  (reload = false) => {
     return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "metrics"];
-        const existingStatePath = requestStatePath;
-        const getData = async () => {
-            const metrics = await MetricApi.list();
-            return normalize(metrics, [MetricSchema]);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+      const requestStatePath = ["metadata", "metrics"];
+      const existingStatePath = requestStatePath;
+      const getData = async () => {
+        const metrics = await MetricApi.list();
+        return normalize(metrics, [MetricSchema]);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
+  },
+);
 
 const UPDATE_METRIC = "metabase/metadata/UPDATE_METRIC";
 export const updateMetric = createThunkAction(UPDATE_METRIC, function(metric) {
-    return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "metrics", metric.id];
-        const existingStatePath = ["metadata", "metrics"];
-        const dependentRequestStatePaths = [['metadata', 'revisions', 'metric', metric.id]];
-        const putData = async () => {
-            const updatedMetric = await MetricApi.update(metric);
-            return normalize(updatedMetric, MetricSchema);
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            dependentRequestStatePaths,
-            putData
-        });
+  return async (dispatch, getState) => {
+    const requestStatePath = ["metadata", "metrics", metric.id];
+    const existingStatePath = ["metadata", "metrics"];
+    const dependentRequestStatePaths = [
+      ["metadata", "revisions", "metric", metric.id],
+    ];
+    const putData = async () => {
+      const updatedMetric = await MetricApi.update(metric);
+      return normalize(updatedMetric, MetricSchema);
     };
+
+    return await updateData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      dependentRequestStatePaths,
+      putData,
+    });
+  };
 });
 
-const UPDATE_METRIC_IMPORTANT_FIELDS = "metabase/guide/UPDATE_METRIC_IMPORTANT_FIELDS";
-export const updateMetricImportantFields = createThunkAction(UPDATE_METRIC_IMPORTANT_FIELDS, function(metricId, importantFieldIds) {
+const UPDATE_METRIC_IMPORTANT_FIELDS =
+  "metabase/guide/UPDATE_METRIC_IMPORTANT_FIELDS";
+export const updateMetricImportantFields = createThunkAction(
+  UPDATE_METRIC_IMPORTANT_FIELDS,
+  function(metricId, importantFieldIds) {
     return async (dispatch, getState) => {
-        const requestStatePath = ["reference", "guide", "metric_important_fields", metricId];
-        const existingStatePath = requestStatePath;
-        const dependentRequestStatePaths = [['reference', 'guide']];
-        const putData = async () => {
-            await MetricApi.update_important_fields({ metricId, important_field_ids: importantFieldIds });
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            dependentRequestStatePaths,
-            putData
+      const requestStatePath = [
+        "reference",
+        "guide",
+        "metric_important_fields",
+        metricId,
+      ];
+      const existingStatePath = requestStatePath;
+      const dependentRequestStatePaths = [["reference", "guide"]];
+      const putData = async () => {
+        await MetricApi.update_important_fields({
+          metricId,
+          important_field_ids: importantFieldIds,
         });
+      };
+
+      return await updateData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        dependentRequestStatePaths,
+        putData,
+      });
     };
-});
+  },
+);
 
 export const FETCH_SEGMENTS = "metabase/metadata/FETCH_SEGMENTS";
-export const fetchSegments = createThunkAction(FETCH_SEGMENTS, (reload = false) => {
+export const fetchSegments = createThunkAction(
+  FETCH_SEGMENTS,
+  (reload = false) => {
     return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "segments"];
-        const existingStatePath = requestStatePath;
-        const getData = async () => {
-            const segments = await SegmentApi.list();
-            return normalize(segments, [SegmentSchema]);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+      const requestStatePath = ["metadata", "segments"];
+      const existingStatePath = requestStatePath;
+      const getData = async () => {
+        const segments = await SegmentApi.list();
+        return normalize(segments, [SegmentSchema]);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
+  },
+);
 
 const UPDATE_SEGMENT = "metabase/metadata/UPDATE_SEGMENT";
-export const updateSegment = createThunkAction(UPDATE_SEGMENT, function(segment) {
-    return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "segments", segment.id];
-        const existingStatePath = ["metadata", "segments"];
-        const dependentRequestStatePaths = [['metadata', 'revisions', 'segment', segment.id]];
-        const putData = async () => {
-            const updatedSegment = await SegmentApi.update(segment);
-            return normalize(updatedSegment, SegmentSchema);
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            dependentRequestStatePaths,
-            putData
-        });
+export const updateSegment = createThunkAction(UPDATE_SEGMENT, function(
+  segment,
+) {
+  return async (dispatch, getState) => {
+    const requestStatePath = ["metadata", "segments", segment.id];
+    const existingStatePath = ["metadata", "segments"];
+    const dependentRequestStatePaths = [
+      ["metadata", "revisions", "segment", segment.id],
+    ];
+    const putData = async () => {
+      const updatedSegment = await SegmentApi.update(segment);
+      return normalize(updatedSegment, SegmentSchema);
     };
+
+    return await updateData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      dependentRequestStatePaths,
+      putData,
+    });
+  };
 });
 
 export const FETCH_DATABASES = "metabase/metadata/FETCH_DATABASES";
-export const fetchDatabases = createThunkAction(FETCH_DATABASES, (reload = false) => {
+export const fetchDatabases = createThunkAction(
+  FETCH_DATABASES,
+  (reload = false) => {
     return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "databases"];
-        const existingStatePath = requestStatePath;
-        const getData = async () => {
-            const databases = await MetabaseApi.db_list_with_tables();
-            return normalize(databases, [DatabaseSchema]);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+      const requestStatePath = ["metadata", "databases"];
+      const existingStatePath = requestStatePath;
+      const getData = async () => {
+        const databases = await MetabaseApi.db_list_with_tables();
+        return normalize(databases, [DatabaseSchema]);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
+  },
+);
 
 export const FETCH_REAL_DATABASES = "metabase/metadata/FETCH_REAL_DATABASES";
-export const fetchRealDatabases = createThunkAction(FETCH_REAL_DATABASES, (reload = false) => {
+export const fetchRealDatabases = createThunkAction(
+  FETCH_REAL_DATABASES,
+  (reload = false) => {
     return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "databases"];
-        const existingStatePath = requestStatePath;
-        const getData = async () => {
-            const databases = await MetabaseApi.db_real_list_with_tables();
-            return normalize(databases, [DatabaseSchema]);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+      const requestStatePath = ["metadata", "databases"];
+      const existingStatePath = requestStatePath;
+      const getData = async () => {
+        const databases = await MetabaseApi.db_real_list_with_tables();
+        return normalize(databases, [DatabaseSchema]);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
-
-export const FETCH_DATABASE_METADATA = "metabase/metadata/FETCH_DATABASE_METADATA";
-export const fetchDatabaseMetadata = createThunkAction(FETCH_DATABASE_METADATA, function(dbId, reload = false) {
+  },
+);
+
+export const FETCH_DATABASE_METADATA =
+  "metabase/metadata/FETCH_DATABASE_METADATA";
+export const fetchDatabaseMetadata = createThunkAction(
+  FETCH_DATABASE_METADATA,
+  function(dbId, reload = false) {
     return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "databases", dbId];
-        const existingStatePath = ["metadata"];
-        const getData = async () => {
-            const databaseMetadata = await MetabaseApi.db_metadata({ dbId });
-            return normalize(databaseMetadata, DatabaseSchema);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+      const requestStatePath = ["metadata", "databases", dbId];
+      const existingStatePath = ["metadata"];
+      const getData = async () => {
+        const databaseMetadata = await MetabaseApi.db_metadata({ dbId });
+        return normalize(databaseMetadata, DatabaseSchema);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
+  },
+);
 
 const UPDATE_DATABASE = "metabase/metadata/UPDATE_DATABASE";
-export const updateDatabase = createThunkAction(UPDATE_DATABASE, function(database) {
-    return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "databases", database.id];
-        const existingStatePath = ["metadata", "databases"];
-        const putData = async () => {
-            // make sure we don't send all the computed metadata
-            // there may be more that I'm missing?
-            const slimDatabase = _.omit(database, "tables", "tables_lookup");
-            const updatedDatabase = await MetabaseApi.db_update(slimDatabase);
-            return normalize(updatedDatabase, DatabaseSchema);
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            putData
-        });
+export const updateDatabase = createThunkAction(UPDATE_DATABASE, function(
+  database,
+) {
+  return async (dispatch, getState) => {
+    const requestStatePath = ["metadata", "databases", database.id];
+    const existingStatePath = ["metadata", "databases"];
+    const putData = async () => {
+      // make sure we don't send all the computed metadata
+      // there may be more that I'm missing?
+      const slimDatabase = _.omit(database, "tables", "tables_lookup");
+      const updatedDatabase = await MetabaseApi.db_update(slimDatabase);
+      return normalize(updatedDatabase, DatabaseSchema);
     };
+
+    return await updateData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      putData,
+    });
+  };
 });
 
 const UPDATE_TABLE = "metabase/metadata/UPDATE_TABLE";
 export const updateTable = createThunkAction(UPDATE_TABLE, function(table) {
-    return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "tables", table.id];
-        const existingStatePath = ["metadata", "tables"];
-        const putData = async () => {
-            // make sure we don't send all the computed metadata
-            const slimTable = _.omit(table, "fields", "fields_lookup", "aggregation_options", "breakout_options", "metrics", "segments");
-
-            const updatedTable = await MetabaseApi.table_update(slimTable);
-            return normalize(updatedTable, TableSchema);
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            putData
-        });
+  return async (dispatch, getState) => {
+    const requestStatePath = ["metadata", "tables", table.id];
+    const existingStatePath = ["metadata", "tables"];
+    const putData = async () => {
+      // make sure we don't send all the computed metadata
+      const slimTable = _.omit(
+        table,
+        "fields",
+        "fields_lookup",
+        "aggregation_options",
+        "breakout_options",
+        "metrics",
+        "segments",
+      );
+
+      const updatedTable = await MetabaseApi.table_update(slimTable);
+      return normalize(updatedTable, TableSchema);
     };
+
+    return await updateData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      putData,
+    });
+  };
 });
 
 const FETCH_TABLES = "metabase/metadata/FETCH_TABLES";
 export const fetchTables = createThunkAction(FETCH_TABLES, (reload = false) => {
-    return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "tables"];
-        const existingStatePath = requestStatePath;
-        const getData = async () => {
-            const tables = await MetabaseApi.table_list();
-            return normalize(tables, [TableSchema]);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+  return async (dispatch, getState) => {
+    const requestStatePath = ["metadata", "tables"];
+    const existingStatePath = requestStatePath;
+    const getData = async () => {
+      const tables = await MetabaseApi.table_list();
+      return normalize(tables, [TableSchema]);
     };
+
+    return await fetchData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      getData,
+      reload,
+    });
+  };
 });
 
 export const FETCH_TABLE_METADATA = "metabase/metadata/FETCH_TABLE_METADATA";
-export const fetchTableMetadata = createThunkAction(FETCH_TABLE_METADATA, function(tableId, reload = false) {
+export const fetchTableMetadata = createThunkAction(
+  FETCH_TABLE_METADATA,
+  function(tableId, reload = false) {
     return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "tables", tableId];
-        const existingStatePath = ["metadata"];
-        const getData = async () => {
-            const tableMetadata = await MetabaseApi.table_query_metadata({ tableId });
-            const fkTableIds = _.chain(tableMetadata.fields).filter(field => field.target).map(field => field.target.table_id).uniq().value();
-            const fkTables = await Promise.all(fkTableIds.map(tableId => MetabaseApi.table_query_metadata({ tableId })));
-            return normalize([tableMetadata].concat(fkTables), [TableSchema]);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
+      const requestStatePath = ["metadata", "tables", tableId];
+      const existingStatePath = ["metadata"];
+      const getData = async () => {
+        const tableMetadata = await MetabaseApi.table_query_metadata({
+          tableId,
         });
+        const fkTableIds = _.chain(tableMetadata.fields)
+          .filter(field => field.target)
+          .map(field => field.target.table_id)
+          .uniq()
+          .value();
+        const fkTables = await Promise.all(
+          fkTableIds.map(tableId =>
+            MetabaseApi.table_query_metadata({ tableId }),
+          ),
+        );
+        return normalize([tableMetadata].concat(fkTables), [TableSchema]);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
+  },
+);
 
 export const FETCH_FIELD = "metabase/metadata/FETCH_FIELD";
-export const fetchField = createThunkAction(FETCH_FIELD, function(fieldId, reload) {
-    return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "fields", fieldId];
-        const existingStatePath = requestStatePath;
-        const getData = () => MetabaseApi.field_get({ fieldId })
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload: true
-        });
-    };
+export const fetchField = createThunkAction(FETCH_FIELD, function(
+  fieldId,
+  reload,
+) {
+  return async function(dispatch, getState) {
+    const requestStatePath = ["metadata", "fields", fieldId];
+    const existingStatePath = requestStatePath;
+    const getData = () => MetabaseApi.field_get({ fieldId });
+
+    return await fetchData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      getData,
+      reload: true,
+    });
+  };
 });
 export 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
-        });
-    };
+export const fetchFieldValues = createThunkAction(FETCH_FIELD_VALUES, function(
+  fieldId,
+  reload,
+) {
+  return async function(dispatch, getState) {
+    const requestStatePath = ["metadata", "fields", fieldId, "values"];
+    const existingStatePath = requestStatePath;
+    const getData = () => MetabaseApi.field_values({ fieldId });
+
+    return await fetchData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      getData,
+      reload,
+    });
+  };
 });
 
 // Docstring from m.api.field:
 // Update the human-readable values for a `Field` whose special type is
 // `category`/`city`/`state`/`country` or whose base type is `type/Boolean`."
 export const UPDATE_FIELD_VALUES = "metabase/metadata/UPDATE_FIELD_VALUES";
-export const updateFieldValues = createThunkAction(UPDATE_FIELD_VALUES, function(fieldId, fieldValuePairs) {
+export const updateFieldValues = createThunkAction(
+  UPDATE_FIELD_VALUES,
+  function(fieldId, fieldValuePairs) {
     return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "fields", fieldId, "dimension"];
-        const existingStatePath = ["metadata", "fields", fieldId];
-
-        const putData = async () => {
-            return await MetabaseApi.field_values_update({ fieldId, values: fieldValuePairs })
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            putData
+      const requestStatePath = ["metadata", "fields", fieldId, "dimension"];
+      const existingStatePath = ["metadata", "fields", fieldId];
+
+      const putData = async () => {
+        return await MetabaseApi.field_values_update({
+          fieldId,
+          values: fieldValuePairs,
         });
+      };
+
+      return await updateData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        putData,
+      });
     };
-});
+  },
+);
 
 export const ADD_PARAM_VALUES = "metabase/metadata/ADD_PARAM_VALUES";
 export const addParamValues = createAction(ADD_PARAM_VALUES);
 
 export const UPDATE_FIELD = "metabase/metadata/UPDATE_FIELD";
 export const updateField = createThunkAction(UPDATE_FIELD, function(field) {
-    return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "fields", field.id];
-        const existingStatePath = ["metadata", "fields"];
-        const putData = async () => {
-            // make sure we don't send all the computed metadata
-            // there may be more that I'm missing?
-            const slimField = _.omit(field, "operators_lookup");
-
-            const updatedField = await MetabaseApi.field_update(slimField);
-            return normalize(updatedField, FieldSchema);
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            putData
-        });
+  return async function(dispatch, getState) {
+    const requestStatePath = ["metadata", "fields", field.id];
+    const existingStatePath = ["metadata", "fields"];
+    const putData = async () => {
+      // make sure we don't send all the computed metadata
+      // there may be more that I'm missing?
+      const slimField = _.omit(field, "operators_lookup");
+
+      const updatedField = await MetabaseApi.field_update(slimField);
+      return normalize(updatedField, FieldSchema);
     };
+
+    return await updateData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      putData,
+    });
+  };
 });
 
-export const DELETE_FIELD_DIMENSION = "metabase/metadata/DELETE_FIELD_DIMENSION";
-export const deleteFieldDimension = createThunkAction(DELETE_FIELD_DIMENSION, function(fieldId) {
+export const DELETE_FIELD_DIMENSION =
+  "metabase/metadata/DELETE_FIELD_DIMENSION";
+export const deleteFieldDimension = createThunkAction(
+  DELETE_FIELD_DIMENSION,
+  function(fieldId) {
     return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "fields", fieldId, "dimension"];
-        const existingStatePath = ["metadata", "fields", fieldId];
-
-        const putData = async () => {
-            return await MetabaseApi.field_dimension_delete({ fieldId });
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            putData
-        });
+      const requestStatePath = ["metadata", "fields", fieldId, "dimension"];
+      const existingStatePath = ["metadata", "fields", fieldId];
+
+      const putData = async () => {
+        return await MetabaseApi.field_dimension_delete({ fieldId });
+      };
+
+      return await updateData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        putData,
+      });
     };
-});
-
-export const UPDATE_FIELD_DIMENSION = "metabase/metadata/UPDATE_FIELD_DIMENSION";
-export const updateFieldDimension = createThunkAction(UPDATE_FIELD_DIMENSION, function(fieldId, dimension) {
+  },
+);
+
+export const UPDATE_FIELD_DIMENSION =
+  "metabase/metadata/UPDATE_FIELD_DIMENSION";
+export const updateFieldDimension = createThunkAction(
+  UPDATE_FIELD_DIMENSION,
+  function(fieldId, dimension) {
     return async function(dispatch, getState) {
-        const requestStatePath = ["metadata", "fields", fieldId, "dimension"];
-        const existingStatePath = ["metadata", "fields", fieldId];
-
-        const putData = async () => {
-            return await MetabaseApi.field_dimension_update({ fieldId, ...dimension });
-        };
-
-        return await updateData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            putData
+      const requestStatePath = ["metadata", "fields", fieldId, "dimension"];
+      const existingStatePath = ["metadata", "fields", fieldId];
+
+      const putData = async () => {
+        return await MetabaseApi.field_dimension_update({
+          fieldId,
+          ...dimension,
         });
+      };
+
+      return await updateData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        putData,
+      });
     };
-});
+  },
+);
 
 export const FETCH_REVISIONS = "metabase/metadata/FETCH_REVISIONS";
-export const fetchRevisions = createThunkAction(FETCH_REVISIONS, (type, id, reload = false) => {
+export const fetchRevisions = createThunkAction(
+  FETCH_REVISIONS,
+  (type, id, reload = false) => {
     return async (dispatch, getState) => {
-        const requestStatePath = ["metadata", "revisions", type, id];
-        const existingStatePath = ["metadata", "revisions"];
-        const getData = async () => {
-            const revisions = await RevisionsApi.get({id, entity: type});
-            const revisionMap = resourceListToMap(revisions);
-
-            const existingRevisions = getIn(getState(), existingStatePath);
-            return assocIn(existingRevisions, [type, id], revisionMap);
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+      const requestStatePath = ["metadata", "revisions", type, id];
+      const existingStatePath = ["metadata", "revisions"];
+      const getData = async () => {
+        const revisions = await RevisionsApi.get({ id, entity: type });
+        const revisionMap = resourceListToMap(revisions);
+
+        const existingRevisions = getIn(getState(), existingStatePath);
+        return assocIn(existingRevisions, [type, id], revisionMap);
+      };
+
+      return await fetchData({
+        dispatch,
+        getState,
+        requestStatePath,
+        existingStatePath,
+        getData,
+        reload,
+      });
     };
-});
+  },
+);
 
 // for fetches with data dependencies in /reference
 export const FETCH_METRIC_TABLE = "metabase/metadata/FETCH_METRIC_TABLE";
-export const fetchMetricTable = createThunkAction(FETCH_METRIC_TABLE, (metricId, reload = false) => {
+export const fetchMetricTable = createThunkAction(
+  FETCH_METRIC_TABLE,
+  (metricId, reload = false) => {
     return async (dispatch, getState) => {
-        await dispatch(fetchMetrics()); // FIXME: fetchMetric?
-        const metric = getIn(getState(), ['metadata', 'metrics', metricId]);
-        const tableId = metric.table_id;
-        await dispatch(fetchTableMetadata(tableId));
+      await dispatch(fetchMetrics()); // FIXME: fetchMetric?
+      const metric = getIn(getState(), ["metadata", "metrics", metricId]);
+      const tableId = metric.table_id;
+      await dispatch(fetchTableMetadata(tableId));
     };
-});
-
-export const FETCH_METRIC_REVISIONS = "metabase/metadata/FETCH_METRIC_REVISIONS";
-export const fetchMetricRevisions = createThunkAction(FETCH_METRIC_REVISIONS, (metricId, reload = false) => {
+  },
+);
+
+export const FETCH_METRIC_REVISIONS =
+  "metabase/metadata/FETCH_METRIC_REVISIONS";
+export const fetchMetricRevisions = createThunkAction(
+  FETCH_METRIC_REVISIONS,
+  (metricId, reload = false) => {
     return async (dispatch, getState) => {
-        await Promise.all([
-            dispatch(fetchRevisions('metric', metricId)),
-            dispatch(fetchMetrics())
-        ]);
-        const metric = getIn(getState(), ['metadata', 'metrics', metricId]);
-        const tableId = metric.table_id;
-        await dispatch(fetchTableMetadata(tableId));
+      await Promise.all([
+        dispatch(fetchRevisions("metric", metricId)),
+        dispatch(fetchMetrics()),
+      ]);
+      const metric = getIn(getState(), ["metadata", "metrics", metricId]);
+      const tableId = metric.table_id;
+      await dispatch(fetchTableMetadata(tableId));
     };
-});
+  },
+);
 
 export const FETCH_SEGMENT_FIELDS = "metabase/metadata/FETCH_SEGMENT_FIELDS";
-export const fetchSegmentFields = createThunkAction(FETCH_SEGMENT_FIELDS, (segmentId, reload = false) => {
+export const fetchSegmentFields = createThunkAction(
+  FETCH_SEGMENT_FIELDS,
+  (segmentId, reload = false) => {
     return async (dispatch, getState) => {
-        await dispatch(fetchSegments()); // FIXME: fetchSegment?
-        const segment = getIn(getState(), ['metadata', 'segments', segmentId]);
-        const tableId = segment.table_id;
-        await dispatch(fetchTableMetadata(tableId));
-        const table = getIn(getState(), ['metadata', 'tables', tableId]);
-        const databaseId = table.db_id;
-        await dispatch(fetchDatabaseMetadata(databaseId));
+      await dispatch(fetchSegments()); // FIXME: fetchSegment?
+      const segment = getIn(getState(), ["metadata", "segments", segmentId]);
+      const tableId = segment.table_id;
+      await dispatch(fetchTableMetadata(tableId));
+      const table = getIn(getState(), ["metadata", "tables", tableId]);
+      const databaseId = table.db_id;
+      await dispatch(fetchDatabaseMetadata(databaseId));
     };
-});
+  },
+);
 
 export const FETCH_SEGMENT_TABLE = "metabase/metadata/FETCH_SEGMENT_TABLE";
-export const fetchSegmentTable = createThunkAction(FETCH_SEGMENT_TABLE, (segmentId, reload = false) => {
+export const fetchSegmentTable = createThunkAction(
+  FETCH_SEGMENT_TABLE,
+  (segmentId, reload = false) => {
     return async (dispatch, getState) => {
-        await dispatch(fetchSegments()); // FIXME: fetchSegment?
-        const segment = getIn(getState(), ['metadata', 'segments', segmentId]);
-        const tableId = segment.table_id;
-        await dispatch(fetchTableMetadata(tableId));
+      await dispatch(fetchSegments()); // FIXME: fetchSegment?
+      const segment = getIn(getState(), ["metadata", "segments", segmentId]);
+      const tableId = segment.table_id;
+      await dispatch(fetchTableMetadata(tableId));
     };
-});
-
-export const FETCH_SEGMENT_REVISIONS = "metabase/metadata/FETCH_SEGMENT_REVISIONS";
-export const fetchSegmentRevisions = createThunkAction(FETCH_SEGMENT_REVISIONS, (segmentId, reload = false) => {
+  },
+);
+
+export const FETCH_SEGMENT_REVISIONS =
+  "metabase/metadata/FETCH_SEGMENT_REVISIONS";
+export const fetchSegmentRevisions = createThunkAction(
+  FETCH_SEGMENT_REVISIONS,
+  (segmentId, reload = false) => {
     return async (dispatch, getState) => {
-        await Promise.all([
-            dispatch(fetchRevisions('segment', segmentId)),
-            dispatch(fetchSegments())
-        ]);
-        const segment = getIn(getState(), ['metadata', 'segments', segmentId]);
-        const tableId = segment.table_id;
-        await dispatch(fetchTableMetadata(tableId));
+      await Promise.all([
+        dispatch(fetchRevisions("segment", segmentId)),
+        dispatch(fetchSegments()),
+      ]);
+      const segment = getIn(getState(), ["metadata", "segments", segmentId]);
+      const tableId = segment.table_id;
+      await dispatch(fetchTableMetadata(tableId));
     };
-});
-
-const FETCH_DATABASES_WITH_METADATA = "metabase/metadata/FETCH_DATABASES_WITH_METADATA";
-export const fetchDatabasesWithMetadata = createThunkAction(FETCH_DATABASES_WITH_METADATA, (reload = false) => {
+  },
+);
+
+const FETCH_DATABASES_WITH_METADATA =
+  "metabase/metadata/FETCH_DATABASES_WITH_METADATA";
+export const fetchDatabasesWithMetadata = createThunkAction(
+  FETCH_DATABASES_WITH_METADATA,
+  (reload = false) => {
     return async (dispatch, getState) => {
-        await dispatch(fetchDatabases());
-        const databases = getIn(getState(), ['metadata', 'databases']);
-        await Promise.all(Object.values(databases).map(database =>
-            dispatch(fetchDatabaseMetadata(database.id))
-        ));
+      await dispatch(fetchDatabases());
+      const databases = getIn(getState(), ["metadata", "databases"]);
+      await Promise.all(
+        Object.values(databases).map(database =>
+          dispatch(fetchDatabaseMetadata(database.id)),
+        ),
+      );
     };
-});
-
-const FETCH_REAL_DATABASES_WITH_METADATA = "metabase/metadata/FETCH_REAL_DATABASES_WITH_METADATA";
-export const fetchRealDatabasesWithMetadata = createThunkAction(FETCH_REAL_DATABASES_WITH_METADATA, (reload = false) => {
+  },
+);
+
+const ADD_REMAPPINGS = "metabase/metadata/ADD_REMAPPINGS";
+export const addRemappings = createAction(
+  ADD_REMAPPINGS,
+  (fieldId, remappings) => ({ fieldId, remappings }),
+);
+
+const FETCH_REMAPPING = "metabase/metadata/FETCH_REMAPPING";
+export const fetchRemapping = createThunkAction(
+  FETCH_REMAPPING,
+  (value, fieldId) => async (dispatch, getState) => {
+    const metadata = getMetadata(getState());
+    const field = metadata.fields[fieldId];
+    const remappedField = field && field.remappedField();
+    if (field && remappedField && !field.hasRemappedValue(value)) {
+      const fieldId = (field.target || field).id;
+      const remappedFieldId = remappedField.id;
+      fetchData({
+        dispatch,
+        getState,
+        requestStatePath: [
+          "metadata",
+          "remapping",
+          fieldId,
+          JSON.stringify(value),
+        ],
+        getData: async () => {
+          const remapping = await MetabaseApi.field_remapping({
+            value,
+            fieldId,
+            remappedFieldId,
+          });
+          if (remapping) {
+            // FIXME: should this be field.id (potentially the FK) or fieldId (always the PK)?
+            dispatch(addRemappings(field.id, [remapping]));
+          }
+        },
+      });
+    }
+  },
+);
+
+const FETCH_REAL_DATABASES_WITH_METADATA =
+  "metabase/metadata/FETCH_REAL_DATABASES_WITH_METADATA";
+export const fetchRealDatabasesWithMetadata = createThunkAction(
+  FETCH_REAL_DATABASES_WITH_METADATA,
+  (reload = false) => {
     return async (dispatch, getState) => {
-        await dispatch(fetchRealDatabases())
-        const databases = getIn(getState(), ['metadata', 'databases']);
-        await Promise.all(Object.values(databases).map(database =>
-            dispatch(fetchDatabaseMetadata(database.id))
-        ));
+      await dispatch(fetchRealDatabases());
+      const databases = getIn(getState(), ["metadata", "databases"]);
+      await Promise.all(
+        Object.values(databases).map(database =>
+          dispatch(fetchDatabaseMetadata(database.id)),
+        ),
+      );
     };
-});
-
-const databases = handleActions({
-}, {});
-
-const databasesList = handleActions({
-    [FETCH_DATABASES]: { next: (state, { payload }) => (payload && payload.result) || state }
-}, []);
-
-const tables = handleActions({
-}, {});
-
-const fields = handleActions({
-    [FETCH_FIELD]: { next: (state, { payload: field }) =>
-        ({
-            ...state,
-            [field.id]: {
-                ...(state[field.id] || {}),
-                ...field
-            }
-        })},
-    [FETCH_FIELD_VALUES]: { next: (state, { payload: fieldValues }) =>
-        fieldValues ? assocIn(state, [fieldValues.field_id, "values"], fieldValues) : state },
-    [ADD_PARAM_VALUES]: { next: (state, { payload: paramValues }) => {
+  },
+);
+
+const databases = handleActions({}, {});
+
+const databasesList = handleActions(
+  {
+    [FETCH_DATABASES]: {
+      next: (state, { payload }) => (payload && payload.result) || state,
+    },
+  },
+  [],
+);
+
+const tables = handleActions({}, {});
+
+const fields = handleActions(
+  {
+    [FETCH_FIELD]: {
+      next: (state, { payload: field }) => ({
+        ...state,
+        [field.id]: {
+          ...(state[field.id] || {}),
+          ...field,
+        },
+      }),
+    },
+    [FETCH_FIELD_VALUES]: {
+      next: (state, { payload: fieldValues }) =>
+        fieldValues
+          ? assocIn(state, [fieldValues.field_id, "values"], fieldValues.values)
+          : state,
+    },
+    [ADD_PARAM_VALUES]: {
+      next: (state, { payload: paramValues }) => {
         for (const fieldValues of Object.values(paramValues)) {
-            state = assocIn(state, [fieldValues.field_id, "values"], fieldValues);
+          state = assocIn(state, [fieldValues.field_id, "values"], fieldValues);
         }
         return state;
-    }}
-}, {});
-
-const metrics = handleActions({
-}, {});
-
-const segments = handleActions({
-}, {});
-
-const revisions = handleActions({
-    [FETCH_REVISIONS]: { next: (state, { payload }) => payload }
-}, {});
+      },
+    },
+    [ADD_REMAPPINGS]: (state, { payload: { fieldId, remappings } }) =>
+      updateIn(state, [fieldId, "remappings"], (existing = []) =>
+        Array.from(new Map(existing.concat(remappings))),
+      ),
+  },
+  {},
+);
+
+const metrics = handleActions({}, {});
+
+const segments = handleActions({}, {});
+
+const revisions = handleActions(
+  {
+    [FETCH_REVISIONS]: { next: (state, { payload }) => payload },
+  },
+  {},
+);
 
 export default combineReducers({
-    metrics:   handleEntities(/^metabase\/metadata\//, "metrics", metrics),
-    segments:  handleEntities(/^metabase\/metadata\//, "segments", segments),
-    databases: handleEntities(/^metabase\/metadata\//, "databases", databases),
-    tables:    handleEntities(/^metabase\/metadata\//, "tables", tables),
-    fields:    handleEntities(/^metabase\/metadata\//, "fields", fields),
-    revisions,
-    databasesList,
+  metrics: handleEntities(/^metabase\/metadata\//, "metrics", metrics),
+  segments: handleEntities(/^metabase\/metadata\//, "segments", segments),
+  databases: handleEntities(/^metabase\/metadata\//, "databases", databases),
+  tables: handleEntities(/^metabase\/metadata\//, "tables", tables),
+  fields: handleEntities(/^metabase\/metadata\//, "fields", fields),
+  revisions,
+  databasesList,
 });
diff --git a/frontend/src/metabase/redux/requests.js b/frontend/src/metabase/redux/requests.js
index 958d8be731aaa47aa8f99cd2e05c816f214073cc..dd852273f9fe6aa1c290d8f79dd057344c92a168 100644
--- a/frontend/src/metabase/redux/requests.js
+++ b/frontend/src/metabase/redux/requests.js
@@ -11,41 +11,45 @@ export const setRequestState = createAction(SET_REQUEST_STATE);
 export const clearRequestState = createAction(CLEAR_REQUEST_STATE);
 
 // For a given state path, returns the current request state ("LOADING", "LOADED" or a request error)
-export const states = handleActions({
+export const states = handleActions(
+  {
     [SET_REQUEST_STATE]: {
-        next: (state, { payload }) => assocIn(
-            state,
-            payload.statePath,
-            { state: payload.state, error: payload.error }
-        )
+      next: (state, { payload }) =>
+        assocIn(state, payload.statePath, {
+          state: payload.state,
+          error: payload.error,
+        }),
     },
     [CLEAR_REQUEST_STATE]: {
-        next: (state, { payload }) => assocIn(
-            state,
-            payload.statePath,
-            undefined
-        )
-    }
-}, {});
+      next: (state, { payload }) =>
+        assocIn(state, payload.statePath, undefined),
+    },
+  },
+  {},
+);
 
 // For given state path, returns true if the data has been successfully fetched at least once
-export const fetched = handleActions({
+export const fetched = handleActions(
+  {
     [SET_REQUEST_STATE]: {
-        next: (state, {payload}) => {
-            const isFetch = payload.statePath[payload.statePath.length - 1] === "fetch"
+      next: (state, { payload }) => {
+        const isFetch =
+          payload.statePath[payload.statePath.length - 1] === "fetch";
 
-            if (isFetch) {
-                const statePathWithoutFetch = payload.statePath.slice(0, -1)
-                return assocIn(
-                    state,
-                    statePathWithoutFetch,
-                    getIn(state, statePathWithoutFetch) || payload.state === "LOADED"
-                )
-            } else {
-                return state
-            }
+        if (isFetch) {
+          const statePathWithoutFetch = payload.statePath.slice(0, -1);
+          return assocIn(
+            state,
+            statePathWithoutFetch,
+            getIn(state, statePathWithoutFetch) || payload.state === "LOADED",
+          );
+        } else {
+          return state;
         }
-    }
-}, {})
+      },
+    },
+  },
+  {},
+);
 
-export default combineReducers({ states, fetched })
+export default combineReducers({ states, fetched });
diff --git a/frontend/src/metabase/redux/settings.js b/frontend/src/metabase/redux/settings.js
index 8ba854a190cea79dafb4d150fe1b5394cecc81f8..61d454450706f5140e915a0e697caa687d476756 100644
--- a/frontend/src/metabase/redux/settings.js
+++ b/frontend/src/metabase/redux/settings.js
@@ -2,7 +2,12 @@
 
 import MetabaseSettings from "metabase/lib/settings";
 
-import { handleActions, createAction, createThunkAction, combineReducers } from "metabase/lib/redux";
+import {
+  handleActions,
+  createAction,
+  createThunkAction,
+  combineReducers,
+} from "metabase/lib/redux";
 
 import { SessionApi, SettingsApi } from "metabase/services";
 
@@ -12,49 +17,66 @@ import { getUserIsAdmin } from "metabase/selectors/user";
 export const REFRESH_SITE_SETTINGS = "metabase/settings/REFRESH_SITE_SETTINGS";
 const REFRESH_SETTINGS_LIST = "metabase/settings/REFRESH_SETTINGS_LIST";
 
-export const refreshSiteSettings = createThunkAction(REFRESH_SITE_SETTINGS, () =>
-    async (dispatch, getState) => {
-        // public settings
-        const settings = await SessionApi.properties();
-        MetabaseSettings.setAll(settings);
+export const refreshSiteSettings = createThunkAction(
+  REFRESH_SITE_SETTINGS,
+  () => async (dispatch, getState) => {
+    // public settings
+    const settings = await SessionApi.properties();
+    MetabaseSettings.setAll(settings);
 
-        // also load admin-only settings, if user is an admin
-        await dispatch(loadCurrentUser());
-        if (getUserIsAdmin(getState())) {
-            await dispatch(refreshSettingsList());
-        }
-
-        return settings;
+    // also load admin-only settings, if user is an admin
+    await dispatch(loadCurrentUser());
+    if (getUserIsAdmin(getState())) {
+      await dispatch(refreshSettingsList());
     }
+
+    return settings;
+  },
 );
 
-export const refreshSettingsList = createAction(REFRESH_SETTINGS_LIST, async () => {
+export const refreshSettingsList = createAction(
+  REFRESH_SETTINGS_LIST,
+  async () => {
     let settingsList = await SettingsApi.list();
     MetabaseSettings.setAll(collectSettingsValues(settingsList));
-    return settingsList.map((setting) => {
-        setting.originalValue = setting.value;
-        return setting;
+    return settingsList.map(setting => {
+      setting.originalValue = setting.value;
+      return setting;
     });
-});
+  },
+);
 
-const collectSettingsValues = (settingsList) => {
-    let settings = {};
-    for (const setting of settingsList) {
-        settings[setting.key] = setting.value;
-    }
-    return settings;
-}
+const collectSettingsValues = settingsList => {
+  let settings = {};
+  for (const setting of settingsList) {
+    settings[setting.key] = setting.value;
+  }
+  return settings;
+};
 
-const values = handleActions({
-    [REFRESH_SITE_SETTINGS]: { next: (state, { payload }) => ({ ...state, ...payload }) },
-    [REFRESH_SETTINGS_LIST]: { next: (state, { payload }) => ({ ...state, ...collectSettingsValues(payload) }) },
-}, {});
+const values = handleActions(
+  {
+    [REFRESH_SITE_SETTINGS]: {
+      next: (state, { payload }) => ({ ...state, ...payload }),
+    },
+    [REFRESH_SETTINGS_LIST]: {
+      next: (state, { payload }) => ({
+        ...state,
+        ...collectSettingsValues(payload),
+      }),
+    },
+  },
+  {},
+);
 
-const settings = handleActions({
-    [REFRESH_SETTINGS_LIST]: { next: (state, { payload }) => payload }
-}, []);
+const settings = handleActions(
+  {
+    [REFRESH_SETTINGS_LIST]: { next: (state, { payload }) => payload },
+  },
+  [],
+);
 
 export default combineReducers({
-    values,
-    settings,
-})
+  values,
+  settings,
+});
diff --git a/frontend/src/metabase/redux/undo.js b/frontend/src/metabase/redux/undo.js
index 230a943e896ef60102f9b3daa20ba48ae2143b3e..0448f6f3efeb31f36bed107bb7c39ced1d3f612e 100644
--- a/frontend/src/metabase/redux/undo.js
+++ b/frontend/src/metabase/redux/undo.js
@@ -5,75 +5,76 @@ import _ from "underscore";
 import { createAction, createThunkAction } from "metabase/lib/redux";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-const ADD_UNDO = 'metabase/questions/ADD_UNDO';
-const DISMISS_UNDO = 'metabase/questions/DISMISS_UNDO';
-const PERFORM_UNDO = 'metabase/questions/PERFORM_UNDO';
+const ADD_UNDO = "metabase/questions/ADD_UNDO";
+const DISMISS_UNDO = "metabase/questions/DISMISS_UNDO";
+const PERFORM_UNDO = "metabase/questions/PERFORM_UNDO";
 
 let nextUndoId = 0;
 
 // A convenience shorthand for creating single undos
 export function createUndo({ type, message, action }) {
-    return {
-        type: type,
-        count: 1,
-        message,
-        actions: action ? [action] : null
-    };
+  return {
+    type: type,
+    count: 1,
+    message,
+    actions: action ? [action] : null,
+  };
 }
 
-export const addUndo = createThunkAction(ADD_UNDO, (undo) => {
-    return (dispatch, getState) => {
-        let id = nextUndoId++;
-        setTimeout(() => dispatch(dismissUndo(id, false)), 5000);
-        return { ...undo, id, _domId: id };
-    };
+export const addUndo = createThunkAction(ADD_UNDO, undo => {
+  return (dispatch, getState) => {
+    let id = nextUndoId++;
+    setTimeout(() => dispatch(dismissUndo(id, false)), 5000);
+    return { ...undo, id, _domId: id };
+  };
 });
 
-export const dismissUndo = createAction(DISMISS_UNDO, (undoId, track = true) => {
+export const dismissUndo = createAction(
+  DISMISS_UNDO,
+  (undoId, track = true) => {
     if (track) {
-        MetabaseAnalytics.trackEvent("Undo", "Dismiss Undo");
+      MetabaseAnalytics.trackEvent("Undo", "Dismiss Undo");
     }
     return undoId;
-});
+  },
+);
 
-export const performUndo = createThunkAction(PERFORM_UNDO, (undoId) => {
-    return (dispatch, getState) => {
-        MetabaseAnalytics.trackEvent("Undo", "Perform Undo");
-        let undo = _.findWhere(getState().undo, { id: undoId });
-        if (undo) {
-            undo.actions.map(action =>
-                dispatch(action)
-            );
-            dispatch(dismissUndo(undoId, false));
-        }
-    };
+export const performUndo = createThunkAction(PERFORM_UNDO, undoId => {
+  return (dispatch, getState) => {
+    MetabaseAnalytics.trackEvent("Undo", "Perform Undo");
+    let undo = _.findWhere(getState().undo, { id: undoId });
+    if (undo) {
+      undo.actions.map(action => dispatch(action));
+      dispatch(dismissUndo(undoId, false));
+    }
+  };
 });
 
 export default function(state = [], { type, payload, error }) {
-    switch (type) {
-        case ADD_UNDO:
-            if (error) {
-                console.warn("ADD_UNDO", payload);
-                return state;
-            }
-            let previous = state[state.length - 1];
-            // if last undo was same type then merge them
-            if (previous && payload.type != null && payload.type === previous.type) {
-                return state.slice(0, -1).concat({
-                    ...payload,
-                    count: previous.count + payload.count,
-                    actions: [...previous.actions, ...payload.actions],
-                    _domId: previous._domId // use original _domId so we don't get funky animations swapping for the new one
-                });
-            } else {
-                return state.concat(payload);
-            }
-        case DISMISS_UNDO:
-            if (error) {
-                console.warn("DISMISS_UNDO", payload);
-                return state;
-            }
-            return state.filter(undo => undo.id !== payload);
-    }
-    return state;
+  switch (type) {
+    case ADD_UNDO:
+      if (error) {
+        console.warn("ADD_UNDO", payload);
+        return state;
+      }
+      let previous = state[state.length - 1];
+      // if last undo was same type then merge them
+      if (previous && payload.type != null && payload.type === previous.type) {
+        return state.slice(0, -1).concat({
+          ...payload,
+          count: previous.count + payload.count,
+          actions: [...previous.actions, ...payload.actions],
+          _domId: previous._domId, // use original _domId so we don't get funky animations swapping for the new one
+        });
+      } else {
+        return state.concat(payload);
+      }
+    case DISMISS_UNDO:
+      if (error) {
+        console.warn("DISMISS_UNDO", payload);
+        return state;
+      }
+      return state.filter(undo => undo.id !== payload);
+  }
+  return state;
 }
diff --git a/frontend/src/metabase/redux/user.js b/frontend/src/metabase/redux/user.js
index 071a08cd4611c9c1165385fdb5492298a96667e7..c39be7f85b57e0c3fc8765ffe323cd6fc6a65b57 100644
--- a/frontend/src/metabase/redux/user.js
+++ b/frontend/src/metabase/redux/user.js
@@ -1,6 +1,10 @@
 /* @flow */
 
-import { createAction, handleActions, createThunkAction } from "metabase/lib/redux";
+import {
+  createAction,
+  handleActions,
+  createThunkAction,
+} from "metabase/lib/redux";
 
 import { CLOSE_QB_NEWB_MODAL } from "metabase/query_builder/actions";
 import { LOGOUT } from "metabase/auth/auth";
@@ -9,28 +13,34 @@ import { UserApi } from "metabase/services";
 
 export const REFRESH_CURRENT_USER = "metabase/user/REFRESH_CURRENT_USER";
 export const refreshCurrentUser = createAction(REFRESH_CURRENT_USER, () => {
-    try {
-        return UserApi.current();
-    } catch (e) {
-        return null;
-    }
+  try {
+    return UserApi.current();
+  } catch (e) {
+    return null;
+  }
 });
 
 export const LOAD_CURRENT_USER = "metabase/user/LOAD_CURRENT_USER";
-export const loadCurrentUser = createThunkAction(LOAD_CURRENT_USER, () =>
-    async (dispatch, getState) => {
-        if (!getState().currentUser) {
-            await dispatch(refreshCurrentUser());
-        }
+export const loadCurrentUser = createThunkAction(
+  LOAD_CURRENT_USER,
+  () => async (dispatch, getState) => {
+    if (!getState().currentUser) {
+      await dispatch(refreshCurrentUser());
     }
-)
+  },
+);
 
-export const CLEAR_CURRENT_USER = "metabase/user/CLEAR_CURRENT_USER"
-export const clearCurrentUser = createAction(CLEAR_CURRENT_USER)
+export const CLEAR_CURRENT_USER = "metabase/user/CLEAR_CURRENT_USER";
+export const clearCurrentUser = createAction(CLEAR_CURRENT_USER);
 
-export const currentUser = handleActions({
+export const currentUser = handleActions(
+  {
     [LOGOUT]: { next: (state, { payload }) => null },
     [CLEAR_CURRENT_USER]: { next: (state, payload) => null },
     [REFRESH_CURRENT_USER]: { next: (state, { payload }) => payload },
-    [CLOSE_QB_NEWB_MODAL]: { next: (state, { payload }) => ({ ...state, is_qbnewb: false }) },
-}, null);
+    [CLOSE_QB_NEWB_MODAL]: {
+      next: (state, { payload }) => ({ ...state, is_qbnewb: false }),
+    },
+  },
+  null,
+);
diff --git a/frontend/src/metabase/reference/Reference.css b/frontend/src/metabase/reference/Reference.css
index 42975fa68eee68a584f6bbba71f762a162f08b68..16bd412f9db4cd2012afe6690b8f650aa0eeb501 100644
--- a/frontend/src/metabase/reference/Reference.css
+++ b/frontend/src/metabase/reference/Reference.css
@@ -1,194 +1,192 @@
 :root {
-    --title-color: #606E7B;
-    --subtitle-color: #AAB7C3;
-    --icon-width: calc(48px + 1rem);
+  --title-color: #606e7b;
+  --subtitle-color: #aab7c3;
+  --icon-width: calc(48px + 1rem);
 }
 
 :local(.guideEmpty) {
-    composes: flex full justify-center from "style";
-    padding-top: 75px;
+  composes: flex full justify-center from "style";
+  padding-top: 75px;
 }
 
 :local(.guideEmptyBody) {
-    composes: text-centered from "style";
-    max-width: 400px;
+  composes: text-centered from "style";
+  max-width: 400px;
 }
 
 :local(.guideEmptyMessage) {
-    composes: text-dark text-paragraph text-centered mt3 from "style";
+  composes: text-dark text-paragraph text-centered mt3 from "style";
 }
 
-
 :local(.columnHeader) {
-    composes: flex flex-full from "style";
-    margin-left: var(--icon-width);
-    padding-top: 20px;
-    padding-bottom: 20px;
+  composes: flex flex-full from "style";
+  margin-left: var(--icon-width);
+  padding-top: 20px;
+  padding-bottom: 20px;
 }
 
 :local(.revisionsWrapper) {
-    padding-top: 20px;
-    padding-left: var(--icon-width);
+  padding-top: 20px;
+  padding-left: var(--icon-width);
 }
 
 :local(.schemaSeparator) {
-    composes: text-grey-2 mt2 from "style";
-    margin-left: var(--icon-width);
-    font-size: 18px;
+  composes: text-grey-2 mt2 from "style";
+  margin-left: var(--icon-width);
+  font-size: 18px;
 }
 
 :local(.tableActualName) {
-    color: var(--subtitle-color);
+  color: var(--subtitle-color);
 }
 
 :local(.guideLeftPadded) {
-    composes: flex full justify-center from "style";
+  composes: flex full justify-center from "style";
 }
 
 :local(.guideLeftPadded)::before {
-    /*FIXME: not sure how to share this with other components
+  /*FIXME: not sure how to share this with other components
      because we can't use composes here apparently. any workarounds?*/
-    content: '';
-    display: block;
-    flex: 0.3;
-    max-width: 250px;
-    margin-right: 50px;
+  content: "";
+  display: block;
+  flex: 0.3;
+  max-width: 250px;
+  margin-right: 50px;
 }
 
 :local(.guideLeftPaddedBody) {
-    flex: 0.7;
-    max-width: 550px;
+  flex: 0.7;
+  max-width: 550px;
 }
 
 :local(.guideWrapper) {
-    margin-bottom: 50px;
+  margin-bottom: 50px;
 }
 
 :local(.guideTitle) {
-    composes: guideLeftPadded;
-    font-size: 24px;
-    margin-top: 50px;
+  composes: guideLeftPadded;
+  font-size: 24px;
+  margin-top: 50px;
 }
 
 :local(.guideTitleBody) {
-    composes: full text-dark text-bold from "style";
-    composes: guideLeftPaddedBody;
+  composes: full text-dark text-bold from "style";
+  composes: guideLeftPaddedBody;
 }
 
 :local(.guideSeeAll) {
-    composes: guideLeftPadded;
-    font-size: 18px;
+  composes: guideLeftPadded;
+  font-size: 18px;
 }
 
 :local(.guideSeeAllBody) {
-    composes: flex full text-dark text-bold mt4 from "style";
-    composes: guideLeftPaddedBody;
+  composes: flex full text-dark text-bold mt4 from "style";
+  composes: guideLeftPaddedBody;
 }
 
 :local(.guideSeeAllLink) {
-    composes: flex-full block no-decoration py1 border-top from "style";
+  composes: flex-full block no-decoration py1 border-top from "style";
 }
 
 :local(.guideContact) {
-    composes: mt4 from "style";
-    composes: guideLeftPadded;
-    margin-bottom: 100px;
+  composes: mt4 from "style";
+  composes: guideLeftPadded;
+  margin-bottom: 100px;
 }
 
 :local(.guideContactBody) {
-    composes: full from "style";
-    composes: guideLeftPaddedBody;
-    font-size: 16px;
+  composes: full from "style";
+  composes: guideLeftPaddedBody;
+  font-size: 16px;
 }
 
 :local(.guideEditHeader) {
-    composes: full text-body my4 from "style";
-    max-width: 550px;
-    color: var(--dark-color);
+  composes: full text-body my4 from "style";
+  max-width: 550px;
+  color: var(--dark-color);
 }
 
 :local(.guideEditHeaderTitle) {
-    composes: text-bold mb2 from "style";
-    font-size: 24px;
+  composes: text-bold mb2 from "style";
+  font-size: 24px;
 }
 
 :local(.guideEditCards) {
-    composes: mt2 mb4 from "style";
+  composes: mt2 mb4 from "style";
 }
 
 :local(.guideEditCard) {
-    composes: input p4 from "style";
+  composes: input p4 from "style";
 }
 
-
 :local(.guideEditLabel) {
-    composes: block text-bold mb2 from "style";
-    font-size: 16px;
-    color: var(--title-color);
+  composes: block text-bold mb2 from "style";
+  font-size: 16px;
+  color: var(--title-color);
 }
 
 :local(.guideEditHeaderDescription) {
-    font-size: 16px;
+  font-size: 16px;
 }
 
 :local(.guideEditTitle) {
-    composes: block text-body text-bold from "style";
-    color: var(--title-color);
-    font-size: 16px;
-    margin-top: 50px;
+  composes: block text-body text-bold from "style";
+  color: var(--title-color);
+  font-size: 16px;
+  margin-top: 50px;
 }
 
 :local(.guideEditSubtitle) {
-    composes: text-body from "style";
-    color: var(--grey-2);
-    font-size: 16px;
-    max-width: 700px;
+  composes: text-body from "style";
+  color: var(--grey-2);
+  font-size: 16px;
+  max-width: 700px;
 }
 
 :local(.guideEditAddButton) {
-    composes: flex full my2 pl4 from "style";
-    padding-right: 3.5rem;
+  composes: flex full my2 pl4 from "style";
+  padding-right: 3.5rem;
 }
 
 :local(.guideEditAddButton)::before {
-    content: '';
-    display: block;
-    flex: 250;
-    max-width: 250px;
-    margin-right: 50px;
+  content: "";
+  display: block;
+  flex: 250;
+  max-width: 250px;
+  margin-right: 50px;
 }
 
 :local(.guideEditAddButtonBody) {
-    flex: 550;
-    max-width: 550px;    
+  flex: 550;
+  max-width: 550px;
 }
 
 :local(.guideEditTextarea) {
-    composes: text-dark input p2 from "style";
-    resize: none;
-    font-size: 16px;
-    width: 100%;
-    max-width: 850px;
-    min-height: 100px;
+  composes: text-dark input p2 from "style";
+  resize: none;
+  font-size: 16px;
+  width: 100%;
+  max-width: 850px;
+  min-height: 100px;
 }
 
 :local(.guideEditContact) {
-    composes: flex from "style";
+  composes: flex from "style";
 }
 
 :local(.guideEditContactName) {
-    flex: 250;
-    max-width: 250px;
-    margin-right: 50px;
+  flex: 250;
+  max-width: 250px;
+  margin-right: 50px;
 }
 
 :local(.guideEditContactEmail) {
-    flex: 550;
-    max-width: 550px;
+  flex: 550;
+  max-width: 550px;
 }
 
 :local(.guideEditInput) {
-    composes: full text-dark input p2 from "style";
-    font-size: 16px;
-    display: block;
+  composes: full text-dark input p2 from "style";
+  font-size: 16px;
+  display: block;
 }
diff --git a/frontend/src/metabase/reference/components/Detail.css b/frontend/src/metabase/reference/components/Detail.css
index 2f87df61e2a3a8ac08a7fc8c8bd1c12fab9cb4ec..027b4273e7eb0505d8d37896e398facd651fbf7e 100644
--- a/frontend/src/metabase/reference/components/Detail.css
+++ b/frontend/src/metabase/reference/components/Detail.css
@@ -1,44 +1,44 @@
 :root {
-    --title-color: #606E7B;
-    --subtitle-color: #AAB7C3;
-    --muted-color: #DEEAF1;
-    --blue-color: #2D86D4;
-    --icon-width: calc(48px + 1rem);
+  --title-color: #606e7b;
+  --subtitle-color: #aab7c3;
+  --muted-color: #deeaf1;
+  --blue-color: #2d86d4;
+  --icon-width: calc(48px + 1rem);
 }
 
 :local(.detail) {
-    composes: flex align-center from "style";
-    composes: relative from "style";
-    margin-left: var(--icon-width);
+  composes: flex align-center from "style";
+  composes: relative from "style";
+  margin-left: var(--icon-width);
 }
 
 :local(.detailBody) {
-    composes: flex-full from "style";
-    max-width: 550px;
-    padding-top: 20px;
-    padding-bottom: 20px;
+  composes: flex-full from "style";
+  max-width: 550px;
+  padding-top: 20px;
+  padding-bottom: 20px;
 }
 
 :local(.detailTitle) {
-    composes: text-bold inline-block from "style";
-    color: var(--title-color);
-    font-size: 18px;
+  composes: text-bold inline-block from "style";
+  color: var(--title-color);
+  font-size: 18px;
 }
 
 :local(.detailSubtitle) {
-    composes: text-dark mt2 text-paragraph from "style";
-    white-space: pre-wrap;
+  composes: text-dark mt2 text-paragraph from "style";
+  white-space: pre-wrap;
 }
 
 :local(.detailSubtitleLight) {
-    composes: mt2 text-paragraph from "style";
-    color: var(--subtitle-color);
+  composes: mt2 text-paragraph from "style";
+  color: var(--subtitle-color);
 }
 
 :local(.detailTextarea) {
-    composes: text-dark input p2 from "style";
-    resize: none;
-    font-size: 16px;
-    width: 100%;
-    min-height: 100px;
+  composes: text-dark input p2 from "style";
+  resize: none;
+  font-size: 16px;
+  width: 100%;
+  min-height: 100px;
 }
diff --git a/frontend/src/metabase/reference/components/Detail.jsx b/frontend/src/metabase/reference/components/Detail.jsx
index 1421f08eb9ca1371d023e972604774abda7c398f..9c211e72b9070876b153babb91c86fa0f9eef621 100644
--- a/frontend/src/metabase/reference/components/Detail.jsx
+++ b/frontend/src/metabase/reference/components/Detail.jsx
@@ -3,47 +3,67 @@ import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
 import S from "./Detail.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 import pure from "recompose/pure";
 
-const Detail = ({ name, description, placeholder, subtitleClass, url, icon, isEditing, field }) =>
-    <div className={cx(S.detail)}>
-        <div className={S.detailBody}>
-            <div className={S.detailTitle}>
-                { url ?
-                    <Link to={url} className={S.detailName}>{name}</Link> :
-                    <span className={S.detailName}>{name}</span>
-                }
-            </div>
-            <div className={cx(description ? S.detailSubtitle : S.detailSubtitleLight, { "mt1" : true })}>
-                { isEditing ?
-                    <textarea
-                        className={S.detailTextarea}
-                        placeholder={placeholder}
-                        onChange={field.onChange}
-                        //FIXME: use initialValues from redux forms instead of default value
-                        // to allow for reinitializing on cancel (see GettingStartedGuide.jsx)
-                        defaultValue={description}
-                    /> :
-                    <span className={subtitleClass}>{description || placeholder || t`No description yet`}</span>
-                }
-                { isEditing && field.error && field.touched &&
-                    <span className="text-error">{field.error}</span>
-                }
-            </div>
-        </div>
+const Detail = ({
+  name,
+  description,
+  placeholder,
+  subtitleClass,
+  url,
+  icon,
+  isEditing,
+  field,
+}) => (
+  <div className={cx(S.detail)}>
+    <div className={S.detailBody}>
+      <div className={S.detailTitle}>
+        {url ? (
+          <Link to={url} className={S.detailName}>
+            {name}
+          </Link>
+        ) : (
+          <span className={S.detailName}>{name}</span>
+        )}
+      </div>
+      <div
+        className={cx(description ? S.detailSubtitle : S.detailSubtitleLight, {
+          mt1: true,
+        })}
+      >
+        {isEditing ? (
+          <textarea
+            className={S.detailTextarea}
+            placeholder={placeholder}
+            onChange={field.onChange}
+            //FIXME: use initialValues from redux forms instead of default value
+            // to allow for reinitializing on cancel (see GettingStartedGuide.jsx)
+            defaultValue={description}
+          />
+        ) : (
+          <span className={subtitleClass}>
+            {description || placeholder || t`No description yet`}
+          </span>
+        )}
+        {isEditing &&
+          field.error &&
+          field.touched && <span className="text-error">{field.error}</span>}
+      </div>
     </div>
+  </div>
+);
 
 Detail.propTypes = {
-    name:               PropTypes.string.isRequired,
-    url:                PropTypes.string,
-    description:        PropTypes.string,
-    placeholder:        PropTypes.string,
-    subtitleClass:      PropTypes.string,
-    icon:               PropTypes.string,
-    isEditing:          PropTypes.bool,
-    field:              PropTypes.object
+  name: PropTypes.string.isRequired,
+  url: PropTypes.string,
+  description: PropTypes.string,
+  placeholder: PropTypes.string,
+  subtitleClass: PropTypes.string,
+  icon: PropTypes.string,
+  isEditing: PropTypes.bool,
+  field: PropTypes.object,
 };
 
 export default pure(Detail);
diff --git a/frontend/src/metabase/reference/components/EditButton.css b/frontend/src/metabase/reference/components/EditButton.css
index 41a46282c02ee031aa5e54dfd958ae44657c77c8..31f8a7ebaa523f4b111519ca03c27875fe90f160 100644
--- a/frontend/src/metabase/reference/components/EditButton.css
+++ b/frontend/src/metabase/reference/components/EditButton.css
@@ -1,15 +1,15 @@
 :local(.editButton) {
-    composes: flex align-center text-dark p0 mx1 from "style";
-    color: var(--primary-button-bg-color);
-    font-weight: normal;
-    font-size: 16px;
+  composes: flex align-center text-dark p0 mx1 from "style";
+  color: var(--primary-button-bg-color);
+  font-weight: normal;
+  font-size: 16px;
 }
 
 :local(.editButton):hover {
-    color: color(var(--primary-button-border-color) shade(10%));
-    transition: color .3s linear;
+  color: color(var(--primary-button-border-color) shade(10%));
+  transition: color 0.3s linear;
 }
 
 :local(.editButtonBody) {
-    composes: flex align-center relative from "style";
-}
\ No newline at end of file
+  composes: flex align-center relative from "style";
+}
diff --git a/frontend/src/metabase/reference/components/EditButton.jsx b/frontend/src/metabase/reference/components/EditButton.jsx
index 3d0200e6e99a031231a3272cd8c144dae1786803..fd902fc409c686a9a7444bb0ec6d2641c7775e07 100644
--- a/frontend/src/metabase/reference/components/EditButton.jsx
+++ b/frontend/src/metabase/reference/components/EditButton.jsx
@@ -2,29 +2,27 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./EditButton.css";
 
 import Icon from "metabase/components/Icon.jsx";
 
-const EditButton = ({
-    className,
-    startEditing
-}) =>
-    <button
-        className={cx("Button", "Button--borderless", S.editButton, className)}
-        type="button"
-        onClick={startEditing}
-    >
-        <div className={S.editButtonBody}>
-            <Icon name="pencil" size={16} />
-            <span className="ml1">{t`Edit`}</span>
-        </div>
-    </button>
+const EditButton = ({ className, startEditing }) => (
+  <button
+    className={cx("Button", "Button--borderless", S.editButton, className)}
+    type="button"
+    onClick={startEditing}
+  >
+    <div className={S.editButtonBody}>
+      <Icon name="pencil" size={16} />
+      <span className="ml1">{t`Edit`}</span>
+    </div>
+  </button>
+);
 
 EditButton.propTypes = {
-    className: PropTypes.string,
-    startEditing: PropTypes.func.isRequired
+  className: PropTypes.string,
+  startEditing: PropTypes.func.isRequired,
 };
 
 export default pure(EditButton);
diff --git a/frontend/src/metabase/reference/components/EditHeader.css b/frontend/src/metabase/reference/components/EditHeader.css
index 2e6e704fb09e8fe36b297bdd6000b3f41e0a6077..fc3ddd5cfcf8c5aa06c8fc70f2933d9d2c770051 100644
--- a/frontend/src/metabase/reference/components/EditHeader.css
+++ b/frontend/src/metabase/reference/components/EditHeader.css
@@ -1,32 +1,31 @@
 :root {
-    --edit-header-color: #6CAFED;
+  --edit-header-color: #6cafed;
 }
 
-
 :local(.editHeader) {
-    composes: text-white flex align-center from "style";
-    position: absolute;
-    top: 0;
-    left: 0;
-    right: 0;
-    height: 43px;
-    background-color: var(--edit-header-color);
+  composes: text-white flex align-center from "style";
+  position: absolute;
+  top: 0;
+  left: 0;
+  right: 0;
+  height: 43px;
+  background-color: var(--edit-header-color);
 }
 
 :local(.editHeaderButtons) {
-    composes: flex-align-right from "style";
+  composes: flex-align-right from "style";
 }
 
 :local(.editHeaderButton) {
-    border: none;
-    color: var(--edit-header-color);
+  border: none;
+  color: var(--edit-header-color);
 }
 
 :local(.saveButton) {
-    composes: editHeaderButton;
+  composes: editHeaderButton;
 }
 
 :local(.cancelButton) {
-    composes: editHeaderButton;
-    opacity: 0.5;
+  composes: editHeaderButton;
+  opacity: 0.5;
 }
diff --git a/frontend/src/metabase/reference/components/EditHeader.jsx b/frontend/src/metabase/reference/components/EditHeader.jsx
index 687654e4bbd5fc070c89aab04926ef5bc847543d..b7c62f2865952d9c97f3b4a80e0a192de5372a64 100644
--- a/frontend/src/metabase/reference/components/EditHeader.jsx
+++ b/frontend/src/metabase/reference/components/EditHeader.jsx
@@ -2,66 +2,83 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./EditHeader.css";
 
 import RevisionMessageModal from "metabase/reference/components/RevisionMessageModal.jsx";
 
 const EditHeader = ({
-    hasRevisionHistory,
-    endEditing,
-    reinitializeForm = () => undefined,
-    submitting,
-    onSubmit,
-    revisionMessageFormField
-}) =>
-    <div className={cx("EditHeader wrapper py1", S.editHeader)}>
-        <div>
-            {t`You are editing this page`}
-        </div>
-        <div className={S.editHeaderButtons}>
-            <button
-                type="button"
-                className={cx("Button", "Button--white", "Button--small", S.cancelButton)}
-                onClick={() => {
-                    endEditing();
-                    reinitializeForm();
-                }}
-            >
-                {t`Cancel`}
-            </button>
+  hasRevisionHistory,
+  endEditing,
+  reinitializeForm = () => undefined,
+  submitting,
+  onSubmit,
+  revisionMessageFormField,
+}) => (
+  <div className={cx("EditHeader wrapper py1", S.editHeader)}>
+    <div>{t`You are editing this page`}</div>
+    <div className={S.editHeaderButtons}>
+      <button
+        type="button"
+        className={cx(
+          "Button",
+          "Button--white",
+          "Button--small",
+          S.cancelButton,
+        )}
+        onClick={() => {
+          endEditing();
+          reinitializeForm();
+        }}
+      >
+        {t`Cancel`}
+      </button>
 
-            { hasRevisionHistory ?
-                <RevisionMessageModal
-                    action={() => onSubmit()}
-                    field={revisionMessageFormField}
-                    submitting={submitting}
-                >
-                    <button
-                        className={cx("Button", "Button--primary", "Button--white", "Button--small", S.saveButton)}
-                        type="button"
-                        disabled={submitting}
-                    >
-                        {t`Save`}
-                    </button>
-                </RevisionMessageModal> :
-                <button
-                    className={cx("Button", "Button--primary", "Button--white", "Button--small", S.saveButton)}
-                    type="submit"
-                    disabled={submitting}
-                >
-                    {t`Save`}
-                </button>
-            }
-        </div>
-    </div>;
+      {hasRevisionHistory ? (
+        <RevisionMessageModal
+          action={() => onSubmit()}
+          field={revisionMessageFormField}
+          submitting={submitting}
+        >
+          <button
+            className={cx(
+              "Button",
+              "Button--primary",
+              "Button--white",
+              "Button--small",
+              S.saveButton,
+            )}
+            type="button"
+            disabled={submitting}
+          >
+            {t`Save`}
+          </button>
+        </RevisionMessageModal>
+      ) : (
+        <button
+          className={cx(
+            "Button",
+            "Button--primary",
+            "Button--white",
+            "Button--small",
+            S.saveButton,
+          )}
+          type="submit"
+          disabled={submitting}
+        >
+          {t`Save`}
+        </button>
+      )}
+    </div>
+  </div>
+);
 EditHeader.propTypes = {
-    hasRevisionHistory: PropTypes.bool,
-    endEditing: PropTypes.func.isRequired,
-    reinitializeForm: PropTypes.func,
-    submitting: PropTypes.bool.isRequired,
-    onSubmit: PropTypes.func,
-    revisionMessageFormField: PropTypes.object
+  hasRevisionHistory: PropTypes.bool,
+  endEditing: PropTypes.func.isRequired,
+  reinitializeForm: PropTypes.func,
+  submitting: PropTypes.bool.isRequired,
+  onSubmit: PropTypes.func,
+  revisionMessageFormField: PropTypes.object,
 };
 
 export default pure(EditHeader);
diff --git a/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx b/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx
index e79917865d4bb20b1e03e06ea6da1f62a4022133..83d06049d4a4208293b4bdeac19b34ad325b1acd 100644
--- a/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx
+++ b/frontend/src/metabase/reference/components/EditableReferenceHeader.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 import { Link } from "react-router";
 import cx from "classnames";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./ReferenceHeader.css";
 import L from "metabase/components/List.css";
 import E from "metabase/reference/components/EditButton.css";
@@ -14,120 +14,130 @@ import Input from "metabase/components/Input.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 import EditButton from "metabase/reference/components/EditButton.jsx";
 
-
 const EditableReferenceHeader = ({
-    entity = {},
-    table,
-    type,
-    headerIcon,
-    headerLink,
-    name,
-    user,
-    isEditing,
-    hasSingleSchema,
-    hasDisplayName,
-    startEditing,
-    displayNameFormField,
-    nameFormField
-}) =>
-    <div className="wrapper wrapper--trim">
-        <div className={cx("relative", L.header)} style={type === 'segment' ? {marginBottom: 0} : {}}>
-            <div className={L.leftIcons}>
-                { headerIcon &&
-                    <IconBorder
-                        borderWidth="0"
-                        style={{backgroundColor: "#E9F4F8"}}
-                    >
-                        <Icon
-                            className="text-brand"
-                            name={headerIcon}
-                            width={24}
-                            height={24}
-                        />
-                    </IconBorder>
-                }
-            </div>
-            { type === 'table' && !hasSingleSchema && !isEditing &&
-                <div className={S.headerSchema}>{entity.schema}</div>
+  entity = {},
+  table,
+  type,
+  headerIcon,
+  headerLink,
+  name,
+  user,
+  isEditing,
+  hasSingleSchema,
+  hasDisplayName,
+  startEditing,
+  displayNameFormField,
+  nameFormField,
+}) => (
+  <div className="wrapper wrapper--trim">
+    <div
+      className={cx("relative", L.header)}
+      style={type === "segment" ? { marginBottom: 0 } : {}}
+    >
+      <div className={L.leftIcons}>
+        {headerIcon && (
+          <IconBorder borderWidth="0" style={{ backgroundColor: "#E9F4F8" }}>
+            <Icon
+              className="text-brand"
+              name={headerIcon}
+              width={24}
+              height={24}
+            />
+          </IconBorder>
+        )}
+      </div>
+      {type === "table" &&
+        !hasSingleSchema &&
+        !isEditing && <div className={S.headerSchema}>{entity.schema}</div>}
+      <div
+        className={S.headerBody}
+        style={
+          isEditing && name === "Details" ? { alignItems: "flex-start" } : {}
+        }
+      >
+        {isEditing && name === "Details" ? (
+          <Input
+            className={S.headerTextInput}
+            type="text"
+            placeholder={entity.name}
+            onChange={
+              hasDisplayName
+                ? displayNameFormField.onChange
+                : nameFormField.onChange
             }
-            <div
-                className={S.headerBody}
-                style={isEditing && name === 'Details' ? {alignItems: "flex-start"} : {}}
+            defaultValue={hasDisplayName ? entity.display_name : entity.name}
+          />
+        ) : (
+          [
+            <Ellipsified
+              key="1"
+              className={!headerLink && "flex-full"}
+              tooltipMaxWidth="100%"
             >
-                { isEditing && name === 'Details' ?
-                        <Input
-                            className={S.headerTextInput}
-                            type="text"
-                            placeholder={entity.name}
-                            onChange={
-                                hasDisplayName ? displayNameFormField.onChange : nameFormField.onChange
-                            }
-                            defaultValue={
-                                hasDisplayName ? entity.display_name : entity.name
-                            }
-
-                        />
-                        :
-                    [
-                        <Ellipsified
-                            key="1"
-                            className={!headerLink && "flex-full"}
-                            tooltipMaxWidth="100%"
-                        >
-                            { name === 'Details' ?
-                                hasDisplayName ?
-                                    entity.display_name || entity.name :
-                                    entity.name :
-                                name
-                            }
-                        </Ellipsified>,
-                        headerLink &&
-                            <div key="2" className={cx("flex-full", S.headerButton)}>
-                                <Link
-                                    to={headerLink}
-                                    className={cx("Button", "Button--borderless", "ml3", E.editButton)}
-                                    data-metabase-event={`Data Reference;Entity -> QB click;${type}`}
-                                >
-                                    <div className="flex align-center relative">
-                                        <span className="mr1 flex-no-shrink">{t`See this ${type}`}</span>
-                                        <Icon name="chevronright" size={16} />
-                                    </div>
-                                </Link>
-                            </div>
-                    ]
-                }
-                { user && user.is_superuser && !isEditing &&
-                    <EditButton className="ml1" startEditing={startEditing} />
-                }
-            </div>
+              {name === "Details"
+                ? hasDisplayName
+                  ? entity.display_name || entity.name
+                  : entity.name
+                : name}
+            </Ellipsified>,
+            headerLink && (
+              <div key="2" className={cx("flex-full", S.headerButton)}>
+                <Link
+                  to={headerLink}
+                  className={cx(
+                    "Button",
+                    "Button--borderless",
+                    "ml3",
+                    E.editButton,
+                  )}
+                  data-metabase-event={`Data Reference;Entity -> QB click;${type}`}
+                >
+                  <div className="flex align-center relative">
+                    <span className="mr1 flex-no-shrink">{t`See this ${type}`}</span>
+                    <Icon name="chevronright" size={16} />
+                  </div>
+                </Link>
+              </div>
+            ),
+          ]
+        )}
+        {user &&
+          user.is_superuser &&
+          !isEditing && (
+            <EditButton className="ml1" startEditing={startEditing} />
+          )}
+      </div>
+    </div>
+    {type === "segment" &&
+      table && (
+        <div className={S.subheader}>
+          <div className={cx(S.subheaderBody)}>
+            {t`A subset of`}{" "}
+            <Link
+              className={S.subheaderLink}
+              to={`/reference/databases/${table.db_id}/tables/${table.id}`}
+            >
+              {table.display_name}
+            </Link>
+          </div>
         </div>
-        { type === 'segment' && table &&
-            <div className={S.subheader}>
-                <div className={cx(S.subheaderBody)}>
-                    {t`A subset of`} <Link
-                        className={S.subheaderLink}
-                        to={`/reference/databases/${table.db_id}/tables/${table.id}`}
-                    >
-                        {table.display_name}
-                    </Link>
-                </div>
-            </div>
-        }
-    </div>;
+      )}
+  </div>
+);
 EditableReferenceHeader.propTypes = {
-    entity: PropTypes.object,
-    table: PropTypes.object,
-    type: PropTypes.string,
-    headerIcon: PropTypes.string,
-    headerLink: PropTypes.string,
-    name: PropTypes.string,
-    user: PropTypes.object,
-    isEditing: PropTypes.bool,
-    hasSingleSchema: PropTypes.bool,
-    hasDisplayName: PropTypes.bool,
-    startEditing: PropTypes.func,
-    displayNameFormField: PropTypes.object,
-    nameFormField: PropTypes.object
+  entity: PropTypes.object,
+  table: PropTypes.object,
+  type: PropTypes.string,
+  headerIcon: PropTypes.string,
+  headerLink: PropTypes.string,
+  name: PropTypes.string,
+  user: PropTypes.object,
+  isEditing: PropTypes.bool,
+  hasSingleSchema: PropTypes.bool,
+  hasDisplayName: PropTypes.bool,
+  startEditing: PropTypes.func,
+  displayNameFormField: PropTypes.object,
+  nameFormField: PropTypes.object,
 };
 
 export default pure(EditableReferenceHeader);
diff --git a/frontend/src/metabase/reference/components/Field.css b/frontend/src/metabase/reference/components/Field.css
index 85bbe4026a3138d874a91036e3b1caeb5eb5c1e6..b233ab73b53385b793748289a2294254f80d7035 100644
--- a/frontend/src/metabase/reference/components/Field.css
+++ b/frontend/src/metabase/reference/components/Field.css
@@ -1,55 +1,55 @@
 :root {
-    --title-color: #606E7B;
+  --title-color: #606e7b;
 }
 
 :local(.field) {
-    composes: flex align-center from "style";
+  composes: flex align-center from "style";
 }
 
 :local(.fieldNameTitle) {
-    composes: flex-full pr2 from "style";
+  composes: flex-full pr2 from "style";
 }
 
 :local(.fieldName) {
-    composes: fieldNameTitle;
-    font-size: 16px;
+  composes: fieldNameTitle;
+  font-size: 16px;
 }
 
 :local(.fieldNameTextInput) {
-    composes: input p1 from "style";
-    color: var(--title-color);
-    width: 100%;
-    font-size: 14px;
+  composes: input p1 from "style";
+  color: var(--title-color);
+  width: 100%;
+  font-size: 14px;
 }
 
 :local(.fieldSelect) {
-    composes: input p1 block from "style";
+  composes: input p1 block from "style";
 }
 
 :local(.fieldType) {
-    composes: flex-half pr2 from "style";
-    overflow: hidden;
-    white-space: nowrap;
+  composes: flex-half pr2 from "style";
+  overflow: hidden;
+  white-space: nowrap;
 }
 
 :local(.fieldDataType) {
-    composes: flex-half from "style";
+  composes: flex-half from "style";
 }
 
 :local(.fieldSecondary) {
-    composes: field;
-    font-size: 13px;
+  composes: field;
+  font-size: 13px;
 }
 
 :local(.fieldActualName) {
-    composes: fieldNameTitle;
-    composes: text-monospace from "style";
+  composes: fieldNameTitle;
+  composes: text-monospace from "style";
 }
 
 :local(.fieldForeignKey) {
-    composes: fieldType;
+  composes: fieldType;
 }
 
 :local(.fieldOther) {
-    composes: fieldDataType;
+  composes: fieldDataType;
 }
diff --git a/frontend/src/metabase/reference/components/Field.jsx b/frontend/src/metabase/reference/components/Field.jsx
index 7816c81b34884a8db027c9e1ae9d2e59ae223378..8523d82d72cf3e097836255280ea6f8f9765852c 100644
--- a/frontend/src/metabase/reference/components/Field.jsx
+++ b/frontend/src/metabase/reference/components/Field.jsx
@@ -2,12 +2,12 @@
 import React from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import * as MetabaseCore from "metabase/lib/core";
 import { isNumericBaseType } from "metabase/lib/schema_metadata";
 import { isa, isFK, TYPE } from "metabase/lib/types";
 
-import { getIn } from 'icepick';
+import { getIn } from "icepick";
 
 import S from "metabase/components/List.css";
 import F from "./Field.css";
@@ -18,106 +18,103 @@ import Icon from "metabase/components/Icon.jsx";
 import cx from "classnames";
 import pure from "recompose/pure";
 
-const Field = ({
-    field,
-    foreignKeys,
-    url,
-    icon,
-    isEditing,
-    formField
-}) =>
-    <div className={cx(S.item)}>
-        <div className={S.leftIcons}>
-            { icon && <Icon className={S.chartIcon} name={icon} size={20} /> }
+const Field = ({ field, foreignKeys, url, icon, isEditing, formField }) => (
+  <div className={cx(S.item)}>
+    <div className={S.leftIcons}>
+      {icon && <Icon className={S.chartIcon} name={icon} size={20} />}
+    </div>
+    <div className={S.itemBody} style={{ maxWidth: "100%", borderTop: "none" }}>
+      <div className={F.field}>
+        <div className={cx(S.itemTitle, F.fieldName)}>
+          {isEditing ? (
+            <input
+              className={F.fieldNameTextInput}
+              type="text"
+              placeholder={field.name}
+              {...formField.display_name}
+              defaultValue={field.display_name}
+            />
+          ) : (
+            <Link to={url} className={S.itemName}>
+              {field.display_name}
+            </Link>
+          )}
         </div>
-        <div className={S.itemBody} style={{maxWidth: "100%", borderTop: "none"}}>
-            <div className={F.field}>
-                <div className={cx(S.itemTitle, F.fieldName)}>
-                    { isEditing ?
-                        <input
-                            className={F.fieldNameTextInput}
-                            type="text"
-                            placeholder={field.name}
-                            {...formField.display_name}
-                            defaultValue={field.display_name}
-                        /> :
-                        <Link to={url} className={S.itemName}>{field.display_name}</Link>
-                    }
-                </div>
-                <div className={F.fieldType}>
-                    { isEditing ?
-                        <Select
-                            triggerClasses={F.fieldSelect}
-                            placeholder={t`Select a field type`}
-                            value={
-                                MetabaseCore.field_special_types_map[formField.special_type.value] ||
-                                MetabaseCore.field_special_types_map[field.special_type]
-                            }
-                            options={
-                                MetabaseCore.field_special_types
-                                    .concat({
-                                        'id': null,
-                                        'name': t`No field type`,
-                                        'section': t`Other`
-                                    })
-                                    .filter(type =>
-                                        isNumericBaseType(field) || !isa(type && type.id, TYPE.UNIXTimestamp)
-                                    )
-                            }
-                            onChange={(type) => formField.special_type.onChange(type.id)}
-                        /> :
-                        <span>
-                            { getIn(
-                                    MetabaseCore.field_special_types_map,
-                                    [field.special_type, 'name']
-                                ) || t`No field type`
-                            }
-                        </span>
-                    }
-                </div>
-                <div className={F.fieldDataType}>
-                    {field.base_type}
-                </div>
-            </div>
-            <div className={cx(S.itemSubtitle, F.fieldSecondary, { "mt1" : true })}>
-                <div className={F.fieldActualName}>
-                    { field.name }
-                </div>
-                <div className={F.fieldForeignKey}>
-                    { isEditing ?
-                        (isFK(formField.special_type.value) ||
-                        (isFK(field.special_type) && formField.special_type.value === undefined)) &&
-                        <Select
-                            triggerClasses={F.fieldSelect}
-                            placeholder={t`Select a field type`}
-                            value={
-                                foreignKeys[formField.fk_target_field_id.value] ||
-                                foreignKeys[field.fk_target_field_id] ||
-                                {}
-                            }
-                            options={Object.values(foreignKeys)}
-                            onChange={(foreignKey) => formField.fk_target_field_id.onChange(foreignKey.id)}
-                            optionNameFn={(foreignKey) => foreignKey.name}
-                        /> :
-                        isFK(field.special_type) &&
-                        <span>
-                            {getIn(foreignKeys, [field.fk_target_field_id, "name"])}
-                        </span>
-                    }
-                </div>
-                <div className={F.fieldOther}>
-                </div>
-            </div>
+        <div className={F.fieldType}>
+          {isEditing ? (
+            <Select
+              triggerClasses={F.fieldSelect}
+              placeholder={t`Select a field type`}
+              value={
+                MetabaseCore.field_special_types_map[
+                  formField.special_type.value
+                ] || MetabaseCore.field_special_types_map[field.special_type]
+              }
+              options={MetabaseCore.field_special_types
+                .concat({
+                  id: null,
+                  name: t`No field type`,
+                  section: t`Other`,
+                })
+                .filter(
+                  type =>
+                    isNumericBaseType(field) ||
+                    !isa(type && type.id, TYPE.UNIXTimestamp),
+                )}
+              onChange={type => formField.special_type.onChange(type.id)}
+            />
+          ) : (
+            <span>
+              {getIn(MetabaseCore.field_special_types_map, [
+                field.special_type,
+                "name",
+              ]) || t`No field type`}
+            </span>
+          )}
         </div>
-    </div>;
+        <div className={F.fieldDataType}>{field.base_type}</div>
+      </div>
+      <div className={cx(S.itemSubtitle, F.fieldSecondary, { mt1: true })}>
+        <div className={F.fieldActualName}>{field.name}</div>
+        <div className={F.fieldForeignKey}>
+          {isEditing
+            ? (isFK(formField.special_type.value) ||
+                (isFK(field.special_type) &&
+                  formField.special_type.value === undefined)) && (
+                <Select
+                  triggerClasses={F.fieldSelect}
+                  placeholder={t`Select a field type`}
+                  value={
+                    foreignKeys[formField.fk_target_field_id.value] ||
+                    foreignKeys[field.fk_target_field_id] ||
+                    {}
+                  }
+                  options={Object.values(foreignKeys)}
+                  onChange={foreignKey =>
+                    formField.fk_target_field_id.onChange(foreignKey.id)
+                  }
+                  optionNameFn={foreignKey => foreignKey.name}
+                />
+              )
+            : isFK(field.special_type) && (
+                <span>
+                  {getIn(foreignKeys, [field.fk_target_field_id, "name"])}
+                </span>
+              )}
+        </div>
+        <div className={F.fieldOther} />
+      </div>
+    </div>
+  </div>
+);
 Field.propTypes = {
-    field: PropTypes.object.isRequired,
-    foreignKeys: PropTypes.object.isRequired,
-    url: PropTypes.string.isRequired,
-    placeholder: PropTypes.string,
-    icon: PropTypes.string,
-    isEditing: PropTypes.bool,
-    formField: PropTypes.object
+  field: PropTypes.object.isRequired,
+  foreignKeys: PropTypes.object.isRequired,
+  url: PropTypes.string.isRequired,
+  placeholder: PropTypes.string,
+  icon: PropTypes.string,
+  isEditing: PropTypes.bool,
+  formField: PropTypes.object,
 };
 
 export default pure(Field);
diff --git a/frontend/src/metabase/reference/components/FieldToGroupBy.css b/frontend/src/metabase/reference/components/FieldToGroupBy.css
index fc52b5652b9d325a50ffc688ddb630c03762b537..18f16afe546a63225b4484eb6d32a2ec2f8ed2c7 100644
--- a/frontend/src/metabase/reference/components/FieldToGroupBy.css
+++ b/frontend/src/metabase/reference/components/FieldToGroupBy.css
@@ -1,5 +1,5 @@
 :local(.fieldToGroupByText) {
-    composes: flex-full from "style";
-    font-size: 16px;
-    color: #AAB7C3;
-}
\ No newline at end of file
+  composes: flex-full from "style";
+  font-size: 16px;
+  color: #aab7c3;
+}
diff --git a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
index 4d9e06aa8e4c9f21536688897f1112b16fc24437..1b53b00e329202e3e36007d43a1d16c63134c875 100644
--- a/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
+++ b/frontend/src/metabase/reference/components/FieldToGroupBy.jsx
@@ -1,46 +1,43 @@
 import React from "react";
 import PropTypes from "prop-types";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./FieldToGroupBy.css";
 import Q from "metabase/components/QueryButton.css";
 
 import Icon from "metabase/components/Icon.jsx";
 
 const FieldToGroupBy = ({
-    className,
-    metric,
-    field,
-    icon,
-    iconClass,
-    onClick,
-    secondaryOnClick,
-}) => 
-    <div className={className}>
-        <a className={Q.queryButton} onClick={onClick}>
-            <span className={S.fieldToGroupByText}>
-                <span>
-                    {`${metric.name} ` + t`by` + ` `}
-                </span>
-                <span className="ml1 text-brand">
-                    {field.display_name}
-                </span>
-            </span>
-            <Icon 
-                className={iconClass} 
-                size={20} 
-                name="reference"
-                onClick={secondaryOnClick}
-            />
-        </a>
-    </div>;
+  className,
+  metric,
+  field,
+  icon,
+  iconClass,
+  onClick,
+  secondaryOnClick,
+}) => (
+  <div className={className}>
+    <a className={Q.queryButton} onClick={onClick}>
+      <span className={S.fieldToGroupByText}>
+        <span>{`${metric.name} ` + t`by` + ` `}</span>
+        <span className="ml1 text-brand">{field.display_name}</span>
+      </span>
+      <Icon
+        className={iconClass}
+        size={20}
+        name="reference"
+        onClick={secondaryOnClick}
+      />
+    </a>
+  </div>
+);
 FieldToGroupBy.propTypes = {
-    className: PropTypes.string,
-    metric: PropTypes.object.isRequired,
-    field: PropTypes.object.isRequired,
-    iconClass: PropTypes.string,
-    onClick: PropTypes.func,
-    secondaryOnClick: PropTypes.func
+  className: PropTypes.string,
+  metric: PropTypes.object.isRequired,
+  field: PropTypes.object.isRequired,
+  iconClass: PropTypes.string,
+  onClick: PropTypes.func,
+  secondaryOnClick: PropTypes.func,
 };
 
 export default pure(FieldToGroupBy);
diff --git a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
index 7c36b62a2543bdb6a48dd2fcc7461e90e1fa5e70..e9037c1f9c73e3b91c86753f12c642814241023b 100644
--- a/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
+++ b/frontend/src/metabase/reference/components/FieldTypeDetail.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 import cx from "classnames";
 import { getIn } from "icepick";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import * as MetabaseCore from "metabase/lib/core";
 import { isNumericBaseType } from "metabase/lib/schema_metadata";
 import { isFK } from "metabase/lib/types";
@@ -13,81 +13,88 @@ import Select from "metabase/components/Select.jsx";
 import D from "metabase/reference/components/Detail.css";
 
 const FieldTypeDetail = ({
-    field,
-    foreignKeys,
-    fieldTypeFormField,
-    foreignKeyFormField,
-    isEditing
-}) =>
-    <div className={cx(D.detail)}>
-        <div className={D.detailBody}>
-            <div className={D.detailTitle}>
-                <span className={D.detailName}>{t`Field type`}</span>
-            </div>
-            <div className={cx(D.detailSubtitle, { "mt1" : true })}>
-                <span>
-                    { isEditing ?
-                        <Select
-                            triggerClasses="rounded bordered p1 inline-block"
-                            placeholder={t`Select a field type`}
-                            value={
-                                MetabaseCore.field_special_types_map[fieldTypeFormField.value] ||
-                                MetabaseCore.field_special_types_map[field.special_type]
-                            }
-                            options={
-                                MetabaseCore.field_special_types
-                                    .concat({
-                                        'id': null,
-                                        'name': t`No field type`,
-                                        'section': t`Other`
-                                    })
-                                    .filter(type => !isNumericBaseType(field) ?
-                                        !(type.id && type.id.startsWith("timestamp_")) :
-                                        true
-                                    )
-                            }
-                            onChange={(type) => fieldTypeFormField.onChange(type.id)}
-                        /> :
-                        <span>
-                            { getIn(
-                                    MetabaseCore.field_special_types_map,
-                                    [field.special_type, 'name']
-                                ) || t`No field type`
-                            }
-                        </span>
-                    }
-                </span>
-                <span className="ml4">
-                    { isEditing ?
-                        (isFK(fieldTypeFormField.value) ||
-                        (isFK(field.special_type) && fieldTypeFormField.value === undefined)) &&
-                        <Select
-                            triggerClasses="rounded bordered p1 inline-block"
-                            placeholder={t`Select a field type`}
-                            value={
-                                foreignKeys[foreignKeyFormField.value] ||
-                                foreignKeys[field.fk_target_field_id] ||
-                                {name: t`Select a Foreign Key`}
-                            }
-                            options={Object.values(foreignKeys)}
-                            onChange={(foreignKey) => foreignKeyFormField.onChange(foreignKey.id)}
-                            optionNameFn={(foreignKey) => foreignKey.name}
-                        /> :
-                        isFK(field.special_type) &&
-                        <span>
-                            {getIn(foreignKeys, [field.fk_target_field_id, "name"])}
-                        </span>
+  field,
+  foreignKeys,
+  fieldTypeFormField,
+  foreignKeyFormField,
+  isEditing,
+}) => (
+  <div className={cx(D.detail)}>
+    <div className={D.detailBody}>
+      <div className={D.detailTitle}>
+        <span className={D.detailName}>{t`Field type`}</span>
+      </div>
+      <div className={cx(D.detailSubtitle, { mt1: true })}>
+        <span>
+          {isEditing ? (
+            <Select
+              triggerClasses="rounded bordered p1 inline-block"
+              placeholder={t`Select a field type`}
+              value={
+                MetabaseCore.field_special_types_map[
+                  fieldTypeFormField.value
+                ] || MetabaseCore.field_special_types_map[field.special_type]
+              }
+              options={MetabaseCore.field_special_types
+                .concat({
+                  id: null,
+                  name: t`No field type`,
+                  section: t`Other`,
+                })
+                .filter(
+                  type =>
+                    !isNumericBaseType(field)
+                      ? !(type.id && type.id.startsWith("timestamp_"))
+                      : true,
+                )}
+              onChange={type => fieldTypeFormField.onChange(type.id)}
+            />
+          ) : (
+            <span>
+              {getIn(MetabaseCore.field_special_types_map, [
+                field.special_type,
+                "name",
+              ]) || t`No field type`}
+            </span>
+          )}
+        </span>
+        <span className="ml4">
+          {isEditing
+            ? (isFK(fieldTypeFormField.value) ||
+                (isFK(field.special_type) &&
+                  fieldTypeFormField.value === undefined)) && (
+                <Select
+                  triggerClasses="rounded bordered p1 inline-block"
+                  placeholder={t`Select a field type`}
+                  value={
+                    foreignKeys[foreignKeyFormField.value] ||
+                    foreignKeys[field.fk_target_field_id] || {
+                      name: t`Select a Foreign Key`,
                     }
+                  }
+                  options={Object.values(foreignKeys)}
+                  onChange={foreignKey =>
+                    foreignKeyFormField.onChange(foreignKey.id)
+                  }
+                  optionNameFn={foreignKey => foreignKey.name}
+                />
+              )
+            : isFK(field.special_type) && (
+                <span>
+                  {getIn(foreignKeys, [field.fk_target_field_id, "name"])}
                 </span>
-            </div>
-        </div>
-    </div>;
+              )}
+        </span>
+      </div>
+    </div>
+  </div>
+);
 FieldTypeDetail.propTypes = {
-    field: PropTypes.object.isRequired,
-    foreignKeys: PropTypes.object.isRequired,
-    fieldTypeFormField: PropTypes.object.isRequired,
-    foreignKeyFormField: PropTypes.object.isRequired,
-    isEditing: PropTypes.bool.isRequired,
+  field: PropTypes.object.isRequired,
+  foreignKeys: PropTypes.object.isRequired,
+  fieldTypeFormField: PropTypes.object.isRequired,
+  foreignKeyFormField: PropTypes.object.isRequired,
+  isEditing: PropTypes.bool.isRequired,
 };
 
 export default pure(FieldTypeDetail);
diff --git a/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx b/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx
index 7781be696a1135925e3337dce4973c19835d4ace..94ef3a6d5fde6edf334a8255541d676db7d7973e 100644
--- a/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx
+++ b/frontend/src/metabase/reference/components/FieldsToGroupBy.jsx
@@ -6,9 +6,7 @@ import S from "./UsefulQuestions.css";
 import D from "metabase/reference/components/Detail.css";
 import L from "metabase/components/List.css";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import FieldToGroupBy from "metabase/reference/components/FieldToGroupBy.jsx";
 
@@ -17,59 +15,73 @@ import { getMetadata } from "metabase/selectors/metadata";
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 
 const mapDispatchToProps = {
-    fetchTableMetadata,
+  fetchTableMetadata,
 };
 
 const mapStateToProps = (state, props) => ({
-    metadata: getMetadata(state, props)
-})
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class FieldsToGroupBy extends Component {
-    props: {
-        fields: Object,
-        databaseId: number,
-        metric: Object,
-        title: string,
-        onChangeLocation: (string) => void,
-        metadata: Metadata
-    }
+  props: {
+    fields: Object,
+    databaseId: number,
+    metric: Object,
+    title: string,
+    onChangeLocation: string => void,
+    metadata: Metadata,
+  };
 
-    render() {
-        const { fields, databaseId, metric, title, onChangeLocation, metadata } = this.props;
+  render() {
+    const {
+      fields,
+      databaseId,
+      metric,
+      title,
+      onChangeLocation,
+      metadata,
+    } = this.props;
 
-        return (
-            <div className={cx(D.detail)}>
-                <div className={D.detailBody}>
-                    <div className={D.detailTitle}>
-                        <span className={D.detailName}>{title}</span>
-                    </div>
-                    <div className={S.usefulQuestions}>
-                        { fields && Object.values(fields)
-                            .map((field, index, fields) =>
-                                <FieldToGroupBy
-                                    key={field.id}
-                                    className={cx("border-bottom", "pt1", "pb1")}
-                                    iconClass={L.icon}
-                                    field={field}
-                                    metric={metric}
-                                    onClick={() => onChangeLocation(getQuestionUrl({
-                                        dbId: databaseId,
-                                        tableId: field.table_id,
-                                        fieldId: field.id,
-                                        metricId: metric.id,
-                                        metadata
-                                    }))}
-                                    secondaryOnClick={(event) => {
-                                        event.stopPropagation();
-                                        onChangeLocation(`/reference/databases/${databaseId}/tables/${field.table_id}/fields/${field.id}`);
-                                    }}
-                                />
-                            )
-                        }
-                    </div>
-                </div>
-            </div>
-        )
-    }
+    return (
+      <div className={cx(D.detail)}>
+        <div className={D.detailBody}>
+          <div className={D.detailTitle}>
+            <span className={D.detailName}>{title}</span>
+          </div>
+          <div className={S.usefulQuestions}>
+            {fields &&
+              Object.values(fields).map((field, index, fields) => (
+                <FieldToGroupBy
+                  key={field.id}
+                  className={cx("border-bottom", "pt1", "pb1")}
+                  iconClass={L.icon}
+                  field={field}
+                  metric={metric}
+                  onClick={() =>
+                    onChangeLocation(
+                      getQuestionUrl({
+                        dbId: databaseId,
+                        tableId: field.table_id,
+                        fieldId: field.id,
+                        metricId: metric.id,
+                        metadata,
+                      }),
+                    )
+                  }
+                  secondaryOnClick={event => {
+                    event.stopPropagation();
+                    onChangeLocation(
+                      `/reference/databases/${databaseId}/tables/${
+                        field.table_id
+                      }/fields/${field.id}`,
+                    );
+                  }}
+                />
+              ))}
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/components/Formula.css b/frontend/src/metabase/reference/components/Formula.css
index 9a4df4dfe9808c91beeab0cef1d90de56d844be2..2ece8eac4c48338425d96439d97e0463ca979cbc 100644
--- a/frontend/src/metabase/reference/components/Formula.css
+++ b/frontend/src/metabase/reference/components/Formula.css
@@ -1,44 +1,44 @@
 :root {
-    --icon-width: calc(48px + 1rem);
+  --icon-width: calc(48px + 1rem);
 }
 
 :local(.formula) {
-    composes: bordered rounded my2 from "style";
-    background-color: #FBFCFD;
-    margin-left: var(--icon-width);
-    max-width: 550px;
-    cursor: pointer;
+  composes: bordered rounded my2 from "style";
+  background-color: #fbfcfd;
+  margin-left: var(--icon-width);
+  max-width: 550px;
+  cursor: pointer;
 }
 
 :local(.formulaHeader) {
-    composes: flex align-center text-brand py1 px2 from "style";
+  composes: flex align-center text-brand py1 px2 from "style";
 }
 
 :local(.formulaTitle) {
-    composes: ml2 from "style";
-    font-size: 16px;
+  composes: ml2 from "style";
+  font-size: 16px;
 }
 
 :local(.formulaDefinitionInner) {
-    composes: p2 from "style";
+  composes: p2 from "style";
 }
 
 .formulaDefinition {
-    overflow: hidden;
+  overflow: hidden;
 }
 
 .formulaDefinition-enter {
-    max-height: 0px;
+  max-height: 0px;
 }
 .formulaDefinition-enter.formulaDefinition-enter-active {
-    /* using 100% max-height breaks the transition */
-    max-height: 150px;
-    transition: max-height 300ms ease-out;
+  /* using 100% max-height breaks the transition */
+  max-height: 150px;
+  transition: max-height 300ms ease-out;
 }
 .formulaDefinition-leave {
-    max-height: 150px;
+  max-height: 150px;
 }
 .formulaDefinition-leave.formulaDefinition-leave-active {
-    max-height: 0px;
-    transition: max-height 300ms ease-out;
+  max-height: 0px;
+  transition: max-height 300ms ease-out;
 }
diff --git a/frontend/src/metabase/reference/components/Formula.jsx b/frontend/src/metabase/reference/components/Formula.jsx
index 3373453fbe91b79b316725df28a98e83ab6ab8ce..e7aa3ccb1a2484d71aa4405544fd5464da85a9bf 100644
--- a/frontend/src/metabase/reference/components/Formula.jsx
+++ b/frontend/src/metabase/reference/components/Formula.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import cx from "classnames";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ReactCSSTransitionGroup from "react-addons-css-transition-group";
 
 import S from "./Formula.css";
@@ -15,52 +15,59 @@ import { getMetadata } from "metabase/selectors/metadata";
 import type Metadata from "metabase-lib/lib/metadata/Metadata";
 
 const mapDispatchToProps = {
-    fetchTableMetadata,
+  fetchTableMetadata,
 };
 
 const mapStateToProps = (state, props) => ({
-    metadata: getMetadata(state, props)
-})
+  metadata: getMetadata(state, props),
+});
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class Formula extends Component {
-    props: {
-        type: string,
-        entity: Object,
-        isExpanded: boolean,
-        expandFormula: any,
-        collapseFormula: any,
-        metadata: Metadata
-    }
+  props: {
+    type: string,
+    entity: Object,
+    isExpanded: boolean,
+    expandFormula: any,
+    collapseFormula: any,
+    metadata: Metadata,
+  };
 
-    render() {
-        const { type, entity, isExpanded, expandFormula, collapseFormula, metadata } = this.props;
+  render() {
+    const {
+      type,
+      entity,
+      isExpanded,
+      expandFormula,
+      collapseFormula,
+      metadata,
+    } = this.props;
 
-        return (
-            <div
-                className={cx(S.formula)}
-                onClick={isExpanded ? collapseFormula : expandFormula}
-            >
-                <div className={S.formulaHeader}>
-                    <Icon name="beaker" size={24}/>
-                    <span className={S.formulaTitle}>{t`View the ${type} formula`}</span>
-                </div>
-                <ReactCSSTransitionGroup
-                    transitionName="formulaDefinition"
-                    transitionEnterTimeout={300}
-                    transitionLeaveTimeout={300}
-                >
-                    { isExpanded &&
-                    <div key="formulaDefinition" className="formulaDefinition">
-                        <QueryDefinition
-                            className={S.formulaDefinitionInner}
-                            object={entity}
-                            tableMetadata={metadata.tables[entity.table_id]}
-                        />
-                    </div>
-                    }
-                </ReactCSSTransitionGroup>
+    return (
+      <div
+        className={cx(S.formula)}
+        onClick={isExpanded ? collapseFormula : expandFormula}
+      >
+        <div className={S.formulaHeader}>
+          <Icon name="beaker" size={24} />
+          <span className={S.formulaTitle}>{t`View the ${type} formula`}</span>
+        </div>
+        <ReactCSSTransitionGroup
+          transitionName="formulaDefinition"
+          transitionEnterTimeout={300}
+          transitionLeaveTimeout={300}
+        >
+          {isExpanded && (
+            <div key="formulaDefinition" className="formulaDefinition">
+              <QueryDefinition
+                className={S.formulaDefinitionInner}
+                object={entity}
+                tableMetadata={metadata.tables[entity.table_id]}
+              />
             </div>
-        )
-    }
+          )}
+        </ReactCSSTransitionGroup>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/components/GuideDetail.jsx b/frontend/src/metabase/reference/components/GuideDetail.jsx
index cfc9a14618428c1203f3e5bbc7f6f745386dde3e..dc032bc3ec128e192ec5ac49377f4abe743fe130 100644
--- a/frontend/src/metabase/reference/components/GuideDetail.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetail.jsx
@@ -3,141 +3,164 @@ import PropTypes from "prop-types";
 import { Link } from "react-router";
 import pure from "recompose/pure";
 import cx from "classnames";
-import { t } from 'c-3po';
-import Icon from "metabase/components/Icon"
+import { t } from "c-3po";
+import Icon from "metabase/components/Icon";
 import * as Urls from "metabase/lib/urls";
 
-import {
-    getQuestionUrl,
-    has,
-    typeToBgClass,
-    typeToLinkClass,
-} from "../utils";
+import { getQuestionUrl, has, typeToBgClass, typeToLinkClass } from "../utils";
 
 const GuideDetail = ({
-    entity = {},
-    tables,
-    type,
-    exploreLinks,
-    detailLabelClasses
+  entity = {},
+  tables,
+  type,
+  exploreLinks,
+  detailLabelClasses,
 }) => {
-    const title = entity.display_name || entity.name;
-    const { caveats, points_of_interest } = entity;
-    const typeToLink = {
-        dashboard: Urls.dashboard(entity.id),
-        metric: getQuestionUrl({
-            dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
-            tableId: entity.table_id,
-            metricId: entity.id
-        }),
-        segment: getQuestionUrl({
-            dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
-            tableId: entity.table_id,
-            segmentId: entity.id
-        }),
-        table: getQuestionUrl({
-            dbId: entity.db_id,
-            tableId: entity.id
-        })
-    };
-    const link = typeToLink[type];
-    const typeToLearnMoreLink = {
-        metric: `/reference/metrics/${entity.id}`,
-        segment: `/reference/segments/${entity.id}`,
-        table: `/reference/databases/${entity.db_id}/tables/${entity.id}`
-    };
-    const learnMoreLink = typeToLearnMoreLink[type];
+  const title = entity.display_name || entity.name;
+  const { caveats, points_of_interest } = entity;
+  const typeToLink = {
+    dashboard: Urls.dashboard(entity.id),
+    metric: getQuestionUrl({
+      dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
+      tableId: entity.table_id,
+      metricId: entity.id,
+    }),
+    segment: getQuestionUrl({
+      dbId: tables[entity.table_id] && tables[entity.table_id].db_id,
+      tableId: entity.table_id,
+      segmentId: entity.id,
+    }),
+    table: getQuestionUrl({
+      dbId: entity.db_id,
+      tableId: entity.id,
+    }),
+  };
+  const link = typeToLink[type];
+  const typeToLearnMoreLink = {
+    metric: `/reference/metrics/${entity.id}`,
+    segment: `/reference/segments/${entity.id}`,
+    table: `/reference/databases/${entity.db_id}/tables/${entity.id}`,
+  };
+  const learnMoreLink = typeToLearnMoreLink[type];
 
-    const linkClass = typeToLinkClass[type]
-    const linkHoverClass = `${typeToLinkClass[type]}-hover`
-    const bgClass = typeToBgClass[type]
-    const hasLearnMore = type === 'metric' || type === 'segment' || type === 'table';
+  const linkClass = typeToLinkClass[type];
+  const linkHoverClass = `${typeToLinkClass[type]}-hover`;
+  const bgClass = typeToBgClass[type];
+  const hasLearnMore =
+    type === "metric" || type === "segment" || type === "table";
 
-    return <div className="relative mt2 pb3">
-        <div className="flex align-center">
-            <div style={{
-                width: 40,
-                height: 40,
-                left: -60
-            }}
-            className={cx('absolute text-white flex align-center justify-center', bgClass)}
-            >
-                <Icon name={type === 'metric' ? 'ruler' : type} />
-            </div>
-            { title && <ItemTitle link={link} title={title} linkColorClass={linkClass} linkHoverClass={linkHoverClass} /> }
+  return (
+    <div className="relative mt2 pb3">
+      <div className="flex align-center">
+        <div
+          style={{
+            width: 40,
+            height: 40,
+            left: -60,
+          }}
+          className={cx(
+            "absolute text-white flex align-center justify-center",
+            bgClass,
+          )}
+        >
+          <Icon name={type === "metric" ? "ruler" : type} />
         </div>
-        <div className="mt2">
-            <ContextHeading>
-                { type === 'dashboard' ? t`Why this ${type} is important` : t`Why this ${type} is interesting` }
-            </ContextHeading>
+        {title && (
+          <ItemTitle
+            link={link}
+            title={title}
+            linkColorClass={linkClass}
+            linkHoverClass={linkHoverClass}
+          />
+        )}
+      </div>
+      <div className="mt2">
+        <ContextHeading>
+          {type === "dashboard"
+            ? t`Why this ${type} is important`
+            : t`Why this ${type} is interesting`}
+        </ContextHeading>
 
-            <ContextContent empty={!points_of_interest}>
-                {points_of_interest || (type === 'dashboard' ? t`Nothing important yet` : t`Nothing interesting yet`)}
-            </ContextContent>
+        <ContextContent empty={!points_of_interest}>
+          {points_of_interest ||
+            (type === "dashboard"
+              ? t`Nothing important yet`
+              : t`Nothing interesting yet`)}
+        </ContextContent>
 
-            <div className="mt2">
-                <ContextHeading>
-                    {t`Things to be aware of about this ${type}`}
-                </ContextHeading>
+        <div className="mt2">
+          <ContextHeading>
+            {t`Things to be aware of about this ${type}`}
+          </ContextHeading>
 
-                <ContextContent empty={!caveats}>
-                    {caveats || t`Nothing to be aware of yet`}
-                </ContextContent>
-            </div>
+          <ContextContent empty={!caveats}>
+            {caveats || t`Nothing to be aware of yet`}
+          </ContextContent>
+        </div>
 
-            { has(exploreLinks) && [
-                <div className="mt2">
-                    <ContextHeading key="detailLabel">{t`Explore this metric`}</ContextHeading>
-                    <div key="detailLinks">
-                        <h4 className="inline-block mr2 link text-bold">{t`View this metric`}</h4>
-                        { exploreLinks.map(link =>
-                            <Link
-                                className="inline-block text-bold text-brand mr2 link"
-                                key={link.url}
-                                to={link.url}
-                            >
-                                {t`By ${link.name}`}
-                            </Link>
-                        )}
-                    </div>
-                </div>
-            ]}
-            { hasLearnMore &&
+        {has(exploreLinks) && [
+          <div className="mt2">
+            <ContextHeading key="detailLabel">{t`Explore this metric`}</ContextHeading>
+            <div key="detailLinks">
+              <h4 className="inline-block mr2 link text-bold">{t`View this metric`}</h4>
+              {exploreLinks.map(link => (
                 <Link
-                    className={cx('block mt3 no-decoration text-underline-hover text-bold', linkClass)}
-                    to={learnMoreLink}
+                  className="inline-block text-bold text-brand mr2 link"
+                  key={link.url}
+                  to={link.url}
                 >
-                    {t`Learn more`}
+                  {t`By ${link.name}`}
                 </Link>
-            }
-        </div>
-    </div>;
+              ))}
+            </div>
+          </div>,
+        ]}
+        {hasLearnMore && (
+          <Link
+            className={cx(
+              "block mt3 no-decoration text-underline-hover text-bold",
+              linkClass,
+            )}
+            to={learnMoreLink}
+          >
+            {t`Learn more`}
+          </Link>
+        )}
+      </div>
+    </div>
+  );
 };
 
 GuideDetail.propTypes = {
-    entity: PropTypes.object,
-    type: PropTypes.string,
-    exploreLinks: PropTypes.array
+  entity: PropTypes.object,
+  type: PropTypes.string,
+  exploreLinks: PropTypes.array,
 };
 
-const ItemTitle = ({ title, link, linkColorClass, linkHoverClass }) =>
-    <h2>
-        <Link
-            className={ cx(linkColorClass, linkHoverClass) }
-            style={{ textDecoration: 'none' }}
-            to={ link }
-        >
-            { title }
-        </Link>
-    </h2>
-
-const ContextHeading = ({ children }) =>
-    <h3 className="my2 text-grey-4">{ children }</h3>
+const ItemTitle = ({ title, link, linkColorClass, linkHoverClass }) => (
+  <h2>
+    <Link
+      className={cx(linkColorClass, linkHoverClass)}
+      style={{ textDecoration: "none" }}
+      to={link}
+    >
+      {title}
+    </Link>
+  </h2>
+);
 
-const ContextContent = ({ empty, children }) =>
-    <p className={cx('m0 text-paragraph text-measure text-pre-wrap', { 'text-grey-3': empty })}>
-        { children }
-    </p>
+const ContextHeading = ({ children }) => (
+  <h3 className="my2 text-grey-4">{children}</h3>
+);
 
+const ContextContent = ({ empty, children }) => (
+  <p
+    className={cx("m0 text-paragraph text-measure text-pre-wrap", {
+      "text-grey-3": empty,
+    })}
+  >
+    {children}
+  </p>
+);
 
 export default pure(GuideDetail);
diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.css b/frontend/src/metabase/reference/components/GuideDetailEditor.css
index 308f16906011cae13ab304f4e108ad74387e0cef..643b609e513f78a83ab303e529169fffb793a4d3 100644
--- a/frontend/src/metabase/reference/components/GuideDetailEditor.css
+++ b/frontend/src/metabase/reference/components/GuideDetailEditor.css
@@ -1,16 +1,16 @@
 :local(.guideDetailEditor):last-child {
-    margin-bottom: 0;
+  margin-bottom: 0;
 }
 
 :local(.guideDetailEditorTextarea) {
-    composes: text-dark input p2 mb4 from "style";
-    resize: none;
-    font-size: 16px;
-    width: 100%;
-    min-height: 100px;
-    background-color: unset;
+  composes: text-dark input p2 mb4 from "style";
+  resize: none;
+  font-size: 16px;
+  width: 100%;
+  min-height: 100px;
+  background-color: unset;
 }
 
 :local(.guideDetailEditorTextarea):last-child {
-    margin-bottom: 0;
+  margin-bottom: 0;
 }
diff --git a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
index 69a3bf434cd185666da35af85bff87ca7f8fd8fa..f6daefd5c3f17475659c5b51c213c295f5439615 100644
--- a/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
+++ b/frontend/src/metabase/reference/components/GuideDetailEditor.jsx
@@ -3,7 +3,7 @@ import PropTypes from "prop-types";
 // FIXME: using pure seems to mess with redux form updates
 // import pure from "recompose/pure";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./GuideDetailEditor.css";
 
 import Select from "metabase/components/Select.jsx";
@@ -14,204 +14,224 @@ import { typeToBgClass } from "../utils.js";
 import { SchemaTableAndSegmentDataSelector } from "metabase/query_builder/components/DataSelector";
 
 const GuideDetailEditor = ({
-    className,
-    type,
-    entities,
-    metadata = {},
-    selectedIds = [],
-    selectedIdTypePairs = [],
-    formField,
-    removeField,
-    editLabelClasses
+  className,
+  type,
+  entities,
+  metadata = {},
+  selectedIds = [],
+  selectedIdTypePairs = [],
+  formField,
+  removeField,
+  editLabelClasses,
 }) => {
-    const {
-        databases,
-        tables,
-        segments,
-        metrics,
-        fields,
-        metricImportantFields
-    } = metadata;
+  const {
+    databases,
+    tables,
+    segments,
+    metrics,
+    fields,
+    metricImportantFields,
+  } = metadata;
 
-    const bgClass = typeToBgClass[type];
-    const entityId = formField.id.value;
-    const disabled = formField.id.value === null || formField.id.value === undefined
-    const tableId = metrics && metrics[entityId] && metrics[entityId].table_id;
-    const tableFields = tables && tables[tableId] && tables[tableId].fields || [];
-    const fieldsByMetric = type === 'metric' ?
-        tableFields.map(fieldId => fields[fieldId]) :
-        [];
+  const bgClass = typeToBgClass[type];
+  const entityId = formField.id.value;
+  const disabled =
+    formField.id.value === null || formField.id.value === undefined;
+  const tableId = metrics && metrics[entityId] && metrics[entityId].table_id;
+  const tableFields =
+    (tables && tables[tableId] && tables[tableId].fields) || [];
+  const fieldsByMetric =
+    type === "metric" ? tableFields.map(fieldId => fields[fieldId]) : [];
 
-    const selectClasses = 'input h3 px2 py1'
+  const selectClasses = "input h3 px2 py1";
 
-    return <div className={cx('mb2 border-bottom pb4 text-measure', className)}>
-        <div className="relative mt2 flex align-center">
-            <div
-                style={{
-                    width: 40,
-                    height: 40,
-                    left: -60
-                }}
-                className={cx(
-                    'absolute text-white flex align-center justify-center',
-                    bgClass
-                )}
-            >
-                <Icon name={type === 'metric' ? 'ruler' : type} />
-            </div>
-            <div className="py2">
-                { entities ?
-                    <Select
-                        value={entities[formField.id.value]}
-                        options={Object.values(entities)}
-                        disabledOptionIds={selectedIds}
-                        optionNameFn={option => option.display_name || option.name}
-                        onChange={(entity) => {
-                            //TODO: refactor into function
-                            formField.id.onChange(entity.id);
-                            formField.points_of_interest.onChange(entity.points_of_interest || '');
-                            formField.caveats.onChange(entity.caveats || '');
-                            if (type === 'metric') {
-                                formField.important_fields.onChange(metricImportantFields[entity.id] &&
-                                    metricImportantFields[entity.id]
-                                        .map(fieldId => fields[fieldId])
-                                );
-                            }
-                        }}
-                        placeholder={t`Select...`}
-                    /> :
-                    <SchemaTableAndSegmentDataSelector
-                        className={cx(selectClasses, 'inline-block', 'rounded', 'text-bold')}
-                        triggerIconSize={12}
-                        selectedTableId={
-                            formField.type.value === 'table' && Number.parseInt(formField.id.value)
-                        }
-                        selectedDatabaseId={
-                            formField.type.value === 'table' &&
-                            tables[formField.id.value] &&
-                            tables[formField.id.value].db_id
-                        }
-                        selectedSegmentId={
-                            formField.type.value === 'segment' && Number.parseInt(formField.id.value)
-                        }
-                        databases={
-                            Object.values(databases)
-                                .map(database => ({
-                                    ...database,
-                                    tables: database.tables.map(tableId => tables[tableId])
-                                }))
-                        }
-                        setDatabaseFn={() => null}
-                        tables={Object.values(tables)}
-                        disabledTableIds={selectedIdTypePairs
-                            .filter(idTypePair => idTypePair[1] === 'table')
-                            .map(idTypePair => idTypePair[0])
-                        }
-                        setSourceTableFn={(tableId) => {
-                            const table = tables[tableId];
-                            formField.id.onChange(table.id);
-                            formField.type.onChange('table');
-                            formField.points_of_interest.onChange(table.points_of_interest || null);
-                            formField.caveats.onChange(table.caveats || null);
-                        }}
-                        segments={Object.values(segments)}
-                        disabledSegmentIds={selectedIdTypePairs
-                            .filter(idTypePair => idTypePair[1] === 'segment')
-                            .map(idTypePair => idTypePair[0])
-                        }
-                        setSourceSegmentFn={(segmentId) => {
-                            const segment = segments[segmentId];
-                            formField.id.onChange(segment.id);
-                            formField.type.onChange('segment');
-                            formField.points_of_interest.onChange(segment.points_of_interest || '');
-                            formField.caveats.onChange(segment.caveats || '');
-                        }}
-                    />
+  return (
+    <div className={cx("mb2 border-bottom pb4 text-measure", className)}>
+      <div className="relative mt2 flex align-center">
+        <div
+          style={{
+            width: 40,
+            height: 40,
+            left: -60,
+          }}
+          className={cx(
+            "absolute text-white flex align-center justify-center",
+            bgClass,
+          )}
+        >
+          <Icon name={type === "metric" ? "ruler" : type} />
+        </div>
+        <div className="py2">
+          {entities ? (
+            <Select
+              value={entities[formField.id.value]}
+              options={Object.values(entities)}
+              disabledOptionIds={selectedIds}
+              optionNameFn={option => option.display_name || option.name}
+              onChange={entity => {
+                //TODO: refactor into function
+                formField.id.onChange(entity.id);
+                formField.points_of_interest.onChange(
+                  entity.points_of_interest || "",
+                );
+                formField.caveats.onChange(entity.caveats || "");
+                if (type === "metric") {
+                  formField.important_fields.onChange(
+                    metricImportantFields[entity.id] &&
+                      metricImportantFields[entity.id].map(
+                        fieldId => fields[fieldId],
+                      ),
+                  );
                 }
-            </div>
-            <div className="ml-auto cursor-pointer text-grey-2">
-                <Tooltip tooltip={t`Remove item`}>
-                    <Icon
-                        name="close"
-                        width={16}
-                        height={16}
-                        onClick={removeField}
-                    />
-                </Tooltip>
-            </div>
+              }}
+              placeholder={t`Select...`}
+            />
+          ) : (
+            <SchemaTableAndSegmentDataSelector
+              className={cx(
+                selectClasses,
+                "inline-block",
+                "rounded",
+                "text-bold",
+              )}
+              triggerIconSize={12}
+              selectedTableId={
+                formField.type.value === "table" &&
+                Number.parseInt(formField.id.value)
+              }
+              selectedDatabaseId={
+                formField.type.value === "table" &&
+                tables[formField.id.value] &&
+                tables[formField.id.value].db_id
+              }
+              selectedSegmentId={
+                formField.type.value === "segment" &&
+                Number.parseInt(formField.id.value)
+              }
+              databases={Object.values(databases).map(database => ({
+                ...database,
+                tables: database.tables.map(tableId => tables[tableId]),
+              }))}
+              setDatabaseFn={() => null}
+              tables={Object.values(tables)}
+              disabledTableIds={selectedIdTypePairs
+                .filter(idTypePair => idTypePair[1] === "table")
+                .map(idTypePair => idTypePair[0])}
+              setSourceTableFn={tableId => {
+                const table = tables[tableId];
+                formField.id.onChange(table.id);
+                formField.type.onChange("table");
+                formField.points_of_interest.onChange(
+                  table.points_of_interest || null,
+                );
+                formField.caveats.onChange(table.caveats || null);
+              }}
+              segments={Object.values(segments)}
+              disabledSegmentIds={selectedIdTypePairs
+                .filter(idTypePair => idTypePair[1] === "segment")
+                .map(idTypePair => idTypePair[0])}
+              setSourceSegmentFn={segmentId => {
+                const segment = segments[segmentId];
+                formField.id.onChange(segment.id);
+                formField.type.onChange("segment");
+                formField.points_of_interest.onChange(
+                  segment.points_of_interest || "",
+                );
+                formField.caveats.onChange(segment.caveats || "");
+              }}
+            />
+          )}
+        </div>
+        <div className="ml-auto cursor-pointer text-grey-2">
+          <Tooltip tooltip={t`Remove item`}>
+            <Icon name="close" width={16} height={16} onClick={removeField} />
+          </Tooltip>
+        </div>
+      </div>
+      <div className="mt2 text-measure">
+        <div className={cx("mb2", { disabled: disabled })}>
+          <EditLabel>
+            {type === "dashboard"
+              ? t`Why is this dashboard the most important?`
+              : t`What is useful or interesting about this ${type}?`}
+          </EditLabel>
+          <textarea
+            className={S.guideDetailEditorTextarea}
+            placeholder={t`Write something helpful here`}
+            {...formField.points_of_interest}
+            disabled={disabled}
+          />
         </div>
-        <div className="mt2 text-measure">
-            <div className={cx('mb2', { 'disabled' : disabled })}>
-                <EditLabel>
-                    { type === 'dashboard' ?
-                            t`Why is this dashboard the most important?` :
-                            t`What is useful or interesting about this ${type}?`
-                    }
-                </EditLabel>
-                <textarea
-                    className={S.guideDetailEditorTextarea}
-                    placeholder={t`Write something helpful here`}
-                    {...formField.points_of_interest}
-                    disabled={disabled}
-                />
-            </div>
 
-            <div className={cx('mb2', { 'disabled' : disabled })}>
-                <EditLabel>
-                    { type === 'dashboard' ?
-                            t`Is there anything users of this dashboard should be aware of?` :
-                            t`Anything users should be aware of about this ${type}?`
-                    }
-                </EditLabel>
-                <textarea
-                    className={S.guideDetailEditorTextarea}
-                    placeholder={t`Write something helpful here`}
-                    {...formField.caveats}
-                    disabled={disabled}
-                />
-            </div>
-            { type === 'metric' &&
-                <div className={cx('mb2', { 'disabled' : disabled })}>
-                    <EditLabel key="metricFieldsLabel">
-                        {t`Which 2-3 fields do you usually group this metric by?`}
-                    </EditLabel>
-                    <Select
-                        options={fieldsByMetric}
-                        optionNameFn={option => option.display_name || option.name}
-                        placeholder={t`Select...`}
-                        values={formField.important_fields.value || []}
-                        disabledOptionIds={formField.important_fields.value && formField.important_fields.value.length === 3 ?
-                            fieldsByMetric
-                                .filter(field => !formField.important_fields.value.includes(field))
-                                .map(field => field.id) :
-                            []
-                        }
-                        onChange={(field) => {
-                            const importantFields = formField.important_fields.value || [];
-                            return importantFields.includes(field) ?
-                                formField.important_fields.onChange(importantFields.filter(importantField => importantField !== field)) :
-                                importantFields.length < 3 && formField.important_fields.onChange(importantFields.concat(field));
-                        }}
-                        disabled={formField.id.value === null || formField.id.value === undefined}
-                    />
-                </div>
-            }
+        <div className={cx("mb2", { disabled: disabled })}>
+          <EditLabel>
+            {type === "dashboard"
+              ? t`Is there anything users of this dashboard should be aware of?`
+              : t`Anything users should be aware of about this ${type}?`}
+          </EditLabel>
+          <textarea
+            className={S.guideDetailEditorTextarea}
+            placeholder={t`Write something helpful here`}
+            {...formField.caveats}
+            disabled={disabled}
+          />
         </div>
-    </div>;
+        {type === "metric" && (
+          <div className={cx("mb2", { disabled: disabled })}>
+            <EditLabel key="metricFieldsLabel">
+              {t`Which 2-3 fields do you usually group this metric by?`}
+            </EditLabel>
+            <Select
+              options={fieldsByMetric}
+              optionNameFn={option => option.display_name || option.name}
+              placeholder={t`Select...`}
+              values={formField.important_fields.value || []}
+              disabledOptionIds={
+                formField.important_fields.value &&
+                formField.important_fields.value.length === 3
+                  ? fieldsByMetric
+                      .filter(
+                        field =>
+                          !formField.important_fields.value.includes(field),
+                      )
+                      .map(field => field.id)
+                  : []
+              }
+              onChange={field => {
+                const importantFields = formField.important_fields.value || [];
+                return importantFields.includes(field)
+                  ? formField.important_fields.onChange(
+                      importantFields.filter(
+                        importantField => importantField !== field,
+                      ),
+                    )
+                  : importantFields.length < 3 &&
+                      formField.important_fields.onChange(
+                        importantFields.concat(field),
+                      );
+              }}
+              disabled={
+                formField.id.value === null || formField.id.value === undefined
+              }
+            />
+          </div>
+        )}
+      </div>
+    </div>
+  );
 };
 
-const EditLabel = ({ children } ) =>
-    <h3 className="mb1">{ children }</h3>
+const EditLabel = ({ children }) => <h3 className="mb1">{children}</h3>;
 
 GuideDetailEditor.propTypes = {
-    className: PropTypes.string,
-    type: PropTypes.string.isRequired,
-    entities: PropTypes.object,
-    metadata: PropTypes.object,
-    selectedIds: PropTypes.array,
-    selectedIdTypePairs: PropTypes.array,
-    formField: PropTypes.object.isRequired,
-    removeField: PropTypes.func.isRequired
+  className: PropTypes.string,
+  type: PropTypes.string.isRequired,
+  entities: PropTypes.object,
+  metadata: PropTypes.object,
+  selectedIds: PropTypes.array,
+  selectedIdTypePairs: PropTypes.array,
+  formField: PropTypes.object.isRequired,
+  removeField: PropTypes.func.isRequired,
 };
 
 export default GuideDetailEditor;
diff --git a/frontend/src/metabase/reference/components/GuideEditSection.css b/frontend/src/metabase/reference/components/GuideEditSection.css
index 409bc25d2387c9c730c80f282a816948b8d78313..62c0d835be74e5c49b332745799a41d5e7879ae4 100644
--- a/frontend/src/metabase/reference/components/GuideEditSection.css
+++ b/frontend/src/metabase/reference/components/GuideEditSection.css
@@ -1,21 +1,21 @@
 :local(.guideEditSectionCollapsed) {
-    composes: flex flex-full align-center mt4 p3 input text-brand text-bold from "style";
-    font-size: 16px;
+  composes: flex flex-full align-center mt4 p3 input text-brand text-bold from "style";
+  font-size: 16px;
 }
 
 :local(.guideEditSectionDisabled) {
-    composes: text-grey-3 from "style";
+  composes: text-grey-3 from "style";
 }
 
 :local(.guideEditSectionCollapsedIcon) {
-    composes: mr3 from "style";
+  composes: mr3 from "style";
 }
 
 :local(.guideEditSectionCollapsedTitle) {
-    composes: flex-full mr3 from "style";
+  composes: flex-full mr3 from "style";
 }
 
 :local(.guideEditSectionCollapsedLink) {
-    composes: text-brand no-decoration from "style";
-    font-size: 14px;
-}
\ No newline at end of file
+  composes: text-brand no-decoration from "style";
+  font-size: 14px;
+}
diff --git a/frontend/src/metabase/reference/components/GuideEditSection.jsx b/frontend/src/metabase/reference/components/GuideEditSection.jsx
index f3b30093b2448bb13c0d610ae384f176fe22eee5..7931724618cacd5f84cb529cd47b55cc01ccfe62 100644
--- a/frontend/src/metabase/reference/components/GuideEditSection.jsx
+++ b/frontend/src/metabase/reference/components/GuideEditSection.jsx
@@ -9,59 +9,59 @@ import S from "./GuideEditSection.css";
 import Icon from "metabase/components/Icon.jsx";
 
 const GuideEditSection = ({
-    children,
-    isCollapsed,
-    isDisabled,
-    showLink,
-    collapsedIcon,
-    collapsedTitle,
-    linkMessage,
-    link,
-    action,
-    expand
-}) => isCollapsed ?
+  children,
+  isCollapsed,
+  isDisabled,
+  showLink,
+  collapsedIcon,
+  collapsedTitle,
+  linkMessage,
+  link,
+  action,
+  expand,
+}) =>
+  isCollapsed ? (
     <div
-        className={cx(
-            'text-measure',
-            S.guideEditSectionCollapsed,
-            {
-                'cursor-pointer border-brand-hover': !isDisabled,
-                [S.guideEditSectionDisabled]: isDisabled
-            }
-        )}
-        onClick={!isDisabled && expand}
+      className={cx("text-measure", S.guideEditSectionCollapsed, {
+        "cursor-pointer border-brand-hover": !isDisabled,
+        [S.guideEditSectionDisabled]: isDisabled,
+      })}
+      onClick={!isDisabled && expand}
     >
-        <Icon className={S.guideEditSectionCollapsedIcon} name={collapsedIcon} size={24} />
-        <span className={S.guideEditSectionCollapsedTitle}>{collapsedTitle}</span>
-        {(showLink || isDisabled) && (link ? (link.startsWith('http') ?
-                <a
-                    className={S.guideEditSectionCollapsedLink}
-                    href={link}
-                    target="_blank"
-                >
-                    {linkMessage}
-                </a> :
-                <Link
-                    className={S.guideEditSectionCollapsedLink}
-                    to={link}
-                >
-                    {linkMessage}
-                </Link>
-            ) :
-            action &&
-                <a
-                    className={S.guideEditSectionCollapsedLink}
-                    onClick={action}
-                >
-                    {linkMessage}
-                </a>
-        )}
-    </div> :
-    <div className={cx('my4', S.guideEditSection)}>
-        {children}
-    </div>;
+      <Icon
+        className={S.guideEditSectionCollapsedIcon}
+        name={collapsedIcon}
+        size={24}
+      />
+      <span className={S.guideEditSectionCollapsedTitle}>{collapsedTitle}</span>
+      {(showLink || isDisabled) &&
+        (link ? (
+          link.startsWith("http") ? (
+            <a
+              className={S.guideEditSectionCollapsedLink}
+              href={link}
+              target="_blank"
+            >
+              {linkMessage}
+            </a>
+          ) : (
+            <Link className={S.guideEditSectionCollapsedLink} to={link}>
+              {linkMessage}
+            </Link>
+          )
+        ) : (
+          action && (
+            <a className={S.guideEditSectionCollapsedLink} onClick={action}>
+              {linkMessage}
+            </a>
+          )
+        ))}
+    </div>
+  ) : (
+    <div className={cx("my4", S.guideEditSection)}>{children}</div>
+  );
 GuideEditSection.propTypes = {
-    isCollapsed: PropTypes.bool.isRequired
+  isCollapsed: PropTypes.bool.isRequired,
 };
 
 export default pure(GuideEditSection);
diff --git a/frontend/src/metabase/reference/components/GuideHeader.jsx b/frontend/src/metabase/reference/components/GuideHeader.jsx
index 84b0a4586538fe5cc2a896d2307a6e062cde3b90..a560cb0a8f1a6afc5658eab3b20b9a0f46f0eff1 100644
--- a/frontend/src/metabase/reference/components/GuideHeader.jsx
+++ b/frontend/src/metabase/reference/components/GuideHeader.jsx
@@ -1,30 +1,34 @@
 import React from "react";
 import PropTypes from "prop-types";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import EditButton from "metabase/reference/components/EditButton.jsx";
 
-const GuideHeader = ({
-    startEditing,
-    isSuperuser
-}) =>
-    <div>
-        <div className="wrapper wrapper--trim py4 my3">
-            <div className="flex align-center">
-                <h1 className="text-dark" style={{fontWeight: 700}}>{t`Start here.`}</h1>
-                { isSuperuser &&
-                    <span className="ml-auto">
-                        <EditButton startEditing={startEditing}/>
-                    </span>
-                }
-            </div>
-            <p className="text-paragraph" style={{maxWidth: 620}}>{t`This is the perfect place to start if you’re new to your company’s data, or if you just want to check in on what’s going on.`}</p>
-        </div>
-    </div>;
+const GuideHeader = ({ startEditing, isSuperuser }) => (
+  <div>
+    <div className="wrapper wrapper--trim py4 my3">
+      <div className="flex align-center">
+        <h1
+          className="text-dark"
+          style={{ fontWeight: 700 }}
+        >{t`Start here.`}</h1>
+        {isSuperuser && (
+          <span className="ml-auto">
+            <EditButton startEditing={startEditing} />
+          </span>
+        )}
+      </div>
+      <p
+        className="text-paragraph"
+        style={{ maxWidth: 620 }}
+      >{t`This is the perfect place to start if you’re new to your company’s data, or if you just want to check in on what’s going on.`}</p>
+    </div>
+  </div>
+);
 
 GuideHeader.propTypes = {
-    startEditing: PropTypes.func.isRequired,
-    isSuperuser: PropTypes.bool
+  startEditing: PropTypes.func.isRequired,
+  isSuperuser: PropTypes.bool,
 };
 
 export default pure(GuideHeader);
diff --git a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
index f64b66f8e4660a573009ceb81cac2f8cbbaa82c9..af3737a4dae941e2a3b26a1b173b1e1e20693b53 100644
--- a/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
+++ b/frontend/src/metabase/reference/components/MetricImportantFieldsDetail.jsx
@@ -2,7 +2,7 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import FieldsToGroupBy from "metabase/reference/components/FieldsToGroupBy.jsx";
 
 import Select from "metabase/components/Select.jsx";
@@ -10,62 +10,69 @@ import Select from "metabase/components/Select.jsx";
 import D from "metabase/reference/components/Detail.css";
 
 const MetricImportantFieldsDetail = ({
-    fields,
-    metric,
-    table,
-    allFields,
-    isEditing,
-    onChangeLocation,
-    formField
-}) => isEditing ? 
+  fields,
+  metric,
+  table,
+  allFields,
+  isEditing,
+  onChangeLocation,
+  formField,
+}) =>
+  isEditing ? (
     <div className={cx(D.detail)}>
-        <div className={D.detailBody}>
-            <div className={D.detailTitle}>
-                <span className={D.detailName}>
-                    {t`Which 2-3 fields do you usually group this metric by?`}
-                </span>
-            </div>
-            <div className={cx(D.detailSubtitle, { "mt1" : true })}>
-                <Select
-                    key="metricFieldsSelect"
-                    triggerClasses="input p1 block"
-                    options={table.fields.map(fieldId => allFields[fieldId])} 
-                    optionNameFn={option => option.display_name || option.name}
-                    placeholder={t`Select...`}
-                    values={formField.value || []}
-                    disabledOptionIds={formField.value && formField.value.length === 3 ?
-                        table.fields
-                            .map(fieldId => allFields[fieldId])
-                            .filter(field => !formField.value.includes(field))
-                            .map(field => field.id) :
-                        []
-                    }
-                    onChange={(field) => {
-                        const importantFields = formField.value || [];
-                        return importantFields.includes(field) ?
-                            formField.onChange(importantFields.filter(importantField => importantField !== field)) :
-                            importantFields.length < 3 && formField.onChange(importantFields.concat(field));
-                    }}
-                />
-            </div>
+      <div className={D.detailBody}>
+        <div className={D.detailTitle}>
+          <span className={D.detailName}>
+            {t`Which 2-3 fields do you usually group this metric by?`}
+          </span>
         </div>
-    </div> : 
-    fields ? 
-        <FieldsToGroupBy
-            fields={fields}
-            databaseId={table.db_id} 
-            metric={metric}
-            title={t`Most useful fields to group this metric by`}
-            onChangeLocation={onChangeLocation}
-        /> :
-        null; 
+        <div className={cx(D.detailSubtitle, { mt1: true })}>
+          <Select
+            key="metricFieldsSelect"
+            triggerClasses="input p1 block"
+            options={table.fields.map(fieldId => allFields[fieldId])}
+            optionNameFn={option => option.display_name || option.name}
+            placeholder={t`Select...`}
+            values={formField.value || []}
+            disabledOptionIds={
+              formField.value && formField.value.length === 3
+                ? table.fields
+                    .map(fieldId => allFields[fieldId])
+                    .filter(field => !formField.value.includes(field))
+                    .map(field => field.id)
+                : []
+            }
+            onChange={field => {
+              const importantFields = formField.value || [];
+              return importantFields.includes(field)
+                ? formField.onChange(
+                    importantFields.filter(
+                      importantField => importantField !== field,
+                    ),
+                  )
+                : importantFields.length < 3 &&
+                    formField.onChange(importantFields.concat(field));
+            }}
+          />
+        </div>
+      </div>
+    </div>
+  ) : fields ? (
+    <FieldsToGroupBy
+      fields={fields}
+      databaseId={table.db_id}
+      metric={metric}
+      title={t`Most useful fields to group this metric by`}
+      onChangeLocation={onChangeLocation}
+    />
+  ) : null;
 MetricImportantFieldsDetail.propTypes = {
-    fields: PropTypes.object,
-    metric: PropTypes.object.isRequired,
-    table: PropTypes.object.isRequired,
-    isEditing: PropTypes.bool.isRequired,
-    onChangeLocation: PropTypes.func.isRequired,
-    formField: PropTypes.object.isRequired
+  fields: PropTypes.object,
+  metric: PropTypes.object.isRequired,
+  table: PropTypes.object.isRequired,
+  isEditing: PropTypes.bool.isRequired,
+  onChangeLocation: PropTypes.func.isRequired,
+  formField: PropTypes.object.isRequired,
 };
 
 export default pure(MetricImportantFieldsDetail);
diff --git a/frontend/src/metabase/reference/components/ReferenceHeader.css b/frontend/src/metabase/reference/components/ReferenceHeader.css
index 8199cae6abc47e902e9731704a0ac3bb7e8838c0..f6ab2aa00f67b452b11a953fec97950c7a1e1074 100644
--- a/frontend/src/metabase/reference/components/ReferenceHeader.css
+++ b/frontend/src/metabase/reference/components/ReferenceHeader.css
@@ -1,46 +1,46 @@
 :root {
-    --title-color: #606E7B;
-    --icon-width: calc(48px + 1rem);
+  --title-color: #606e7b;
+  --icon-width: calc(48px + 1rem);
 }
 
 :local(.headerBody) {
-    composes: flex flex-full border-bottom text-dark text-bold from "style";
-    overflow: hidden;
-    align-items: center;
-    border-color: #EDF5FB;
+  composes: flex flex-full border-bottom text-dark text-bold from "style";
+  overflow: hidden;
+  align-items: center;
+  border-color: #edf5fb;
 }
 
 :local(.headerTextInput) {
-    composes: input p1 pl2 pr2 from "style";
-    font-size: 18px;
-    color: var(--title-color);
-    width: 100%;
-    max-width: 550px;
+  composes: input p1 pl2 pr2 from "style";
+  font-size: 18px;
+  color: var(--title-color);
+  width: 100%;
+  max-width: 550px;
 }
 
 :local(.subheader) {
-    composes: mt1 mb2 from "style";
+  composes: mt1 mb2 from "style";
 }
 
 :local(.subheaderBody) {
-    composes: text-dark from "style";
-    margin-left: var(--icon-width);
-    font-size: 16px;
+  composes: text-dark from "style";
+  margin-left: var(--icon-width);
+  font-size: 16px;
 }
 
 :local(.subheaderLink) {
-    color: var(--primary-button-bg-color);
-    text-decoration: none;
+  color: var(--primary-button-bg-color);
+  text-decoration: none;
 }
 
 :local(.subheaderLink):hover {
-    color: color(var(--primary-button-border-color) shade(10%));
-    transition: color .3s linear;
+  color: color(var(--primary-button-border-color) shade(10%));
+  transition: color 0.3s linear;
 }
 
 :local(.headerSchema) {
-    composes: text-grey-2 absolute from "style";
-    left: var(--icon-width);
-    top: -10px;
-    font-size: 12px;
+  composes: text-grey-2 absolute from "style";
+  left: var(--icon-width);
+  top: -10px;
+  font-size: 12px;
 }
diff --git a/frontend/src/metabase/reference/components/ReferenceHeader.jsx b/frontend/src/metabase/reference/components/ReferenceHeader.jsx
index dbebc2fa803f44bdd28c552f6bda98fa7012f905..77f82e65c6986dbaebfed7cad8fa5df201284c4c 100644
--- a/frontend/src/metabase/reference/components/ReferenceHeader.jsx
+++ b/frontend/src/metabase/reference/components/ReferenceHeader.jsx
@@ -11,68 +11,68 @@ import E from "metabase/reference/components/EditButton.css";
 import IconBorder from "metabase/components/IconBorder.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 const ReferenceHeader = ({
-    name,
-    type,
-    headerIcon,
-    headerBody,
-    headerLink
-}) =>
-    <div className="wrapper wrapper--trim">
-        <div className={cx("relative", L.header)}>
-            <div className={L.leftIcons}>
-                { headerIcon &&
-                    <IconBorder
-                        borderWidth="0"
-                        style={{backgroundColor: "#E9F4F8"}}
-                    >
-                        <Icon
-                            className="text-brand"
-                            name={headerIcon}
-                            width={24}
-                            height={24}
-                        />
-                    </IconBorder>
-                }
-            </div>
-            <div
-                className={S.headerBody}
+  name,
+  type,
+  headerIcon,
+  headerBody,
+  headerLink,
+}) => (
+  <div className="wrapper wrapper--trim">
+    <div className={cx("relative", L.header)}>
+      <div className={L.leftIcons}>
+        {headerIcon && (
+          <IconBorder borderWidth="0" style={{ backgroundColor: "#E9F4F8" }}>
+            <Icon
+              className="text-brand"
+              name={headerIcon}
+              width={24}
+              height={24}
+            />
+          </IconBorder>
+        )}
+      </div>
+      <div className={S.headerBody}>
+        <Ellipsified
+          key="1"
+          className={!headerLink && "flex-full"}
+          tooltipMaxWidth="100%"
+        >
+          {name}
+        </Ellipsified>
+
+        {headerLink && (
+          <div key="2" className={cx("flex-full", S.headerButton)}>
+            <Link
+              to={headerLink}
+              className={cx(
+                "Button",
+                "Button--borderless",
+                "ml3",
+                E.editButton,
+              )}
+              data-metabase-event={`Data Reference;Entity -> QB click;${type}`}
             >
-                <Ellipsified
-                    key="1"
-                    className={!headerLink && "flex-full"}
-                    tooltipMaxWidth="100%"
-                >
-                    { name }
-                </Ellipsified>
-                
-                {headerLink &&
-                    <div key="2" className={cx("flex-full", S.headerButton)}>
-                        <Link
-                            to={headerLink}
-                            className={cx("Button", "Button--borderless", "ml3", E.editButton)}
-                            data-metabase-event={`Data Reference;Entity -> QB click;${type}`}
-                        >
-                            <div className="flex align-center relative">
-                                <span className="mr1 flex-no-shrink">{t`See this ${type}`}</span>
-                                <Icon name="chevronright" size={16} />
-                            </div>
-                        </Link>
-                    </div>
-                }
-            </div>
-        </div>
-    </div>;
+              <div className="flex align-center relative">
+                <span className="mr1 flex-no-shrink">{t`See this ${type}`}</span>
+                <Icon name="chevronright" size={16} />
+              </div>
+            </Link>
+          </div>
+        )}
+      </div>
+    </div>
+  </div>
+);
 
 ReferenceHeader.propTypes = {
-    name: PropTypes.string.isRequired,
-    type: PropTypes.string,
-    headerIcon: PropTypes.string,
-    headerBody: PropTypes.string,
-    headerLink: PropTypes.string
-
+  name: PropTypes.string.isRequired,
+  type: PropTypes.string,
+  headerIcon: PropTypes.string,
+  headerBody: PropTypes.string,
+  headerLink: PropTypes.string,
 };
 
 export default pure(ReferenceHeader);
diff --git a/frontend/src/metabase/reference/components/RevisionMessageModal.css b/frontend/src/metabase/reference/components/RevisionMessageModal.css
index aa711f9015df7d779019a815645fd1e0d27e1121..7c97265c891cbb0efc7fc948cac43b530e84196f 100644
--- a/frontend/src/metabase/reference/components/RevisionMessageModal.css
+++ b/frontend/src/metabase/reference/components/RevisionMessageModal.css
@@ -1,13 +1,13 @@
 :local(.modalBody) {
-    composes: flex justify-center align-center from "style";
-    padding-left: 32px;
-    padding-right: 32px;
-    padding-bottom: 32px;
+  composes: flex justify-center align-center from "style";
+  padding-left: 32px;
+  padding-right: 32px;
+  padding-bottom: 32px;
 }
 
 :local(.modalTextArea) {
-    composes: flex-full text-dark input p2 from "style";
-    resize: none;
-    font-size: 16px;
-    min-height: 100px;
+  composes: flex-full text-dark input p2 from "style";
+  resize: none;
+  font-size: 16px;
+  min-height: 100px;
 }
diff --git a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
index cd94a2a69d9250fc4d829ab801a8fe552a43da2e..15e79802582c72f22c7a4d85a068dd03ebd4eb4f 100644
--- a/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
+++ b/frontend/src/metabase/reference/components/RevisionMessageModal.jsx
@@ -1,52 +1,58 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ModalWithTrigger from "metabase/components/ModalWithTrigger.jsx";
 import ModalContent from "metabase/components/ModalContent.jsx";
 
 import S from "./RevisionMessageModal.css";
 
 export default class RevisionMessageModal extends Component {
-    static propTypes = {
-        action: PropTypes.func.isRequired,
-        field: PropTypes.object.isRequired,
-        submitting: PropTypes.bool,
-        children: PropTypes.any,
-    };
+  static propTypes = {
+    action: PropTypes.func.isRequired,
+    field: PropTypes.object.isRequired,
+    submitting: PropTypes.bool,
+    children: PropTypes.any,
+  };
 
-    render() {
-        const { action, children, field, submitting } = this.props;
+  render() {
+    const { action, children, field, submitting } = this.props;
 
-        const onClose = () => {
-            this.refs.modal.close();
-        }
+    const onClose = () => {
+      this.refs.modal.close();
+    };
 
-        const onAction = () => {
-            onClose();
-            action();
-        }
+    const onAction = () => {
+      onClose();
+      action();
+    };
 
-        return (
-            <ModalWithTrigger ref="modal" triggerElement={children}>
-                <ModalContent
-                    title={t`Reason for changes`}
-                    onClose={onClose}
-                >
-                    <div className={S.modalBody}>
-                        <textarea
-                            className={S.modalTextArea}
-                            placeholder={t`Leave a note to explain what changes you made and why they were required`}
-                            {...field}
-                        />
-                    </div>
+    return (
+      <ModalWithTrigger ref="modal" triggerElement={children}>
+        <ModalContent title={t`Reason for changes`} onClose={onClose}>
+          <div className={S.modalBody}>
+            <textarea
+              className={S.modalTextArea}
+              placeholder={t`Leave a note to explain what changes you made and why they were required`}
+              {...field}
+            />
+          </div>
 
-                    <div className="Form-actions">
-                        <button type="button" className="Button Button--primary" onClick={onAction} disabled={submitting || field.error}>{t`Save changes`}</button>
-                        <button type="button" className="Button ml1" onClick={onClose}>{t`Cancel`}</button>
-                    </div>
-                </ModalContent>
-            </ModalWithTrigger>
-        );
-    }
+          <div className="Form-actions">
+            <button
+              type="button"
+              className="Button Button--primary"
+              onClick={onAction}
+              disabled={submitting || field.error}
+            >{t`Save changes`}</button>
+            <button
+              type="button"
+              className="Button ml1"
+              onClick={onClose}
+            >{t`Cancel`}</button>
+          </div>
+        </ModalContent>
+      </ModalWithTrigger>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/components/UsefulQuestions.css b/frontend/src/metabase/reference/components/UsefulQuestions.css
index 981d0441e2dd41a3470665e26ff65dec95e0c255..eb3e899302d4fe123ca870e8b1fe2ee5a2127652 100644
--- a/frontend/src/metabase/reference/components/UsefulQuestions.css
+++ b/frontend/src/metabase/reference/components/UsefulQuestions.css
@@ -1,4 +1,4 @@
 :local(.usefulQuestions) {
-    composes: text-brand mt1 from "style";
-    font-size: 14px;
+  composes: text-brand mt1 from "style";
+  font-size: 14px;
 }
diff --git a/frontend/src/metabase/reference/components/UsefulQuestions.jsx b/frontend/src/metabase/reference/components/UsefulQuestions.jsx
index 4b0db066365b1b7c1a44d0fd702ead24ace80508..304673c75d3a74efccf8e6a56e3850ef0186d0ba 100644
--- a/frontend/src/metabase/reference/components/UsefulQuestions.jsx
+++ b/frontend/src/metabase/reference/components/UsefulQuestions.jsx
@@ -2,35 +2,34 @@ import React from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
 import pure from "recompose/pure";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "./UsefulQuestions.css";
 import D from "metabase/reference/components/Detail.css";
 import L from "metabase/components/List.css";
 
 import QueryButton from "metabase/components/QueryButton.jsx";
 
-const UsefulQuestions = ({
-    questions
-}) =>
-    <div className={cx(D.detail)}>
-        <div className={D.detailBody}>
-            <div className={D.detailTitle}>
-                <span className={D.detailName}>{t`Potentially useful questions`}</span>
-            </div>
-            <div className={S.usefulQuestions}>
-                { questions.map((question, index, questions) =>
-                    <QueryButton
-                        key={index}
-                        className={cx("border-bottom", "pt1", "pb1")}
-                        iconClass={L.icon}
-                        {...question}
-                    />
-                )}
-            </div>
-        </div>
-    </div>;
+const UsefulQuestions = ({ questions }) => (
+  <div className={cx(D.detail)}>
+    <div className={D.detailBody}>
+      <div className={D.detailTitle}>
+        <span className={D.detailName}>{t`Potentially useful questions`}</span>
+      </div>
+      <div className={S.usefulQuestions}>
+        {questions.map((question, index, questions) => (
+          <QueryButton
+            key={index}
+            className={cx("border-bottom", "pt1", "pb1")}
+            iconClass={L.icon}
+            {...question}
+          />
+        ))}
+      </div>
+    </div>
+  </div>
+);
 UsefulQuestions.propTypes = {
-    questions: PropTypes.array.isRequired
+  questions: PropTypes.array.isRequired,
 };
 
 export default pure(UsefulQuestions);
diff --git a/frontend/src/metabase/reference/databases/DatabaseDetail.jsx b/frontend/src/metabase/reference/databases/DatabaseDetail.jsx
index 31c047bd9b5fcdb6cd6bb67e5f562c71ede31de9..acc3d5cc0e5521293f26cc747a629bf524c38190 100644
--- a/frontend/src/metabase/reference/databases/DatabaseDetail.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import List from "metabase/components/List.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
@@ -13,164 +13,178 @@ import EditableReferenceHeader from "metabase/reference/components/EditableRefer
 import Detail from "metabase/reference/components/Detail.jsx";
 
 import {
-    getDatabase,
-    getTable,
-    getFields,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getIsFormulaExpanded,
-    getForeignKeys
+  getDatabase,
+  getTable,
+  getFields,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getIsFormulaExpanded,
+  getForeignKeys,
 } from "../selectors";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 const mapStateToProps = (state, props) => {
-    const entity = getDatabase(state, props) || {};
-    const fields = getFields(state, props);
+  const entity = getDatabase(state, props) || {};
+  const fields = getFields(state, props);
 
-    return {
-        entity,
-        table: getTable(state, props),
-        metadataFields: fields,
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        foreignKeys: getForeignKeys(state, props),
-        isEditing: getIsEditing(state, props),
-        isFormulaExpanded: getIsFormulaExpanded(state, props),
-    }
+  return {
+    entity,
+    table: getTable(state, props),
+    metadataFields: fields,
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    foreignKeys: getForeignKeys(state, props),
+    isEditing: getIsEditing(state, props),
+    isFormulaExpanded: getIsFormulaExpanded(state, props),
+  };
 };
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions,
-    onChangeLocation: push
+  ...metadataActions,
+  ...actions,
+  onChangeLocation: push,
 };
 
 const validate = (values, props) => {
-    return {};
-}
+  return {};
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'details',
-    fields: ['name', 'display_name', 'description', 'revision_message', 'points_of_interest', 'caveats'],
-    validate
+  form: "details",
+  fields: [
+    "name",
+    "display_name",
+    "description",
+    "revision_message",
+    "points_of_interest",
+    "caveats",
+  ],
+  validate,
 })
 export default class DatabaseDetail extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entity: PropTypes.object.isRequired,
-        table: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        resetForm: PropTypes.func.isRequired,
-        fields: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entity: PropTypes.object.isRequired,
+    table: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    resetForm: PropTypes.func.isRequired,
+    fields: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+  };
 
-    render() {
-        const {
-            fields: { name, display_name, description, revision_message, points_of_interest, caveats },
-            style,
-            entity,
-            table,
-            loadingError,
-            loading,
-            user,
-            isEditing,
-            startEditing,
-            endEditing,
-            handleSubmit,
-            resetForm,
-            submitting
-        } = this.props;
+  render() {
+    const {
+      fields: {
+        name,
+        display_name,
+        description,
+        revision_message,
+        points_of_interest,
+        caveats,
+      },
+      style,
+      entity,
+      table,
+      loadingError,
+      loading,
+      user,
+      isEditing,
+      startEditing,
+      endEditing,
+      handleSubmit,
+      resetForm,
+      submitting,
+    } = this.props;
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.rUpdateDatabaseDetail(fields, this.props)
-        );
+    const onSubmit = handleSubmit(
+      async fields => await actions.rUpdateDatabaseDetail(fields, this.props),
+    );
 
-        return (
-            <form style={style} className="full"
-                onSubmit={onSubmit}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={false}
-                        onSubmit={onSubmit}
-                        endEditing={endEditing}
-                        reinitializeForm={resetForm}
-                        submitting={submitting}
-                        revisionMessageFormField={revision_message}
-                    />
-                }
-                <EditableReferenceHeader
-                    entity={entity}
-                    table={table}
-                    type="database"
-                    headerIcon="database"
-                    name="Details"
-                    user={user}
+    return (
+      <form style={style} className="full" onSubmit={onSubmit}>
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={false}
+            onSubmit={onSubmit}
+            endEditing={endEditing}
+            reinitializeForm={resetForm}
+            submitting={submitting}
+            revisionMessageFormField={revision_message}
+          />
+        )}
+        <EditableReferenceHeader
+          entity={entity}
+          table={table}
+          type="database"
+          headerIcon="database"
+          name="Details"
+          user={user}
+          isEditing={isEditing}
+          hasSingleSchema={false}
+          hasDisplayName={false}
+          startEditing={startEditing}
+          displayNameFormField={display_name}
+          nameFormField={name}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <List>
+                <li className="relative">
+                  <Detail
+                    id="description"
+                    name={t`Description`}
+                    description={entity.description}
+                    placeholder={t`No description yet`}
+                    isEditing={isEditing}
+                    field={description}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="points_of_interest"
+                    name={t`Why this database is interesting`}
+                    description={entity.points_of_interest}
+                    placeholder={t`Nothing interesting yet`}
+                    isEditing={isEditing}
+                    field={points_of_interest}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="caveats"
+                    name={t`Things to be aware of about this database`}
+                    description={entity.caveats}
+                    placeholder={t`Nothing to be aware of yet`}
                     isEditing={isEditing}
-                    hasSingleSchema={false}
-                    hasDisplayName={false}
-                    startEditing={startEditing}
-                    displayNameFormField={display_name}
-                    nameFormField={name}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            <li className="relative">
-                                <Detail
-                                    id="description"
-                                    name={t`Description`}
-                                    description={entity.description}
-                                    placeholder={t`No description yet`}
-                                    isEditing={isEditing}
-                                    field={description}
-                                />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="points_of_interest"
-                                    name={t`Why this database is interesting`}
-                                    description={entity.points_of_interest}
-                                    placeholder={t`Nothing interesting yet`}
-                                    isEditing={isEditing}
-                                    field={points_of_interest}
-                                    />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="caveats"
-                                    name={t`Things to be aware of about this database`}
-                                    description={entity.caveats}
-                                    placeholder={t`Nothing to be aware of yet`}
-                                    isEditing={isEditing}
-                                    field={caveats}
-                                />
-                            </li>
-                        </List>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                    field={caveats}
+                  />
+                </li>
+              </List>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/DatabaseDetailContainer.jsx b/frontend/src/metabase/reference/databases/DatabaseDetailContainer.jsx
index 340f8cd57b9bfb97c3fd4ac2f9cf255597d9bce8..c24c3f5357d99ad56f888c2c333487a463d3fafd 100644
--- a/frontend/src/metabase/reference/databases/DatabaseDetailContainer.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseDetailContainer.jsx
@@ -1,74 +1,68 @@
 /* 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 { connect } from "react-redux";
 
-import DatabaseSidebar from './DatabaseSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import DatabaseDetail from "metabase/reference/databases/DatabaseDetail.jsx"
+import DatabaseSidebar from "./DatabaseSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import DatabaseDetail from "metabase/reference/databases/DatabaseDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
-import {
-    getDatabase,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
+import { getDatabase, getDatabaseId, getIsEditing } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  database: getDatabase(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class DatabaseDetailContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    async fetchContainerData(){
-        await actions.wrappedFetchDatabaseMetadata(this.props, this.props.databaseId);
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabaseMetadata(
+      this.props,
+      this.props.databaseId,
+    );
+  }
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            database,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
+  render() {
+    const { database, isEditing } = this.props;
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<DatabaseSidebar database={database} />}
-            >
-                <DatabaseDetail {...this.props} />
-            </SidebarLayout>
-        );
-    }
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<DatabaseSidebar database={database} />}
+      >
+        <DatabaseDetail {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/DatabaseList.jsx b/frontend/src/metabase/reference/databases/DatabaseList.jsx
index 92ff27535bb375a81b850b5012f5bb811fbb8db3..3979bef2d4530c4d0e0fcf2b71fc850eebbc036c 100644
--- a/frontend/src/metabase/reference/databases/DatabaseList.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -14,76 +14,73 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-import {
-    getDatabases,
-    getError,
-    getLoading
-} from "../selectors";
+import { getDatabases, getError, getLoading } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 import NoDatabasesEmptyState from "metabase/reference/databases/NoDatabasesEmptyState";
 
 const mapStateToProps = (state, props) => ({
-    entities: getDatabases(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  entities: getDatabases(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
-
 @connect(mapStateToProps, mapDispatchToProps)
 export default class DatabaseList extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
 
-    render() {
-        const {
-            entities,
-            style,
-            loadingError,
-            loading
-        } = this.props;
+  render() {
+    const { entities, style, loadingError, loading } = this.props;
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader
-                    name={t`Databases and tables`}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            {
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                          <li className="relative" key={entity.id}>
-                                            <ListItem
-                                                id={entity.id}
-                                                index={index}
-                                                name={entity.display_name || entity.name}
-                                                description={ entity.description }
-                                                url={ `/reference/databases/${entity.id}` }
-                                                icon="database"
-                                            />
-                                        </li>
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <NoDatabasesEmptyState />
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader name={t`Databases and tables`} />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {Object.values(entities)
+                    .filter(isQueryable)
+                    .map(
+                      (entity, index) =>
+                        entity &&
+                        entity.id &&
+                        entity.name && (
+                          <li className="relative" key={entity.id}>
+                            <ListItem
+                              id={entity.id}
+                              index={index}
+                              name={entity.display_name || entity.name}
+                              description={entity.description}
+                              url={`/reference/databases/${entity.id}`}
+                              icon="database"
+                            />
+                          </li>
+                        ),
+                    )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <NoDatabasesEmptyState />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/DatabaseListContainer.jsx b/frontend/src/metabase/reference/databases/DatabaseListContainer.jsx
index c74fbefbc9970444ed919adaff5a82aa54ae0fa9..d179e81ac7e7aaa911dd215728e672154c36a6ee 100644
--- a/frontend/src/metabase/reference/databases/DatabaseListContainer.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseListContainer.jsx
@@ -1,69 +1,63 @@
 /* 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 { connect } from "react-redux";
 
-import BaseSidebar from 'metabase/reference/guide/BaseSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import DatabaseList from "metabase/reference/databases/DatabaseList.jsx"
+import BaseSidebar from "metabase/reference/guide/BaseSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import DatabaseList from "metabase/reference/databases/DatabaseList.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
-import {
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+import { getDatabaseId, getIsEditing } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class DatabaseListContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        location: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchDatabases(this.props);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    location: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+  };
+
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabases(this.props);
+  }
+
+  componentWillMount() {
+    this.fetchContainerData();
+  }
+
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
-
-    }
-
-    render() {
-        const {
-            isEditing
-        } = this.props;
-
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<BaseSidebar/>}
-            >
-                <DatabaseList {...this.props}/>
-            </SidebarLayout>
-        );
-    }
+    actions.clearState(newProps);
+  }
+
+  render() {
+    const { isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<BaseSidebar />}
+      >
+        <DatabaseList {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx b/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx
index 261c60bf7ed16709b6167e03f2ab10a5007e5e55..07244ad94cdd105fefd612c86f19894cd551c470 100644
--- a/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx
+++ b/frontend/src/metabase/reference/databases/DatabaseSidebar.jsx
@@ -2,44 +2,43 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const DatabaseSidebar = ({
-    database,
-    style,
-    className
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <ul>
-            <div className={S.breadcrumbs}>
-                <Breadcrumbs
-                    className="py4"
-                    crumbs={[[t`Databases`,"/reference/databases"],
-                             [database.name]]}
-                    inSidebar={true}
-                    placeholder={t`Data Reference`}
-                />
-            </div>
-                <SidebarItem key={`/reference/databases/${database.id}`} 
-                             href={`/reference/databases/${database.id}`} 
-                             icon="document" 
-                             name={t`Details`} />
-                <SidebarItem key={`/reference/databases/${database.id}/tables`} 
-                             href={`/reference/databases/${database.id}/tables`} 
-                             icon="table2" 
-                             name={t`Tables in ${database.name}`} />
-        </ul>
-    </div>
+const DatabaseSidebar = ({ database, style, className }) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <ul>
+      <div className={S.breadcrumbs}>
+        <Breadcrumbs
+          className="py4"
+          crumbs={[[t`Databases`, "/reference/databases"], [database.name]]}
+          inSidebar={true}
+          placeholder={t`Data Reference`}
+        />
+      </div>
+      <SidebarItem
+        key={`/reference/databases/${database.id}`}
+        href={`/reference/databases/${database.id}`}
+        icon="document"
+        name={t`Details`}
+      />
+      <SidebarItem
+        key={`/reference/databases/${database.id}/tables`}
+        href={`/reference/databases/${database.id}/tables`}
+        icon="table2"
+        name={t`Tables in ${database.name}`}
+      />
+    </ul>
+  </div>
+);
 DatabaseSidebar.propTypes = {
-    database:          PropTypes.object,
-    className:      PropTypes.string,
-    style:          PropTypes.object,
+  database: PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
 };
 
 export default pure(DatabaseSidebar);
-
diff --git a/frontend/src/metabase/reference/databases/FieldDetail.jsx b/frontend/src/metabase/reference/databases/FieldDetail.jsx
index 051021c80d59652a2efd04a2611fbe737abab9d4..10cb2042765b3af9c1f2a9d3890573c77f2d5a58 100644
--- a/frontend/src/metabase/reference/databases/FieldDetail.jsx
+++ b/frontend/src/metabase/reference/databases/FieldDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "metabase/reference/Reference.css";
 
 import List from "metabase/components/List.jsx";
@@ -16,260 +16,271 @@ import Detail from "metabase/reference/components/Detail.jsx";
 import FieldTypeDetail from "metabase/reference/components/FieldTypeDetail.jsx";
 import UsefulQuestions from "metabase/reference/components/UsefulQuestions.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import {
-    getField,
-    getTable,
-    getDatabase,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getIsFormulaExpanded,
-    getForeignKeys
+  getField,
+  getTable,
+  getDatabase,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getIsFormulaExpanded,
+  getForeignKeys,
 } from "../selectors";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 const interestingQuestions = (database, table, field, metadata) => {
-    return [
-        {
-            text: t`Number of ${table.display_name} grouped by ${field.display_name}`,
-            icon: { name: "bar", scale: 1, viewBox: "8 8 16 16" },
-            link: getQuestionUrl({
-                dbId: database.id,
-                tableId: table.id,
-                fieldId: field.id,
-                getCount: true,
-                visualization: 'bar',
-                metadata
-            })
-        },
-        {
-            text: t`Number of ${table.display_name} grouped by ${field.display_name}`,
-            icon: { name: "pie", scale: 1, viewBox: "8 8 16 16" },
-            link: getQuestionUrl({
-                dbId: database.id,
-                tableId: table.id,
-                fieldId: field.id,
-                getCount: true,
-                visualization: 'pie',
-                metadata
-            })
-        },
-        {
-            text: t`All distinct values of ${field.display_name}`,
-            icon: "table2",
-            link: getQuestionUrl({
-                dbId: database.id,
-                tableId: table.id,
-                fieldId: field.id,
-                metadata
-            })
-        }
-    ]
-}
+  return [
+    {
+      text: t`Number of ${table.display_name} grouped by ${field.display_name}`,
+      icon: { name: "bar", scale: 1, viewBox: "8 8 16 16" },
+      link: getQuestionUrl({
+        dbId: database.id,
+        tableId: table.id,
+        fieldId: field.id,
+        getCount: true,
+        visualization: "bar",
+        metadata,
+      }),
+    },
+    {
+      text: t`Number of ${table.display_name} grouped by ${field.display_name}`,
+      icon: { name: "pie", scale: 1, viewBox: "8 8 16 16" },
+      link: getQuestionUrl({
+        dbId: database.id,
+        tableId: table.id,
+        fieldId: field.id,
+        getCount: true,
+        visualization: "pie",
+        metadata,
+      }),
+    },
+    {
+      text: t`All distinct values of ${field.display_name}`,
+      icon: "table2",
+      link: getQuestionUrl({
+        dbId: database.id,
+        tableId: table.id,
+        fieldId: field.id,
+        metadata,
+      }),
+    },
+  ];
+};
 
 const mapStateToProps = (state, props) => {
-    const entity = getField(state, props) || {};
+  const entity = getField(state, props) || {};
 
-    return {
-        entity,
-        field: entity,
-        table: getTable(state, props),
-        database: getDatabase(state, props),
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        foreignKeys: getForeignKeys(state, props),
-        isEditing: getIsEditing(state, props),
-        isFormulaExpanded: getIsFormulaExpanded(state, props),
-    }
+  return {
+    entity,
+    field: entity,
+    table: getTable(state, props),
+    database: getDatabase(state, props),
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    foreignKeys: getForeignKeys(state, props),
+    isEditing: getIsEditing(state, props),
+    isFormulaExpanded: getIsFormulaExpanded(state, props),
+  };
 };
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions,
-    onChangeLocation: push
+  ...metadataActions,
+  ...actions,
+  onChangeLocation: push,
 };
 
 const validate = (values, props) => {
-    return {};
-}
+  return {};
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'details',
-    fields: ['name', 'display_name', 'description', 'revision_message', 'points_of_interest', 'caveats', 'special_type', 'fk_target_field_id'],
-    validate
+  form: "details",
+  fields: [
+    "name",
+    "display_name",
+    "description",
+    "revision_message",
+    "points_of_interest",
+    "caveats",
+    "special_type",
+    "fk_target_field_id",
+  ],
+  validate,
 })
 export default class FieldDetail extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entity: PropTypes.object.isRequired,
-        field:  PropTypes.object.isRequired,
-        table: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        foreignKeys: PropTypes.object,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        resetForm: PropTypes.func.isRequired,
-        fields: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-        metadata: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entity: PropTypes.object.isRequired,
+    field: PropTypes.object.isRequired,
+    table: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    foreignKeys: PropTypes.object,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    resetForm: PropTypes.func.isRequired,
+    fields: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+    metadata: PropTypes.object,
+  };
 
-    render() {
-        const {
-            fields: { name, display_name, description, revision_message, points_of_interest, caveats, special_type, fk_target_field_id },
-            style,
-            entity,
-            table,
-            loadingError,
-            loading,
-            user,
-            foreignKeys,
-            isEditing,
-            startEditing,
-            endEditing,
-            handleSubmit,
-            resetForm,
-            submitting,
-            metadata
-        } = this.props;
+  render() {
+    const {
+      fields: {
+        name,
+        display_name,
+        description,
+        revision_message,
+        points_of_interest,
+        caveats,
+        special_type,
+        fk_target_field_id,
+      },
+      style,
+      entity,
+      table,
+      loadingError,
+      loading,
+      user,
+      foreignKeys,
+      isEditing,
+      startEditing,
+      endEditing,
+      handleSubmit,
+      resetForm,
+      submitting,
+      metadata,
+    } = this.props;
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.rUpdateFieldDetail(fields, this.props)
-        );
+    const onSubmit = handleSubmit(
+      async fields => await actions.rUpdateFieldDetail(fields, this.props),
+    );
 
-        return (
-            <form style={style} className="full"
-                onSubmit={onSubmit}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={false}
-                        onSubmit={onSubmit}
-                        endEditing={endEditing}
-                        reinitializeForm={resetForm}
-                        submitting={submitting}
-                        revisionMessageFormField={revision_message}
+    return (
+      <form style={style} className="full" onSubmit={onSubmit}>
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={false}
+            onSubmit={onSubmit}
+            endEditing={endEditing}
+            reinitializeForm={resetForm}
+            submitting={submitting}
+            revisionMessageFormField={revision_message}
+          />
+        )}
+        <EditableReferenceHeader
+          entity={entity}
+          table={table}
+          type="field"
+          headerIcon="field"
+          name="Details"
+          user={user}
+          isEditing={isEditing}
+          hasSingleSchema={false}
+          hasDisplayName={true}
+          startEditing={startEditing}
+          displayNameFormField={display_name}
+          nameFormField={name}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <List>
+                <li className="relative">
+                  <Detail
+                    id="description"
+                    name={t`Description`}
+                    description={entity.description}
+                    placeholder={t`No description yet`}
+                    isEditing={isEditing}
+                    field={description}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <Detail
+                      id="name"
+                      name={t`Actual name in database`}
+                      description={entity.name}
+                      subtitleClass={S.tableActualName}
                     />
-                }
-                <EditableReferenceHeader
-                    entity={entity}
-                    table={table}
-                    type="field"
-                    headerIcon="field"
-                    name="Details"
-                    user={user}
+                  </li>
+                )}
+                <li className="relative">
+                  <Detail
+                    id="points_of_interest"
+                    name={t`Why this field is interesting`}
+                    description={entity.points_of_interest}
+                    placeholder={t`Nothing interesting yet`}
                     isEditing={isEditing}
-                    hasSingleSchema={false}
-                    hasDisplayName={true}
-                    startEditing={startEditing}
-                    displayNameFormField={display_name}
-                    nameFormField={name}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            <li className="relative">
-                                <Detail
-                                    id="description"
-                                    name={t`Description`}
-                                    description={entity.description}
-                                    placeholder={t`No description yet`}
-                                    isEditing={isEditing}
-                                    field={description}
-                                />
-                            </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <Detail
-                                        id="name"
-                                        name={t`Actual name in database`}
-                                        description={entity.name}
-                                        subtitleClass={S.tableActualName}
-                                    />
-                                </li>
-                            }
-                            <li className="relative">
-                                <Detail
-                                    id="points_of_interest"
-                                    name={t`Why this field is interesting`}
-                                    description={entity.points_of_interest}
-                                    placeholder={t`Nothing interesting yet`}
-                                    isEditing={isEditing}
-                                    field={points_of_interest}
-                                    />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="caveats"
-                                    name={t`Things to be aware of about this field`}
-                                    description={entity.caveats}
-                                    placeholder={t`Nothing to be aware of yet`}
-                                    isEditing={isEditing}
-                                    field={caveats}
-                                />
-                            </li>
-
-
-                            { !isEditing &&
-                                <li className="relative">
-                                    <Detail
-                                        id="base_type"
-                                        name={t`Data type`}
-                                        description={entity.base_type}
-                                    />
-                                </li>
-                            }
-                                <li className="relative">
-                                    <FieldTypeDetail
-                                        field={entity}
-                                        foreignKeys={foreignKeys}
-                                        fieldTypeFormField={special_type}
-                                        foreignKeyFormField={fk_target_field_id}
-                                        isEditing={isEditing}
-                                    />
-                                </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <UsefulQuestions
-                                        questions={
-                                            interestingQuestions(
-                                                this.props.database,
-                                                this.props.table,
-                                                this.props.field,
-                                                metadata
-                                            )
-                                        }
-                                    />
-                                </li>
-                            }
-
+                    field={points_of_interest}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="caveats"
+                    name={t`Things to be aware of about this field`}
+                    description={entity.caveats}
+                    placeholder={t`Nothing to be aware of yet`}
+                    isEditing={isEditing}
+                    field={caveats}
+                  />
+                </li>
 
-                        </List>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                {!isEditing && (
+                  <li className="relative">
+                    <Detail
+                      id="base_type"
+                      name={t`Data type`}
+                      description={entity.base_type}
+                    />
+                  </li>
+                )}
+                <li className="relative">
+                  <FieldTypeDetail
+                    field={entity}
+                    foreignKeys={foreignKeys}
+                    fieldTypeFormField={special_type}
+                    foreignKeyFormField={fk_target_field_id}
+                    isEditing={isEditing}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <UsefulQuestions
+                      questions={interestingQuestions(
+                        this.props.database,
+                        this.props.table,
+                        this.props.field,
+                        metadata,
+                      )}
+                    />
+                  </li>
+                )}
+              </List>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/FieldDetailContainer.jsx b/frontend/src/metabase/reference/databases/FieldDetailContainer.jsx
index 6fc99cb2c95876a3adbf3cc755f3f9fe56f957bc..d15ad5c8164a8afa517a32aa902f4b42fb723cbb 100644
--- a/frontend/src/metabase/reference/databases/FieldDetailContainer.jsx
+++ b/frontend/src/metabase/reference/databases/FieldDetailContainer.jsx
@@ -1,89 +1,92 @@
 /* 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 { connect } from "react-redux";
 
-import FieldSidebar from './FieldSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import FieldDetail from "metabase/reference/databases/FieldDetail.jsx"
+import FieldSidebar from "./FieldSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import FieldDetail from "metabase/reference/databases/FieldDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 import { getMetadata } from "metabase/selectors/metadata";
 
 import {
-    getDatabase,
-    getTable,
-    getField,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+  getDatabase,
+  getTable,
+  getField,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
-import { getXrayEnabled } from 'metabase/xray/selectors'
+import { getXrayEnabled } from "metabase/xray/selectors";
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),
-    table: getTable(state, props),
-    field: getField(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props),
-    metadata: getMetadata(state, props),
-    showXray: getXrayEnabled(state)
+  database: getDatabase(state, props),
+  table: getTable(state, props),
+  field: getField(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
+  metadata: getMetadata(state, props),
+  showXray: getXrayEnabled(state),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class FieldDetailContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        table: PropTypes.object.isRequired,
-        field: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        metadata: PropTypes.object,
-        showXray: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchDatabaseMetadata(this.props, this.props.databaseId);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    table: PropTypes.object.isRequired,
+    field: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    metadata: PropTypes.object,
+    showXray: PropTypes.bool,
+  };
 
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabaseMetadata(
+      this.props,
+      this.props.databaseId,
+    );
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            database,
-            table,
-            field,
-            isEditing,
-            showXray
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<FieldSidebar database={database} table={table} field={field} showXray={showXray}/>}
-            >
-                <FieldDetail {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { database, table, field, isEditing, showXray } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={
+          <FieldSidebar
+            database={database}
+            table={table}
+            field={field}
+            showXray={showXray}
+          />
+        }
+      >
+        <FieldDetail {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/FieldList.jsx b/frontend/src/metabase/reference/databases/FieldList.jsx
index add676b9a65da451a3a3bd0131f4b7eb73170e88..089cc247225ae5f0b47ee2667e999fcfb121949a 100644
--- a/frontend/src/metabase/reference/databases/FieldList.jsx
+++ b/frontend/src/metabase/reference/databases/FieldList.jsx
@@ -3,10 +3,10 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "metabase/components/List.css";
 import R from "metabase/reference/Reference.css";
-import F from "metabase/reference/components/Field.css"
+import F from "metabase/reference/components/Field.css";
 
 import Field from "metabase/reference/components/Field.jsx";
 import List from "metabase/components/List.jsx";
@@ -19,159 +19,173 @@ import EditableReferenceHeader from "metabase/reference/components/EditableRefer
 import cx from "classnames";
 
 import {
-    getTable,
-    getFieldsByTable,
-    getForeignKeys,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
+  getTable,
+  getFieldsByTable,
+  getForeignKeys,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
 } from "../selectors";
 
-import {
-    fieldsToFormFields
-} from '../utils';
+import { fieldsToFormFields } from "../utils";
 
 import { getIconForField } from "metabase/lib/schema_metadata";
 
 import * as metadataActions from "metabase/redux/metadata";
-import * as actions from 'metabase/reference/reference';
-
+import * as actions from "metabase/reference/reference";
 
 const emptyStateData = {
-    message: t`Fields in this table will appear here as they're added`,
-    icon: "fields"
-}
-
+  message: t`Fields in this table will appear here as they're added`,
+  icon: "fields",
+};
 
 const mapStateToProps = (state, props) => {
-    const data = getFieldsByTable(state, props);
-    return {
-        table: getTable(state, props),
-        entities: data,
-        foreignKeys: getForeignKeys(state, props),
-        loading: getLoading(state, props),
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        isEditing: getIsEditing(state, props),
-        fields: fieldsToFormFields(data)
-    };
-}
+  const data = getFieldsByTable(state, props);
+  return {
+    table: getTable(state, props),
+    entities: data,
+    foreignKeys: getForeignKeys(state, props),
+    loading: getLoading(state, props),
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    isEditing: getIsEditing(state, props),
+    fields: fieldsToFormFields(data),
+  };
+};
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 const validate = (values, props) => {
-    return {};
-}
+  return {};
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'fields',
-    validate
+  form: "fields",
+  validate,
 })
 export default class FieldList extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        foreignKeys: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        user: PropTypes.object.isRequired,
-        fields: PropTypes.object.isRequired,
-        table: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-        resetForm: PropTypes.func
-    };
-
-    render() {
-        const {
-            style,
-            entities,
-            fields,
-            foreignKeys,
-            table,
-            loadingError,
-            loading,
-            user,
-            isEditing,
-            startEditing,
-            endEditing,
-            resetForm,
-            handleSubmit,
-            submitting
-        } = this.props;
-
-        return (
-            <form style={style} className="full"
-                onSubmit={handleSubmit(async (formFields) =>
-                    await actions.rUpdateFields(this.props.entities, formFields, this.props)
-                )}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={false}
-                        reinitializeForm={resetForm}
-                        endEditing={endEditing}
-                        submitting={submitting}
-                    />
-                }
-                <EditableReferenceHeader 
-                    headerIcon="table2"
-                    name={t`Fields in ${table.display_name}`}
-                    user={user} 
-                    isEditing={isEditing} 
-                    startEditing={startEditing} 
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <div className={S.item}>
-                            <div className={R.columnHeader}>
-                                <div className={cx(S.itemTitle, F.fieldNameTitle)}>
-                                    {t`Field name`}
-                                </div>
-                                <div className={cx(S.itemTitle, F.fieldType)}>
-                                    {t`Field type`}
-                                </div>
-                                <div className={cx(S.itemTitle, F.fieldDataType)}>
-                                    {t`Data type`}
-                                </div>
-                            </div>
-                        </div>
-                        <List>
-                            { Object.values(entities).map(entity =>
-                                entity && entity.id && entity.name &&
-                                    <li className="relative" key={entity.id}>
-                                        <Field
-                                            field={entity}
-                                            foreignKeys={foreignKeys}
-                                            url={`/reference/databases/${table.db_id}/tables/${table.id}/fields/${entity.id}`}
-                                            icon={getIconForField(entity)}
-                                            isEditing={isEditing}
-                                            formField={fields[entity.id]}
-                                        />
-                                    </li>
-                            )}
-                        </List>
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    foreignKeys: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    user: PropTypes.object.isRequired,
+    fields: PropTypes.object.isRequired,
+    table: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+    resetForm: PropTypes.func,
+  };
+
+  render() {
+    const {
+      style,
+      entities,
+      fields,
+      foreignKeys,
+      table,
+      loadingError,
+      loading,
+      user,
+      isEditing,
+      startEditing,
+      endEditing,
+      resetForm,
+      handleSubmit,
+      submitting,
+    } = this.props;
+
+    return (
+      <form
+        style={style}
+        className="full"
+        onSubmit={handleSubmit(
+          async formFields =>
+            await actions.rUpdateFields(
+              this.props.entities,
+              formFields,
+              this.props,
+            ),
+        )}
+      >
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={false}
+            reinitializeForm={resetForm}
+            endEditing={endEditing}
+            submitting={submitting}
+          />
+        )}
+        <EditableReferenceHeader
+          headerIcon="table2"
+          name={t`Fields in ${table.display_name}`}
+          user={user}
+          isEditing={isEditing}
+          startEditing={startEditing}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <div className={S.item}>
+                  <div className={R.columnHeader}>
+                    <div className={cx(S.itemTitle, F.fieldNameTitle)}>
+                      {t`Field name`}
+                    </div>
+                    <div className={cx(S.itemTitle, F.fieldType)}>
+                      {t`Field type`}
                     </div>
-                    :
-                    <div className={S.empty}>
-                        <EmptyState {...emptyStateData} />
+                    <div className={cx(S.itemTitle, F.fieldDataType)}>
+                      {t`Data type`}
                     </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                  </div>
+                </div>
+                <List>
+                  {Object.values(entities).map(
+                    entity =>
+                      entity &&
+                      entity.id &&
+                      entity.name && (
+                        <li className="relative" key={entity.id}>
+                          <Field
+                            field={entity}
+                            foreignKeys={foreignKeys}
+                            url={`/reference/databases/${table.db_id}/tables/${
+                              table.id
+                            }/fields/${entity.id}`}
+                            icon={getIconForField(entity)}
+                            isEditing={isEditing}
+                            formField={fields[entity.id]}
+                          />
+                        </li>
+                      ),
+                  )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <EmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/FieldListContainer.jsx b/frontend/src/metabase/reference/databases/FieldListContainer.jsx
index 30d9d97c727dff3714c6ef1889519358a3ebb561..42cf4f83cc9f725bc5c29baad08c6821ff69c887 100644
--- a/frontend/src/metabase/reference/databases/FieldListContainer.jsx
+++ b/frontend/src/metabase/reference/databases/FieldListContainer.jsx
@@ -1,78 +1,75 @@
 /* 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 { connect } from "react-redux";
 
-import TableSidebar from './TableSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import FieldList from "metabase/reference/databases/FieldList.jsx"
+import TableSidebar from "./TableSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import FieldList from "metabase/reference/databases/FieldList.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getDatabase,
-    getTable,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
+  getDatabase,
+  getTable,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),    
-    table: getTable(state, props),    
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  database: getDatabase(state, props),
+  table: getTable(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class FieldListContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        table: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool
-    };
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    table: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    async fetchContainerData(){
-        await actions.wrappedFetchDatabaseMetadata(this.props, this.props.databaseId);
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabaseMetadata(
+      this.props,
+      this.props.databaseId,
+    );
+  }
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            database,
-            table,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
+  render() {
+    const { database, table, isEditing } = this.props;
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<TableSidebar database={database} table={table}/>}
-            >
-                <FieldList {...this.props} />
-            </SidebarLayout>
-        );
-    }
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<TableSidebar database={database} table={table} />}
+      >
+        <FieldList {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/FieldSidebar.jsx b/frontend/src/metabase/reference/databases/FieldSidebar.jsx
index da0092589a264ebe3dceb339fa70f224ab3a1be9..407308cff5bb4ccb99f93109c12ad299971ee50e 100644
--- a/frontend/src/metabase/reference/databases/FieldSidebar.jsx
+++ b/frontend/src/metabase/reference/databases/FieldSidebar.jsx
@@ -2,54 +2,67 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const FieldSidebar =({
-    database,
-    table,
-    field,
-    style,
-    className,
-    showXray
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <ul>
-            <div className={S.breadcrumbs}>
-                <Breadcrumbs
-                    className="py4"
-                    crumbs={[[database.name, `/reference/databases/${database.id}`],
-                             [table.name,`/reference/databases/${database.id}/tables/${table.id}`],
-                             [field.name]]}
-                    inSidebar={true}
-                    placeholder={t`Data Reference`}
-                />
-            </div>
-            <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
-                         href={`/reference/databases/${database.id}/tables/${table.id}/fields/${field.id}`}
-                         icon="document"
-                         name={t`Details`} />
-             { showXray && (
-                 <SidebarItem
-                     key={`/xray/field/${field.id}/approximate`}
-                     href={`/xray/field/${field.id}/approximate`}
-                     icon="beaker"
-                     name={t`X-ray this Field`} />
-             )}
-        </ul>
-    </div>
+const FieldSidebar = ({
+  database,
+  table,
+  field,
+  style,
+  className,
+  showXray,
+}) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <ul>
+      <div className={S.breadcrumbs}>
+        <Breadcrumbs
+          className="py4"
+          crumbs={[
+            [database.name, `/reference/databases/${database.id}`],
+            [
+              table.name,
+              `/reference/databases/${database.id}/tables/${table.id}`,
+            ],
+            [field.name],
+          ]}
+          inSidebar={true}
+          placeholder={t`Data Reference`}
+        />
+      </div>
+      <SidebarItem
+        key={`/reference/databases/${database.id}/tables/${table.id}/fields/${
+          field.id
+        }`}
+        href={`/reference/databases/${database.id}/tables/${table.id}/fields/${
+          field.id
+        }`}
+        icon="document"
+        name={t`Details`}
+      />
+      {showXray && (
+        <SidebarItem
+          key={`/xray/field/${field.id}/approximate`}
+          href={`/xray/field/${field.id}/approximate`}
+          icon="beaker"
+          name={t`X-ray this Field`}
+        />
+      )}
+    </ul>
+  </div>
+);
 
 FieldSidebar.propTypes = {
-    database:       PropTypes.object,
-    table:          PropTypes.object,
-    field:          PropTypes.object,
-    className:      PropTypes.string,
-    style:          PropTypes.object,
-    showXray:       PropTypes.bool
+  database: PropTypes.object,
+  table: PropTypes.object,
+  field: PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
+  showXray: PropTypes.bool,
 };
 
 export default pure(FieldSidebar);
diff --git a/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx b/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx
index c7a850ae52f5c65fc40011a3221b61100a963b1c..5ad64b6a0cd85a839e81b5260fae02a42f75b348 100644
--- a/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx
+++ b/frontend/src/metabase/reference/databases/NoDatabasesEmptyState.jsx
@@ -1,15 +1,18 @@
 import * as React from "react";
+import { t } from "c-3po";
+
 import AdminAwareEmptyState from "metabase/components/AdminAwareEmptyState";
 
-const NoDatabasesEmptyState = (user) =>
-    <AdminAwareEmptyState
-        title={t`Metabase is no fun without any data`}
-        adminMessage={t`Your databases will appear here once you connect one`}
-        message={t`Databases will appear here once your admins have added some`}
-        image={"app/assets/img/databases-list"}
-        adminAction={t`Connect a database`}
-        adminLink={"/admin/databases/create"}
-        user={user}
-    />
+const NoDatabasesEmptyState = user => (
+  <AdminAwareEmptyState
+    title={t`Metabase is no fun without any data`}
+    adminMessage={t`Your databases will appear here once you connect one`}
+    message={t`Databases will appear here once your admins have added some`}
+    image={"app/assets/img/databases-list"}
+    adminAction={t`Connect a database`}
+    adminLink={"/admin/databases/create"}
+    user={user}
+  />
+);
 
-export default NoDatabasesEmptyState
+export default NoDatabasesEmptyState;
diff --git a/frontend/src/metabase/reference/databases/TableDetail.jsx b/frontend/src/metabase/reference/databases/TableDetail.jsx
index 973207104189c9cec6beb87561206ec220ac74ff..2518e02b05516ec108635759ba946b538e862f8a 100644
--- a/frontend/src/metabase/reference/databases/TableDetail.jsx
+++ b/frontend/src/metabase/reference/databases/TableDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "metabase/reference/Reference.css";
 
 import List from "metabase/components/List.jsx";
@@ -15,209 +15,226 @@ import EditableReferenceHeader from "metabase/reference/components/EditableRefer
 import Detail from "metabase/reference/components/Detail.jsx";
 import UsefulQuestions from "metabase/reference/components/UsefulQuestions.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import {
-    getTable,
-    getFields,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getHasSingleSchema,
-    getIsFormulaExpanded,
-    getForeignKeys
+  getTable,
+  getFields,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getHasSingleSchema,
+  getIsFormulaExpanded,
+  getForeignKeys,
 } from "../selectors";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
-const interestingQuestions = (table) => {
-    return [
-        {
-            text: t`Count of ${table.display_name}`,
-            icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
-            link: getQuestionUrl({
-                dbId: table.db_id,
-                tableId: table.id,
-                getCount: true
-            })
-        },
-        {
-            text: t`See raw data for ${table.display_name}`,
-            icon: "table2",
-            link: getQuestionUrl({
-                dbId: table.db_id,
-                tableId: table.id,
-            })
-        }
-    ]
-}
+const interestingQuestions = table => {
+  return [
+    {
+      text: t`Count of ${table.display_name}`,
+      icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
+      link: getQuestionUrl({
+        dbId: table.db_id,
+        tableId: table.id,
+        getCount: true,
+      }),
+    },
+    {
+      text: t`See raw data for ${table.display_name}`,
+      icon: "table2",
+      link: getQuestionUrl({
+        dbId: table.db_id,
+        tableId: table.id,
+      }),
+    },
+  ];
+};
 const mapStateToProps = (state, props) => {
-    const entity = getTable(state, props) || {};
-    const fields = getFields(state, props);
+  const entity = getTable(state, props) || {};
+  const fields = getFields(state, props);
 
-    return {
-        entity,
-        table: getTable(state, props),
-        metadataFields: fields,
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        foreignKeys: getForeignKeys(state, props),
-        isEditing: getIsEditing(state, props),
-        hasSingleSchema: getHasSingleSchema(state, props),
-        isFormulaExpanded: getIsFormulaExpanded(state, props),
-    }
+  return {
+    entity,
+    table: getTable(state, props),
+    metadataFields: fields,
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    foreignKeys: getForeignKeys(state, props),
+    isEditing: getIsEditing(state, props),
+    hasSingleSchema: getHasSingleSchema(state, props),
+    isFormulaExpanded: getIsFormulaExpanded(state, props),
+  };
 };
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions,
-    onChangeLocation: push
+  ...metadataActions,
+  ...actions,
+  onChangeLocation: push,
 };
 
 const validate = (values, props) => {
-    return {};
-}
+  return {};
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'details',
-    fields: ['name', 'display_name', 'description', 'revision_message', 'points_of_interest', 'caveats'],
-    validate
+  form: "details",
+  fields: [
+    "name",
+    "display_name",
+    "description",
+    "revision_message",
+    "points_of_interest",
+    "caveats",
+  ],
+  validate,
 })
 export default class TableDetail extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entity: PropTypes.object.isRequired,
-        table: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        resetForm: PropTypes.func.isRequired,
-        fields: PropTypes.object.isRequired,
-        hasSingleSchema: PropTypes.bool,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entity: PropTypes.object.isRequired,
+    table: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    resetForm: PropTypes.func.isRequired,
+    fields: PropTypes.object.isRequired,
+    hasSingleSchema: PropTypes.bool,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+  };
 
-    render() {
-        const {
-            fields: { name, display_name, description, revision_message, points_of_interest, caveats },
-            style,
-            entity,
-            table,
-            loadingError,
-            loading,
-            user,
-            isEditing,
-            startEditing,
-            endEditing,
-            hasSingleSchema,
-            handleSubmit,
-            resetForm,
-            submitting,
-        } = this.props;
+  render() {
+    const {
+      fields: {
+        name,
+        display_name,
+        description,
+        revision_message,
+        points_of_interest,
+        caveats,
+      },
+      style,
+      entity,
+      table,
+      loadingError,
+      loading,
+      user,
+      isEditing,
+      startEditing,
+      endEditing,
+      hasSingleSchema,
+      handleSubmit,
+      resetForm,
+      submitting,
+    } = this.props;
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.rUpdateTableDetail(fields, this.props)
-        );
+    const onSubmit = handleSubmit(
+      async fields => await actions.rUpdateTableDetail(fields, this.props),
+    );
 
-        return (
-            <form style={style} className="full"
-                onSubmit={onSubmit}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={false}
-                        onSubmit={onSubmit}
-                        endEditing={endEditing}
-                        reinitializeForm={resetForm}
-                        submitting={submitting}
-                        revisionMessageFormField={revision_message}
+    return (
+      <form style={style} className="full" onSubmit={onSubmit}>
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={false}
+            onSubmit={onSubmit}
+            endEditing={endEditing}
+            reinitializeForm={resetForm}
+            submitting={submitting}
+            revisionMessageFormField={revision_message}
+          />
+        )}
+        <EditableReferenceHeader
+          entity={entity}
+          table={table}
+          type="table"
+          headerIcon="table2"
+          headerLink={getQuestionUrl({
+            dbId: entity.db_id,
+            tableId: entity.id,
+          })}
+          name={t`Details`}
+          user={user}
+          isEditing={isEditing}
+          hasSingleSchema={hasSingleSchema}
+          hasDisplayName={true}
+          startEditing={startEditing}
+          displayNameFormField={display_name}
+          nameFormField={name}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <List>
+                <li className="relative">
+                  <Detail
+                    id="description"
+                    name={t`Description`}
+                    description={entity.description}
+                    placeholder={t`No description yet`}
+                    isEditing={isEditing}
+                    field={description}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <Detail
+                      id="name"
+                      name={t`Actual name in database`}
+                      description={entity.name}
+                      subtitleClass={S.tableActualName}
                     />
-                }
-                <EditableReferenceHeader
-                    entity={entity}
-                    table={table}
-                    type="table"
-                    headerIcon="table2"
-                    headerLink={getQuestionUrl({ dbId: entity.db_id, tableId: entity.id})}
-                    name={t`Details`}
-                    user={user}
+                  </li>
+                )}
+                <li className="relative">
+                  <Detail
+                    id="points_of_interest"
+                    name={t`Why this table is interesting`}
+                    description={entity.points_of_interest}
+                    placeholder={t`Nothing interesting yet`}
                     isEditing={isEditing}
-                    hasSingleSchema={hasSingleSchema}
-                    hasDisplayName={true}
-                    startEditing={startEditing}
-                    displayNameFormField={display_name}
-                    nameFormField={name}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            <li className="relative">
-                                <Detail
-                                    id="description"
-                                    name={t`Description`}
-                                    description={entity.description}
-                                    placeholder={t`No description yet`}
-                                    isEditing={isEditing}
-                                    field={description}
-                                />
-                            </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <Detail
-                                        id="name"
-                                        name={t`Actual name in database`}
-                                        description={entity.name}
-                                        subtitleClass={S.tableActualName}
-                                    />
-                                </li>
-                            }
-                            <li className="relative">
-                                <Detail
-                                    id="points_of_interest"
-                                    name={t`Why this table is interesting`}
-                                    description={entity.points_of_interest}
-                                    placeholder={t`Nothing interesting yet`}
-                                    isEditing={isEditing}
-                                    field={points_of_interest}
-                                    />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="caveats"
-                                    name={t`Things to be aware of about this table`}
-                                    description={entity.caveats}
-                                    placeholder={t`Nothing to be aware of yet`}
-                                    isEditing={isEditing}
-                                    field={caveats}
-                                />
-                            </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <UsefulQuestions questions={interestingQuestions(this.props.table)} />
-                                </li>
-                            }
-                        </List>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                    field={points_of_interest}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="caveats"
+                    name={t`Things to be aware of about this table`}
+                    description={entity.caveats}
+                    placeholder={t`Nothing to be aware of yet`}
+                    isEditing={isEditing}
+                    field={caveats}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <UsefulQuestions
+                      questions={interestingQuestions(this.props.table)}
+                    />
+                  </li>
+                )}
+              </List>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/TableDetailContainer.jsx b/frontend/src/metabase/reference/databases/TableDetailContainer.jsx
index 6c7c0fb62ca3002c0f08229122db56704a8ca8b5..a6f16ee7f0fb157a4d9bc924b20059a40eaa12f4 100644
--- a/frontend/src/metabase/reference/databases/TableDetailContainer.jsx
+++ b/frontend/src/metabase/reference/databases/TableDetailContainer.jsx
@@ -1,84 +1,81 @@
 /* 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 { connect } from "react-redux";
 
-import TableSidebar from './TableSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import TableDetail from "metabase/reference/databases/TableDetail.jsx"
+import TableSidebar from "./TableSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import TableDetail from "metabase/reference/databases/TableDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getDatabase,
-    getTable,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
-import { getXrayEnabled } from 'metabase/xray/selectors'
+  getDatabase,
+  getTable,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
+import { getXrayEnabled } from "metabase/xray/selectors";
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),
-    table: getTable(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props),
-    showXray: getXrayEnabled(state)
+  database: getDatabase(state, props),
+  table: getTable(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
+  showXray: getXrayEnabled(state),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class TableDetailContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        table: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        showXray: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchDatabaseMetadata(this.props, this.props.databaseId);
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    table: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    showXray: PropTypes.bool,
+  };
+
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabaseMetadata(
+      this.props,
+      this.props.databaseId,
+    );
+  }
+
+  componentWillMount() {
+    this.fetchContainerData();
+  }
+
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+    actions.clearState(newProps);
+  }
 
+  render() {
+    const { database, table, isEditing, showXray } = this.props;
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={
+          <TableSidebar database={database} table={table} showXray={showXray} />
         }
-
-        actions.clearState(newProps)
-    }
-
-    render() {
-        const {
-            database,
-            table,
-            isEditing,
-            showXray
-        } = this.props;
-
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-
-                sidebar={<TableSidebar database={database} table={table} showXray={showXray}/>}
-            >
-                <TableDetail {...this.props} />
-            </SidebarLayout>
-        );
-    }
+      >
+        <TableDetail {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/TableList.jsx b/frontend/src/metabase/reference/databases/TableList.jsx
index 30445649cc19733b6d4b9cd62cf8ce25060eab5d..dfe5837602c22085cdbed7027c05fc8e80149bfb 100644
--- a/frontend/src/metabase/reference/databases/TableList.jsx
+++ b/frontend/src/metabase/reference/databases/TableList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -12,132 +12,139 @@ import List from "metabase/components/List.jsx";
 import ListItem from "metabase/components/ListItem.jsx";
 import EmptyState from "metabase/components/EmptyState.jsx";
 
-
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
 import {
-    getDatabase,
-    getTablesByDatabase,
-    getHasSingleSchema,
-    getError,
-    getLoading
+  getDatabase,
+  getTablesByDatabase,
+  getHasSingleSchema,
+  getError,
+  getLoading,
 } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 
-
 const emptyStateData = {
-    message: t`Tables in this database will appear here as they're added`,
-    icon: "table2"
-}
+  message: t`Tables in this database will appear here as they're added`,
+  icon: "table2",
+};
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),
-    entities: getTablesByDatabase(state, props),
-    hasSingleSchema: getHasSingleSchema(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  database: getDatabase(state, props),
+  entities: getTablesByDatabase(state, props),
+  hasSingleSchema: getHasSingleSchema(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
-const createListItem = (entity, index) =>
-    <li className="relative" key={entity.id}>
-        <ListItem
-            id={entity.id}
-            index={index}
-            name={entity.display_name || entity.name}
-            description={ entity.description }
-            url={ `/reference/databases/${entity.db_id}/tables/${entity.id}` }
-            icon="table2"
-        />
-    </li>;
-
-
-const createSchemaSeparator = (entity) =>
-    <li className={R.schemaSeparator}>{entity.schema}</li>;
-
+const createListItem = (entity, index) => (
+  <li className="relative" key={entity.id}>
+    <ListItem
+      id={entity.id}
+      index={index}
+      name={entity.display_name || entity.name}
+      description={entity.description}
+      url={`/reference/databases/${entity.db_id}/tables/${entity.id}`}
+      icon="table2"
+    />
+  </li>
+);
+
+const createSchemaSeparator = entity => (
+  <li className={R.schemaSeparator}>{entity.schema}</li>
+);
 
 export const separateTablesBySchema = (
-    tables,
-    createSchemaSeparator,
-    createListItem
-) => Object.values(tables)
-    .sort((table1, table2) => table1.schema > table2.schema ? 1 :
-        table1.schema === table2.schema ? 0 : -1
+  tables,
+  createSchemaSeparator,
+  createListItem,
+) =>
+  Object.values(tables)
+    .sort(
+      (table1, table2) =>
+        table1.schema > table2.schema
+          ? 1
+          : table1.schema === table2.schema ? 0 : -1,
     )
     .map((table, index, sortedTables) => {
-        if (!table || !table.id || !table.name) {
-            return;
-        }
-        // add schema header for first element and if schema is different from previous
-        const previousTableId = Object.keys(sortedTables)[index - 1];
-        return index === 0 ||
-            sortedTables[previousTableId].schema !== table.schema ?
-                [
-                    createSchemaSeparator(table),
-                    createListItem(table, index)
-                ] :
-                createListItem(table, index);
+      if (!table || !table.id || !table.name) {
+        return;
+      }
+      // add schema header for first element and if schema is different from previous
+      const previousTableId = Object.keys(sortedTables)[index - 1];
+      return index === 0 ||
+        sortedTables[previousTableId].schema !== table.schema
+        ? [createSchemaSeparator(table), createListItem(table, index)]
+        : createListItem(table, index);
     });
 
-
 @connect(mapStateToProps, mapDispatchToProps)
 export default class TableList extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        hasSingleSchema: PropTypes.bool,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
-
-    render() {
-        const {
-            entities,
-            style,
-            database,
-            hasSingleSchema,
-            loadingError,
-            loading
-        } = this.props;
-
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader 
-                    name={t`Tables in ${database.name}`}
-                    type="tables"
-                    headerIcon="database"
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            { !hasSingleSchema ?
-                                separateTablesBySchema(
-                                    entities,
-                                    createSchemaSeparator,
-                                    createListItem
-                                ) :
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                        createListItem(entity, index)
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <EmptyState {...emptyStateData}/>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    hasSingleSchema: PropTypes.bool,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
+
+  render() {
+    const {
+      entities,
+      style,
+      database,
+      hasSingleSchema,
+      loadingError,
+      loading,
+    } = this.props;
+
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader
+          name={t`Tables in ${database.name}`}
+          type="tables"
+          headerIcon="database"
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {!hasSingleSchema
+                    ? separateTablesBySchema(
+                        entities,
+                        createSchemaSeparator,
+                        createListItem,
+                      )
+                    : Object.values(entities)
+                        .filter(isQueryable)
+                        .map(
+                          (entity, index) =>
+                            entity &&
+                            entity.id &&
+                            entity.name &&
+                            createListItem(entity, index),
+                        )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <EmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/TableListContainer.jsx b/frontend/src/metabase/reference/databases/TableListContainer.jsx
index 7eb48ef50aed613061f6100c3c0199b1508c8c58..7a7d4db68484c9662a58e6ae6c9f54d8dd2ab325 100644
--- a/frontend/src/metabase/reference/databases/TableListContainer.jsx
+++ b/frontend/src/metabase/reference/databases/TableListContainer.jsx
@@ -1,74 +1,68 @@
 /* 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 { connect } from "react-redux";
 
-import DatabaseSidebar from './DatabaseSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import TableList from "metabase/reference/databases/TableList.jsx"
+import DatabaseSidebar from "./DatabaseSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import TableList from "metabase/reference/databases/TableList.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
-import {
-    getDatabase,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
+import { getDatabase, getDatabaseId, getIsEditing } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),    
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  database: getDatabase(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class TableListContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchDatabaseMetadata(this.props, this.props.databaseId);
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabaseMetadata(
+      this.props,
+      this.props.databaseId,
+    );
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            database,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<DatabaseSidebar database={database} />}
-            >
-                <TableList {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { database, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<DatabaseSidebar database={database} />}
+      >
+        <TableList {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/TableQuestions.jsx b/frontend/src/metabase/reference/databases/TableQuestions.jsx
index 0907f41451720a77195bac97d82f9c37f0629aa7..e0f1a8e7673a4ee9b67c77582092f60b3c6538cd 100644
--- a/frontend/src/metabase/reference/databases/TableQuestions.jsx
+++ b/frontend/src/metabase/reference/databases/TableQuestions.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
 import * as Urls from "metabase/lib/urls";
@@ -18,98 +18,99 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
-
+import { getQuestionUrl } from "../utils";
 
 import {
-    getTableQuestions,
-    getError,
-    getLoading,
-    getTable
+  getTableQuestions,
+  getError,
+  getLoading,
+  getTable,
 } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 
-const emptyStateData = (table) =>  {
-    return {
-        message: t`Questions about this table will appear here as they're added`,
-        icon: "all",
-        action: t`Ask a question`,
-        link: getQuestionUrl({
-            dbId: table.db_id,
-            tableId: table.id,
-        })
-    }
-}
-
+const emptyStateData = table => {
+  return {
+    message: t`Questions about this table will appear here as they're added`,
+    icon: "all",
+    action: t`Ask a question`,
+    link: getQuestionUrl({
+      dbId: table.db_id,
+      tableId: table.id,
+    }),
+  };
+};
 
 const mapStateToProps = (state, props) => ({
-    table: getTable(state, props),
-    entities: getTableQuestions(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  table: getTable(state, props),
+  entities: getTableQuestions(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
-
 @connect(mapStateToProps, mapDispatchToProps)
 export default class TableQuestions extends Component {
-    static propTypes = {
-        table: PropTypes.object.isRequired,
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
-
-    render() {
-        const {
-            entities,
-            style,
-            loadingError,
-            loading
-        } = this.props;
-
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader 
-                    name={t`Questions about ${this.props.table.display_name}`}
-                    type="questions"
-                    headerIcon="table2"
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            { 
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                            <li className="relative" key={entity.id}>
-                                                <ListItem
-                                                    id={entity.id}
-                                                    index={index}
-                                                    name={entity.display_name || entity.name}
-                                                    description={ t`Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
-                                                    url={ Urls.question(entity.id) }
-                                                    icon={ visualizations.get(entity.display).iconName }
-                                                />
-                                            </li>
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <AdminAwareEmptyState {...emptyStateData(this.props.table)}/>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+  static propTypes = {
+    table: PropTypes.object.isRequired,
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
+
+  render() {
+    const { entities, style, loadingError, loading } = this.props;
+
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader
+          name={t`Questions about ${this.props.table.display_name}`}
+          type="questions"
+          headerIcon="table2"
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {Object.values(entities)
+                    .filter(isQueryable)
+                    .map(
+                      (entity, index) =>
+                        entity &&
+                        entity.id &&
+                        entity.name && (
+                          <li className="relative" key={entity.id}>
+                            <ListItem
+                              id={entity.id}
+                              index={index}
+                              name={entity.display_name || entity.name}
+                              description={t`Created ${moment(
+                                entity.created_at,
+                              ).fromNow()} by ${entity.creator.common_name}`}
+                              url={Urls.question(entity.id)}
+                              icon={visualizations.get(entity.display).iconName}
+                            />
+                          </li>
+                        ),
+                    )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <AdminAwareEmptyState {...emptyStateData(this.props.table)} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/TableQuestionsContainer.jsx b/frontend/src/metabase/reference/databases/TableQuestionsContainer.jsx
index 33f9893606133562223507b65a8aee7dc2fcff4f..8e020f04092b8182cf70c25168c9d5ba113d32d2 100644
--- a/frontend/src/metabase/reference/databases/TableQuestionsContainer.jsx
+++ b/frontend/src/metabase/reference/databases/TableQuestionsContainer.jsx
@@ -1,82 +1,78 @@
 /* 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 { connect } from "react-redux";
 
-import TableSidebar from './TableSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
+import TableSidebar from "./TableSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
 
-import TableQuestions from "metabase/reference/databases/TableQuestions.jsx"
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import TableQuestions from "metabase/reference/databases/TableQuestions.jsx";
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getDatabase,
-    getTable,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
-import {
-    loadEntities
-} from 'metabase/questions/questions';
+  getDatabase,
+  getTable,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
+import { loadEntities } from "metabase/questions/questions";
 
 const mapStateToProps = (state, props) => ({
-    database: getDatabase(state, props),    
-    table: getTable(state, props),    
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  database: getDatabase(state, props),
+  table: getTable(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    fetchQuestions: () => loadEntities("cards", {}),
-    ...metadataActions,
-    ...actions
+  fetchQuestions: () => loadEntities("cards", {}),
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class TableQuestionsContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        database: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        table: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool
-    };
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    database: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    table: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    async fetchContainerData() {
-        await actions.wrappedFetchDatabaseMetadataAndQuestion(this.props, this.props.databaseId);
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchDatabaseMetadataAndQuestion(
+      this.props,
+      this.props.databaseId,
+    );
+  }
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            database,
-            table,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<TableSidebar database={database} table={table}/>}
-            >
-                <TableQuestions {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { database, table, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<TableSidebar database={database} table={table} />}
+      >
+        <TableQuestions {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/databases/TableSidebar.jsx b/frontend/src/metabase/reference/databases/TableSidebar.jsx
index 2181e234919ee453c3d1f1802765bd949d9fb79d..45b02723236282d01c02e38f23f9881aa1b32e8f 100644
--- a/frontend/src/metabase/reference/databases/TableSidebar.jsx
+++ b/frontend/src/metabase/reference/databases/TableSidebar.jsx
@@ -2,59 +2,66 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const TableSidebar = ({
-    database,
-    table,
-    style,
-    className,
-    showXray
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <div className={S.breadcrumbs}>
-            <Breadcrumbs
-                className="py4"
-                crumbs={[[t`Databases`,"/reference/databases"],
-                         [database.name, `/reference/databases/${database.id}`],
-                         [table.name]]}
-                inSidebar={true}
-                placeholder={t`Data Reference`}
-            />
-        </div>
-        <ol>
-            <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}`}
-                         href={`/reference/databases/${database.id}/tables/${table.id}`}
-                         icon="document"
-                         name={t`Details`} />
-            <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/fields`}
-                         href={`/reference/databases/${database.id}/tables/${table.id}/fields`}
-                         icon="fields"
-                         name={t`Fields in this table`} />
-            <SidebarItem key={`/reference/databases/${database.id}/tables/${table.id}/questions`}
-                         href={`/reference/databases/${database.id}/tables/${table.id}/questions`}
-                         icon="all"
-                         name={t`Questions about this table`} />
-            { showXray && (
-                <SidebarItem key={`/xray/table/${table.id}/approximate`}
-                             href={`/xray/table/${table.id}/approximate`}
-                             icon="beaker"
-                             name={t`X-ray this table`} />
-            )}
-        </ol>
+const TableSidebar = ({ database, table, style, className, showXray }) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <div className={S.breadcrumbs}>
+      <Breadcrumbs
+        className="py4"
+        crumbs={[
+          [t`Databases`, "/reference/databases"],
+          [database.name, `/reference/databases/${database.id}`],
+          [table.name],
+        ]}
+        inSidebar={true}
+        placeholder={t`Data Reference`}
+      />
     </div>
+    <ol>
+      <SidebarItem
+        key={`/reference/databases/${database.id}/tables/${table.id}`}
+        href={`/reference/databases/${database.id}/tables/${table.id}`}
+        icon="document"
+        name={t`Details`}
+      />
+      <SidebarItem
+        key={`/reference/databases/${database.id}/tables/${table.id}/fields`}
+        href={`/reference/databases/${database.id}/tables/${table.id}/fields`}
+        icon="fields"
+        name={t`Fields in this table`}
+      />
+      <SidebarItem
+        key={`/reference/databases/${database.id}/tables/${table.id}/questions`}
+        href={`/reference/databases/${database.id}/tables/${
+          table.id
+        }/questions`}
+        icon="all"
+        name={t`Questions about this table`}
+      />
+      {showXray && (
+        <SidebarItem
+          key={`/xray/table/${table.id}/approximate`}
+          href={`/xray/table/${table.id}/approximate`}
+          icon="beaker"
+          name={t`X-ray this table`}
+        />
+      )}
+    </ol>
+  </div>
+);
 
 TableSidebar.propTypes = {
-    database:       PropTypes.object,
-    table:          PropTypes.object,
-    className:      PropTypes.string,
-    style:          PropTypes.object,
-    showXray:       PropTypes.bool
+  database: PropTypes.object,
+  table: PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
+  showXray: PropTypes.bool,
 };
 
 export default pure(TableSidebar);
diff --git a/frontend/src/metabase/reference/guide/BaseSidebar.jsx b/frontend/src/metabase/reference/guide/BaseSidebar.jsx
index d1626df9f9ba5a7d02e4954a115ca48ac6ede2b4..52add1ae1da71ad0e1c5cf566f09e8c4b5228a44 100644
--- a/frontend/src/metabase/reference/guide/BaseSidebar.jsx
+++ b/frontend/src/metabase/reference/guide/BaseSidebar.jsx
@@ -2,50 +2,55 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const BaseSidebar = ({
-    style,
-    className
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <div className={S.breadcrumbs}>
-            <Breadcrumbs
-                className="py4"
-                crumbs={[[t`Data Reference`]]}
-                inSidebar={true}
-                placeholder={t`Data Reference`}
-            />
-        </div>
-        <ol>
-            <SidebarItem key="/reference/guide" 
-                         href="/reference/guide" 
-                         icon="reference" 
-                         name={t`Start here`} />
-            <SidebarItem key="/reference/metrics" 
-                         href="/reference/metrics" 
-                         icon="ruler" 
-                         name={t`Metrics`} />
-            <SidebarItem key="/reference/segments" 
-                         href="/reference/segments" 
-                         icon="segment" 
-                         name={t`Segments`} />
-            <SidebarItem key="/reference/databases" 
-                         href="/reference/databases" 
-                         icon="database" 
-                         name={t`Databases and tables`} />
-        </ol>
+const BaseSidebar = ({ style, className }) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <div className={S.breadcrumbs}>
+      <Breadcrumbs
+        className="py4"
+        crumbs={[[t`Data Reference`]]}
+        inSidebar={true}
+        placeholder={t`Data Reference`}
+      />
     </div>
+    <ol>
+      <SidebarItem
+        key="/reference/guide"
+        href="/reference/guide"
+        icon="reference"
+        name={t`Start here`}
+      />
+      <SidebarItem
+        key="/reference/metrics"
+        href="/reference/metrics"
+        icon="ruler"
+        name={t`Metrics`}
+      />
+      <SidebarItem
+        key="/reference/segments"
+        href="/reference/segments"
+        icon="segment"
+        name={t`Segments`}
+      />
+      <SidebarItem
+        key="/reference/databases"
+        href="/reference/databases"
+        icon="database"
+        name={t`Databases and tables`}
+      />
+    </ol>
+  </div>
+);
 
 BaseSidebar.propTypes = {
-    className:      PropTypes.string,
-    style:          PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
 };
 
 export default pure(BaseSidebar);
-
diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx
index f551ec832a89c37b5cfd1277ba72eba7392dc9c4..584b453f1d807086a4b7d6ea24b34471350ea22c 100644
--- a/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx
+++ b/frontend/src/metabase/reference/guide/GettingStartedGuide.jsx
@@ -2,8 +2,8 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { connect } from 'react-redux';
-import { t, jt } from 'c-3po';
+import { connect } from "react-redux";
+import { t, jt } from "c-3po";
 import cx from "classnames";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
@@ -11,293 +11,360 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 import GuideHeader from "metabase/reference/components/GuideHeader.jsx";
 import GuideDetail from "metabase/reference/components/GuideDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 import { clearRequestState } from "metabase/redux/requests";
-import { createDashboard, updateDashboard } from 'metabase/dashboards/dashboards';
+import {
+  createDashboard,
+  updateDashboard,
+} from "metabase/dashboards/dashboards";
 
-import { updateSetting } from 'metabase/admin/settings/settings';
+import { updateSetting } from "metabase/admin/settings/settings";
 
 import {
-    getGuide,
-    getUser,
-    getDashboards,
-    getLoading,
-    getError,
-    getIsEditing,
-    getTables,
-    getFields,
-    getMetrics,
-    getSegments,
-} from '../selectors';
+  getGuide,
+  getUser,
+  getDashboards,
+  getLoading,
+  getError,
+  getIsEditing,
+  getTables,
+  getFields,
+  getMetrics,
+  getSegments,
+} from "../selectors";
 
-import {
-    getQuestionUrl,
-    has
-} from '../utils';
+import { getQuestionUrl, has } from "../utils";
 
 const isGuideEmpty = ({
-    things_to_know,
-    contact,
-    most_important_dashboard,
-    important_metrics,
-    important_segments,
-    important_tables
-} = {}) => things_to_know ? false :
-    contact && contact.name ? false :
-    contact && contact.email ? false :
-    most_important_dashboard ? false :
-    important_metrics && important_metrics.length !== 0 ? false :
-    important_segments && important_segments.length !== 0 ? false :
-    important_tables && important_tables.length !== 0 ? false :
-    true;
+  things_to_know,
+  contact,
+  most_important_dashboard,
+  important_metrics,
+  important_segments,
+  important_tables,
+} = {}) =>
+  things_to_know
+    ? false
+    : contact && contact.name
+      ? false
+      : contact && contact.email
+        ? false
+        : most_important_dashboard
+          ? false
+          : important_metrics && important_metrics.length !== 0
+            ? false
+            : important_segments && important_segments.length !== 0
+              ? false
+              : important_tables && important_tables.length !== 0
+                ? false
+                : true;
 
 // This function generates a link for each important field of a Metric.
 // The link goes to a question comprised of this Metric broken out by
 // That important field.
 const exploreLinksForMetric = (metricId, guide, metadataFields, tables) => {
-    if (guide.metric_important_fields[metricId]) {
-        return guide.metric_important_fields[metricId]
-                .map(fieldId => metadataFields[fieldId])
-                .map(field => ({
-                    name: field.display_name || field.name,
-                    url: getQuestionUrl({
-                        dbId: tables[field.table_id] && tables[field.table_id].db_id,
-                        tableId: field.table_id,
-                        fieldId: field.id,
-                        metricId
-                    })
-                }))
-    }
-}
+  if (guide.metric_important_fields[metricId]) {
+    return guide.metric_important_fields[metricId]
+      .map(fieldId => metadataFields[fieldId])
+      .map(field => ({
+        name: field.display_name || field.name,
+        url: getQuestionUrl({
+          dbId: tables[field.table_id] && tables[field.table_id].db_id,
+          tableId: field.table_id,
+          fieldId: field.id,
+          metricId,
+        }),
+      }));
+  }
+};
 
 const mapStateToProps = (state, props) => ({
-    guide: getGuide(state, props),
-    user: getUser(state, props),
-    dashboards: getDashboards(state, props),
-    metrics: getMetrics(state, props),
-    segments: getSegments(state, props),
-    tables: getTables(state, props),
-    // FIXME: avoids naming conflict, tried using the propNamespace option
-    // version but couldn't quite get it to work together with passing in
-    // dynamic initialValues
-    metadataFields: getFields(state, props),
-    loading: getLoading(state, props),
-    // naming this 'error' will conflict with redux form
-    loadingError: getError(state, props),
-    isEditing: getIsEditing(state, props),
+  guide: getGuide(state, props),
+  user: getUser(state, props),
+  dashboards: getDashboards(state, props),
+  metrics: getMetrics(state, props),
+  segments: getSegments(state, props),
+  tables: getTables(state, props),
+  // FIXME: avoids naming conflict, tried using the propNamespace option
+  // version but couldn't quite get it to work together with passing in
+  // dynamic initialValues
+  metadataFields: getFields(state, props),
+  loading: getLoading(state, props),
+  // naming this 'error' will conflict with redux form
+  loadingError: getError(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    updateDashboard,
-    createDashboard,
-    updateSetting,
-    clearRequestState,
-    ...metadataActions,
-    ...actions
+  updateDashboard,
+  createDashboard,
+  updateSetting,
+  clearRequestState,
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class GettingStartedGuide extends Component {
-    static propTypes = {
-        fields: PropTypes.object,
-        style: PropTypes.object,
-        guide: PropTypes.object,
-        user: PropTypes.object,
-        dashboards: PropTypes.object,
-        metrics: PropTypes.object,
-        segments: PropTypes.object,
-        tables: PropTypes.object,
-        metadataFields: PropTypes.object,
-        loadingError: PropTypes.any,
-        loading: PropTypes.bool,
-        startEditing: PropTypes.func,
-    };
+  static propTypes = {
+    fields: PropTypes.object,
+    style: PropTypes.object,
+    guide: PropTypes.object,
+    user: PropTypes.object,
+    dashboards: PropTypes.object,
+    metrics: PropTypes.object,
+    segments: PropTypes.object,
+    tables: PropTypes.object,
+    metadataFields: PropTypes.object,
+    loadingError: PropTypes.any,
+    loading: PropTypes.bool,
+    startEditing: PropTypes.func,
+  };
 
-    render() {
-        const {
-            style,
-            guide,
-            user,
-            dashboards,
-            metrics,
-            segments,
-            tables,
-            metadataFields,
-            loadingError,
-            loading,
-            startEditing,
-        } = this.props;
+  render() {
+    const {
+      style,
+      guide,
+      user,
+      dashboards,
+      metrics,
+      segments,
+      tables,
+      metadataFields,
+      loadingError,
+      loading,
+      startEditing,
+    } = this.props;
 
-        return (
-            <div className="full relative py4" style={style}>
-                <LoadingAndErrorWrapper className="full" style={style} loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div>
-                        <GuideHeader
-                            startEditing={startEditing}
-                            isSuperuser={user && user.is_superuser}
-                        />
+    return (
+      <div className="full relative py4" style={style}>
+        <LoadingAndErrorWrapper
+          className="full"
+          style={style}
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div>
+              <GuideHeader
+                startEditing={startEditing}
+                isSuperuser={user && user.is_superuser}
+              />
 
-                        <div className="wrapper wrapper--trim">
-                            { (!guide || isGuideEmpty(guide)) && user && user.is_superuser && (
-                                <AdminInstructions>
-                                    <h2 className="py2">{t`Help your team get started with your data.`}</h2>
-                                    <GuideText>
-                                        {t`Show your team what’s most important by choosing your top dashboard, metrics, and segments.`}
-                                    </GuideText>
-                                    <button
-                                        className="Button Button--primary"
-                                        onClick={startEditing}
-                                    >
-                                        {t`Get started`}
-                                    </button>
-                                </AdminInstructions>
-                            )}
+              <div className="wrapper wrapper--trim">
+                {(!guide || isGuideEmpty(guide)) &&
+                  user &&
+                  user.is_superuser && (
+                    <AdminInstructions>
+                      <h2 className="py2">{t`Help your team get started with your data.`}</h2>
+                      <GuideText>
+                        {t`Show your team what’s most important by choosing your top dashboard, metrics, and segments.`}
+                      </GuideText>
+                      <button
+                        className="Button Button--primary"
+                        onClick={startEditing}
+                      >
+                        {t`Get started`}
+                      </button>
+                    </AdminInstructions>
+                  )}
 
-                            { guide.most_important_dashboard !== null && [
-                                <div className="my2">
-                                    <SectionHeader key={'dashboardTitle'}>
-                                        {t`Our most important dashboard`}
-                                    </SectionHeader>
-                                    <GuideDetail
-                                        key={'dashboardDetail'}
-                                        type="dashboard"
-                                        entity={dashboards[guide.most_important_dashboard]}
-                                        tables={tables}
-                                    />
-                                </div>
-                            ]}
-                            { Object.keys(metrics).length > 0  && (
-                                    <div className="my4 pt4">
-                                        <SectionHeader trim={guide.important_metrics.length === 0}>
-                                            { guide.important_metrics && guide.important_metrics.length > 0 ? t`Numbers that we pay attention to` : t`Metrics` }
-                                        </SectionHeader>
-                                        { (guide.important_metrics && guide.important_metrics.length > 0) ? [
-                                            <div className="my2">
-                                                { guide.important_metrics.map((metricId) =>
-                                                    <GuideDetail
-                                                        key={metricId}
-                                                        type="metric"
-                                                        entity={metrics[metricId]}
-                                                        tables={tables}
-                                                        exploreLinks={exploreLinksForMetric(metricId, guide, metadataFields, tables)}
-                                                    />
-                                                )}
-                                            </div>
-                                        ] :
-                                            <GuideText>
-                                                {t`Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.`}
-                                            </GuideText>
-                                        }
-                                        <div>
-                                            <Link className="Button Button--primary" to={'/reference/metrics'}>
-                                                {t`See all metrics`}
-                                            </Link>
-                                        </div>
-                                    </div>
-                                )
-                            }
+                {guide.most_important_dashboard !== null && [
+                  <div className="my2">
+                    <SectionHeader key={"dashboardTitle"}>
+                      {t`Our most important dashboard`}
+                    </SectionHeader>
+                    <GuideDetail
+                      key={"dashboardDetail"}
+                      type="dashboard"
+                      entity={dashboards[guide.most_important_dashboard]}
+                      tables={tables}
+                    />
+                  </div>,
+                ]}
+                {Object.keys(metrics).length > 0 && (
+                  <div className="my4 pt4">
+                    <SectionHeader trim={guide.important_metrics.length === 0}>
+                      {guide.important_metrics &&
+                      guide.important_metrics.length > 0
+                        ? t`Numbers that we pay attention to`
+                        : t`Metrics`}
+                    </SectionHeader>
+                    {guide.important_metrics &&
+                    guide.important_metrics.length > 0 ? (
+                      [
+                        <div className="my2">
+                          {guide.important_metrics.map(metricId => (
+                            <GuideDetail
+                              key={metricId}
+                              type="metric"
+                              entity={metrics[metricId]}
+                              tables={tables}
+                              exploreLinks={exploreLinksForMetric(
+                                metricId,
+                                guide,
+                                metadataFields,
+                                tables,
+                              )}
+                            />
+                          ))}
+                        </div>,
+                      ]
+                    ) : (
+                      <GuideText>
+                        {t`Metrics are important numbers your company cares about. They often represent a core indicator of how the business is performing.`}
+                      </GuideText>
+                    )}
+                    <div>
+                      <Link
+                        className="Button Button--primary"
+                        to={"/reference/metrics"}
+                      >
+                        {t`See all metrics`}
+                      </Link>
+                    </div>
+                  </div>
+                )}
 
-                            <div className="mt4 pt4">
-                                <SectionHeader trim={(!has(guide.important_segments) && !has(guide.important_tables))}>
-                                    { Object.keys(segments).length > 0 ? t`Segments and tables` : t`Tables` }
-                                </SectionHeader>
-                                { has(guide.important_segments) || has(guide.important_tables) ?
-                                    <div className="my2">
-                                        { guide.important_segments.map((segmentId) =>
-                                            <GuideDetail
-                                                key={segmentId}
-                                                type="segment"
-                                                entity={segments[segmentId]}
-                                                tables={tables}
-                                            />
-                                        )}
-                                        { guide.important_tables.map((tableId) =>
-                                            <GuideDetail
-                                                key={tableId}
-                                                type="table"
-                                                entity={tables[tableId]}
-                                                tables={tables}
-                                            />
-                                        )}
-                                    </div>
-                                :
-                                    <GuideText>
-                                        { Object.keys(segments).length > 0 ? (
-                                            <span>
-                                                {jt`Segments and tables are the building blocks of your company's data. Tables are collections of the raw information while segments are specific slices with specific meanings, like ${<b>"Recent orders."</b>}`}
-                                            </span>
-                                        ) : t`Tables are the building blocks of your company's data.`
-                                        }
-                                    </GuideText>
-                                }
-                                <div>
-                                    { Object.keys(segments).length > 0 && (
-                                        <Link className="Button Button--purple mr2" to={'/reference/segments'}>
-                                            {t`See all segments`}
-                                        </Link>
-                                    )}
-                                    <Link
-                                        className={cx(
-                                            { 'text-purple text-bold no-decoration text-underline-hover' : Object.keys(segments).length > 0 },
-                                            { 'Button Button--purple' : Object.keys(segments).length === 0 }
-                                        )}
-                                        to={'/reference/databases'}
-                                    >
-                                        {t`See all tables`}
-                                    </Link>
-                                </div>
-                            </div>
+                <div className="mt4 pt4">
+                  <SectionHeader
+                    trim={
+                      !has(guide.important_segments) &&
+                      !has(guide.important_tables)
+                    }
+                  >
+                    {Object.keys(segments).length > 0
+                      ? t`Segments and tables`
+                      : t`Tables`}
+                  </SectionHeader>
+                  {has(guide.important_segments) ||
+                  has(guide.important_tables) ? (
+                    <div className="my2">
+                      {guide.important_segments.map(segmentId => (
+                        <GuideDetail
+                          key={segmentId}
+                          type="segment"
+                          entity={segments[segmentId]}
+                          tables={tables}
+                        />
+                      ))}
+                      {guide.important_tables.map(tableId => (
+                        <GuideDetail
+                          key={tableId}
+                          type="table"
+                          entity={tables[tableId]}
+                          tables={tables}
+                        />
+                      ))}
+                    </div>
+                  ) : (
+                    <GuideText>
+                      {Object.keys(segments).length > 0 ? (
+                        <span>
+                          {jt`Segments and tables are the building blocks of your company's data. Tables are collections of the raw information while segments are specific slices with specific meanings, like ${(
+                            <b>"Recent orders."</b>
+                          )}`}
+                        </span>
+                      ) : (
+                        t`Tables are the building blocks of your company's data.`
+                      )}
+                    </GuideText>
+                  )}
+                  <div>
+                    {Object.keys(segments).length > 0 && (
+                      <Link
+                        className="Button Button--purple mr2"
+                        to={"/reference/segments"}
+                      >
+                        {t`See all segments`}
+                      </Link>
+                    )}
+                    <Link
+                      className={cx(
+                        {
+                          "text-purple text-bold no-decoration text-underline-hover":
+                            Object.keys(segments).length > 0,
+                        },
+                        {
+                          "Button Button--purple":
+                            Object.keys(segments).length === 0,
+                        },
+                      )}
+                      to={"/reference/databases"}
+                    >
+                      {t`See all tables`}
+                    </Link>
+                  </div>
+                </div>
 
-                            <div className="mt4 pt4">
-                                <SectionHeader trim={!guide.things_to_know}>
-                                    { guide.things_to_know ? t`Other things to know about our data` : t`Find out more` }
-                                </SectionHeader>
-                                <GuideText>
-                                    { guide.things_to_know ? guide.things_to_know : t`A good way to get to know your data is by spending a bit of time exploring the different tables and other info available to you. It may take a while, but you'll start to recognize names and meanings over time.`
-                                    }
-                                </GuideText>
-                                <Link className="Button link text-bold" to={'/reference/databases'}>
-                                    {t`Explore our data`}
-                                </Link>
-                            </div>
+                <div className="mt4 pt4">
+                  <SectionHeader trim={!guide.things_to_know}>
+                    {guide.things_to_know
+                      ? t`Other things to know about our data`
+                      : t`Find out more`}
+                  </SectionHeader>
+                  <GuideText>
+                    {guide.things_to_know
+                      ? guide.things_to_know
+                      : t`A good way to get to know your data is by spending a bit of time exploring the different tables and other info available to you. It may take a while, but you'll start to recognize names and meanings over time.`}
+                  </GuideText>
+                  <Link
+                    className="Button link text-bold"
+                    to={"/reference/databases"}
+                  >
+                    {t`Explore our data`}
+                  </Link>
+                </div>
 
-                            <div className="mt4">
-                                { guide.contact && (guide.contact.name || guide.contact.email) && [
-                                    <SectionHeader key={'contactTitle'}>
-                                        {t`Have questions?`}
-                                    </SectionHeader>,
-                                    <div className="mb4 pb4" key={'contactDetails'}>
-                                            { guide.contact.name &&
-                                                <span className="text-dark mr3">
-                                                    {t`Contact ${guide.contact.name}`}
-                                                </span>
-                                            }
-                                            { guide.contact.email &&
-                                                <a className="text-brand text-bold no-decoration" href={`mailto:${guide.contact.email}`}>
-                                                    {guide.contact.email}
-                                                </a>
-                                            }
-                                    </div>
-                                ]}
-                            </div>
-                        </div>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
+                <div className="mt4">
+                  {guide.contact &&
+                    (guide.contact.name || guide.contact.email) && [
+                      <SectionHeader key={"contactTitle"}>
+                        {t`Have questions?`}
+                      </SectionHeader>,
+                      <div className="mb4 pb4" key={"contactDetails"}>
+                        {guide.contact.name && (
+                          <span className="text-dark mr3">
+                            {t`Contact ${guide.contact.name}`}
+                          </span>
+                        )}
+                        {guide.contact.email && (
+                          <a
+                            className="text-brand text-bold no-decoration"
+                            href={`mailto:${guide.contact.email}`}
+                          >
+                            {guide.contact.email}
+                          </a>
+                        )}
+                      </div>,
+                    ]}
+                </div>
+              </div>
             </div>
-        );
-    }
+          )}
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
 
-const GuideText = ({ children }) => // eslint-disable-line react/prop-types
-    <p className="text-paragraph text-measure">{children}</p>
+const GuideText = (
+  { children }, // eslint-disable-line react/prop-types
+) => <p className="text-paragraph text-measure">{children}</p>;
 
-const AdminInstructions = ({ children }) => // eslint-disable-line react/prop-types
-    <div className="bordered border-brand rounded p3 text-brand text-measure text-centered bg-light-blue">
-        {children}
-    </div>
+const AdminInstructions = (
+  { children }, // eslint-disable-line react/prop-types
+) => (
+  <div className="bordered border-brand rounded p3 text-brand text-measure text-centered bg-light-blue">
+    {children}
+  </div>
+);
 
-const SectionHeader = ({ trim, children }) => // eslint-disable-line react/prop-types
-    <h2 className={cx('text-dark text-measure', {  "mb0" : trim }, { "mb4" : !trim })}>{children}</h2>
+const SectionHeader = (
+  { trim, children }, // eslint-disable-line react/prop-types
+) => (
+  <h2 className={cx("text-dark text-measure", { mb0: trim }, { mb4: !trim })}>
+    {children}
+  </h2>
+);
diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx
index bfad648263e9b713a96c0c6a651e40cbff9f33c4..9942adf31817ed04f3caf1aeaa48f03d85e9ccb7 100644
--- a/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx
+++ b/frontend/src/metabase/reference/guide/GettingStartedGuideContainer.jsx
@@ -1,69 +1,63 @@
 /* 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 { connect } from "react-redux";
 
-import GettingStartedGuide from "metabase/reference/guide/GettingStartedGuide.jsx"
-import GettingStartedGuideEditForm from "metabase/reference/guide/GettingStartedGuideEditForm.jsx"
+import GettingStartedGuide from "metabase/reference/guide/GettingStartedGuide.jsx";
+import GettingStartedGuideEditForm from "metabase/reference/guide/GettingStartedGuideEditForm.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
-import {
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+import { getDatabaseId, getIsEditing } from "../selectors";
 
-
-import {
-    fetchDashboards
-} from 'metabase/dashboards/dashboards';
+import { fetchDashboards } from "metabase/dashboards/dashboards";
 
 const mapStateToProps = (state, props) => ({
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    fetchDashboards,
-    ...metadataActions,
-    ...actions
+  fetchDashboards,
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class GettingStartedGuideContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData() {
-        await actions.wrappedFetchGuide(this.props);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
+
+  async fetchContainerData() {
+    await actions.wrappedFetchGuide(this.props);
+  }
+
+  componentWillMount() {
+    this.fetchContainerData();
+  }
+
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
-    }
-
-    render() {
-        return (
-            <div>
-                
-            { this.props.isEditing ? 
-                <GettingStartedGuideEditForm {...this.props} /> :
-                <GettingStartedGuide {...this.props} />
-            }            
-            </div>
-        );
-    }
+    actions.clearState(newProps);
+  }
+
+  render() {
+    return (
+      <div>
+        {this.props.isEditing ? (
+          <GettingStartedGuideEditForm {...this.props} />
+        ) : (
+          <GettingStartedGuide {...this.props} />
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx b/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx
index 81161dee31e84eeed4c0b7da24b89104084bcd42..e6f56301f8940bbfc33a83ce4fea2016ed04454f 100644
--- a/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx
+++ b/frontend/src/metabase/reference/guide/GettingStartedGuideEditForm.jsx
@@ -1,431 +1,497 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { connect } from 'react-redux';
+import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import cx from "classnames";
 
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
-import CreateDashboardModal from 'metabase/components/CreateDashboardModal.jsx';
-import Modal from 'metabase/components/Modal.jsx';
+import CreateDashboardModal from "metabase/components/CreateDashboardModal.jsx";
+import Modal from "metabase/components/Modal.jsx";
 
 import EditHeader from "metabase/reference/components/EditHeader.jsx";
 import GuideEditSection from "metabase/reference/components/GuideEditSection.jsx";
 import GuideDetailEditor from "metabase/reference/components/GuideDetailEditor.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 import { clearRequestState } from "metabase/redux/requests";
-import { createDashboard, updateDashboard } from 'metabase/dashboards/dashboards';
-
 import {
-    updateSetting
-} from 'metabase/admin/settings/settings';
+  createDashboard,
+  updateDashboard,
+} from "metabase/dashboards/dashboards";
+
+import { updateSetting } from "metabase/admin/settings/settings";
 
 import S from "../components/GuideDetailEditor.css";
 
 import {
-    getGuide,
-    getDashboards,
-    getLoading,
-    getError,
-    getIsEditing,
-    getIsDashboardModalOpen,
-    getDatabases,
-    getTables,
-    getFields,
-    getMetrics,
-    getSegments,
-} from '../selectors';
-
+  getGuide,
+  getDashboards,
+  getLoading,
+  getError,
+  getIsEditing,
+  getIsDashboardModalOpen,
+  getDatabases,
+  getTables,
+  getFields,
+  getMetrics,
+  getSegments,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => {
-    const guide = getGuide(state, props);
-    const dashboards = getDashboards(state, props);
-    const metrics = getMetrics(state, props);
-    const segments = getSegments(state, props);
-    const tables = getTables(state, props);
-    const fields = getFields(state, props);
-    const databases = getDatabases(state, props);
+  const guide = getGuide(state, props);
+  const dashboards = getDashboards(state, props);
+  const metrics = getMetrics(state, props);
+  const segments = getSegments(state, props);
+  const tables = getTables(state, props);
+  const fields = getFields(state, props);
+  const databases = getDatabases(state, props);
 
-    // redux-form populates fields with stale values after update
-    // if we dont specify nulls here
-    // could use a lot of refactoring
-    const initialValues = guide && {
-        things_to_know: guide.things_to_know || null,
-        contact: guide.contact || {name: null, email: 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 ?
-            guide.important_metrics
-                .map(metricId => metrics[metricId] && {
-                    ...metrics[metricId],
-                    important_fields: guide.metric_important_fields[metricId] && guide.metric_important_fields[metricId].map(fieldId => fields[fieldId])
-                }) :
-            [],
-        important_segments_and_tables:
-            (guide.important_segments && guide.important_segments.length > 0) ||
-            (guide.important_tables && guide.important_tables.length > 0) ?
-                guide.important_segments
-                    .map(segmentId => segments[segmentId] && { ...segments[segmentId], type: 'segment' })
-                    .concat(guide.important_tables
-                        .map(tableId => tables[tableId] && { ...tables[tableId], type: 'table' })
-                    ) :
-                []
-    };
+  // redux-form populates fields with stale values after update
+  // if we dont specify nulls here
+  // could use a lot of refactoring
+  const initialValues = guide && {
+    things_to_know: guide.things_to_know || null,
+    contact: guide.contact || { name: null, email: 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
+        ? guide.important_metrics.map(
+            metricId =>
+              metrics[metricId] && {
+                ...metrics[metricId],
+                important_fields:
+                  guide.metric_important_fields[metricId] &&
+                  guide.metric_important_fields[metricId].map(
+                    fieldId => fields[fieldId],
+                  ),
+              },
+          )
+        : [],
+    important_segments_and_tables:
+      (guide.important_segments && guide.important_segments.length > 0) ||
+      (guide.important_tables && guide.important_tables.length > 0)
+        ? guide.important_segments
+            .map(
+              segmentId =>
+                segments[segmentId] && {
+                  ...segments[segmentId],
+                  type: "segment",
+                },
+            )
+            .concat(
+              guide.important_tables.map(
+                tableId =>
+                  tables[tableId] && { ...tables[tableId], type: "table" },
+              ),
+            )
+        : [],
+  };
 
-    return {
-        guide,
-        dashboards,
-        metrics,
-        segments,
-        tables,
-        databases,
-        // FIXME: avoids naming conflict, tried using the propNamespace option
-        // version but couldn't quite get it to work together with passing in
-        // dynamic initialValues
-        metadataFields: fields,
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        isEditing: getIsEditing(state, props),
-        isDashboardModalOpen: getIsDashboardModalOpen(state, props),
-        // redux form doesn't pass this through to component
-        // need to use to reset form field arrays
-        initialValues: initialValues,
-        initialFormValues: initialValues
-    };
+  return {
+    guide,
+    dashboards,
+    metrics,
+    segments,
+    tables,
+    databases,
+    // FIXME: avoids naming conflict, tried using the propNamespace option
+    // version but couldn't quite get it to work together with passing in
+    // dynamic initialValues
+    metadataFields: fields,
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    isEditing: getIsEditing(state, props),
+    isDashboardModalOpen: getIsDashboardModalOpen(state, props),
+    // redux form doesn't pass this through to component
+    // need to use to reset form field arrays
+    initialValues: initialValues,
+    initialFormValues: initialValues,
+  };
 };
 
 const mapDispatchToProps = {
-    updateDashboard,
-    createDashboard,
-    updateSetting,
-    clearRequestState,
-    ...metadataActions,
-    ...actions
+  updateDashboard,
+  createDashboard,
+  updateSetting,
+  clearRequestState,
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'guide',
-    fields: [
-        'things_to_know',
-        'contact.name',
-        'contact.email',
-        'most_important_dashboard.id',
-        'most_important_dashboard.caveats',
-        'most_important_dashboard.points_of_interest',
-        'important_metrics[].id',
-        'important_metrics[].caveats',
-        'important_metrics[].points_of_interest',
-        'important_metrics[].important_fields',
-        'important_segments_and_tables[].id',
-        'important_segments_and_tables[].type',
-        'important_segments_and_tables[].caveats',
-        'important_segments_and_tables[].points_of_interest'
-    ]
+  form: "guide",
+  fields: [
+    "things_to_know",
+    "contact.name",
+    "contact.email",
+    "most_important_dashboard.id",
+    "most_important_dashboard.caveats",
+    "most_important_dashboard.points_of_interest",
+    "important_metrics[].id",
+    "important_metrics[].caveats",
+    "important_metrics[].points_of_interest",
+    "important_metrics[].important_fields",
+    "important_segments_and_tables[].id",
+    "important_segments_and_tables[].type",
+    "important_segments_and_tables[].caveats",
+    "important_segments_and_tables[].points_of_interest",
+  ],
 })
 export default class GettingStartedGuideEditForm extends Component {
-    static propTypes = {
-        fields: PropTypes.object,
-        style: PropTypes.object,
-        guide: PropTypes.object,
-        dashboards: PropTypes.object,
-        metrics: PropTypes.object,
-        segments: PropTypes.object,
-        tables: PropTypes.object,
-        databases: PropTypes.object,
-        metadataFields: PropTypes.object,
-        loadingError: PropTypes.any,
-        loading: PropTypes.bool,
-        isEditing: PropTypes.bool,
-        endEditing: PropTypes.func,
-        handleSubmit: PropTypes.func,
-        submitting: PropTypes.bool,
-        initialFormValues: PropTypes.object,
-        initializeForm: PropTypes.func,
-        createDashboard: PropTypes.func,
-        isDashboardModalOpen: PropTypes.bool,
-        showDashboardModal: PropTypes.func,
-        hideDashboardModal: PropTypes.func,
-    };
-
-    render() {
-        const {
-            fields: {
-                things_to_know,
-                contact,
-                most_important_dashboard,
-                important_metrics,
-                important_segments_and_tables
-            },
-            style,
-            guide,
-            dashboards,
-            metrics,
-            segments,
-            tables,
-            databases,
-            metadataFields,
-            loadingError,
-            loading,
-            isEditing,
-            endEditing,
-            handleSubmit,
-            submitting,
-            initialFormValues,
-            initializeForm,
-            createDashboard,
-            isDashboardModalOpen,
-            showDashboardModal,
-            hideDashboardModal,
-        } = this.props;
+  static propTypes = {
+    fields: PropTypes.object,
+    style: PropTypes.object,
+    guide: PropTypes.object,
+    dashboards: PropTypes.object,
+    metrics: PropTypes.object,
+    segments: PropTypes.object,
+    tables: PropTypes.object,
+    databases: PropTypes.object,
+    metadataFields: PropTypes.object,
+    loadingError: PropTypes.any,
+    loading: PropTypes.bool,
+    isEditing: PropTypes.bool,
+    endEditing: PropTypes.func,
+    handleSubmit: PropTypes.func,
+    submitting: PropTypes.bool,
+    initialFormValues: PropTypes.object,
+    initializeForm: PropTypes.func,
+    createDashboard: PropTypes.func,
+    isDashboardModalOpen: PropTypes.bool,
+    showDashboardModal: PropTypes.func,
+    hideDashboardModal: PropTypes.func,
+  };
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.tryUpdateGuide(fields, this.props)
-        );
+  render() {
+    const {
+      fields: {
+        things_to_know,
+        contact,
+        most_important_dashboard,
+        important_metrics,
+        important_segments_and_tables,
+      },
+      style,
+      guide,
+      dashboards,
+      metrics,
+      segments,
+      tables,
+      databases,
+      metadataFields,
+      loadingError,
+      loading,
+      isEditing,
+      endEditing,
+      handleSubmit,
+      submitting,
+      initialFormValues,
+      initializeForm,
+      createDashboard,
+      isDashboardModalOpen,
+      showDashboardModal,
+      hideDashboardModal,
+    } = this.props;
 
-        const getSelectedIds = fields => fields
-            .map(field => field.id.value)
-            .filter(id => id !== null);
+    const onSubmit = handleSubmit(
+      async fields => await actions.tryUpdateGuide(fields, this.props),
+    );
 
-        const getSelectedIdTypePairs = fields => fields
-            .map(field => [field.id.value, field.type.value])
-            .filter(idTypePair => idTypePair[0] !== null);
+    const getSelectedIds = fields =>
+      fields.map(field => field.id.value).filter(id => id !== null);
 
+    const getSelectedIdTypePairs = fields =>
+      fields
+        .map(field => [field.id.value, field.type.value])
+        .filter(idTypePair => idTypePair[0] !== null);
 
-        return (
-            <form className="full relative py4" style={style} onSubmit={onSubmit}>
-                { isDashboardModalOpen &&
-                    <Modal>
-                        <CreateDashboardModal
-                            createDashboardFn={async (newDashboard) => {
-                                try {
-                                    await createDashboard(newDashboard, { redirect: true });
-                                }
-                                catch(error) {
-                                    console.error(error);
-                                }
-                            }}
-                            onClose={hideDashboardModal}
-                        />
-                    </Modal>
-                }
-                { isEditing &&
-                    <EditHeader
-                        endEditing={endEditing}
-                        // resetForm doesn't reset field arrays
-                        reinitializeForm={() => initializeForm(initialFormValues)}
-                        submitting={submitting}
-                    />
+    return (
+      <form className="full relative py4" style={style} onSubmit={onSubmit}>
+        {isDashboardModalOpen && (
+          <Modal>
+            <CreateDashboardModal
+              createDashboardFn={async newDashboard => {
+                try {
+                  await createDashboard(newDashboard, { redirect: true });
+                } catch (error) {
+                  console.error(error);
                 }
-                <LoadingAndErrorWrapper className="full" style={style} loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <div className="mt4 py2">
-                            <h1 className="my3 text-dark">
-                                {t`Help new Metabase users find their way around.`}
-                            </h1>
-                            <p className="text-paragraph text-measure">
-                                {t`The Getting Started guide highlights the dashboard, metrics, segments, and tables that matter most, and informs your users of important things they should know before digging into the data.`}
-                            </p>
-                        </div>
+              }}
+              onClose={hideDashboardModal}
+            />
+          </Modal>
+        )}
+        {isEditing && (
+          <EditHeader
+            endEditing={endEditing}
+            // resetForm doesn't reset field arrays
+            reinitializeForm={() => initializeForm(initialFormValues)}
+            submitting={submitting}
+          />
+        )}
+        <LoadingAndErrorWrapper
+          className="full"
+          style={style}
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <div className="mt4 py2">
+                <h1 className="my3 text-dark">
+                  {t`Help new Metabase users find their way around.`}
+                </h1>
+                <p className="text-paragraph text-measure">
+                  {t`The Getting Started guide highlights the dashboard, metrics, segments, and tables that matter most, and informs your users of important things they should know before digging into the data.`}
+                </p>
+              </div>
 
-                        <GuideEditSection
-                            isCollapsed={most_important_dashboard.id.value === undefined}
-                            isDisabled={!dashboards || Object.keys(dashboards).length === 0}
-                            collapsedTitle={t`Is there an important dashboard for your team?`}
-                            collapsedIcon="dashboard"
-                            linkMessage={t`Create a dashboard now`}
-                            action={showDashboardModal}
-                            expand={() => most_important_dashboard.id.onChange(null)}
-                        >
-                            <div>
-                                <SectionHeader>
-                                    {t`What is your most important dashboard?`}
-                                </SectionHeader>
-                                <GuideDetailEditor
-                                    type="dashboard"
-                                    entities={dashboards}
-                                    selectedIds={[most_important_dashboard.id.value]}
-                                    formField={most_important_dashboard}
-                                    removeField={() => {
-                                        most_important_dashboard.id.onChange(null);
-                                        most_important_dashboard.points_of_interest.onChange('');
-                                        most_important_dashboard.caveats.onChange('');
-                                    }}
-                                />
-                            </div>
-                        </GuideEditSection>
+              <GuideEditSection
+                isCollapsed={most_important_dashboard.id.value === undefined}
+                isDisabled={!dashboards || Object.keys(dashboards).length === 0}
+                collapsedTitle={t`Is there an important dashboard for your team?`}
+                collapsedIcon="dashboard"
+                linkMessage={t`Create a dashboard now`}
+                action={showDashboardModal}
+                expand={() => most_important_dashboard.id.onChange(null)}
+              >
+                <div>
+                  <SectionHeader>
+                    {t`What is your most important dashboard?`}
+                  </SectionHeader>
+                  <GuideDetailEditor
+                    type="dashboard"
+                    entities={dashboards}
+                    selectedIds={[most_important_dashboard.id.value]}
+                    formField={most_important_dashboard}
+                    removeField={() => {
+                      most_important_dashboard.id.onChange(null);
+                      most_important_dashboard.points_of_interest.onChange("");
+                      most_important_dashboard.caveats.onChange("");
+                    }}
+                  />
+                </div>
+              </GuideEditSection>
 
-                        <GuideEditSection
-                            isCollapsed={important_metrics.length === 0}
-                            isDisabled={!metrics || Object.keys(metrics).length === 0}
-                            collapsedTitle={t`Do you have any commonly referenced metrics?`}
-                            collapsedIcon="ruler"
-                            linkMessage={t`Learn how to define a metric`}
-                            link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-metric"
-                            expand={() => important_metrics.addField({id: null, caveats: null, points_of_interest: null, important_fields: null})}
-                        >
-                            <div className="my2">
-                                <SectionHeader>
-                                    {t`What are your 3-5 most commonly referenced metrics?`}
-                                </SectionHeader>
-                                <div>
-                                    { important_metrics.map((metricField, index, metricFields) =>
-                                        <GuideDetailEditor
-                                            key={index}
-                                            type="metric"
-                                            metadata={{
-                                                tables,
-                                                metrics,
-                                                fields: metadataFields,
-                                                metricImportantFields: guide.metric_important_fields
-                                            }}
-                                            entities={metrics}
-                                            formField={metricField}
-                                            selectedIds={getSelectedIds(metricFields)}
-                                            removeField={() => {
-                                                if (metricFields.length > 1) {
-                                                    return metricFields.removeField(index);
-                                                }
-                                                metricField.id.onChange(null);
-                                                metricField.points_of_interest.onChange('');
-                                                metricField.caveats.onChange('');
-                                                metricField.important_fields.onChange(null);
-                                            }}
-                                        />
-                                    )}
-                                </div>
-                                { important_metrics.length < 5 &&
-                                    important_metrics.length < Object.keys(metrics).length &&
-                                        <button
-                                            className="Button Button--primary Button--large"
-                                            type="button"
-                                            onClick={() => important_metrics.addField({id: null, caveats: null, points_of_interest: null})}
-                                        >
-                                            {t`Add another metric`}
-                                        </button>
-                                }
-                            </div>
-                        </GuideEditSection>
+              <GuideEditSection
+                isCollapsed={important_metrics.length === 0}
+                isDisabled={!metrics || Object.keys(metrics).length === 0}
+                collapsedTitle={t`Do you have any commonly referenced metrics?`}
+                collapsedIcon="ruler"
+                linkMessage={t`Learn how to define a metric`}
+                link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-metric"
+                expand={() =>
+                  important_metrics.addField({
+                    id: null,
+                    caveats: null,
+                    points_of_interest: null,
+                    important_fields: null,
+                  })
+                }
+              >
+                <div className="my2">
+                  <SectionHeader>
+                    {t`What are your 3-5 most commonly referenced metrics?`}
+                  </SectionHeader>
+                  <div>
+                    {important_metrics.map(
+                      (metricField, index, metricFields) => (
+                        <GuideDetailEditor
+                          key={index}
+                          type="metric"
+                          metadata={{
+                            tables,
+                            metrics,
+                            fields: metadataFields,
+                            metricImportantFields:
+                              guide.metric_important_fields,
+                          }}
+                          entities={metrics}
+                          formField={metricField}
+                          selectedIds={getSelectedIds(metricFields)}
+                          removeField={() => {
+                            if (metricFields.length > 1) {
+                              return metricFields.removeField(index);
+                            }
+                            metricField.id.onChange(null);
+                            metricField.points_of_interest.onChange("");
+                            metricField.caveats.onChange("");
+                            metricField.important_fields.onChange(null);
+                          }}
+                        />
+                      ),
+                    )}
+                  </div>
+                  {important_metrics.length < 5 &&
+                    important_metrics.length < Object.keys(metrics).length && (
+                      <button
+                        className="Button Button--primary Button--large"
+                        type="button"
+                        onClick={() =>
+                          important_metrics.addField({
+                            id: null,
+                            caveats: null,
+                            points_of_interest: null,
+                          })
+                        }
+                      >
+                        {t`Add another metric`}
+                      </button>
+                    )}
+                </div>
+              </GuideEditSection>
 
-                        <GuideEditSection
-                            isCollapsed={important_segments_and_tables.length === 0}
-                            isDisabled={(!segments || Object.keys(segments).length === 0) && (!tables || Object.keys(tables).length === 0)}
-                            showLink={!segments || Object.keys(segments).length === 0}
-                            collapsedTitle={t`Do you have any commonly referenced segments or tables?`}
-                            collapsedIcon="table2"
-                            linkMessage={t`Learn how to create a segment`}
-                            link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-segment"
-                            expand={() => important_segments_and_tables.addField({id: null, type: null, caveats: null, points_of_interest: null})}
-                        >
-                            <div>
-                                <h2 className="text-measure text-dark">
-                                    {t`What are 3-5 commonly referenced segments or tables that would be useful for this audience?`}
-                                </h2>
-                                <div className="mb2">
-                                    { important_segments_and_tables.map((segmentOrTableField, index, segmentOrTableFields) =>
-                                        <GuideDetailEditor
-                                            key={index}
-                                            type="segment"
-                                            metadata={{
-                                                databases,
-                                                tables,
-                                                segments
-                                            }}
-                                            formField={segmentOrTableField}
-                                            selectedIdTypePairs={getSelectedIdTypePairs(segmentOrTableFields)}
-                                            removeField={() => {
-                                                if (segmentOrTableFields.length > 1) {
-                                                    return segmentOrTableFields.removeField(index);
-                                                }
-                                                segmentOrTableField.id.onChange(null);
-                                                segmentOrTableField.type.onChange(null);
-                                                segmentOrTableField.points_of_interest.onChange('');
-                                                segmentOrTableField.caveats.onChange('');
-                                            }}
-                                        />
-                                    )}
-                                </div>
-                                { important_segments_and_tables.length < 5 &&
-                                    important_segments_and_tables.length < Object.keys(tables).concat(Object.keys.segments).length &&
-                                        <button
-                                            className="Button Button--primary Button--large"
-                                            type="button"
-                                            onClick={() => important_segments_and_tables.addField({id: null, type: null, caveats: null, points_of_interest: null})}
-                                        >
-                                            {t`Add another segment or table`}
-                                        </button>
-                                }
-                            </div>
-                        </GuideEditSection>
+              <GuideEditSection
+                isCollapsed={important_segments_and_tables.length === 0}
+                isDisabled={
+                  (!segments || Object.keys(segments).length === 0) &&
+                  (!tables || Object.keys(tables).length === 0)
+                }
+                showLink={!segments || Object.keys(segments).length === 0}
+                collapsedTitle={t`Do you have any commonly referenced segments or tables?`}
+                collapsedIcon="table2"
+                linkMessage={t`Learn how to create a segment`}
+                link="http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html#creating-a-segment"
+                expand={() =>
+                  important_segments_and_tables.addField({
+                    id: null,
+                    type: null,
+                    caveats: null,
+                    points_of_interest: null,
+                  })
+                }
+              >
+                <div>
+                  <h2 className="text-measure text-dark">
+                    {t`What are 3-5 commonly referenced segments or tables that would be useful for this audience?`}
+                  </h2>
+                  <div className="mb2">
+                    {important_segments_and_tables.map(
+                      (segmentOrTableField, index, segmentOrTableFields) => (
+                        <GuideDetailEditor
+                          key={index}
+                          type="segment"
+                          metadata={{
+                            databases,
+                            tables,
+                            segments,
+                          }}
+                          formField={segmentOrTableField}
+                          selectedIdTypePairs={getSelectedIdTypePairs(
+                            segmentOrTableFields,
+                          )}
+                          removeField={() => {
+                            if (segmentOrTableFields.length > 1) {
+                              return segmentOrTableFields.removeField(index);
+                            }
+                            segmentOrTableField.id.onChange(null);
+                            segmentOrTableField.type.onChange(null);
+                            segmentOrTableField.points_of_interest.onChange("");
+                            segmentOrTableField.caveats.onChange("");
+                          }}
+                        />
+                      ),
+                    )}
+                  </div>
+                  {important_segments_and_tables.length < 5 &&
+                    important_segments_and_tables.length <
+                      Object.keys(tables).concat(Object.keys.segments)
+                        .length && (
+                      <button
+                        className="Button Button--primary Button--large"
+                        type="button"
+                        onClick={() =>
+                          important_segments_and_tables.addField({
+                            id: null,
+                            type: null,
+                            caveats: null,
+                            points_of_interest: null,
+                          })
+                        }
+                      >
+                        {t`Add another segment or table`}
+                      </button>
+                    )}
+                </div>
+              </GuideEditSection>
 
-                        <GuideEditSection
-                            isCollapsed={things_to_know.value === null}
-                            isDisabled={false}
-                            collapsedTitle={t`Is there anything your users should understand or know before they start accessing the data?`}
-                            collapsedIcon="reference"
-                            expand={() => things_to_know.onChange('')}
-                        >
-                            <div className="text-measure">
-                                <SectionHeader>
-                                    {t`What should a user of this data know before they start accessing it?`}
-                                </SectionHeader>
-                                <textarea
-                                    className={S.guideDetailEditorTextarea}
-                                    placeholder={t`E.g., expectations around data privacy and use, common pitfalls or misunderstandings, information about data warehouse performance, legal notices, etc.`}
-                                    {...things_to_know}
-                                />
-                            </div>
-                        </GuideEditSection>
+              <GuideEditSection
+                isCollapsed={things_to_know.value === null}
+                isDisabled={false}
+                collapsedTitle={t`Is there anything your users should understand or know before they start accessing the data?`}
+                collapsedIcon="reference"
+                expand={() => things_to_know.onChange("")}
+              >
+                <div className="text-measure">
+                  <SectionHeader>
+                    {t`What should a user of this data know before they start accessing it?`}
+                  </SectionHeader>
+                  <textarea
+                    className={S.guideDetailEditorTextarea}
+                    placeholder={t`E.g., expectations around data privacy and use, common pitfalls or misunderstandings, information about data warehouse performance, legal notices, etc.`}
+                    {...things_to_know}
+                  />
+                </div>
+              </GuideEditSection>
 
-                        <GuideEditSection
-                            isCollapsed={contact.name.value === null && contact.email.value === null}
-                            isDisabled={false}
-                            collapsedTitle={t`Is there someone your users could contact for help if they're confused about this guide?`}
-                            collapsedIcon="mail"
-                            expand={() => {
-                                contact.name.onChange('');
-                                contact.email.onChange('');
-                            }}
-                        >
-                            <div>
-                                <SectionHeader>
-                                    {t`Who should users contact for help if they're confused about this data?`}
-                                </SectionHeader>
-                                <div className="flex">
-                                    <div className="flex-full">
-                                        <h3 className="mb1">{t`Name`}</h3>
-                                        <input
-                                            className="input text-paragraph"
-                                            placeholder="Julie McHelpfulson"
-                                            type="text"
-                                            {...contact.name}
-                                        />
-                                    </div>
-                                    <div className="flex-full">
-                                        <h3 className="mb1">{t`Email address`}</h3>
-                                        <input
-                                            className="input text-paragraph"
-                                            placeholder="julie.mchelpfulson@acme.com"
-                                            type="text"
-                                            {...contact.email}
-                                        />
-                                    </div>
-                                </div>
-                            </div>
-                        </GuideEditSection>
-                    </div>
+              <GuideEditSection
+                isCollapsed={
+                  contact.name.value === null && contact.email.value === null
                 }
-                </LoadingAndErrorWrapper>
-            </form>
-        );
-    }
+                isDisabled={false}
+                collapsedTitle={t`Is there someone your users could contact for help if they're confused about this guide?`}
+                collapsedIcon="mail"
+                expand={() => {
+                  contact.name.onChange("");
+                  contact.email.onChange("");
+                }}
+              >
+                <div>
+                  <SectionHeader>
+                    {t`Who should users contact for help if they're confused about this data?`}
+                  </SectionHeader>
+                  <div className="flex">
+                    <div className="flex-full">
+                      <h3 className="mb1">{t`Name`}</h3>
+                      <input
+                        className="input text-paragraph"
+                        placeholder="Julie McHelpfulson"
+                        type="text"
+                        {...contact.name}
+                      />
+                    </div>
+                    <div className="flex-full">
+                      <h3 className="mb1">{t`Email address`}</h3>
+                      <input
+                        className="input text-paragraph"
+                        placeholder="julie.mchelpfulson@acme.com"
+                        type="text"
+                        {...contact.email}
+                      />
+                    </div>
+                  </div>
+                </div>
+              </GuideEditSection>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
 
-const SectionHeader = ({ trim, children }) => // eslint-disable-line react/prop-types
-    <h2 className={cx('text-dark text-measure', {  "mb0" : trim }, { "mb4" : !trim })}>{children}</h2>
+const SectionHeader = (
+  { trim, children }, // eslint-disable-line react/prop-types
+) => (
+  <h2 className={cx("text-dark text-measure", { mb0: trim }, { mb4: !trim })}>
+    {children}
+  </h2>
+);
diff --git a/frontend/src/metabase/reference/metrics/MetricDetail.jsx b/frontend/src/metabase/reference/metrics/MetricDetail.jsx
index be4694c8f6cf29151c6a7a503561a95809e9d216..3449ceb2db3e6fab1dc643750050e01330073c4f 100644
--- a/frontend/src/metabase/reference/metrics/MetricDetail.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricDetail.jsx
@@ -4,7 +4,7 @@ import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
 import { push } from "react-router-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import List from "metabase/components/List.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 import EditHeader from "metabase/reference/components/EditHeader.jsx";
@@ -14,249 +14,294 @@ import FieldsToGroupBy from "metabase/reference/components/FieldsToGroupBy.jsx";
 import Formula from "metabase/reference/components/Formula.jsx";
 import MetricImportantFieldsDetail from "metabase/reference/components/MetricImportantFieldsDetail.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import {
-    getMetric,
-    getTable,
-    getFields,
-    getGuide,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getIsFormulaExpanded,
-    getForeignKeys
+  getMetric,
+  getTable,
+  getFields,
+  getGuide,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getIsFormulaExpanded,
+  getForeignKeys,
 } from "../selectors";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 const mapStateToProps = (state, props) => {
-    const entity = getMetric(state, props) || {};
-    const guide = getGuide(state, props);
-    const fields = getFields(state, props);
+  const entity = getMetric(state, props) || {};
+  const guide = getGuide(state, props);
+  const fields = getFields(state, props);
 
-    const initialValues = {
-        important_fields: guide && guide.metric_important_fields &&
-            guide.metric_important_fields[entity.id] &&
-            guide.metric_important_fields[entity.id]
-                .map(fieldId => fields[fieldId]) ||
-                []
-    };
+  const initialValues = {
+    important_fields:
+      (guide &&
+        guide.metric_important_fields &&
+        guide.metric_important_fields[entity.id] &&
+        guide.metric_important_fields[entity.id].map(
+          fieldId => fields[fieldId],
+        )) ||
+      [],
+  };
 
-    return {
-        entity,
-        table: getTable(state, props),
-        metadataFields: fields,
-        guide,
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        foreignKeys: getForeignKeys(state, props),
-        isEditing: getIsEditing(state, props),
-        isFormulaExpanded: getIsFormulaExpanded(state, props),
-        initialValues,
-    }
+  return {
+    entity,
+    table: getTable(state, props),
+    metadataFields: fields,
+    guide,
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    foreignKeys: getForeignKeys(state, props),
+    isEditing: getIsEditing(state, props),
+    isFormulaExpanded: getIsFormulaExpanded(state, props),
+    initialValues,
+  };
 };
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions,
-    onChangeLocation: push
+  ...metadataActions,
+  ...actions,
+  onChangeLocation: push,
 };
 
-const validate = (values, props) =>  !values.revision_message ? 
-    { revision_message: t`Please enter a revision message` } : {} 
+const validate = (values, props) =>
+  !values.revision_message
+    ? { revision_message: t`Please enter a revision message` }
+    : {};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'details',
-    fields: ['name', 'display_name', 'description', 'revision_message', 'points_of_interest', 'caveats', 'how_is_this_calculated', 'important_fields'],
-    validate
+  form: "details",
+  fields: [
+    "name",
+    "display_name",
+    "description",
+    "revision_message",
+    "points_of_interest",
+    "caveats",
+    "how_is_this_calculated",
+    "important_fields",
+  ],
+  validate,
 })
 export default class MetricDetail extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entity: PropTypes.object.isRequired,
-        table: PropTypes.object,
-        metadataFields: PropTypes.object,
-        guide: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        expandFormula: PropTypes.func.isRequired,
-        collapseFormula: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        resetForm: PropTypes.func.isRequired,
-        fields: PropTypes.object.isRequired,
-        isFormulaExpanded: PropTypes.bool,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-        onChangeLocation: PropTypes.func.isRequired
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entity: PropTypes.object.isRequired,
+    table: PropTypes.object,
+    metadataFields: PropTypes.object,
+    guide: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    expandFormula: PropTypes.func.isRequired,
+    collapseFormula: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    resetForm: PropTypes.func.isRequired,
+    fields: PropTypes.object.isRequired,
+    isFormulaExpanded: PropTypes.bool,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+    onChangeLocation: PropTypes.func.isRequired,
+  };
 
-    render() {
-        const {
-            fields: { name, display_name, description, revision_message, points_of_interest, caveats, how_is_this_calculated, important_fields },
-            style,
-            entity,
-            table,
-            metadataFields,
-            guide,
-            loadingError,
-            loading,
-            user,
-            isEditing,
-            startEditing,
-            endEditing,
-            expandFormula,
-            collapseFormula,
-            isFormulaExpanded,
-            handleSubmit,
-            resetForm,
-            submitting,
-            onChangeLocation
-        } = this.props;
+  render() {
+    const {
+      fields: {
+        name,
+        display_name,
+        description,
+        revision_message,
+        points_of_interest,
+        caveats,
+        how_is_this_calculated,
+        important_fields,
+      },
+      style,
+      entity,
+      table,
+      metadataFields,
+      guide,
+      loadingError,
+      loading,
+      user,
+      isEditing,
+      startEditing,
+      endEditing,
+      expandFormula,
+      collapseFormula,
+      isFormulaExpanded,
+      handleSubmit,
+      resetForm,
+      submitting,
+      onChangeLocation,
+    } = this.props;
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.rUpdateMetricDetail(this.props.entity, this.props.guide, fields, this.props)
-        );
+    const onSubmit = handleSubmit(
+      async fields =>
+        await actions.rUpdateMetricDetail(
+          this.props.entity,
+          this.props.guide,
+          fields,
+          this.props,
+        ),
+    );
 
-        return (
-            <form style={style} className="full"
-                onSubmit={onSubmit}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={true}
-                        onSubmit={onSubmit}
-                        endEditing={endEditing}
-                        reinitializeForm={resetForm}
-                        submitting={submitting}
-                        revisionMessageFormField={revision_message}
-                    />
-                }
-                <EditableReferenceHeader
-                    entity={entity}
+    return (
+      <form style={style} className="full" onSubmit={onSubmit}>
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={true}
+            onSubmit={onSubmit}
+            endEditing={endEditing}
+            reinitializeForm={resetForm}
+            submitting={submitting}
+            revisionMessageFormField={revision_message}
+          />
+        )}
+        <EditableReferenceHeader
+          entity={entity}
+          table={table}
+          type="metric"
+          headerIcon="ruler"
+          headerLink={getQuestionUrl({
+            dbId: table && table.db_id,
+            tableId: entity.table_id,
+            metricId: entity.id,
+          })}
+          name={t`Details`}
+          user={user}
+          isEditing={isEditing}
+          hasSingleSchema={false}
+          hasDisplayName={false}
+          startEditing={startEditing}
+          displayNameFormField={display_name}
+          nameFormField={name}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <List>
+                <li className="relative">
+                  <Detail
+                    id="description"
+                    name={t`Description`}
+                    description={entity.description}
+                    placeholder={t`No description yet`}
+                    isEditing={isEditing}
+                    field={description}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="points_of_interest"
+                    name={t`Why this Metric is interesting`}
+                    description={entity.points_of_interest}
+                    placeholder={t`Nothing interesting yet`}
+                    isEditing={isEditing}
+                    field={points_of_interest}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="caveats"
+                    name={t`Things to be aware of about this Metric`}
+                    description={entity.caveats}
+                    placeholder={t`Nothing to be aware of yet`}
+                    isEditing={isEditing}
+                    field={caveats}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="how_is_this_calculated"
+                    name={t`How this Metric is calculated`}
+                    description={entity.how_is_this_calculated}
+                    placeholder={t`Nothing on how it's calculated yet`}
+                    isEditing={isEditing}
+                    field={how_is_this_calculated}
+                  />
+                </li>
+                {table &&
+                  !isEditing && (
+                    <li className="relative">
+                      <Formula
+                        type="metric"
+                        entity={entity}
+                        isExpanded={isFormulaExpanded}
+                        expandFormula={expandFormula}
+                        collapseFormula={collapseFormula}
+                      />
+                    </li>
+                  )}
+                <li className="relative">
+                  <MetricImportantFieldsDetail
+                    fields={
+                      guide &&
+                      guide.metric_important_fields[entity.id] &&
+                      Object.values(guide.metric_important_fields[entity.id])
+                        .map(fieldId => metadataFields[fieldId])
+                        .reduce(
+                          (map, field) => ({ ...map, [field.id]: field }),
+                          {},
+                        )
+                    }
                     table={table}
-                    type="metric"
-                    headerIcon="ruler"
-                    headerLink={getQuestionUrl({ dbId: table && table.db_id, tableId: entity.table_id, metricId: entity.id})}
-                    name={t`Details`}
-                    user={user}
+                    allFields={metadataFields}
+                    metric={entity}
+                    onChangeLocation={onChangeLocation}
                     isEditing={isEditing}
-                    hasSingleSchema={false}
-                    hasDisplayName={false}
-                    startEditing={startEditing}
-                    displayNameFormField={display_name}
-                    nameFormField={name}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            <li className="relative">
-                                <Detail
-                                    id="description"
-                                    name={t`Description`}
-                                    description={entity.description}
-                                    placeholder={t`No description yet`}
-                                    isEditing={isEditing}
-                                    field={description}
-                                />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="points_of_interest"
-                                    name={t`Why this Metric is interesting`}
-                                    description={entity.points_of_interest}
-                                    placeholder={t`Nothing interesting yet`}
-                                    isEditing={isEditing}
-                                    field={points_of_interest}
-                                    />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="caveats"
-                                    name={t`Things to be aware of about this Metric`}
-                                    description={entity.caveats}
-                                    placeholder={t`Nothing to be aware of yet`}
-                                    isEditing={isEditing}
-                                    field={caveats}
-                                />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="how_is_this_calculated"
-                                    name={t`How this Metric is calculated`}
-                                    description={entity.how_is_this_calculated}
-                                    placeholder={t`Nothing on how it's calculated yet`}
-                                    isEditing={isEditing}
-                                    field={how_is_this_calculated}
-                                />
-                            </li>
-                            {   table && !isEditing &&
-                                <li className="relative">
-                                    <Formula
-                                        type="metric"
-                                        entity={entity}
-                                        isExpanded={isFormulaExpanded}
-                                        expandFormula={expandFormula}
-                                        collapseFormula={collapseFormula}
-                                    />
-                                </li>
-                            }
-                            <li className="relative">
-                                <MetricImportantFieldsDetail
-                                    fields={guide && guide.metric_important_fields[entity.id] &&
-                                        Object.values(guide.metric_important_fields[entity.id])
-                                            .map(fieldId => metadataFields[fieldId])
-                                            .reduce((map, field) => ({ ...map, [field.id]: field }), {})
-                                    }
-                                    table={table}
-                                    allFields={metadataFields}
-                                    metric={entity}
-                                    onChangeLocation={onChangeLocation}
-                                    isEditing={isEditing}
-                                    formField={important_fields}
-                                />
-                            </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <FieldsToGroupBy
-                                        fields={table.fields
-                                            .filter(fieldId => !guide || !guide.metric_important_fields[entity.id] ||
-                                                !guide.metric_important_fields[entity.id].includes(fieldId)
-                                            )
-                                            .map(fieldId => metadataFields[fieldId])
-                                            .reduce((map, field) => ({ ...map, [field.id]: field }), {})
-                                        }
-                                        databaseId={table && table.db_id}
-                                        metric={entity}
-                                        title={ guide && guide.metric_important_fields[entity.id] ?
-                                            t`Other fields you can group this metric by` :
-                                            t`Fields you can group this metric by`
-                                        }
-                                        onChangeLocation={onChangeLocation}
-                                    />
-                                </li>
-                            }
-                        </List>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                    formField={important_fields}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <FieldsToGroupBy
+                      fields={table.fields
+                        .filter(
+                          fieldId =>
+                            !guide ||
+                            !guide.metric_important_fields[entity.id] ||
+                            !guide.metric_important_fields[entity.id].includes(
+                              fieldId,
+                            ),
+                        )
+                        .map(fieldId => metadataFields[fieldId])
+                        .reduce(
+                          (map, field) => ({ ...map, [field.id]: field }),
+                          {},
+                        )}
+                      databaseId={table && table.db_id}
+                      metric={entity}
+                      title={
+                        guide && guide.metric_important_fields[entity.id]
+                          ? t`Other fields you can group this metric by`
+                          : t`Fields you can group this metric by`
+                      }
+                      onChangeLocation={onChangeLocation}
+                    />
+                  </li>
+                )}
+              </List>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx b/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx
index 3592e969be44b7d221e31e72559ad93b2ed06ef7..026c2c0b20b67bf91fe796a4ee0e3bebce966762 100644
--- a/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricDetailContainer.jsx
@@ -1,80 +1,75 @@
 /* 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 { connect } from "react-redux";
 
-import MetricSidebar from './MetricSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import MetricDetail from "metabase/reference/metrics/MetricDetail.jsx"
+import MetricSidebar from "./MetricSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import MetricDetail from "metabase/reference/metrics/MetricDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getMetric,
-    getMetricId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
+  getUser,
+  getMetric,
+  getMetricId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    metric: getMetric(state, props),
-    metricId: getMetricId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  metric: getMetric(state, props),
+  metricId: getMetricId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricDetailContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        metric: PropTypes.object.isRequired,
-        metricId: PropTypes.number.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchMetricDetail(this.props, this.props.metricId);
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    metric: PropTypes.object.isRequired,
+    metricId: PropTypes.number.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchMetricDetail(this.props, this.props.metricId);
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            isEditing,
-            user,
-            metric
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<MetricSidebar metric={metric} user={user}/>}
-            >
-                <MetricDetail {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { isEditing, user, metric } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<MetricSidebar metric={metric} user={user} />}
+      >
+        <MetricDetail {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricList.jsx b/frontend/src/metabase/reference/metrics/MetricList.jsx
index d7df6aeb2a59a7cdb0312e47caca1d7d0787310f..4adf058777e7599ceef146f0b5827a69400f8258 100644
--- a/frontend/src/metabase/reference/metrics/MetricList.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -15,85 +15,82 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-import {
-    getMetrics,
-    getError,
-    getLoading
-} from "../selectors";
+import { getMetrics, getError, getLoading } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 
-
 const emptyStateData = {
-    title: t`Metrics are the official numbers that your team cares about`,
-    adminMessage: t`Defining common metrics for your team makes it even easier to ask questions`,
-    message: t`Metrics will appear here once your admins have created some`,
-    image: "app/assets/img/metrics-list",
-    adminAction: t`Learn how to create metrics`,
-    adminLink: "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
-}
+  title: t`Metrics are the official numbers that your team cares about`,
+  adminMessage: t`Defining common metrics for your team makes it even easier to ask questions`,
+  message: t`Metrics will appear here once your admins have created some`,
+  image: "app/assets/img/metrics-list",
+  adminAction: t`Learn how to create metrics`,
+  adminLink:
+    "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html",
+};
 
 const mapStateToProps = (state, props) => ({
-    entities: getMetrics(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  entities: getMetrics(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
-
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricList extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
 
-    render() {
-        const {
-            entities,
-            style,
-            loadingError,
-            loading
-        } = this.props;
+  render() {
+    const { entities, style, loadingError, loading } = this.props;
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader
-                    name={t`Metrics`}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            {
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                          <li className="relative" key={entity.id}>
-                                                <ListItem
-                                                    id={entity.id}
-                                                    index={index}
-                                                    name={entity.display_name || entity.name}
-                                                    description={ entity.description }
-                                                    url={ `/reference/metrics/${entity.id}` }
-                                                    icon="ruler"
-                                                />
-                                            </li>
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <AdminAwareEmptyState {...emptyStateData}/>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader name={t`Metrics`} />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {Object.values(entities)
+                    .filter(isQueryable)
+                    .map(
+                      (entity, index) =>
+                        entity &&
+                        entity.id &&
+                        entity.name && (
+                          <li className="relative" key={entity.id}>
+                            <ListItem
+                              id={entity.id}
+                              index={index}
+                              name={entity.display_name || entity.name}
+                              description={entity.description}
+                              url={`/reference/metrics/${entity.id}`}
+                              icon="ruler"
+                            />
+                          </li>
+                        ),
+                    )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <AdminAwareEmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricListContainer.jsx b/frontend/src/metabase/reference/metrics/MetricListContainer.jsx
index 5a566c44d1402be76165bd625f088a6da3eecb8b..acb44ae98b9d60e8540d289c8b1a3b9a1b1b04e3 100644
--- a/frontend/src/metabase/reference/metrics/MetricListContainer.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricListContainer.jsx
@@ -1,70 +1,63 @@
 /* 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 { connect } from "react-redux";
 
-import BaseSidebar from 'metabase/reference/guide/BaseSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import MetricList from "metabase/reference/metrics/MetricList.jsx"
+import BaseSidebar from "metabase/reference/guide/BaseSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import MetricList from "metabase/reference/metrics/MetricList.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
-import {
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+import { getDatabaseId, getIsEditing } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricListContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-
-    async fetchContainerData(){
-        await actions.wrappedFetchMetrics(this.props);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
-    }
-
-
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
+
+  async fetchContainerData() {
+    await actions.wrappedFetchMetrics(this.props);
+  }
+
+  componentWillMount() {
+    this.fetchContainerData();
+  }
+
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            isEditing
-        } = this.props;
-
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<BaseSidebar/>}
-            >
-                <MetricList {...this.props} />
-            </SidebarLayout>
-        );
-    }
+    actions.clearState(newProps);
+  }
+
+  render() {
+    const { isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<BaseSidebar />}
+      >
+        <MetricList {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricQuestions.jsx b/frontend/src/metabase/reference/metrics/MetricQuestions.jsx
index 262a0bfbc16d6509e1a2aa1d59b7a7088e53f95f..8670945ee85c97e9e2f7e333860dfeed4974ddc6 100644
--- a/frontend/src/metabase/reference/metrics/MetricQuestions.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricQuestions.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
 import * as Urls from "metabase/lib/urls";
@@ -18,101 +18,105 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import {
-    getMetricQuestions,
-    getError,
-    getLoading,
-    getTable,
-    getMetric
+  getMetricQuestions,
+  getError,
+  getLoading,
+  getTable,
+  getMetric,
 } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 
 const emptyStateData = (table, metric) => {
-    return {
-        message: t`Questions about this metric will appear here as they're added`,
-        icon: "all",
-        action: t`Ask a question`,
-        link: getQuestionUrl({
-            dbId: table && table.db_id,
-            tableId: metric.table_id,
-            metricId: metric.id
-        })
-    };
-    }
-
+  return {
+    message: t`Questions about this metric will appear here as they're added`,
+    icon: "all",
+    action: t`Ask a question`,
+    link: getQuestionUrl({
+      dbId: table && table.db_id,
+      tableId: metric.table_id,
+      metricId: metric.id,
+    }),
+  };
+};
 
 const mapStateToProps = (state, props) => ({
-    metric: getMetric(state, props),
-    table: getTable(state, props),
-    entities: getMetricQuestions(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  metric: getMetric(state, props),
+  table: getTable(state, props),
+  entities: getMetricQuestions(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
-
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricQuestions extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        metric: PropTypes.object,
-        table: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    metric: PropTypes.object,
+    table: PropTypes.object,
+  };
 
-    render() {
-        const {
-            entities,
-            style,
-            loadingError,
-            loading
-        } = this.props;
+  render() {
+    const { entities, style, loadingError, loading } = this.props;
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader 
-                    name={t`Questions about ${this.props.metric.name}`}
-                    type="questions"
-                    headerIcon="ruler"
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader
+          name={t`Questions about ${this.props.metric.name}`}
+          type="questions"
+          headerIcon="ruler"
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {Object.values(entities)
+                    .filter(isQueryable)
+                    .map(
+                      (entity, index) =>
+                        entity &&
+                        entity.id &&
+                        entity.name && (
+                          <li className="relative" key={entity.id}>
+                            <ListItem
+                              id={entity.id}
+                              index={index}
+                              name={entity.display_name || entity.name}
+                              description={t`Created ${moment(
+                                entity.created_at,
+                              ).fromNow()} by ${entity.creator.common_name}`}
+                              url={Urls.question(entity.id)}
+                              icon={visualizations.get(entity.display).iconName}
+                            />
+                          </li>
+                        ),
+                    )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <AdminAwareEmptyState
+                  {...emptyStateData(this.props.table, this.props.metric)}
                 />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            { 
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                        <li className="relative" key={entity.id}>
-                                            <ListItem
-                                                id={entity.id}
-                                                index={index}
-                                                name={entity.display_name || entity.name}
-                                                description={ t`Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
-                                                url={ Urls.question(entity.id) }
-                                                icon={ visualizations.get(entity.display).iconName }
-                                            />
-                                        </li>
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <AdminAwareEmptyState {...emptyStateData(this.props.table, this.props.metric)}/>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricQuestionsContainer.jsx b/frontend/src/metabase/reference/metrics/MetricQuestionsContainer.jsx
index 6c00b2b626ef5d7619731d7a9983cc81c16ca1e4..892f5be2c08ffe9f12f478268711eae7f4484e37 100644
--- a/frontend/src/metabase/reference/metrics/MetricQuestionsContainer.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricQuestionsContainer.jsx
@@ -1,84 +1,78 @@
 /* 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 { connect } from "react-redux";
 
-import MetricSidebar from './MetricSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import MetricQuestions from "metabase/reference/metrics/MetricQuestions.jsx"
+import MetricSidebar from "./MetricSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import MetricQuestions from "metabase/reference/metrics/MetricQuestions.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getMetric,
-    getMetricId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+  getUser,
+  getMetric,
+  getMetricId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
-import {
-    loadEntities
-} from 'metabase/questions/questions';
+import { loadEntities } from "metabase/questions/questions";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    metric: getMetric(state, props),
-    metricId: getMetricId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  metric: getMetric(state, props),
+  metricId: getMetricId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    fetchQuestions: () => loadEntities("cards", {}),
-    ...metadataActions,
-    ...actions
+  fetchQuestions: () => loadEntities("cards", {}),
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricQuestionsContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        metric: PropTypes.object.isRequired,
-        metricId: PropTypes.number.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchMetricQuestions(this.props, this.props.metricId);
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    metric: PropTypes.object.isRequired,
+    metricId: PropTypes.number.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchMetricQuestions(this.props, this.props.metricId);
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            user,
-            metric,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<MetricSidebar metric={metric} user={user}/>}
-            >
-                <MetricQuestions {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { user, metric, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<MetricSidebar metric={metric} user={user} />}
+      >
+        <MetricQuestions {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricRevisions.jsx b/frontend/src/metabase/reference/metrics/MetricRevisions.jsx
index c2af974958069e96fbdd145cc3d1aeb25f9e70ea..6045d805a53d797a055bc32ebf7794f0d20a4496 100644
--- a/frontend/src/metabase/reference/metrics/MetricRevisions.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricRevisions.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { getIn } from "icepick";
 
 import S from "metabase/components/List.css";
@@ -11,13 +11,13 @@ import * as metadataActions from "metabase/redux/metadata";
 import { assignUserColors } from "metabase/lib/formatting";
 
 import {
-    getMetricRevisions,
-    getMetric,
-    getSegment,
-    getTables,
-    getUser,
-    getLoading,
-    getError
+  getMetricRevisions,
+  getMetric,
+  getSegment,
+  getTables,
+  getUser,
+  getLoading,
+  getError,
 } from "../selectors";
 
 import Revision from "metabase/admin/datamodel/components/revisions/Revision.jsx";
@@ -25,94 +25,106 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 import EmptyState from "metabase/components/EmptyState.jsx";
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-
-const emptyStateData =  {
-    message: t`There are no revisions for this metric`
-}
+const emptyStateData = {
+  message: t`There are no revisions for this metric`,
+};
 
 const mapStateToProps = (state, props) => {
-    return {
-        revisions: getMetricRevisions(state, props),
-        metric: getMetric(state, props),
-        segment: getSegment(state, props),
-        tables: getTables(state, props),
-        user: getUser(state, props),
-        loading: getLoading(state, props),
-        loadingError: getError(state, props)
-    }
-}
+  return {
+    revisions: getMetricRevisions(state, props),
+    metric: getMetric(state, props),
+    segment: getSegment(state, props),
+    tables: getTables(state, props),
+    user: getUser(state, props),
+    loading: getLoading(state, props),
+    loadingError: getError(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricRevisions extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        revisions: PropTypes.object.isRequired,
-        metric: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        tables: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    revisions: PropTypes.object.isRequired,
+    metric: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    tables: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
 
-    render() {
-        const {
-            style,
-            revisions,
-            metric,
-            segment,
-            tables,
-            user,
-            loading,
-            loadingError
-        } = this.props;
+  render() {
+    const {
+      style,
+      revisions,
+      metric,
+      segment,
+      tables,
+      user,
+      loading,
+      loadingError,
+    } = this.props;
 
-        const entity = metric.id ? metric : segment;
+    const entity = metric.id ? metric : segment;
 
-        const userColorAssignments = user && Object.keys(revisions).length > 0 ?
-            assignUserColors(
-                Object.values(revisions)
-                    .map(revision => getIn(revision, ['user', 'id'])),
-                user.id
-            ) : {};
+    const userColorAssignments =
+      user && Object.keys(revisions).length > 0
+        ? assignUserColors(
+            Object.values(revisions).map(revision =>
+              getIn(revision, ["user", "id"]),
+            ),
+            user.id,
+          )
+        : {};
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader 
-                    name={t`Revision history for ${this.props.metric.name}`}
-                    headerIcon="ruler"
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                    { () => Object.keys(revisions).length > 0 && tables[entity.table_id] ?
-                        <div className="wrapper wrapper--trim">
-                            <div className={R.revisionsWrapper}>
-                                {Object.values(revisions)
-                                    .map(revision => revision && revision.diff ?
-                                        <Revision
-                                            key={revision.id}
-                                            revision={revision || {}}
-                                            tableMetadata={tables[entity.table_id] || {}}
-                                            objectName={entity.name}
-                                            currentUser={user || {}}
-                                            userColor={userColorAssignments[getIn(revision, ['user', 'id'])]}
-                                        /> :
-                                        null
-                                    )
-                                    .reverse()
-                                }
-                            </div>
-                        </div>
-                        :
-                        <div className={S.empty}>
-                          <EmptyState {...emptyStateData}/>
-                        </div>
-                    }
-                </LoadingAndErrorWrapper>
-            </div>
-        );
-    }
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader
+          name={t`Revision history for ${this.props.metric.name}`}
+          headerIcon="ruler"
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(revisions).length > 0 && tables[entity.table_id] ? (
+              <div className="wrapper wrapper--trim">
+                <div className={R.revisionsWrapper}>
+                  {Object.values(revisions)
+                    .map(
+                      revision =>
+                        revision && revision.diff ? (
+                          <Revision
+                            key={revision.id}
+                            revision={revision || {}}
+                            tableMetadata={tables[entity.table_id] || {}}
+                            objectName={entity.name}
+                            currentUser={user || {}}
+                            userColor={
+                              userColorAssignments[
+                                getIn(revision, ["user", "id"])
+                              ]
+                            }
+                          />
+                        ) : null,
+                    )
+                    .reverse()}
+                </div>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <EmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricRevisionsContainer.jsx b/frontend/src/metabase/reference/metrics/MetricRevisionsContainer.jsx
index 2c858ae51c2756450f9acda0395e72d7d4e7dfc0..23f25bfb89a873a95c6b78684c400583b8e1f449 100644
--- a/frontend/src/metabase/reference/metrics/MetricRevisionsContainer.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricRevisionsContainer.jsx
@@ -1,82 +1,75 @@
 /* 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 { connect } from "react-redux";
 
-import MetricSidebar from './MetricSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import MetricRevisions from "metabase/reference/metrics/MetricRevisions.jsx"
+import MetricSidebar from "./MetricSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import MetricRevisions from "metabase/reference/metrics/MetricRevisions.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getMetric,
-    getMetricId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
+  getUser,
+  getMetric,
+  getMetricId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    metric: getMetric(state, props),
-    metricId: getMetricId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  metric: getMetric(state, props),
+  metricId: getMetricId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class MetricRevisionsContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        metric: PropTypes.object.isRequired,
-        metricId: PropTypes.number.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-
-    async fetchContainerData(){
-        await actions.wrappedFetchMetricRevisions(this.props, this.props.metricId);
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    metric: PropTypes.object.isRequired,
+    metricId: PropTypes.number.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchMetricRevisions(this.props, this.props.metricId);
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            user,
-            metric,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
+  render() {
+    const { user, metric, isEditing } = this.props;
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<MetricSidebar metric={metric} user={user}/>}
-            >
-                <MetricRevisions {...this.props} />
-            </SidebarLayout>
-        );
-    }
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<MetricSidebar metric={metric} user={user} />}
+      >
+        <MetricRevisions {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/metrics/MetricSidebar.jsx b/frontend/src/metabase/reference/metrics/MetricSidebar.jsx
index 5e0fe9e5686e4f10c5cbc2f344c7ac3cc302c503..5f82db2624a0da6418fc31bb08dffdedc5624c70 100644
--- a/frontend/src/metabase/reference/metrics/MetricSidebar.jsx
+++ b/frontend/src/metabase/reference/metrics/MetricSidebar.jsx
@@ -2,54 +2,54 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const MetricSidebar = ({
-    metric,
-    user,
-    style,
-    className
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <ul>
-            <div className={S.breadcrumbs}>
-                <Breadcrumbs
-                    className="py4"
-                    crumbs={[[t`Metrics`,"/reference/metrics"],
-                             [metric.name]]}
-                    inSidebar={true}
-                    placeholder={t`Data Reference`}
-                />
-            </div>
-                <SidebarItem key={`/reference/metrics/${metric.id}`} 
-                             href={`/reference/metrics/${metric.id}`} 
-                             icon="document" 
-                             name={t`Details`} />
-                <SidebarItem key={`/reference/metrics/${metric.id}/questions`} 
-                             href={`/reference/metrics/${metric.id}/questions`} 
-                             icon="all" 
-                             name={t`Questions about ${metric.name}`} />
-             { user && user.is_superuser &&
-
-                <SidebarItem key={`/reference/metrics/${metric.id}/revisions`}
-                             href={`/reference/metrics/${metric.id}/revisions`}
-                             icon="history" 
-                             name={t`Revision history for ${metric.name}`} />
-             }
-        </ul>
-    </div>
+const MetricSidebar = ({ metric, user, style, className }) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <ul>
+      <div className={S.breadcrumbs}>
+        <Breadcrumbs
+          className="py4"
+          crumbs={[[t`Metrics`, "/reference/metrics"], [metric.name]]}
+          inSidebar={true}
+          placeholder={t`Data Reference`}
+        />
+      </div>
+      <SidebarItem
+        key={`/reference/metrics/${metric.id}`}
+        href={`/reference/metrics/${metric.id}`}
+        icon="document"
+        name={t`Details`}
+      />
+      <SidebarItem
+        key={`/reference/metrics/${metric.id}/questions`}
+        href={`/reference/metrics/${metric.id}/questions`}
+        icon="all"
+        name={t`Questions about ${metric.name}`}
+      />
+      {user &&
+        user.is_superuser && (
+          <SidebarItem
+            key={`/reference/metrics/${metric.id}/revisions`}
+            href={`/reference/metrics/${metric.id}/revisions`}
+            icon="history"
+            name={t`Revision history for ${metric.name}`}
+          />
+        )}
+    </ul>
+  </div>
+);
 
 MetricSidebar.propTypes = {
-    metric:          PropTypes.object,
-    user:          PropTypes.object,
-    className:      PropTypes.string,
-    style:          PropTypes.object,
+  metric: PropTypes.object,
+  user: PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
 };
 
 export default pure(MetricSidebar);
-
diff --git a/frontend/src/metabase/reference/reference.js b/frontend/src/metabase/reference/reference.js
index 3379a78a4ecebf4618c3ea727a49d839ef771e7d..0f25b72c6ca8264277383aeb5fc02c9f37fa9797 100644
--- a/frontend/src/metabase/reference/reference.js
+++ b/frontend/src/metabase/reference/reference.js
@@ -1,21 +1,18 @@
-import { assoc } from 'icepick';
+import { assoc } from "icepick";
 import _ from "underscore";
 
 import {
-    handleActions,
-    createAction,
-    createThunkAction,
-    fetchData
-} from 'metabase/lib/redux';
+  handleActions,
+  createAction,
+  createThunkAction,
+  fetchData,
+} from "metabase/lib/redux";
 
-import MetabaseAnalytics from 'metabase/lib/analytics';
+import MetabaseAnalytics from "metabase/lib/analytics";
 
-import { GettingStartedApi } from 'metabase/services';
+import { GettingStartedApi } from "metabase/services";
 
-import {
-    filterUntouchedFields,
-    isEmptyObject
-} from "./utils.js"
+import { filterUntouchedFields, isEmptyObject } from "./utils.js";
 
 export const FETCH_GUIDE = "metabase/reference/FETCH_GUIDE";
 export const SET_ERROR = "metabase/reference/SET_ERROR";
@@ -29,24 +26,23 @@ export const COLLAPSE_FORMULA = "metabase/reference/COLLAPSE_FORMULA";
 export const SHOW_DASHBOARD_MODAL = "metabase/reference/SHOW_DASHBOARD_MODAL";
 export const HIDE_DASHBOARD_MODAL = "metabase/reference/HIDE_DASHBOARD_MODAL";
 
-
 export const fetchGuide = createThunkAction(FETCH_GUIDE, (reload = false) => {
-    return async (dispatch, getState) => {
-        const requestStatePath = ["reference", 'guide'];
-        const existingStatePath = requestStatePath;
-        const getData = async () => {
-            return await GettingStartedApi.get();
-        };
-
-        return await fetchData({
-            dispatch,
-            getState,
-            requestStatePath,
-            existingStatePath,
-            getData,
-            reload
-        });
+  return async (dispatch, getState) => {
+    const requestStatePath = ["reference", "guide"];
+    const existingStatePath = requestStatePath;
+    const getData = async () => {
+      return await GettingStartedApi.get();
     };
+
+    return await fetchData({
+      dispatch,
+      getState,
+      requestStatePath,
+      existingStatePath,
+      getData,
+      reload,
+    });
+  };
 });
 
 export const setError = createAction(SET_ERROR);
@@ -58,11 +54,11 @@ export const startLoading = createAction(START_LOADING);
 export const endLoading = createAction(END_LOADING);
 
 export const startEditing = createAction(START_EDITING, () => {
-    MetabaseAnalytics.trackEvent('Data Reference', 'Started Editing');
+  MetabaseAnalytics.trackEvent("Data Reference", "Started Editing");
 });
 
 export const endEditing = createAction(END_EDITING, () => {
-    MetabaseAnalytics.trackEvent('Data Reference', 'Ended Editing');
+  MetabaseAnalytics.trackEvent("Data Reference", "Ended Editing");
 });
 
 export const expandFormula = createAction(EXPAND_FORMULA);
@@ -77,85 +73,68 @@ export const hideDashboardModal = createAction(HIDE_DASHBOARD_MODAL);
 // Helper functions. This is meant to be a transitional state to get things out of tryFetchData() and friends
 
 const fetchDataWrapper = (props, fn) => {
-
-    return async (argument) => {
-        props.clearError();
-        props.startLoading();
-        try {
-            await fn(argument)
-        }
-        catch(error) {
-            console.error(error);
-            props.setError(error);
-        }
-
-        props.endLoading();
+  return async argument => {
+    props.clearError();
+    props.startLoading();
+    try {
+      await fn(argument);
+    } catch (error) {
+      console.error(error);
+      props.setError(error);
     }
-}
-export const wrappedFetchGuide = async (props) => {
-
-    fetchDataWrapper(
-        props,
-        async () => {
-                await Promise.all(
-                    [props.fetchGuide(),
-                     props.fetchDashboards(),
-                     props.fetchMetrics(),
-                     props.fetchSegments(),
-                     props.fetchRealDatabasesWithMetadata()]
-                )}
-        )()
-}
+
+    props.endLoading();
+  };
+};
+export const wrappedFetchGuide = async props => {
+  fetchDataWrapper(props, async () => {
+    await Promise.all([
+      props.fetchGuide(),
+      props.fetchDashboards(),
+      props.fetchMetrics(),
+      props.fetchSegments(),
+      props.fetchRealDatabasesWithMetadata(),
+    ]);
+  })();
+};
 export const wrappedFetchDatabaseMetadata = (props, databaseID) => {
-    fetchDataWrapper(props, props.fetchDatabaseMetadata)(databaseID)
-}
-
-export const wrappedFetchDatabaseMetadataAndQuestion = async (props, databaseID) => {
-
-    fetchDataWrapper(
-        props,
-        async (dbID) => {
-                await Promise.all(
-                    [props.fetchDatabaseMetadata(dbID),
-                     props.fetchQuestions()]
-                )}
-        )(databaseID)
-}
-export const wrappedFetchMetricDetail = async (props, metricID) => {
+  fetchDataWrapper(props, props.fetchDatabaseMetadata)(databaseID);
+};
 
-    fetchDataWrapper(
-        props,
-        async (mID) => {
-                await Promise.all(
-                    [props.fetchMetricTable(mID),
-                     props.fetchMetrics(),
-                     props.fetchGuide()]
-                )}
-        )(metricID)
-}
+export const wrappedFetchDatabaseMetadataAndQuestion = async (
+  props,
+  databaseID,
+) => {
+  fetchDataWrapper(props, async dbID => {
+    await Promise.all([
+      props.fetchDatabaseMetadata(dbID),
+      props.fetchQuestions(),
+    ]);
+  })(databaseID);
+};
+export const wrappedFetchMetricDetail = async (props, metricID) => {
+  fetchDataWrapper(props, async mID => {
+    await Promise.all([
+      props.fetchMetricTable(mID),
+      props.fetchMetrics(),
+      props.fetchGuide(),
+    ]);
+  })(metricID);
+};
 export const wrappedFetchMetricQuestions = async (props, metricID) => {
-
-    fetchDataWrapper(
-        props,
-        async (mID) => {
-                await Promise.all(
-                    [props.fetchMetricTable(mID),
-                     props.fetchMetrics(),
-                     props.fetchQuestions()]
-                )}
-        )(metricID)
-}
+  fetchDataWrapper(props, async mID => {
+    await Promise.all([
+      props.fetchMetricTable(mID),
+      props.fetchMetrics(),
+      props.fetchQuestions(),
+    ]);
+  })(metricID);
+};
 export const wrappedFetchMetricRevisions = async (props, metricID) => {
-
-    fetchDataWrapper(
-        props,
-        async (mID) => {
-                await Promise.all(
-                    [props.fetchMetricRevisions(mID),
-                     props.fetchMetrics()]
-                )}
-        )(metricID)
-}
+  fetchDataWrapper(props, async mID => {
+    await Promise.all([props.fetchMetricRevisions(mID), props.fetchMetrics()]);
+  })(metricID);
+};
 
 // export const wrappedFetchDatabaseMetadataAndQuestion = async (props, databaseID) => {
 //         clearError();
@@ -174,76 +153,62 @@ export const wrappedFetchMetricRevisions = async (props, metricID) => {
 //         endLoading();
 // }
 
-export const wrappedFetchDatabases = (props) => {
-    fetchDataWrapper(props, props.fetchRealDatabases)({})
-}
-export const wrappedFetchMetrics = (props) => {
-    fetchDataWrapper(props, props.fetchMetrics)({})
-}
-
-export const wrappedFetchSegments = (props) => {
-    fetchDataWrapper(props, props.fetchSegments)({})
-}
+export const wrappedFetchDatabases = props => {
+  fetchDataWrapper(props, props.fetchRealDatabases)({});
+};
+export const wrappedFetchMetrics = props => {
+  fetchDataWrapper(props, props.fetchMetrics)({});
+};
 
+export const wrappedFetchSegments = props => {
+  fetchDataWrapper(props, props.fetchSegments)({});
+};
 
 export const wrappedFetchSegmentDetail = (props, segmentID) => {
-    fetchDataWrapper(props, props.fetchSegmentTable)(segmentID)
-}
+  fetchDataWrapper(props, props.fetchSegmentTable)(segmentID);
+};
 
 export const wrappedFetchSegmentQuestions = async (props, segmentID) => {
-
-    fetchDataWrapper(
-        props,
-        async (sID) => {
-                await props.fetchSegments(sID);
-                await Promise.all(
-                    [props.fetchSegmentTable(sID),
-                     props.fetchQuestions()]
-                )}
-        )(segmentID)
-}
+  fetchDataWrapper(props, async sID => {
+    await props.fetchSegments(sID);
+    await Promise.all([props.fetchSegmentTable(sID), props.fetchQuestions()]);
+  })(segmentID);
+};
 export const wrappedFetchSegmentRevisions = async (props, segmentID) => {
-
-    fetchDataWrapper(
-        props,
-        async (sID) => {
-                await props.fetchSegments(sID);
-                await Promise.all(
-                    [props.fetchSegmentRevisions(sID),
-                     props.fetchSegmentTable(sID)]
-                )}
-        )(segmentID)
-}
+  fetchDataWrapper(props, async sID => {
+    await props.fetchSegments(sID);
+    await Promise.all([
+      props.fetchSegmentRevisions(sID),
+      props.fetchSegmentTable(sID),
+    ]);
+  })(segmentID);
+};
 export const wrappedFetchSegmentFields = async (props, segmentID) => {
-
-    fetchDataWrapper(
-        props,
-        async (sID) => {
-                await props.fetchSegments(sID);
-                await Promise.all(
-                    [props.fetchSegmentFields(sID),
-                     props.fetchSegmentTable(sID)]
-                )}
-        )(segmentID)
-}
+  fetchDataWrapper(props, async sID => {
+    await props.fetchSegments(sID);
+    await Promise.all([
+      props.fetchSegmentFields(sID),
+      props.fetchSegmentTable(sID),
+    ]);
+  })(segmentID);
+};
 
 // This is called when a component gets a new set of props.
 // I *think* this is un-necessary in all cases as we're using multiple
 // components where the old code re-used the same component
 export const clearState = props => {
-    props.endEditing();
-    props.endLoading();
-    props.clearError();
-    props.collapseFormula();
-}
-
+  props.endEditing();
+  props.endLoading();
+  props.clearError();
+  props.collapseFormula();
+};
 
 // This is called on the success or failure of a form triggered update
-const resetForm = (props) => {
-    props.resetForm();
-    props.endLoading();
-    props.endEditing();
-}
+const resetForm = props => {
+  props.resetForm();
+  props.endLoading();
+  props.endEditing();
+};
 
 // Update actions
 // these use the "fetchDataWrapper" for now. It should probably be renamed.
@@ -252,335 +217,365 @@ const resetForm = (props) => {
 // of that component
 
 const updateDataWrapper = (props, fn) => {
-
-    return async (fields) => {
-        props.clearError();
-        props.startLoading();
-        try {
-            const editedFields = filterUntouchedFields(fields, props.entity);
-            if (!isEmptyObject(editedFields)) {
-                const newEntity = {...props.entity, ...editedFields};
-                await fn(newEntity);
-            }
-        }
-        catch(error) {
-            console.error(error);
-            props.setError(error);
-        }
-        resetForm(props)
+  return async fields => {
+    props.clearError();
+    props.startLoading();
+    try {
+      const editedFields = filterUntouchedFields(fields, props.entity);
+      if (!isEmptyObject(editedFields)) {
+        const newEntity = { ...props.entity, ...editedFields };
+        await fn(newEntity);
+      }
+    } catch (error) {
+      console.error(error);
+      props.setError(error);
     }
-}
+    resetForm(props);
+  };
+};
 
 export const rUpdateSegmentDetail = (formFields, props) => {
-    updateDataWrapper(props, props.updateSegment)(formFields)
-}
+  updateDataWrapper(props, props.updateSegment)(formFields);
+};
 export const rUpdateSegmentFieldDetail = (formFields, props) => {
-    updateDataWrapper(props, props.updateField)(formFields)
-}
+  updateDataWrapper(props, props.updateField)(formFields);
+};
 export const rUpdateDatabaseDetail = (formFields, props) => {
-    updateDataWrapper(props, props.updateDatabase)(formFields)
-}
+  updateDataWrapper(props, props.updateDatabase)(formFields);
+};
 export const rUpdateTableDetail = (formFields, props) => {
-    updateDataWrapper(props, props.updateTable)(formFields)
-}
+  updateDataWrapper(props, props.updateTable)(formFields);
+};
 export const rUpdateFieldDetail = (formFields, props) => {
-    updateDataWrapper(props, props.updateField)(formFields)
-}
+  updateDataWrapper(props, props.updateField)(formFields);
+};
 
 export const rUpdateMetricDetail = async (metric, guide, formFields, props) => {
-    props.startLoading();
-    try {
-        const editedFields = filterUntouchedFields(formFields, metric);
-        if (!isEmptyObject(editedFields)) {
-            const newMetric = {...metric, ...editedFields};
-            await props.updateMetric(newMetric);
-
-            const importantFieldIds = formFields.important_fields.map(field => field.id);
-            const existingImportantFieldIds = guide.metric_important_fields && guide.metric_important_fields[metric.id];
-
-            const areFieldIdsIdentitical = existingImportantFieldIds &&
-                existingImportantFieldIds.length === importantFieldIds.length &&
-                existingImportantFieldIds.every(id => importantFieldIds.includes(id));
-
-            if (!areFieldIdsIdentitical) {
-                await props.updateMetricImportantFields(metric.id, importantFieldIds);
-                wrappedFetchMetricDetail(props, metric.id);
-            }
-        }
-    }
-    catch(error) {
-        props.setError(error);
-        console.error(error);
+  props.startLoading();
+  try {
+    const editedFields = filterUntouchedFields(formFields, metric);
+    if (!isEmptyObject(editedFields)) {
+      const newMetric = { ...metric, ...editedFields };
+      await props.updateMetric(newMetric);
+
+      const importantFieldIds = formFields.important_fields.map(
+        field => field.id,
+      );
+      const existingImportantFieldIds =
+        guide.metric_important_fields &&
+        guide.metric_important_fields[metric.id];
+
+      const areFieldIdsIdentitical =
+        existingImportantFieldIds &&
+        existingImportantFieldIds.length === importantFieldIds.length &&
+        existingImportantFieldIds.every(id => importantFieldIds.includes(id));
+
+      if (!areFieldIdsIdentitical) {
+        await props.updateMetricImportantFields(metric.id, importantFieldIds);
+        wrappedFetchMetricDetail(props, metric.id);
+      }
     }
+  } catch (error) {
+    props.setError(error);
+    console.error(error);
+  }
 
-    resetForm(props)
-}
+  resetForm(props);
+};
 
 export const rUpdateFields = async (fields, formFields, props) => {
-    props.startLoading();
-    try {
-        const updatedFields = Object.keys(formFields)
-            .map(fieldId => ({
-                field: fields[fieldId],
-                formField: filterUntouchedFields(formFields[fieldId], fields[fieldId])
-            }))
-            .filter(({field, formField}) => !isEmptyObject(formField))
-            .map(({field, formField}) => ({...field, ...formField}));
-
-        await Promise.all(updatedFields.map(props.updateField));
-    }
-    catch(error) {
-        props.setError(error);
-        console.error(error);
-    }
+  props.startLoading();
+  try {
+    const updatedFields = Object.keys(formFields)
+      .map(fieldId => ({
+        field: fields[fieldId],
+        formField: filterUntouchedFields(formFields[fieldId], fields[fieldId]),
+      }))
+      .filter(({ field, formField }) => !isEmptyObject(formField))
+      .map(({ field, formField }) => ({ ...field, ...formField }));
+
+    await Promise.all(updatedFields.map(props.updateField));
+  } catch (error) {
+    props.setError(error);
+    console.error(error);
+  }
+
+  resetForm(props);
+};
 
-    resetForm(props)
-}
+export const tryUpdateGuide = async (formFields, props) => {
+  const {
+    guide,
+    dashboards,
+    metrics,
+    segments,
+    tables,
+    startLoading,
+    endLoading,
+    endEditing,
+    setError,
+    resetForm,
+    updateDashboard,
+    updateMetric,
+    updateSegment,
+    updateTable,
+    updateMetricImportantFields,
+    updateSetting,
+    fetchGuide,
+    clearRequestState,
+  } = props;
+
+  startLoading();
+  try {
+    const updateNewEntities = ({ entities, formFields, updateEntity }) =>
+      formFields.map(formField => {
+        if (!formField.id) {
+          return [];
+        }
 
+        const editedEntity = filterUntouchedFields(
+          assoc(formField, "show_in_getting_started", true),
+          entities[formField.id],
+        );
 
-export const tryUpdateGuide = async (formFields, props) => {
-    const {
-        guide,
-        dashboards,
-        metrics,
-        segments,
-        tables,
-        startLoading,
-        endLoading,
-        endEditing,
-        setError,
-        resetForm,
-        updateDashboard,
-        updateMetric,
-        updateSegment,
-        updateTable,
-        updateMetricImportantFields,
-        updateSetting,
-        fetchGuide,
-        clearRequestState
-    } = props;
-
-    startLoading();
-    try {
-        const updateNewEntities = ({
-            entities,
-            formFields,
-            updateEntity
-        }) => formFields.map(formField => {
-            if (!formField.id) {
-                return [];
-            }
-
-            const editedEntity = filterUntouchedFields(
-                assoc(formField, 'show_in_getting_started', true),
-                entities[formField.id]
-            );
-
-            if (isEmptyObject(editedEntity)) {
-                return [];
-            }
-
-            const newEntity = entities[formField.id];
-            const updatedNewEntity = {
-                ...newEntity,
-                ...editedEntity
-            };
-
-            const updatingNewEntity = updateEntity(updatedNewEntity);
-
-            return [updatingNewEntity];
-        });
+        if (isEmptyObject(editedEntity)) {
+          return [];
+        }
+
+        const newEntity = entities[formField.id];
+        const updatedNewEntity = {
+          ...newEntity,
+          ...editedEntity,
+        };
+
+        const updatingNewEntity = updateEntity(updatedNewEntity);
+
+        return [updatingNewEntity];
+      });
+
+    const updateOldEntities = ({
+      newEntityIds,
+      oldEntityIds,
+      entities,
+      updateEntity,
+    }) =>
+      oldEntityIds
+        .filter(oldEntityId => !newEntityIds.includes(oldEntityId))
+        .map(oldEntityId => {
+          const oldEntity = entities[oldEntityId];
 
-        const updateOldEntities = ({
-            newEntityIds,
-            oldEntityIds,
-            entities,
-            updateEntity
-        }) => oldEntityIds
-            .filter(oldEntityId => !newEntityIds.includes(oldEntityId))
-            .map(oldEntityId => {
-                const oldEntity = entities[oldEntityId];
-
-                const updatedOldEntity = assoc(
-                    oldEntity,
-                    'show_in_getting_started',
-                    false
-                );
-
-                const updatingOldEntity = updateEntity(updatedOldEntity);
-
-                return [updatingOldEntity];
-            });
-        //FIXME: necessary because revision_message is a mandatory field
-        // even though we don't actually keep track of changes to caveats/points_of_interest yet
-        const updateWithRevisionMessage = updateEntity => entity => updateEntity(assoc(
-            entity,
-            'revision_message',
-            'Updated in Getting Started guide.'
-        ));
-
-        const updatingDashboards = updateNewEntities({
-                entities: dashboards,
-                formFields: [formFields.most_important_dashboard],
-                updateEntity: updateDashboard
-            })
-            .concat(updateOldEntities({
-                newEntityIds: formFields.most_important_dashboard ?
-                    [formFields.most_important_dashboard.id] : [],
-                oldEntityIds: guide.most_important_dashboard ?
-                    [guide.most_important_dashboard] :
-                    [],
-                entities: dashboards,
-                updateEntity: updateDashboard
-            }));
-
-        const updatingMetrics = updateNewEntities({
-                entities: metrics,
-                formFields: formFields.important_metrics,
-                updateEntity: updateWithRevisionMessage(updateMetric)
-            })
-            .concat(updateOldEntities({
-                newEntityIds: formFields.important_metrics
-                    .map(formField => formField.id),
-                oldEntityIds: guide.important_metrics,
-                entities: metrics,
-                updateEntity: updateWithRevisionMessage(updateMetric)
-            }));
-
-        const updatingMetricImportantFields = formFields.important_metrics
-            .map(metricFormField => {
-                if (!metricFormField.id || !metricFormField.important_fields) {
-                    return [];
-                }
-                const importantFieldIds = metricFormField.important_fields
-                    .map(field => field.id);
-                const existingImportantFieldIds = guide.metric_important_fields[metricFormField.id];
-
-                const areFieldIdsIdentitical = existingImportantFieldIds &&
-                    existingImportantFieldIds.length === importantFieldIds.length &&
-                    existingImportantFieldIds.every(id => importantFieldIds.includes(id));
-                if (areFieldIdsIdentitical) {
-                    return [];
-                }
-
-                return [updateMetricImportantFields(metricFormField.id, importantFieldIds)];
-            });
-
-        const segmentFields = formFields.important_segments_and_tables
-            .filter(field => field.type === 'segment');
-
-        const updatingSegments = updateNewEntities({
-                entities: segments,
-                formFields: segmentFields,
-                updateEntity: updateWithRevisionMessage(updateSegment)
-            })
-            .concat(updateOldEntities({
-                newEntityIds: segmentFields
-                    .map(formField => formField.id),
-                oldEntityIds: guide.important_segments,
-                entities: segments,
-                updateEntity: updateWithRevisionMessage(updateSegment)
-            }));
-
-        const tableFields = formFields.important_segments_and_tables
-            .filter(field => field.type === 'table');
-
-        const updatingTables = updateNewEntities({
-                entities: tables,
-                formFields: tableFields,
-                updateEntity: updateTable
-            })
-            .concat(updateOldEntities({
-                newEntityIds: tableFields
-                    .map(formField => formField.id),
-                oldEntityIds: guide.important_tables,
-                entities: tables,
-                updateEntity: updateTable
-            }));
-
-        const updatingThingsToKnow = guide.things_to_know !== formFields.things_to_know ?
-            [updateSetting({key: 'getting-started-things-to-know', value: formFields.things_to_know })] :
-            [];
-
-        const updatingContactName = guide.contact && formFields.contact &&
-            guide.contact.name !== formFields.contact.name ?
-                [updateSetting({key: 'getting-started-contact-name', value: formFields.contact.name })] :
-                [];
-
-        const updatingContactEmail = guide.contact && formFields.contact &&
-            guide.contact.email !== formFields.contact.email ?
-                [updateSetting({key: 'getting-started-contact-email', value: formFields.contact.email })] :
-                [];
-
-        const updatingData = _.flatten([
-            updatingDashboards,
-            updatingMetrics,
-            updatingMetricImportantFields,
-            updatingSegments,
-            updatingTables,
-            updatingThingsToKnow,
-            updatingContactName,
-            updatingContactEmail
-        ]);
-
-        if (updatingData.length > 0) {
-            await Promise.all(updatingData);
-
-            clearRequestState({statePath: ['reference', 'guide']});
-
-            await fetchGuide();
+          const updatedOldEntity = assoc(
+            oldEntity,
+            "show_in_getting_started",
+            false,
+          );
+
+          const updatingOldEntity = updateEntity(updatedOldEntity);
+
+          return [updatingOldEntity];
+        });
+    //FIXME: necessary because revision_message is a mandatory field
+    // even though we don't actually keep track of changes to caveats/points_of_interest yet
+    const updateWithRevisionMessage = updateEntity => entity =>
+      updateEntity(
+        assoc(entity, "revision_message", "Updated in Getting Started guide."),
+      );
+
+    const updatingDashboards = updateNewEntities({
+      entities: dashboards,
+      formFields: [formFields.most_important_dashboard],
+      updateEntity: updateDashboard,
+    }).concat(
+      updateOldEntities({
+        newEntityIds: formFields.most_important_dashboard
+          ? [formFields.most_important_dashboard.id]
+          : [],
+        oldEntityIds: guide.most_important_dashboard
+          ? [guide.most_important_dashboard]
+          : [],
+        entities: dashboards,
+        updateEntity: updateDashboard,
+      }),
+    );
+
+    const updatingMetrics = updateNewEntities({
+      entities: metrics,
+      formFields: formFields.important_metrics,
+      updateEntity: updateWithRevisionMessage(updateMetric),
+    }).concat(
+      updateOldEntities({
+        newEntityIds: formFields.important_metrics.map(
+          formField => formField.id,
+        ),
+        oldEntityIds: guide.important_metrics,
+        entities: metrics,
+        updateEntity: updateWithRevisionMessage(updateMetric),
+      }),
+    );
+
+    const updatingMetricImportantFields = formFields.important_metrics.map(
+      metricFormField => {
+        if (!metricFormField.id || !metricFormField.important_fields) {
+          return [];
+        }
+        const importantFieldIds = metricFormField.important_fields.map(
+          field => field.id,
+        );
+        const existingImportantFieldIds =
+          guide.metric_important_fields[metricFormField.id];
+
+        const areFieldIdsIdentitical =
+          existingImportantFieldIds &&
+          existingImportantFieldIds.length === importantFieldIds.length &&
+          existingImportantFieldIds.every(id => importantFieldIds.includes(id));
+        if (areFieldIdsIdentitical) {
+          return [];
         }
-    }
-    catch(error) {
-        setError(error);
-        console.error(error);
-    }
 
-    resetForm();
-    endLoading();
-    endEditing();
+        return [
+          updateMetricImportantFields(metricFormField.id, importantFieldIds),
+        ];
+      },
+    );
+
+    const segmentFields = formFields.important_segments_and_tables.filter(
+      field => field.type === "segment",
+    );
+
+    const updatingSegments = updateNewEntities({
+      entities: segments,
+      formFields: segmentFields,
+      updateEntity: updateWithRevisionMessage(updateSegment),
+    }).concat(
+      updateOldEntities({
+        newEntityIds: segmentFields.map(formField => formField.id),
+        oldEntityIds: guide.important_segments,
+        entities: segments,
+        updateEntity: updateWithRevisionMessage(updateSegment),
+      }),
+    );
+
+    const tableFields = formFields.important_segments_and_tables.filter(
+      field => field.type === "table",
+    );
+
+    const updatingTables = updateNewEntities({
+      entities: tables,
+      formFields: tableFields,
+      updateEntity: updateTable,
+    }).concat(
+      updateOldEntities({
+        newEntityIds: tableFields.map(formField => formField.id),
+        oldEntityIds: guide.important_tables,
+        entities: tables,
+        updateEntity: updateTable,
+      }),
+    );
+
+    const updatingThingsToKnow =
+      guide.things_to_know !== formFields.things_to_know
+        ? [
+            updateSetting({
+              key: "getting-started-things-to-know",
+              value: formFields.things_to_know,
+            }),
+          ]
+        : [];
+
+    const updatingContactName =
+      guide.contact &&
+      formFields.contact &&
+      guide.contact.name !== formFields.contact.name
+        ? [
+            updateSetting({
+              key: "getting-started-contact-name",
+              value: formFields.contact.name,
+            }),
+          ]
+        : [];
+
+    const updatingContactEmail =
+      guide.contact &&
+      formFields.contact &&
+      guide.contact.email !== formFields.contact.email
+        ? [
+            updateSetting({
+              key: "getting-started-contact-email",
+              value: formFields.contact.email,
+            }),
+          ]
+        : [];
+
+    const updatingData = _.flatten([
+      updatingDashboards,
+      updatingMetrics,
+      updatingMetricImportantFields,
+      updatingSegments,
+      updatingTables,
+      updatingThingsToKnow,
+      updatingContactName,
+      updatingContactEmail,
+    ]);
+
+    if (updatingData.length > 0) {
+      await Promise.all(updatingData);
+
+      clearRequestState({ statePath: ["reference", "guide"] });
+
+      await fetchGuide();
+    }
+  } catch (error) {
+    setError(error);
+    console.error(error);
+  }
+
+  resetForm();
+  endLoading();
+  endEditing();
 };
 
-
 const initialState = {
-    error: null,
-    isLoading: false,
-    isEditing: false,
-    isFormulaExpanded: false,
-    isDashboardModalOpen: false
+  error: null,
+  isLoading: false,
+  isEditing: false,
+  isFormulaExpanded: false,
+  isDashboardModalOpen: false,
 };
-export default handleActions({
+export default handleActions(
+  {
     [FETCH_GUIDE]: {
-        next: (state, { payload }) => assoc(state, 'guide', payload)
+      next: (state, { payload }) => assoc(state, "guide", payload),
     },
     [SET_ERROR]: {
-        throw: (state, { payload }) => assoc(state, 'error', payload)
+      throw: (state, { payload }) => assoc(state, "error", payload),
     },
     [CLEAR_ERROR]: {
-        next: (state) => assoc(state, 'error', null)
+      next: state => assoc(state, "error", null),
     },
     [START_LOADING]: {
-        next: (state) => assoc(state, 'isLoading', true)
+      next: state => assoc(state, "isLoading", true),
     },
     [END_LOADING]: {
-        next: (state) => assoc(state, 'isLoading', false)
+      next: state => assoc(state, "isLoading", false),
     },
     [START_EDITING]: {
-        next: (state) => assoc(state, 'isEditing', true)
+      next: state => assoc(state, "isEditing", true),
     },
     [END_EDITING]: {
-        next: (state) => assoc(state, 'isEditing', false)
+      next: state => assoc(state, "isEditing", false),
     },
     [EXPAND_FORMULA]: {
-        next: (state) => assoc(state, 'isFormulaExpanded', true)
+      next: state => assoc(state, "isFormulaExpanded", true),
     },
     [COLLAPSE_FORMULA]: {
-        next: (state) => assoc(state, 'isFormulaExpanded', false)
+      next: state => assoc(state, "isFormulaExpanded", false),
     },
     [SHOW_DASHBOARD_MODAL]: {
-        next: (state) => assoc(state, 'isDashboardModalOpen', true)
+      next: state => assoc(state, "isDashboardModalOpen", true),
     },
     [HIDE_DASHBOARD_MODAL]: {
-        next: (state) => assoc(state, 'isDashboardModalOpen', false)
-    }
-}, initialState);
+      next: state => assoc(state, "isDashboardModalOpen", false),
+    },
+  },
+  initialState,
+);
diff --git a/frontend/src/metabase/reference/segments/SegmentDetail.jsx b/frontend/src/metabase/reference/segments/SegmentDetail.jsx
index c69d8ad07f11888f9051a7cc456853bf435c287a..4488e7f14f8da5debf54389dd389756a05803093 100644
--- a/frontend/src/metabase/reference/segments/SegmentDetail.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentDetail.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import List from "metabase/components/List.jsx";
 import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.jsx";
 
@@ -13,225 +13,252 @@ import Detail from "metabase/reference/components/Detail.jsx";
 import UsefulQuestions from "metabase/reference/components/UsefulQuestions.jsx";
 import Formula from "metabase/reference/components/Formula.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import {
-    getSegment,
-    getTable,
-    getFields,
-    getGuide,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getIsFormulaExpanded,
+  getSegment,
+  getTable,
+  getFields,
+  getGuide,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getIsFormulaExpanded,
 } from "../selectors";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 const interestingQuestions = (table, segment) => {
-    return [
-        {
-            text: t`Number of ${segment.name}`,
-            icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
-            link: getQuestionUrl({
-                dbId: table && table.db_id,
-                tableId: table.id,
-                segmentId: segment.id,
-                getCount: true
-            })
-        },
-        {
-            text: t`See all ${segment.name}`,
-            icon: "table2",
-            link: getQuestionUrl({
-                dbId: table && table.db_id,
-                tableId: table.id,
-                segmentId: segment.id
-            })
-        }
-    ]
-}
+  return [
+    {
+      text: t`Number of ${segment.name}`,
+      icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
+      link: getQuestionUrl({
+        dbId: table && table.db_id,
+        tableId: table.id,
+        segmentId: segment.id,
+        getCount: true,
+      }),
+    },
+    {
+      text: t`See all ${segment.name}`,
+      icon: "table2",
+      link: getQuestionUrl({
+        dbId: table && table.db_id,
+        tableId: table.id,
+        segmentId: segment.id,
+      }),
+    },
+  ];
+};
 
 const mapStateToProps = (state, props) => {
-    const entity = getSegment(state, props) || {};
-    const guide = getGuide(state, props);
-    const fields = getFields(state, props);
+  const entity = getSegment(state, props) || {};
+  const guide = getGuide(state, props);
+  const fields = getFields(state, props);
 
-    const initialValues = {
-        important_fields: guide && guide.metric_important_fields &&
-            guide.metric_important_fields[entity.id] &&
-            guide.metric_important_fields[entity.id]
-                .map(fieldId => fields[fieldId]) ||
-                []
-    };
+  const initialValues = {
+    important_fields:
+      (guide &&
+        guide.metric_important_fields &&
+        guide.metric_important_fields[entity.id] &&
+        guide.metric_important_fields[entity.id].map(
+          fieldId => fields[fieldId],
+        )) ||
+      [],
+  };
 
-    return {
-        entity,
-        table: getTable(state, props),
-        metadataFields: fields,
-        guide,
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        isEditing: getIsEditing(state, props),
-        isFormulaExpanded: getIsFormulaExpanded(state, props),
-        initialValues,
-    }
+  return {
+    entity,
+    table: getTable(state, props),
+    metadataFields: fields,
+    guide,
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    isEditing: getIsEditing(state, props),
+    isFormulaExpanded: getIsFormulaExpanded(state, props),
+    initialValues,
+  };
 };
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions,
+  ...metadataActions,
+  ...actions,
 };
 
-const validate = (values, props) =>  !values.revision_message ? 
-    { revision_message: t`Please enter a revision message` } : {} 
-
+const validate = (values, props) =>
+  !values.revision_message
+    ? { revision_message: t`Please enter a revision message` }
+    : {};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'details',
-    fields: ['name', 'display_name', 'description', 'revision_message', 'points_of_interest', 'caveats'],
-    validate
+  form: "details",
+  fields: [
+    "name",
+    "display_name",
+    "description",
+    "revision_message",
+    "points_of_interest",
+    "caveats",
+  ],
+  validate,
 })
 export default class SegmentDetail extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entity: PropTypes.object.isRequired,
-        table: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        expandFormula: PropTypes.func.isRequired,
-        collapseFormula: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        resetForm: PropTypes.func.isRequired,
-        fields: PropTypes.object.isRequired,
-        isFormulaExpanded: PropTypes.bool,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entity: PropTypes.object.isRequired,
+    table: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    expandFormula: PropTypes.func.isRequired,
+    collapseFormula: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    resetForm: PropTypes.func.isRequired,
+    fields: PropTypes.object.isRequired,
+    isFormulaExpanded: PropTypes.bool,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+  };
 
-    render() {
-        const {
-            fields: { name, display_name, description, revision_message, points_of_interest, caveats },
-            style,
-            entity,
-            table,
-            loadingError,
-            loading,
-            user,
-            isEditing,
-            startEditing,
-            endEditing,
-            expandFormula,
-            collapseFormula,
-            isFormulaExpanded,
-            handleSubmit,
-            resetForm,
-            submitting,
-        } = this.props;
+  render() {
+    const {
+      fields: {
+        name,
+        display_name,
+        description,
+        revision_message,
+        points_of_interest,
+        caveats,
+      },
+      style,
+      entity,
+      table,
+      loadingError,
+      loading,
+      user,
+      isEditing,
+      startEditing,
+      endEditing,
+      expandFormula,
+      collapseFormula,
+      isFormulaExpanded,
+      handleSubmit,
+      resetForm,
+      submitting,
+    } = this.props;
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.rUpdateSegmentDetail(fields, this.props)
-        );
+    const onSubmit = handleSubmit(
+      async fields => await actions.rUpdateSegmentDetail(fields, this.props),
+    );
 
-        return (
-            <form style={style} className="full"
-                onSubmit={onSubmit}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={true}
-                        onSubmit={onSubmit}
-                        endEditing={endEditing}
-                        reinitializeForm={resetForm}
-                        submitting={submitting}
-                        revisionMessageFormField={revision_message}
-                    />
-                }
-                <EditableReferenceHeader
-                    entity={entity}
-                    table={table}
-                    type="segment"
-                    headerIcon="segment"
-                    headerLink={getQuestionUrl({ dbId: table&&table.db_id, tableId: entity.table_id, segmentId: entity.id})}
-                    name={t`Details`}
-                    user={user}
+    return (
+      <form style={style} className="full" onSubmit={onSubmit}>
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={true}
+            onSubmit={onSubmit}
+            endEditing={endEditing}
+            reinitializeForm={resetForm}
+            submitting={submitting}
+            revisionMessageFormField={revision_message}
+          />
+        )}
+        <EditableReferenceHeader
+          entity={entity}
+          table={table}
+          type="segment"
+          headerIcon="segment"
+          headerLink={getQuestionUrl({
+            dbId: table && table.db_id,
+            tableId: entity.table_id,
+            segmentId: entity.id,
+          })}
+          name={t`Details`}
+          user={user}
+          isEditing={isEditing}
+          hasSingleSchema={false}
+          hasDisplayName={false}
+          startEditing={startEditing}
+          displayNameFormField={display_name}
+          nameFormField={name}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <List>
+                <li className="relative">
+                  <Detail
+                    id="description"
+                    name={t`Description`}
+                    description={entity.description}
+                    placeholder={t`No description yet`}
+                    isEditing={isEditing}
+                    field={description}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="points_of_interest"
+                    name={t`Why this Segment is interesting`}
+                    description={entity.points_of_interest}
+                    placeholder={t`Nothing interesting yet`}
                     isEditing={isEditing}
-                    hasSingleSchema={false}
-                    hasDisplayName={false}
-                    startEditing={startEditing}
-                    displayNameFormField={display_name}
-                    nameFormField={name}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            <li className="relative">
-                                <Detail
-                                    id="description"
-                                    name={t`Description`}
-                                    description={entity.description}
-                                    placeholder={t`No description yet`}
-                                    isEditing={isEditing}
-                                    field={description}
-                                />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="points_of_interest"
-                                    name={t`Why this Segment is interesting`}
-                                    description={entity.points_of_interest}
-                                    placeholder={t`Nothing interesting yet`}
-                                    isEditing={isEditing}
-                                    field={points_of_interest}
-                                    />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="caveats"
-                                    name={t`Things to be aware of about this Segment`}
-                                    description={entity.caveats}
-                                    placeholder={t`Nothing to be aware of yet`}
-                                    isEditing={isEditing}
-                                    field={caveats}
-                                />
-                            </li>
-                            { table && !isEditing &&
-                                <li className="relative">
-                                    <Formula
-                                        type="segment"
-                                        entity={entity}
-                                        table={table}
-                                        isExpanded={isFormulaExpanded}
-                                        expandFormula={expandFormula}
-                                        collapseFormula={collapseFormula}
-                                    />
-                                </li>
-                            }
-                            { !isEditing &&
-                                <li className="relative">
-                                    <UsefulQuestions questions={interestingQuestions(this.props.table, this.props.entity)} />
-                                </li>
-                            }
-                        </List>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                    field={points_of_interest}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="caveats"
+                    name={t`Things to be aware of about this Segment`}
+                    description={entity.caveats}
+                    placeholder={t`Nothing to be aware of yet`}
+                    isEditing={isEditing}
+                    field={caveats}
+                  />
+                </li>
+                {table &&
+                  !isEditing && (
+                    <li className="relative">
+                      <Formula
+                        type="segment"
+                        entity={entity}
+                        table={table}
+                        isExpanded={isFormulaExpanded}
+                        expandFormula={expandFormula}
+                        collapseFormula={collapseFormula}
+                      />
+                    </li>
+                  )}
+                {!isEditing && (
+                  <li className="relative">
+                    <UsefulQuestions
+                      questions={interestingQuestions(
+                        this.props.table,
+                        this.props.entity,
+                      )}
+                    />
+                  </li>
+                )}
+              </List>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentDetailContainer.jsx b/frontend/src/metabase/reference/segments/SegmentDetailContainer.jsx
index 1bb72f4b2d831aff28006a34c5760878ccc065c7..6876c8268201173bb96da85aa5ad51d6a244b013 100644
--- a/frontend/src/metabase/reference/segments/SegmentDetailContainer.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentDetailContainer.jsx
@@ -1,80 +1,75 @@
 /* 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 { connect } from "react-redux";
 
-import SegmentSidebar from './SegmentSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import SegmentDetail from "metabase/reference/segments/SegmentDetail.jsx"
+import SegmentSidebar from "./SegmentSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import SegmentDetail from "metabase/reference/segments/SegmentDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getSegment,
-    getSegmentId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+  getUser,
+  getSegment,
+  getSegmentId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    segment: getSegment(state, props),
-    segmentId: getSegmentId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  segment: getSegment(state, props),
+  segmentId: getSegmentId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentDetailContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        user: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        segmentId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    user: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    segmentId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    async fetchContainerData(){
-        await actions.wrappedFetchSegmentDetail(this.props, this.props.segmentId);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchSegmentDetail(this.props, this.props.segmentId);
+  }
 
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            user,
-            segment,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<SegmentSidebar segment={segment} user={user}/>}
-            >
-                <SegmentDetail {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { user, segment, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<SegmentSidebar segment={segment} user={user} />}
+      >
+        <SegmentDetail {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx b/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx
index 022f029560cd348303732708f1fb0828202116d3..931a1d212aca6fd449a4eeb4f70f91caf76528da 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldDetail.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "metabase/reference/Reference.css";
 
 import List from "metabase/components/List.jsx";
@@ -15,242 +15,266 @@ import Detail from "metabase/reference/components/Detail.jsx";
 import FieldTypeDetail from "metabase/reference/components/FieldTypeDetail.jsx";
 import UsefulQuestions from "metabase/reference/components/UsefulQuestions.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
+import { getQuestionUrl } from "../utils";
 
 import {
-    getFieldBySegment,
-    getTable,
-    getFields,
-    getGuide,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getForeignKeys,
-    getIsFormulaExpanded
+  getFieldBySegment,
+  getTable,
+  getFields,
+  getGuide,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getForeignKeys,
+  getIsFormulaExpanded,
 } from "../selectors";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 const interestingQuestions = (table, field) => {
-    return [
-        {
-            text: t`Number of ${table && table.display_name} grouped by ${field.display_name}`,
-            icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
-            link: getQuestionUrl({
-                dbId: table && table.db_id,
-                tableId: table.id,
-                fieldId: field.id,
-                getCount: true
-            })
-        },
-        {
-            text: t`All distinct values of ${field.display_name}`,
-            icon: "table2",
-            link: getQuestionUrl({
-                dbId: table && table.db_id,
-                tableId: table.id,
-                fieldId: field.id
-            })
-        }
-    ]
-}
+  return [
+    {
+      text: t`Number of ${table && table.display_name} grouped by ${
+        field.display_name
+      }`,
+      icon: { name: "number", scale: 1, viewBox: "8 8 16 16" },
+      link: getQuestionUrl({
+        dbId: table && table.db_id,
+        tableId: table.id,
+        fieldId: field.id,
+        getCount: true,
+      }),
+    },
+    {
+      text: t`All distinct values of ${field.display_name}`,
+      icon: "table2",
+      link: getQuestionUrl({
+        dbId: table && table.db_id,
+        tableId: table.id,
+        fieldId: field.id,
+      }),
+    },
+  ];
+};
 
 const mapStateToProps = (state, props) => {
-    const entity = getFieldBySegment(state, props) || {};
-    const guide = getGuide(state, props);
-    const fields = getFields(state, props);
+  const entity = getFieldBySegment(state, props) || {};
+  const guide = getGuide(state, props);
+  const fields = getFields(state, props);
 
-    const initialValues = {
-        important_fields: guide && guide.metric_important_fields &&
-            guide.metric_important_fields[entity.id] &&
-            guide.metric_important_fields[entity.id]
-                .map(fieldId => fields[fieldId]) ||
-                []
-    };
+  const initialValues = {
+    important_fields:
+      (guide &&
+        guide.metric_important_fields &&
+        guide.metric_important_fields[entity.id] &&
+        guide.metric_important_fields[entity.id].map(
+          fieldId => fields[fieldId],
+        )) ||
+      [],
+  };
 
-    return {
-        entity,
-        table: getTable(state, props),
-        guide,
-        loading: getLoading(state, props),
-        // naming this 'error' will conflict with redux form
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        foreignKeys: getForeignKeys(state, props),
-        isEditing: getIsEditing(state, props),
-        isFormulaExpanded: getIsFormulaExpanded(state, props),
-        initialValues,
-    }
+  return {
+    entity,
+    table: getTable(state, props),
+    guide,
+    loading: getLoading(state, props),
+    // naming this 'error' will conflict with redux form
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    foreignKeys: getForeignKeys(state, props),
+    isEditing: getIsEditing(state, props),
+    isFormulaExpanded: getIsFormulaExpanded(state, props),
+    initialValues,
+  };
 };
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
-
 const validate = (values, props) => {
-    return {};
-}
+  return {};
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'details',
-    fields: ['name', 'display_name', 'description', 'revision_message', 'points_of_interest', 'caveats',  'special_type', 'fk_target_field_id'],
-    validate
+  form: "details",
+  fields: [
+    "name",
+    "display_name",
+    "description",
+    "revision_message",
+    "points_of_interest",
+    "caveats",
+    "special_type",
+    "fk_target_field_id",
+  ],
+  validate,
 })
 export default class SegmentFieldDetail extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entity: PropTypes.object.isRequired,
-        table: PropTypes.object,
-        user: PropTypes.object.isRequired,
-        foreignKeys: PropTypes.object,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        resetForm: PropTypes.func.isRequired,
-        fields: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entity: PropTypes.object.isRequired,
+    table: PropTypes.object,
+    user: PropTypes.object.isRequired,
+    foreignKeys: PropTypes.object,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    resetForm: PropTypes.func.isRequired,
+    fields: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+  };
 
-    render() {
-        const {
-            fields: { name, display_name, description, revision_message, points_of_interest, caveats, special_type, fk_target_field_id },
-            style,
-            entity,
-            table,
-            loadingError,
-            loading,
-            user,
-            foreignKeys,
-            isEditing,
-            startEditing,
-            endEditing,
-            handleSubmit,
-            resetForm,
-            submitting,
-        } = this.props;
+  render() {
+    const {
+      fields: {
+        name,
+        display_name,
+        description,
+        revision_message,
+        points_of_interest,
+        caveats,
+        special_type,
+        fk_target_field_id,
+      },
+      style,
+      entity,
+      table,
+      loadingError,
+      loading,
+      user,
+      foreignKeys,
+      isEditing,
+      startEditing,
+      endEditing,
+      handleSubmit,
+      resetForm,
+      submitting,
+    } = this.props;
 
-        const onSubmit = handleSubmit(async (fields) =>
-            await actions.rUpdateSegmentFieldDetail(fields, this.props)
-        );
+    const onSubmit = handleSubmit(
+      async fields =>
+        await actions.rUpdateSegmentFieldDetail(fields, this.props),
+    );
 
-        return (
-            <form style={style} className="full"
-                onSubmit={onSubmit}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={false}
-                        onSubmit={onSubmit}
-                        endEditing={endEditing}
-                        reinitializeForm={resetForm}
-                        submitting={submitting}
-                        revisionMessageFormField={revision_message}
+    return (
+      <form style={style} className="full" onSubmit={onSubmit}>
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={false}
+            onSubmit={onSubmit}
+            endEditing={endEditing}
+            reinitializeForm={resetForm}
+            submitting={submitting}
+            revisionMessageFormField={revision_message}
+          />
+        )}
+        <EditableReferenceHeader
+          entity={entity}
+          table={table}
+          headerIcon="field"
+          name={t`Details`}
+          type="field"
+          user={user}
+          isEditing={isEditing}
+          hasSingleSchema={false}
+          hasDisplayName={true}
+          startEditing={startEditing}
+          displayNameFormField={display_name}
+          nameFormField={name}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() => (
+            <div className="wrapper wrapper--trim">
+              <List>
+                <li className="relative">
+                  <Detail
+                    id="description"
+                    name={t`Description`}
+                    description={entity.description}
+                    placeholder={t`No description yet`}
+                    isEditing={isEditing}
+                    field={description}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <Detail
+                      id="name"
+                      name={t`Actual name in database`}
+                      description={entity.name}
+                      subtitleClass={S.tableActualName}
                     />
-                }
-                <EditableReferenceHeader
-                    entity={entity}
-                    table={table}
-                    headerIcon="field"
-                    name={t`Details`}
-                    type="field"
-                    user={user}
+                  </li>
+                )}
+                <li className="relative">
+                  <Detail
+                    id="points_of_interest"
+                    name={t`Why this field is interesting`}
+                    description={entity.points_of_interest}
+                    placeholder={t`Nothing interesting yet`}
                     isEditing={isEditing}
-                    hasSingleSchema={false}
-                    hasDisplayName={true}
-                    startEditing={startEditing}
-                    displayNameFormField={display_name}
-                    nameFormField={name}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () =>
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            <li className="relative">
-                                <Detail
-                                    id="description"
-                                    name={t`Description`}
-                                    description={entity.description}
-                                    placeholder={t`No description yet`}
-                                    isEditing={isEditing}
-                                    field={description}
-                                />
-                            </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <Detail
-                                        id="name"
-                                        name={t`Actual name in database`}
-                                        description={entity.name}
-                                        subtitleClass={S.tableActualName}
-                                    />
-                                </li>
-                            }
-                            <li className="relative">
-                                <Detail
-                                    id="points_of_interest"
-                                    name={t`Why this field is interesting`}
-                                    description={entity.points_of_interest}
-                                    placeholder={t`Nothing interesting yet`}
-                                    isEditing={isEditing}
-                                    field={points_of_interest}
-                                    />
-                            </li>
-                            <li className="relative">
-                                <Detail
-                                    id="caveats"
-                                    name={t`Things to be aware of about this field`}
-                                    description={entity.caveats}
-                                    placeholder={t`Nothing to be aware of yet`}
-                                    isEditing={isEditing}
-                                    field={caveats}
-                                />
-                            </li>
-
-
-                            { !isEditing && 
-                                <li className="relative">
-                                    <Detail
-                                        id="base_type"
-                                        name={t`Data type`}
-                                        description={entity.base_type}
-                                    />
-                                </li>
-                            }
-                                <li className="relative">
-                                    <FieldTypeDetail
-                                        field={entity}
-                                        foreignKeys={foreignKeys}
-                                        fieldTypeFormField={special_type}
-                                        foreignKeyFormField={fk_target_field_id}
-                                        isEditing={isEditing}
-                                    />
-                                </li>
-                            { !isEditing &&
-                                <li className="relative">
-                                    <UsefulQuestions questions={interestingQuestions(this.props.table, this.props.entity)} />
-                                </li>
-                            }
-
+                    field={points_of_interest}
+                  />
+                </li>
+                <li className="relative">
+                  <Detail
+                    id="caveats"
+                    name={t`Things to be aware of about this field`}
+                    description={entity.caveats}
+                    placeholder={t`Nothing to be aware of yet`}
+                    isEditing={isEditing}
+                    field={caveats}
+                  />
+                </li>
 
-                        </List>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                {!isEditing && (
+                  <li className="relative">
+                    <Detail
+                      id="base_type"
+                      name={t`Data type`}
+                      description={entity.base_type}
+                    />
+                  </li>
+                )}
+                <li className="relative">
+                  <FieldTypeDetail
+                    field={entity}
+                    foreignKeys={foreignKeys}
+                    fieldTypeFormField={special_type}
+                    foreignKeyFormField={fk_target_field_id}
+                    isEditing={isEditing}
+                  />
+                </li>
+                {!isEditing && (
+                  <li className="relative">
+                    <UsefulQuestions
+                      questions={interestingQuestions(
+                        this.props.table,
+                        this.props.entity,
+                      )}
+                    />
+                  </li>
+                )}
+              </List>
+            </div>
+          )}
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldDetailContainer.jsx b/frontend/src/metabase/reference/segments/SegmentFieldDetailContainer.jsx
index f0379db76a481da43144ad55d5521c5a2bb71aff..7fdaf349c3eba3a286b6b3bc9c832951d0dbabbe 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldDetailContainer.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldDetailContainer.jsx
@@ -1,80 +1,75 @@
 /* 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 { connect } from "react-redux";
 
-import SegmentFieldSidebar from './SegmentFieldSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import SegmentFieldDetail from "metabase/reference/segments/SegmentFieldDetail.jsx"
+import SegmentFieldSidebar from "./SegmentFieldSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import SegmentFieldDetail from "metabase/reference/segments/SegmentFieldDetail.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getSegment,
-    getSegmentId,
-    getField,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
+  getSegment,
+  getSegmentId,
+  getField,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    segment: getSegment(state, props),    
-    segmentId: getSegmentId(state, props),
-    field: getField(state, props),    
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  segment: getSegment(state, props),
+  segmentId: getSegmentId(state, props),
+  field: getField(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentFieldDetailContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        segment: PropTypes.object.isRequired,
-        segmentId: PropTypes.number.isRequired,
-        field: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchSegmentFields(this.props, this.props.segmentId);
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    segment: PropTypes.object.isRequired,
+    segmentId: PropTypes.number.isRequired,
+    field: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchSegmentFields(this.props, this.props.segmentId);
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            segment,
-            field,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<SegmentFieldSidebar segment={segment} field={field}/>}
-            >
-                <SegmentFieldDetail {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { segment, field, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<SegmentFieldSidebar segment={segment} field={field} />}
+      >
+        <SegmentFieldDetail {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldList.jsx b/frontend/src/metabase/reference/segments/SegmentFieldList.jsx
index 0cbc96fde6d42f086ec9247c448c92468e3248e6..c9991483236ef5631c1950d1193e3929d3d16055 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldList.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldList.jsx
@@ -3,10 +3,10 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import { reduxForm } from "redux-form";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import S from "metabase/components/List.css";
 import R from "metabase/reference/Reference.css";
-import F from "metabase/reference/components/Field.css"
+import F from "metabase/reference/components/Field.css";
 
 import Field from "metabase/reference/components/Field.jsx";
 import List from "metabase/components/List.jsx";
@@ -19,160 +19,174 @@ import EditableReferenceHeader from "metabase/reference/components/EditableRefer
 import cx from "classnames";
 
 import {
-    getFieldsBySegment,
-    getForeignKeys,
-    getError,
-    getLoading,
-    getUser,
-    getIsEditing,
-    getSegment
+  getFieldsBySegment,
+  getForeignKeys,
+  getError,
+  getLoading,
+  getUser,
+  getIsEditing,
+  getSegment,
 } from "../selectors";
 
-import {
-    fieldsToFormFields
-} from '../utils';
+import { fieldsToFormFields } from "../utils";
 
 import { getIconForField } from "metabase/lib/schema_metadata";
 
 import * as metadataActions from "metabase/redux/metadata";
-import * as actions from 'metabase/reference/reference';
-
+import * as actions from "metabase/reference/reference";
 
 const emptyStateData = {
-    message: t`Fields in this table will appear here as they're added`,
-    icon: "fields"
-}
-
+  message: t`Fields in this table will appear here as they're added`,
+  icon: "fields",
+};
 
 const mapStateToProps = (state, props) => {
-    const data = getFieldsBySegment(state, props);
-    return {
-        segment: getSegment(state,props),
-        entities: data,
-        foreignKeys: getForeignKeys(state, props),
-        loading: getLoading(state, props),
-        loadingError: getError(state, props),
-        user: getUser(state, props),
-        isEditing: getIsEditing(state, props),
-        fields: fieldsToFormFields(data)
-    };
-}
+  const data = getFieldsBySegment(state, props);
+  return {
+    segment: getSegment(state, props),
+    entities: data,
+    foreignKeys: getForeignKeys(state, props),
+    loading: getLoading(state, props),
+    loadingError: getError(state, props),
+    user: getUser(state, props),
+    isEditing: getIsEditing(state, props),
+    fields: fieldsToFormFields(data),
+  };
+};
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 const validate = (values, props) => {
-    return {};
-}
+  return {};
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @reduxForm({
-    form: 'fields',
-    validate
+  form: "fields",
+  validate,
 })
 export default class SegmentFieldList extends Component {
-    static propTypes = {
-        segment: PropTypes.object.isRequired,
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        foreignKeys: PropTypes.object.isRequired,
-        isEditing: PropTypes.bool,
-        startEditing: PropTypes.func.isRequired,
-        endEditing: PropTypes.func.isRequired,
-        startLoading: PropTypes.func.isRequired,
-        endLoading: PropTypes.func.isRequired,
-        setError: PropTypes.func.isRequired,
-        updateField: PropTypes.func.isRequired,
-        handleSubmit: PropTypes.func.isRequired,
-        user: PropTypes.object.isRequired,
-        fields: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object,
-        submitting: PropTypes.bool,
-        resetForm: PropTypes.func
-    };
-
-    render() {
-        const {
-            segment,
-            style,
-            entities,
-            fields,
-            foreignKeys,
-            loadingError,
-            loading,
-            user,
-            isEditing,
-            startEditing,
-            endEditing,
-            resetForm,
-            handleSubmit,
-            submitting
-        } = this.props;
-
-        return (
-            <form style={style} className="full"
-                onSubmit={handleSubmit(async (formFields) =>
-                    await actions.rUpdateFields(this.props.entities, formFields, this.props)
-                )}
-            >
-                { isEditing &&
-                    <EditHeader
-                        hasRevisionHistory={false}
-                        reinitializeForm={resetForm}
-                        endEditing={endEditing}
-                        submitting={submitting}
-                    />
-                }
-                <EditableReferenceHeader 
-                    type="segment"
-                    headerIcon="segment"
-                    name={t`Fields in ${segment.name}`}
-                    user={user} 
-                    isEditing={isEditing} 
-                    startEditing={startEditing} 
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <div className={S.item}>
-                            <div className={R.columnHeader}>
-                                <div className={cx(S.itemTitle, F.fieldNameTitle)}>
-                                    {t`Field name`}
-                                </div>
-                                <div className={cx(S.itemTitle, F.fieldType)}>
-                                    {t`Field type`}
-                                </div>
-                                <div className={cx(S.itemTitle, F.fieldDataType)}>
-                                    {t`Data type`}
-                                </div>
-                            </div>
-                        </div>
-                        <List>
-                            { Object.values(entities).map(entity =>
-                                entity && entity.id && entity.name &&
-                                    <li className="relative" key={entity.id}>
-                                        <Field
-                                            field={entity}
-                                            foreignKeys={foreignKeys}
-                                            url={`/reference/segments/${segment.id}/fields/${entity.id}`}
-                                            icon={getIconForField(entity)}
-                                            isEditing={isEditing}
-                                            formField={fields[entity.id]}
-                                        />
-                                    </li>
-                            )}
-                        </List>
+  static propTypes = {
+    segment: PropTypes.object.isRequired,
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    foreignKeys: PropTypes.object.isRequired,
+    isEditing: PropTypes.bool,
+    startEditing: PropTypes.func.isRequired,
+    endEditing: PropTypes.func.isRequired,
+    startLoading: PropTypes.func.isRequired,
+    endLoading: PropTypes.func.isRequired,
+    setError: PropTypes.func.isRequired,
+    updateField: PropTypes.func.isRequired,
+    handleSubmit: PropTypes.func.isRequired,
+    user: PropTypes.object.isRequired,
+    fields: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+    submitting: PropTypes.bool,
+    resetForm: PropTypes.func,
+  };
+
+  render() {
+    const {
+      segment,
+      style,
+      entities,
+      fields,
+      foreignKeys,
+      loadingError,
+      loading,
+      user,
+      isEditing,
+      startEditing,
+      endEditing,
+      resetForm,
+      handleSubmit,
+      submitting,
+    } = this.props;
+
+    return (
+      <form
+        style={style}
+        className="full"
+        onSubmit={handleSubmit(
+          async formFields =>
+            await actions.rUpdateFields(
+              this.props.entities,
+              formFields,
+              this.props,
+            ),
+        )}
+      >
+        {isEditing && (
+          <EditHeader
+            hasRevisionHistory={false}
+            reinitializeForm={resetForm}
+            endEditing={endEditing}
+            submitting={submitting}
+          />
+        )}
+        <EditableReferenceHeader
+          type="segment"
+          headerIcon="segment"
+          name={t`Fields in ${segment.name}`}
+          user={user}
+          isEditing={isEditing}
+          startEditing={startEditing}
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <div className={S.item}>
+                  <div className={R.columnHeader}>
+                    <div className={cx(S.itemTitle, F.fieldNameTitle)}>
+                      {t`Field name`}
+                    </div>
+                    <div className={cx(S.itemTitle, F.fieldType)}>
+                      {t`Field type`}
                     </div>
-                    :
-                    <div className={S.empty}>
-                        <EmptyState {...emptyStateData}/>
+                    <div className={cx(S.itemTitle, F.fieldDataType)}>
+                      {t`Data type`}
                     </div>
-                }
-                </LoadingAndErrorWrapper>
-            </form>
-        )
-    }
+                  </div>
+                </div>
+                <List>
+                  {Object.values(entities).map(
+                    entity =>
+                      entity &&
+                      entity.id &&
+                      entity.name && (
+                        <li className="relative" key={entity.id}>
+                          <Field
+                            field={entity}
+                            foreignKeys={foreignKeys}
+                            url={`/reference/segments/${segment.id}/fields/${
+                              entity.id
+                            }`}
+                            icon={getIconForField(entity)}
+                            isEditing={isEditing}
+                            formField={fields[entity.id]}
+                          />
+                        </li>
+                      ),
+                  )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <EmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </form>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldListContainer.jsx b/frontend/src/metabase/reference/segments/SegmentFieldListContainer.jsx
index 5834d67c564e4f071dfc6553ed459c6cc77cfaa1..ed7fcb1292178961f7e0149c2b741aef692a9df8 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldListContainer.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldListContainer.jsx
@@ -1,79 +1,75 @@
 /* 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 { connect } from "react-redux";
 
-import SegmentSidebar from './SegmentSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import SegmentFieldList from "metabase/reference/segments/SegmentFieldList.jsx"
+import SegmentSidebar from "./SegmentSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import SegmentFieldList from "metabase/reference/segments/SegmentFieldList.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getSegment,
-    getSegmentId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+  getUser,
+  getSegment,
+  getSegmentId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    segment: getSegment(state, props),
-    segmentId: getSegmentId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  segment: getSegment(state, props),
+  segmentId: getSegmentId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentFieldListContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        user: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        segmentId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    user: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    segmentId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    async fetchContainerData(){
-        await actions.wrappedFetchSegmentFields(this.props, this.props.segmentId);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchSegmentFields(this.props, this.props.segmentId);
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            user,
-            segment,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<SegmentSidebar segment={segment} user={user}/>}
-            >
-                <SegmentFieldList {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { user, segment, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<SegmentSidebar segment={segment} user={user} />}
+      >
+        <SegmentFieldList {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx b/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx
index f3f6ded65a286ca4ec16ac01e59c109873470750..7772deb50b784dbf1b6f6bcafe31641e2cc581cf 100644
--- a/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentFieldSidebar.jsx
@@ -2,44 +2,43 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const SegmentFieldSidebar = ({
-    segment,
-    field,
-    style,
-    className
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <ul>
-            <div className={S.breadcrumbs}>
-                <Breadcrumbs
-                    className="py4"
-                    crumbs={[[t`Segments`,"/reference/segments"],
-                             [segment.name, `/reference/segments/${segment.id}`],
-                             [field.name]]}
-                    inSidebar={true}
-                    placeholder={t`Data Reference`}
-                />
-            </div>
-                <SidebarItem key={`/reference/segments/${segment.id}/fields/${field.id}`} 
-                             href={`/reference/segments/${segment.id}/fields/${field.id}`} 
-                             icon="document" 
-                             name={t`Details`} />
-        </ul>
-    </div>
+const SegmentFieldSidebar = ({ segment, field, style, className }) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <ul>
+      <div className={S.breadcrumbs}>
+        <Breadcrumbs
+          className="py4"
+          crumbs={[
+            [t`Segments`, "/reference/segments"],
+            [segment.name, `/reference/segments/${segment.id}`],
+            [field.name],
+          ]}
+          inSidebar={true}
+          placeholder={t`Data Reference`}
+        />
+      </div>
+      <SidebarItem
+        key={`/reference/segments/${segment.id}/fields/${field.id}`}
+        href={`/reference/segments/${segment.id}/fields/${field.id}`}
+        icon="document"
+        name={t`Details`}
+      />
+    </ul>
+  </div>
+);
 
 SegmentFieldSidebar.propTypes = {
-    segment:          PropTypes.object,
-    field:          PropTypes.object,
-    className:      PropTypes.string,
-    style:          PropTypes.object,
+  segment: PropTypes.object,
+  field: PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
 };
 
 export default pure(SegmentFieldSidebar);
-
diff --git a/frontend/src/metabase/reference/segments/SegmentList.jsx b/frontend/src/metabase/reference/segments/SegmentList.jsx
index 6db7bb7f6550df2ca003a9851406842fc7949b2c..f70667ef526a8cc41315a676adb72d8bdc6871f5 100644
--- a/frontend/src/metabase/reference/segments/SegmentList.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentList.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { isQueryable } from "metabase/lib/table";
 
 import S from "metabase/components/List.css";
@@ -15,84 +15,82 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-import {
-    getSegments,
-    getError,
-    getLoading
-} from "../selectors";
+import { getSegments, getError, getLoading } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 
 const emptyStateData = {
-    title: t`Segments are interesting subsets of tables`,
-    adminMessage: t`Defining common segments for your team makes it even easier to ask questions`,
-    message: t`Segments will appear here once your admins have created some`,
-    image: "app/assets/img/segments-list",
-    adminAction: t`Learn how to create segments`,
-    adminLink: "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html"
-}
+  title: t`Segments are interesting subsets of tables`,
+  adminMessage: t`Defining common segments for your team makes it even easier to ask questions`,
+  message: t`Segments will appear here once your admins have created some`,
+  image: "app/assets/img/segments-list",
+  adminAction: t`Learn how to create segments`,
+  adminLink:
+    "http://www.metabase.com/docs/latest/administration-guide/07-segments-and-metrics.html",
+};
 
 const mapStateToProps = (state, props) => ({
-    entities: getSegments(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  entities: getSegments(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
-
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentList extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
 
-    render() {
-        const {
-            entities,
-            style,
-            loadingError,
-            loading
-        } = this.props;
+  render() {
+    const { entities, style, loadingError, loading } = this.props;
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader
-                    name={t`Segments`}
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            {
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                         <li className="relative" key={entity.id}>
-                                            <ListItem
-                                                id={entity.id}
-                                                index={index}
-                                                name={entity.display_name || entity.name}
-                                                description={ entity.description }
-                                                url={ `/reference/segments/${entity.id}` }
-                                                icon="segment"
-                                            />
-                                        </li>
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <AdminAwareEmptyState {...emptyStateData} />
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader name={t`Segments`} />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {Object.values(entities)
+                    .filter(isQueryable)
+                    .map(
+                      (entity, index) =>
+                        entity &&
+                        entity.id &&
+                        entity.name && (
+                          <li className="relative" key={entity.id}>
+                            <ListItem
+                              id={entity.id}
+                              index={index}
+                              name={entity.display_name || entity.name}
+                              description={entity.description}
+                              url={`/reference/segments/${entity.id}`}
+                              icon="segment"
+                            />
+                          </li>
+                        ),
+                    )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <AdminAwareEmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentListContainer.jsx b/frontend/src/metabase/reference/segments/SegmentListContainer.jsx
index 2516af6d97256bf95ef266ccc5093a9b7e1caf2a..c8ec7e5daad902f1545a555538991e34275af1b4 100644
--- a/frontend/src/metabase/reference/segments/SegmentListContainer.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentListContainer.jsx
@@ -1,70 +1,63 @@
 /* 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 { connect } from "react-redux";
 
-import BaseSidebar from 'metabase/reference/guide/BaseSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import SegmentList from "metabase/reference/segments/SegmentList.jsx"
+import BaseSidebar from "metabase/reference/guide/BaseSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import SegmentList from "metabase/reference/segments/SegmentList.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
-
-import {
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
+import { getDatabaseId, getIsEditing } from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentListContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchSegments(this.props);
-    }
-
-    componentWillMount() {
-        this.fetchContainerData()
-    }
-
-
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
+
+  async fetchContainerData() {
+    await actions.wrappedFetchSegments(this.props);
+  }
+
+  componentWillMount() {
+    this.fetchContainerData();
+  }
+
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            isEditing
-        } = this.props;
-
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<BaseSidebar/>}
-            >
-                <SegmentList {...this.props} />
-            </SidebarLayout>
-        );
-    }
+    actions.clearState(newProps);
+  }
+
+  render() {
+    const { isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<BaseSidebar />}
+      >
+        <SegmentList {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentQuestions.jsx b/frontend/src/metabase/reference/segments/SegmentQuestions.jsx
index 2ca1c5c80ad8608c8b539f6bf301eb005c00000a..25d718408869b0a1bd6c9245f5bef10c7e7ddfa5 100644
--- a/frontend/src/metabase/reference/segments/SegmentQuestions.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentQuestions.jsx
@@ -3,7 +3,7 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
 import moment from "moment";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import visualizations from "metabase/visualizations";
 import { isQueryable } from "metabase/lib/table";
 import * as Urls from "metabase/lib/urls";
@@ -18,99 +18,104 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-import {
-    getQuestionUrl
-} from '../utils';
-
+import { getQuestionUrl } from "../utils";
 
 import {
-    getSegmentQuestions,
-    getError,
-    getLoading,
-    getTableBySegment,
-    getSegment
+  getSegmentQuestions,
+  getError,
+  getLoading,
+  getTableBySegment,
+  getSegment,
 } from "../selectors";
 
 import * as metadataActions from "metabase/redux/metadata";
 
-const emptyStateData = (table, segment) =>{  
-    return {
-        message: t`Questions about this segment will appear here as they're added`,
-        icon: "all",
-        action: t`Ask a question`,
-        link: getQuestionUrl({
-            dbId: table && table.db_id,
-            tableId: segment.table_id,
-            segmentId: segment.id
-        })
-    };
-}
+const emptyStateData = (table, segment) => {
+  return {
+    message: t`Questions about this segment will appear here as they're added`,
+    icon: "all",
+    action: t`Ask a question`,
+    link: getQuestionUrl({
+      dbId: table && table.db_id,
+      tableId: segment.table_id,
+      segmentId: segment.id,
+    }),
+  };
+};
 const mapStateToProps = (state, props) => ({
-    segment: getSegment(state,props),
-    table: getTableBySegment(state,props),
-    entities: getSegmentQuestions(state, props),
-    loading: getLoading(state, props),
-    loadingError: getError(state, props)
+  segment: getSegment(state, props),
+  table: getTableBySegment(state, props),
+  entities: getSegmentQuestions(state, props),
+  loading: getLoading(state, props),
+  loadingError: getError(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentQuestions extends Component {
-    static propTypes = {
-        table: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        style: PropTypes.object.isRequired,
-        entities: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
+  static propTypes = {
+    table: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    style: PropTypes.object.isRequired,
+    entities: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
 
-    render() {
-        const {
-            entities,
-            style,
-            loadingError,
-            loading
-        } = this.props;
+  render() {
+    const { entities, style, loadingError, loading } = this.props;
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader 
-                    name={t`Questions about ${this.props.segment.name}`}
-                    type='questions'
-                    headerIcon="segment"
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader
+          name={t`Questions about ${this.props.segment.name}`}
+          type="questions"
+          headerIcon="segment"
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(entities).length > 0 ? (
+              <div className="wrapper wrapper--trim">
+                <List>
+                  {Object.values(entities)
+                    .filter(isQueryable)
+                    .map(
+                      (entity, index) =>
+                        entity &&
+                        entity.id &&
+                        entity.name && (
+                          <li className="relative" key={entity.id}>
+                            <ListItem
+                              id={entity.id}
+                              index={index}
+                              name={entity.display_name || entity.name}
+                              description={t`Created ${moment(
+                                entity.created_at,
+                              ).fromNow()} by ${entity.creator.common_name}`}
+                              url={Urls.question(entity.id)}
+                              icon={visualizations.get(entity.display).iconName}
+                            />
+                          </li>
+                        ),
+                    )}
+                </List>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <AdminAwareEmptyState
+                  {...emptyStateData(this.props.table, this.props.segment)}
                 />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                { () => Object.keys(entities).length > 0 ?
-                    <div className="wrapper wrapper--trim">
-                        <List>
-                            { 
-                                Object.values(entities).filter(isQueryable).map((entity, index) =>
-                                    entity && entity.id && entity.name &&
-                                            <li className="relative" key={entity.id}>
-                                                <ListItem
-                                                    id={entity.id}
-                                                    index={index}
-                                                    name={entity.display_name || entity.name}
-                                                    description={ t`Created ${moment(entity.created_at).fromNow()} by ${entity.creator.common_name}` }
-                                                    url={ Urls.question(entity.id) }
-                                                    icon={ visualizations.get(entity.display).iconName }
-                                                />
-                                            </li>
-                                )
-                            }
-                        </List>
-                    </div>
-                    :
-                    <div className={S.empty}>
-                        <AdminAwareEmptyState {...emptyStateData(this.props.table, this.props.segment)}/>
-                    </div>
-                }
-                </LoadingAndErrorWrapper>
-            </div>
-        )
-    }
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentQuestionsContainer.jsx b/frontend/src/metabase/reference/segments/SegmentQuestionsContainer.jsx
index db68e6558414fddcced0f552199be5cdb2824a18..2646266750af551ee59ec7ef89c33a8181669d28 100644
--- a/frontend/src/metabase/reference/segments/SegmentQuestionsContainer.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentQuestionsContainer.jsx
@@ -1,85 +1,81 @@
 /* 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 { connect } from "react-redux";
 
-import SegmentSidebar from './SegmentSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import SegmentQuestions from "metabase/reference/segments/SegmentQuestions.jsx"
+import SegmentSidebar from "./SegmentSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import SegmentQuestions from "metabase/reference/segments/SegmentQuestions.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getSegment,
-    getSegmentId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
-import {
-    loadEntities
-} from 'metabase/questions/questions';
+  getUser,
+  getSegment,
+  getSegmentId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
+import { loadEntities } from "metabase/questions/questions";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    segment: getSegment(state, props),
-    segmentId: getSegmentId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  segment: getSegment(state, props),
+  segmentId: getSegmentId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    fetchQuestions: () => loadEntities("cards", {}),
-    ...metadataActions,
-    ...actions
+  fetchQuestions: () => loadEntities("cards", {}),
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentQuestionsContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        user: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        segmentId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    user: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    segmentId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    async fetchContainerData(){
-        await actions.wrappedFetchSegmentQuestions(this.props, this.props.segmentId);
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchSegmentQuestions(
+      this.props,
+      this.props.segmentId,
+    );
+  }
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            user,
-            segment,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<SegmentSidebar segment={segment} user={user}/>}
-            >
-                <SegmentQuestions {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { user, segment, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<SegmentSidebar segment={segment} user={user} />}
+      >
+        <SegmentQuestions {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentRevisions.jsx b/frontend/src/metabase/reference/segments/SegmentRevisions.jsx
index 16b25c00124e500c170aaa8b697b0e3c7dd98ec5..a599c4b9d7796056018c152fa79f5a03b956dddf 100644
--- a/frontend/src/metabase/reference/segments/SegmentRevisions.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentRevisions.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import { connect } from "react-redux";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { getIn } from "icepick";
 
 import S from "metabase/components/List.css";
@@ -11,13 +11,13 @@ import * as metadataActions from "metabase/redux/metadata";
 import { assignUserColors } from "metabase/lib/formatting";
 
 import {
-    getSegmentRevisions,
-    getMetric,
-    getSegment,
-    getTables,
-    getUser,
-    getLoading,
-    getError
+  getSegmentRevisions,
+  getMetric,
+  getSegment,
+  getTables,
+  getUser,
+  getLoading,
+  getError,
 } from "../selectors";
 
 import Revision from "metabase/admin/datamodel/components/revisions/Revision.jsx";
@@ -25,93 +25,106 @@ import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper.j
 import EmptyState from "metabase/components/EmptyState.jsx";
 import ReferenceHeader from "../components/ReferenceHeader.jsx";
 
-const emptyStateData =  {
-    message: t`There are no revisions for this segment`
-}
+const emptyStateData = {
+  message: t`There are no revisions for this segment`,
+};
 
 const mapStateToProps = (state, props) => {
-    return {
-        revisions: getSegmentRevisions(state, props),
-        metric: getMetric(state, props),
-        segment: getSegment(state, props),
-        tables: getTables(state, props),
-        user: getUser(state, props),
-        loading: getLoading(state, props),
-        loadingError: getError(state, props)
-    }
-}
+  return {
+    revisions: getSegmentRevisions(state, props),
+    metric: getMetric(state, props),
+    segment: getSegment(state, props),
+    tables: getTables(state, props),
+    user: getUser(state, props),
+    loading: getLoading(state, props),
+    loadingError: getError(state, props),
+  };
+};
 
 const mapDispatchToProps = {
-    ...metadataActions
+  ...metadataActions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentRevisions extends Component {
-    static propTypes = {
-        style: PropTypes.object.isRequired,
-        revisions: PropTypes.object.isRequired,
-        metric: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        tables: PropTypes.object.isRequired,
-        user: PropTypes.object.isRequired,
-        loading: PropTypes.bool,
-        loadingError: PropTypes.object
-    };
+  static propTypes = {
+    style: PropTypes.object.isRequired,
+    revisions: PropTypes.object.isRequired,
+    metric: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    tables: PropTypes.object.isRequired,
+    user: PropTypes.object.isRequired,
+    loading: PropTypes.bool,
+    loadingError: PropTypes.object,
+  };
 
-    render() {
-        const {
-            style,
-            revisions,
-            metric,
-            segment,
-            tables,
-            user,
-            loading,
-            loadingError
-        } = this.props;
+  render() {
+    const {
+      style,
+      revisions,
+      metric,
+      segment,
+      tables,
+      user,
+      loading,
+      loadingError,
+    } = this.props;
 
-        const entity = metric.id ? metric : segment;
+    const entity = metric.id ? metric : segment;
 
-        const userColorAssignments = user && Object.keys(revisions).length > 0 ?
-            assignUserColors(
-                Object.values(revisions)
-                    .map(revision => getIn(revision, ['user', 'id'])),
-                user.id
-            ) : {};
+    const userColorAssignments =
+      user && Object.keys(revisions).length > 0
+        ? assignUserColors(
+            Object.values(revisions).map(revision =>
+              getIn(revision, ["user", "id"]),
+            ),
+            user.id,
+          )
+        : {};
 
-        return (
-            <div style={style} className="full">
-                <ReferenceHeader 
-                    name={t`Revision history for ${this.props.segment.name}`}
-                    headerIcon="segment"
-                />
-                <LoadingAndErrorWrapper loading={!loadingError && loading} error={loadingError}>
-                    { () => Object.keys(revisions).length > 0 && tables[entity.table_id] ?
-                        <div className="wrapper wrapper--trim">
-                            <div className={R.revisionsWrapper}>
-                                {Object.values(revisions)
-                                    .map(revision => revision && revision.diff ?
-                                        <Revision
-                                            key={revision.id}
-                                            revision={revision || {}}
-                                            tableMetadata={tables[entity.table_id] || {}}
-                                            objectName={entity.name}
-                                            currentUser={user || {}}
-                                            userColor={userColorAssignments[getIn(revision, ['user', 'id'])]}
-                                        /> :
-                                        null
-                                    )
-                                    .reverse()
-                                }
-                            </div>
-                        </div>
-                        :
-                        <div className={S.empty}>
-                          <EmptyState {...emptyStateData}/>
-                        </div>
-                    }
-                </LoadingAndErrorWrapper>
-            </div>
-        );
-    }
+    return (
+      <div style={style} className="full">
+        <ReferenceHeader
+          name={t`Revision history for ${this.props.segment.name}`}
+          headerIcon="segment"
+        />
+        <LoadingAndErrorWrapper
+          loading={!loadingError && loading}
+          error={loadingError}
+        >
+          {() =>
+            Object.keys(revisions).length > 0 && tables[entity.table_id] ? (
+              <div className="wrapper wrapper--trim">
+                <div className={R.revisionsWrapper}>
+                  {Object.values(revisions)
+                    .map(
+                      revision =>
+                        revision && revision.diff ? (
+                          <Revision
+                            key={revision.id}
+                            revision={revision || {}}
+                            tableMetadata={tables[entity.table_id] || {}}
+                            objectName={entity.name}
+                            currentUser={user || {}}
+                            userColor={
+                              userColorAssignments[
+                                getIn(revision, ["user", "id"])
+                              ]
+                            }
+                          />
+                        ) : null,
+                    )
+                    .reverse()}
+                </div>
+              </div>
+            ) : (
+              <div className={S.empty}>
+                <EmptyState {...emptyStateData} />
+              </div>
+            )
+          }
+        </LoadingAndErrorWrapper>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentRevisionsContainer.jsx b/frontend/src/metabase/reference/segments/SegmentRevisionsContainer.jsx
index a74062fab583a13eef478d13356d2866f3d04045..64f3cfb766b1700f3f84083a4aea2d0e747aabed 100644
--- a/frontend/src/metabase/reference/segments/SegmentRevisionsContainer.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentRevisionsContainer.jsx
@@ -1,81 +1,78 @@
 /* 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 { connect } from "react-redux";
 
-import SegmentSidebar from './SegmentSidebar.jsx';
-import SidebarLayout from 'metabase/components/SidebarLayout.jsx';
-import SegmentRevisions from "metabase/reference/segments/SegmentRevisions.jsx"
+import SegmentSidebar from "./SegmentSidebar.jsx";
+import SidebarLayout from "metabase/components/SidebarLayout.jsx";
+import SegmentRevisions from "metabase/reference/segments/SegmentRevisions.jsx";
 
-import * as metadataActions from 'metabase/redux/metadata';
-import * as actions from 'metabase/reference/reference';
+import * as metadataActions from "metabase/redux/metadata";
+import * as actions from "metabase/reference/reference";
 
 import {
-    getUser,
-    getSegment,
-    getSegmentId,
-    getDatabaseId,
-    getIsEditing
-} from '../selectors';
-
+  getUser,
+  getSegment,
+  getSegmentId,
+  getDatabaseId,
+  getIsEditing,
+} from "../selectors";
 
 const mapStateToProps = (state, props) => ({
-    user: getUser(state, props),
-    segment: getSegment(state, props),
-    segmentId: getSegmentId(state, props),
-    databaseId: getDatabaseId(state, props),
-    isEditing: getIsEditing(state, props)
+  user: getUser(state, props),
+  segment: getSegment(state, props),
+  segmentId: getSegmentId(state, props),
+  databaseId: getDatabaseId(state, props),
+  isEditing: getIsEditing(state, props),
 });
 
 const mapDispatchToProps = {
-    ...metadataActions,
-    ...actions
+  ...metadataActions,
+  ...actions,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SegmentRevisionsContainer extends Component {
-    static propTypes = {
-        params: PropTypes.object.isRequired,
-        location: PropTypes.object.isRequired,
-        databaseId: PropTypes.number.isRequired,
-        user: PropTypes.object.isRequired,
-        segment: PropTypes.object.isRequired,
-        segmentId: PropTypes.number.isRequired,
-        isEditing: PropTypes.bool
-    };
-
-    async fetchContainerData(){
-        await actions.wrappedFetchSegmentRevisions(this.props, this.props.segmentId);
-    }
+  static propTypes = {
+    params: PropTypes.object.isRequired,
+    location: PropTypes.object.isRequired,
+    databaseId: PropTypes.number.isRequired,
+    user: PropTypes.object.isRequired,
+    segment: PropTypes.object.isRequired,
+    segmentId: PropTypes.number.isRequired,
+    isEditing: PropTypes.bool,
+  };
 
-    componentWillMount() {
-        this.fetchContainerData()
-    }
+  async fetchContainerData() {
+    await actions.wrappedFetchSegmentRevisions(
+      this.props,
+      this.props.segmentId,
+    );
+  }
 
+  componentWillMount() {
+    this.fetchContainerData();
+  }
 
-    componentWillReceiveProps(newProps) {
-        if (this.props.location.pathname === newProps.location.pathname) {
-            return;
-        }
-
-        actions.clearState(newProps)
+  componentWillReceiveProps(newProps) {
+    if (this.props.location.pathname === newProps.location.pathname) {
+      return;
     }
 
-    render() {
-        const {
-            user,
-            segment,
-            isEditing
-        } = this.props;
+    actions.clearState(newProps);
+  }
 
-        return (
-            <SidebarLayout
-                className="flex-full relative"
-                style={ isEditing ? { paddingTop: '43px' } : {}}
-                sidebar={<SegmentSidebar segment={segment} user={user}/>}
-            >
-                <SegmentRevisions {...this.props} />
-            </SidebarLayout>
-        );
-    }
+  render() {
+    const { user, segment, isEditing } = this.props;
+
+    return (
+      <SidebarLayout
+        className="flex-full relative"
+        style={isEditing ? { paddingTop: "43px" } : {}}
+        sidebar={<SegmentSidebar segment={segment} user={user} />}
+      >
+        <SegmentRevisions {...this.props} />
+      </SidebarLayout>
+    );
+  }
 }
diff --git a/frontend/src/metabase/reference/segments/SegmentSidebar.jsx b/frontend/src/metabase/reference/segments/SegmentSidebar.jsx
index e1e38ccfa86e172de03b4269463da89a548ecee8..d2124cf4613ff97653d5f7054af1b23b0c1a2b33 100644
--- a/frontend/src/metabase/reference/segments/SegmentSidebar.jsx
+++ b/frontend/src/metabase/reference/segments/SegmentSidebar.jsx
@@ -2,61 +2,66 @@
 import React from "react";
 import PropTypes from "prop-types";
 import S from "metabase/components/Sidebar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Breadcrumbs from "metabase/components/Breadcrumbs.jsx";
-import SidebarItem from "metabase/components/SidebarItem.jsx"
+import SidebarItem from "metabase/components/SidebarItem.jsx";
 
-import cx from 'classnames';
+import cx from "classnames";
 import pure from "recompose/pure";
 
-const SegmentSidebar = ({
-    segment,
-    user,
-    style,
-    className
-}) =>
-    <div className={cx(S.sidebar, className)} style={style}>
-        <ul>
-            <div className={S.breadcrumbs}>
-                <Breadcrumbs
-                    className="py4"
-                    crumbs={[[t`Segments`,"/reference/segments"],
-                             [segment.name]]}
-                    inSidebar={true}
-                    placeholder={t`Data Reference`}
-                />
-            </div>
-                <SidebarItem key={`/reference/segments/${segment.id}`}
-                             href={`/reference/segments/${segment.id}`}
-                             icon="document"
-                             name={t`Details`} />
-                <SidebarItem key={`/reference/segments/${segment.id}/fields`}
-                             href={`/reference/segments/${segment.id}/fields`}
-                             icon="fields"
-                             name={t`Fields in this segment`} />
-                <SidebarItem key={`/reference/segments/${segment.id}/questions`}
-                             href={`/reference/segments/${segment.id}/questions`}
-                             icon="all"
-                             name={t`Questions about this segment`} />
-                <SidebarItem key={`/xray/segment/${segment.id}/approximate`}
-                             href={`/xray/segment/${segment.id}/approximate`}
-                             icon="all"
-                             name={t`X-ray this segment`} />
-             { user && user.is_superuser &&
-
-                <SidebarItem key={`/reference/segments/${segment.id}/revisions`}
-                             href={`/reference/segments/${segment.id}/revisions`}
-                             icon="history"
-                             name={t`Revision history`} />
-             }
-        </ul>
-    </div>
+const SegmentSidebar = ({ segment, user, style, className }) => (
+  <div className={cx(S.sidebar, className)} style={style}>
+    <ul>
+      <div className={S.breadcrumbs}>
+        <Breadcrumbs
+          className="py4"
+          crumbs={[[t`Segments`, "/reference/segments"], [segment.name]]}
+          inSidebar={true}
+          placeholder={t`Data Reference`}
+        />
+      </div>
+      <SidebarItem
+        key={`/reference/segments/${segment.id}`}
+        href={`/reference/segments/${segment.id}`}
+        icon="document"
+        name={t`Details`}
+      />
+      <SidebarItem
+        key={`/reference/segments/${segment.id}/fields`}
+        href={`/reference/segments/${segment.id}/fields`}
+        icon="fields"
+        name={t`Fields in this segment`}
+      />
+      <SidebarItem
+        key={`/reference/segments/${segment.id}/questions`}
+        href={`/reference/segments/${segment.id}/questions`}
+        icon="all"
+        name={t`Questions about this segment`}
+      />
+      <SidebarItem
+        key={`/xray/segment/${segment.id}/approximate`}
+        href={`/xray/segment/${segment.id}/approximate`}
+        icon="all"
+        name={t`X-ray this segment`}
+      />
+      {user &&
+        user.is_superuser && (
+          <SidebarItem
+            key={`/reference/segments/${segment.id}/revisions`}
+            href={`/reference/segments/${segment.id}/revisions`}
+            icon="history"
+            name={t`Revision history`}
+          />
+        )}
+    </ul>
+  </div>
+);
 
 SegmentSidebar.propTypes = {
-    segment:          PropTypes.object,
-    user:          PropTypes.object,
-    className:      PropTypes.string,
-    style:          PropTypes.object,
+  segment: PropTypes.object,
+  user: PropTypes.object,
+  className: PropTypes.string,
+  style: PropTypes.object,
 };
 
 export default pure(SegmentSidebar);
diff --git a/frontend/src/metabase/reference/selectors.js b/frontend/src/metabase/reference/selectors.js
index d577b12559c0e62c443534f428f59674461ecd71..33e9d7612bc8a4c96699e0a7ab71e22334d9baf2 100644
--- a/frontend/src/metabase/reference/selectors.js
+++ b/frontend/src/metabase/reference/selectors.js
@@ -1,172 +1,211 @@
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 import { assoc, getIn } from "icepick";
 import { getDashboardListing } from "../dashboards/selectors";
 
-import Query, { AggregationClause } from 'metabase/lib/query';
-import {
-    resourceListToMap
-} from 'metabase/lib/redux';
+import Query, { AggregationClause } from "metabase/lib/query";
+import { resourceListToMap } from "metabase/lib/redux";
 
-import {
-    idsToObjectMap,
-    databaseToForeignKeys
-} from "./utils";
+import { idsToObjectMap, databaseToForeignKeys } from "./utils";
 
 // import { getDatabases, getTables, getFields, getMetrics, getSegments } from "metabase/selectors/metadata";
 
 import {
-    getShallowDatabases as getDatabases, getShallowTables as getTables, getShallowFields as getFields,
-    getShallowMetrics as getMetrics, getShallowSegments as getSegments
+  getShallowDatabases as getDatabases,
+  getShallowTables as getTables,
+  getShallowFields as getFields,
+  getShallowMetrics as getMetrics,
+  getShallowSegments as getSegments,
+} from "metabase/selectors/metadata";
+export {
+  getShallowDatabases as getDatabases,
+  getShallowTables as getTables,
+  getShallowFields as getFields,
+  getShallowMetrics as getMetrics,
+  getShallowSegments as getSegments,
 } from "metabase/selectors/metadata";
-export { getShallowDatabases as getDatabases, getShallowTables as getTables, getShallowFields as getFields, getShallowMetrics as getMetrics, getShallowSegments as getSegments } from "metabase/selectors/metadata";
 
 import _ from "underscore";
 
 export const getUser = (state, props) => state.currentUser;
 
-export const getMetricId = (state, props) => Number.parseInt(props.params.metricId);
+export const getMetricId = (state, props) =>
+  Number.parseInt(props.params.metricId);
 export const getMetric = createSelector(
-    [getMetricId, getMetrics],
-    (metricId, metrics) => metrics[metricId] || { id: metricId }
+  [getMetricId, getMetrics],
+  (metricId, metrics) => metrics[metricId] || { id: metricId },
 );
 
-export const getSegmentId = (state, props) => Number.parseInt(props.params.segmentId);
+export const getSegmentId = (state, props) =>
+  Number.parseInt(props.params.segmentId);
 export const getSegment = createSelector(
-    [getSegmentId, getSegments],
-    (segmentId, segments) => segments[segmentId] || { id: segmentId }
+  [getSegmentId, getSegments],
+  (segmentId, segments) => segments[segmentId] || { id: segmentId },
 );
 
-export const getDatabaseId = (state, props) => Number.parseInt(props.params.databaseId);
+export const getDatabaseId = (state, props) =>
+  Number.parseInt(props.params.databaseId);
 
 export const getDatabase = createSelector(
-    [getDatabaseId, getDatabases],
-    (databaseId, databases) => databases[databaseId] || { id: databaseId }
+  [getDatabaseId, getDatabases],
+  (databaseId, databases) => databases[databaseId] || { id: databaseId },
 );
 
-export const getTableId = (state, props) => Number.parseInt(props.params.tableId);
+export const getTableId = (state, props) =>
+  Number.parseInt(props.params.tableId);
 // export const getTableId = (state, props) => Number.parseInt(props.params.tableId);
 export const getTablesByDatabase = createSelector(
-    [getTables, getDatabase],
-    (tables, database) => tables && database && database.tables ?
-        idsToObjectMap(database.tables, tables) : {}
+  [getTables, getDatabase],
+  (tables, database) =>
+    tables && database && database.tables
+      ? idsToObjectMap(database.tables, tables)
+      : {},
 );
 export const getTableBySegment = createSelector(
-    [getSegment, getTables],
-    (segment, tables) => segment && segment.table_id ? tables[segment.table_id] : {}
+  [getSegment, getTables],
+  (segment, tables) =>
+    segment && segment.table_id ? tables[segment.table_id] : {},
 );
 const getTableByMetric = createSelector(
-    [getMetric, getTables],
-    (metric, tables) => metric && metric.table_id ? tables[metric.table_id] : {}
+  [getMetric, getTables],
+  (metric, tables) =>
+    metric && metric.table_id ? tables[metric.table_id] : {},
 );
 export const getTable = createSelector(
-    [getTableId, getTables, getMetricId, getTableByMetric, getSegmentId, getTableBySegment],
-    (tableId, tables, metricId, tableByMetric, segmentId, tableBySegment) => tableId ?
-        tables[tableId] || { id: tableId } :
-        metricId ? tableByMetric :
-            segmentId ? tableBySegment : {}
-);
-
-export const getFieldId = (state, props) => Number.parseInt(props.params.fieldId);
+  [
+    getTableId,
+    getTables,
+    getMetricId,
+    getTableByMetric,
+    getSegmentId,
+    getTableBySegment,
+  ],
+  (tableId, tables, metricId, tableByMetric, segmentId, tableBySegment) =>
+    tableId
+      ? tables[tableId] || { id: tableId }
+      : metricId ? tableByMetric : segmentId ? tableBySegment : {},
+);
+
+export const getFieldId = (state, props) =>
+  Number.parseInt(props.params.fieldId);
 export const getFieldsByTable = createSelector(
-    [getTable, getFields],
-    (table, fields) => table && table.fields ? idsToObjectMap(table.fields, fields) : {}
+  [getTable, getFields],
+  (table, fields) =>
+    table && table.fields ? idsToObjectMap(table.fields, fields) : {},
 );
 export const getFieldsBySegment = createSelector(
-    [getTableBySegment, getFields],
-    (table, fields) => table && table.fields ? idsToObjectMap(table.fields, fields) : {}
+  [getTableBySegment, getFields],
+  (table, fields) =>
+    table && table.fields ? idsToObjectMap(table.fields, fields) : {},
 );
 export const getField = createSelector(
-    [getFieldId, getFields],
-    (fieldId, fields) => fields[fieldId] || { id: fieldId }
+  [getFieldId, getFields],
+  (fieldId, fields) => fields[fieldId] || { id: fieldId },
 );
 export const getFieldBySegment = createSelector(
-    [getFieldId, getFieldsBySegment],
-    (fieldId, fields) => fields[fieldId] || { id: fieldId }
+  [getFieldId, getFieldsBySegment],
+  (fieldId, fields) => fields[fieldId] || { id: fieldId },
 );
 
-const getQuestions = (state, props) => getIn(state, ['questions', 'entities', 'cards']) || {};
+const getQuestions = (state, props) =>
+  getIn(state, ["questions", "entities", "cards"]) || {};
 
 export const getMetricQuestions = createSelector(
-    [getMetricId, getQuestions],
-    (metricId, questions) => Object.values(questions)
-        .filter(question =>
-            question.dataset_query.type === "query" &&
-            _.any(Query.getAggregations(question.dataset_query.query), (aggregation) =>
-                AggregationClause.getMetric(aggregation) === metricId
-            )
-        )
-        .reduce((map, question) => assoc(map, question.id, question), {})
+  [getMetricId, getQuestions],
+  (metricId, questions) =>
+    Object.values(questions)
+      .filter(
+        question =>
+          question.dataset_query.type === "query" &&
+          _.any(
+            Query.getAggregations(question.dataset_query.query),
+            aggregation =>
+              AggregationClause.getMetric(aggregation) === metricId,
+          ),
+      )
+      .reduce((map, question) => assoc(map, question.id, question), {}),
 );
 
 const getRevisions = (state, props) => state.metadata.revisions;
 
 export const getMetricRevisions = createSelector(
-    [getMetricId, getRevisions],
-    (metricId, revisions) => getIn(revisions, ['metric', metricId]) || {}
+  [getMetricId, getRevisions],
+  (metricId, revisions) => getIn(revisions, ["metric", metricId]) || {},
 );
 
 export const getSegmentRevisions = createSelector(
-    [getSegmentId, getRevisions],
-    (segmentId, revisions) => getIn(revisions, ['segment', segmentId]) || {}
+  [getSegmentId, getRevisions],
+  (segmentId, revisions) => getIn(revisions, ["segment", segmentId]) || {},
 );
 
 export const getSegmentQuestions = createSelector(
-    [getSegmentId, getQuestions],
-    (segmentId, questions) => Object.values(questions)
-        .filter(question =>
-            question.dataset_query.type === "query" &&
-            Query.getFilters(question.dataset_query.query)
-                .some(filter => Query.isSegmentFilter(filter) && filter[1] === segmentId)
-        )
-        .reduce((map, question) => assoc(map, question.id, question), {})
+  [getSegmentId, getQuestions],
+  (segmentId, questions) =>
+    Object.values(questions)
+      .filter(
+        question =>
+          question.dataset_query.type === "query" &&
+          Query.getFilters(question.dataset_query.query).some(
+            filter => Query.isSegmentFilter(filter) && filter[1] === segmentId,
+          ),
+      )
+      .reduce((map, question) => assoc(map, question.id, question), {}),
 );
 
 export const getTableQuestions = createSelector(
-    [getTable, getQuestions],
-    (table, questions) => Object.values(questions)
-        .filter(question => question.table_id === table.id)
+  [getTable, getQuestions],
+  (table, questions) =>
+    Object.values(questions).filter(question => question.table_id === table.id),
 );
 
 const getDatabaseBySegment = createSelector(
-    [getSegment, getTables, getDatabases],
-    (segment, tables, databases) => segment && segment.table_id && tables[segment.table_id] &&
-        databases[tables[segment.table_id].db_id] || {}
+  [getSegment, getTables, getDatabases],
+  (segment, tables, databases) =>
+    (segment &&
+      segment.table_id &&
+      tables[segment.table_id] &&
+      databases[tables[segment.table_id].db_id]) ||
+    {},
 );
 
 const getForeignKeysBySegment = createSelector(
-    [getDatabaseBySegment],
-    databaseToForeignKeys
+  [getDatabaseBySegment],
+  databaseToForeignKeys,
 );
 
 const getForeignKeysByDatabase = createSelector(
-    [getDatabase],
-    databaseToForeignKeys
+  [getDatabase],
+  databaseToForeignKeys,
 );
 
 export const getForeignKeys = createSelector(
-    [getSegmentId, getForeignKeysBySegment, getForeignKeysByDatabase],
-    (segmentId, foreignKeysBySegment, foreignKeysByDatabase) => segmentId ?
-        foreignKeysBySegment : foreignKeysByDatabase
-)
+  [getSegmentId, getForeignKeysBySegment, getForeignKeysByDatabase],
+  (segmentId, foreignKeysBySegment, foreignKeysByDatabase) =>
+    segmentId ? foreignKeysBySegment : foreignKeysByDatabase,
+);
 
 export const getLoading = (state, props) => state.reference.isLoading;
 
 export const getError = (state, props) => state.reference.error;
 
 export const getHasSingleSchema = createSelector(
-    [getTablesByDatabase],
-    (tables) => tables && Object.keys(tables).length > 0 ?
-        Object.values(tables)
-            .every((table, index, tables) => table.schema === tables[0].schema) : true
-)
+  [getTablesByDatabase],
+  tables =>
+    tables && Object.keys(tables).length > 0
+      ? Object.values(tables).every(
+          (table, index, tables) => table.schema === tables[0].schema,
+        )
+      : true,
+);
 
 export const getIsEditing = (state, props) => state.reference.isEditing;
 
-export const getIsFormulaExpanded = (state, props) => state.reference.isFormulaExpanded;
+export const getIsFormulaExpanded = (state, props) =>
+  state.reference.isFormulaExpanded;
 
 export const getGuide = (state, props) => state.reference.guide;
 
-export const getDashboards = (state, props) => getDashboardListing(state) && resourceListToMap(getDashboardListing(state));
-
-export const getIsDashboardModalOpen = (state, props) => state.reference.isDashboardModalOpen;
+export const getDashboards = (state, props) =>
+  getDashboardListing(state) && resourceListToMap(getDashboardListing(state));
 
+export const getIsDashboardModalOpen = (state, props) =>
+  state.reference.isDashboardModalOpen;
diff --git a/frontend/src/metabase/reference/utils.js b/frontend/src/metabase/reference/utils.js
index 1057f5c3ca4e9969150d37383ec30fcb82a72f0d..5f69a8ea600082c60768dd1b32d168a673a9f0a6 100644
--- a/frontend/src/metabase/reference/utils.js
+++ b/frontend/src/metabase/reference/utils.js
@@ -5,101 +5,120 @@ import { startNewCard } from "metabase/lib/card";
 import { isPK } from "metabase/lib/types";
 import * as Urls from "metabase/lib/urls";
 
-export const idsToObjectMap = (ids, objects) => ids
+export const idsToObjectMap = (ids, objects) =>
+  ids
     .map(id => objects[id])
     .reduce((map, object) => ({ ...map, [object.id]: object }), {});
-    // recursive freezing done by assoc here is too expensive
-    // hangs browser for large databases
-    // .reduce((map, object) => assoc(map, object.id, object), {});
+// recursive freezing done by assoc here is too expensive
+// hangs browser for large databases
+// .reduce((map, object) => assoc(map, object.id, object), {});
 
-export const filterUntouchedFields = (fields, entity = {}) => Object.keys(fields)
-    .filter(key =>
-        fields[key] !== undefined &&
-        entity[key] !== fields[key]
-    )
+export const filterUntouchedFields = (fields, entity = {}) =>
+  Object.keys(fields)
+    .filter(key => fields[key] !== undefined && entity[key] !== fields[key])
     .reduce((map, key) => ({ ...map, [key]: fields[key] }), {});
 
-export const isEmptyObject = (object) => Object.keys(object).length === 0;
-
+export const isEmptyObject = object => Object.keys(object).length === 0;
 
-export const databaseToForeignKeys = (database) => database && database.tables_lookup ?
-    Object.values(database.tables_lookup)
+export const databaseToForeignKeys = database =>
+  database && database.tables_lookup
+    ? Object.values(database.tables_lookup)
         // ignore tables without primary key
-        .filter(table => table && table.fields.find(field => isPK(field.special_type)))
+        .filter(
+          table =>
+            table && table.fields.find(field => isPK(field.special_type)),
+        )
         .map(table => ({
-            table: table,
-            field: table && table.fields
-                .find(field => isPK(field.special_type))
+          table: table,
+          field: table && table.fields.find(field => isPK(field.special_type)),
         }))
         .map(({ table, field }) => ({
-            id: field.id,
-            name: table.schema && table.schema !== "public" ?
-                `${titleize(humanize(table.schema))}.${table.display_name} → ${field.display_name}` :
-                `${table.display_name} → ${field.display_name}`,
-            description: field.description
+          id: field.id,
+          name:
+            table.schema && table.schema !== "public"
+              ? `${titleize(humanize(table.schema))}.${table.display_name} → ${
+                  field.display_name
+                }`
+              : `${table.display_name} → ${field.display_name}`,
+          description: field.description,
         }))
-        .reduce((map, foreignKey) => assoc(map, foreignKey.id, foreignKey), {}) :
-    {};
+        .reduce((map, foreignKey) => assoc(map, foreignKey.id, foreignKey), {})
+    : {};
 
-export const fieldsToFormFields = (fields) => Object.keys(fields)
+export const fieldsToFormFields = fields =>
+  Object.keys(fields)
     .map(key => [
-        `${key}.display_name`,
-        `${key}.special_type`,
-        `${key}.fk_target_field_id`
+      `${key}.display_name`,
+      `${key}.special_type`,
+      `${key}.fk_target_field_id`,
     ])
     .reduce((array, keys) => array.concat(keys), []);
 
-
 // TODO Atte Keinänen 7/3/17: Construct question with Question of metabase-lib instead of this using function
-export const getQuestion = ({dbId, tableId, fieldId, metricId, segmentId, getCount, visualization, metadata}) => {
-    const newQuestion = startNewCard('query', dbId, tableId);
-
-    // consider taking a look at Ramda as a possible underscore alternative?
-    // http://ramdajs.com/0.21.0/index.html
-    const question = chain(newQuestion)
-        .updateIn(
-            ['dataset_query', 'query', 'aggregation'],
-            aggregation => getCount ? ['count'] : aggregation
-        )
-        .updateIn(['display'], display => visualization || display)
-        .updateIn(
-            ['dataset_query', 'query', 'breakout'],
-            (oldBreakout) => {
-                if (fieldId && metadata && metadata.fields[fieldId]) return [metadata.fields[fieldId].getDefaultBreakout()]
-                if (fieldId) return [fieldId];
-                return oldBreakout;
-            }
-        )
-        .value();
-
-    if (metricId) {
-        return assocIn(question, ['dataset_query', 'query', 'aggregation'], ['METRIC', metricId]);
-    }
-
-    if (segmentId) {
-        return assocIn(question, ['dataset_query', 'query', 'filter'], ['AND', ['SEGMENT', segmentId]]);
-    }
-
-    return question;
+export const getQuestion = ({
+  dbId,
+  tableId,
+  fieldId,
+  metricId,
+  segmentId,
+  getCount,
+  visualization,
+  metadata,
+}) => {
+  const newQuestion = startNewCard("query", dbId, tableId);
+
+  // consider taking a look at Ramda as a possible underscore alternative?
+  // http://ramdajs.com/0.21.0/index.html
+  const question = chain(newQuestion)
+    .updateIn(
+      ["dataset_query", "query", "aggregation"],
+      aggregation => (getCount ? ["count"] : aggregation),
+    )
+    .updateIn(["display"], display => visualization || display)
+    .updateIn(["dataset_query", "query", "breakout"], oldBreakout => {
+      if (fieldId && metadata && metadata.fields[fieldId])
+        return [metadata.fields[fieldId].getDefaultBreakout()];
+      if (fieldId) return [fieldId];
+      return oldBreakout;
+    })
+    .value();
+
+  if (metricId) {
+    return assocIn(
+      question,
+      ["dataset_query", "query", "aggregation"],
+      ["METRIC", metricId],
+    );
+  }
+
+  if (segmentId) {
+    return assocIn(
+      question,
+      ["dataset_query", "query", "filter"],
+      ["AND", ["SEGMENT", segmentId]],
+    );
+  }
+
+  return question;
 };
 
-export const getQuestionUrl = getQuestionArgs => Urls.question(null, getQuestion(getQuestionArgs));
+export const getQuestionUrl = getQuestionArgs =>
+  Urls.question(null, getQuestion(getQuestionArgs));
 
 export const typeToLinkClass = {
-    dashboard: 'text-green',
-    metric: 'text-brand',
-    segment: 'text-purple',
-    table: 'text-purple'
+  dashboard: "text-green",
+  metric: "text-brand",
+  segment: "text-purple",
+  table: "text-purple",
 };
 
 export const typeToBgClass = {
-    dashboard: 'bg-green',
-    metric: 'bg-brand',
-    segment: 'bg-purple',
-    table: 'bg-purple'
+  dashboard: "bg-green",
+  metric: "bg-brand",
+  segment: "bg-purple",
+  table: "bg-purple",
 };
 
-
 // little utility function to determine if we 'has' things, useful
 // for handling entity empty states
-export const has = (entity) => entity && entity.length > 0;
+export const has = entity => entity && entity.length > 0;
diff --git a/frontend/src/metabase/routes-embed.jsx b/frontend/src/metabase/routes-embed.jsx
index 6697785f74855e4906bdd4925979c69bc472c387..0f2628b0a37db4d9055a0c863613f6f4f5136b76 100644
--- a/frontend/src/metabase/routes-embed.jsx
+++ b/frontend/src/metabase/routes-embed.jsx
@@ -2,7 +2,7 @@
 
 import React from "react";
 
-import { Route } from 'react-router';
+import { Route } from "react-router";
 
 import PublicNotFound from "metabase/public/components/PublicNotFound";
 
@@ -10,12 +10,13 @@ import PublicApp from "metabase/public/containers/PublicApp.jsx";
 import PublicQuestion from "metabase/public/containers/PublicQuestion.jsx";
 import PublicDashboard from "metabase/public/containers/PublicDashboard.jsx";
 
-export const getRoutes = (store) =>
-    <Route>
-        <Route path="embed" component={PublicApp}>
-            <Route path="question/:token" component={PublicQuestion} />
-            <Route path="dashboard/:token" component={PublicDashboard} />
-            <Route path="*" component={PublicNotFound} />
-        </Route>
-        <Route path="*" component={PublicNotFound} />
+export const getRoutes = store => (
+  <Route>
+    <Route path="embed" component={PublicApp}>
+      <Route path="question/:token" component={PublicQuestion} />
+      <Route path="dashboard/:token" component={PublicDashboard} />
+      <Route path="*" component={PublicNotFound} />
     </Route>
+    <Route path="*" component={PublicNotFound} />
+  </Route>
+);
diff --git a/frontend/src/metabase/routes-public.jsx b/frontend/src/metabase/routes-public.jsx
index f40e59cec01b8a3ca84cb667d5d49f9526a57be4..194ed7c61a1fb45f73d9826be55dc1ead91cf1e3 100644
--- a/frontend/src/metabase/routes-public.jsx
+++ b/frontend/src/metabase/routes-public.jsx
@@ -2,7 +2,7 @@
 
 import React from "react";
 
-import { Route } from 'react-router';
+import { Route } from "react-router";
 
 import PublicNotFound from "metabase/public/components/PublicNotFound";
 
@@ -10,12 +10,13 @@ import PublicApp from "metabase/public/containers/PublicApp.jsx";
 import PublicQuestion from "metabase/public/containers/PublicQuestion.jsx";
 import PublicDashboard from "metabase/public/containers/PublicDashboard.jsx";
 
-export const getRoutes = (store) =>
-    <Route>
-        <Route path="public" component={PublicApp}>
-            <Route path="question/:uuid" component={PublicQuestion} />
-            <Route path="dashboard/:uuid" component={PublicDashboard} />
-            <Route path="*" component={PublicNotFound} />
-        </Route>
-        <Route path="*" component={PublicNotFound} />
+export const getRoutes = store => (
+  <Route>
+    <Route path="public" component={PublicApp}>
+      <Route path="question/:uuid" component={PublicQuestion} />
+      <Route path="dashboard/:uuid" component={PublicDashboard} />
+      <Route path="*" component={PublicNotFound} />
     </Route>
+    <Route path="*" component={PublicNotFound} />
+  </Route>
+);
diff --git a/frontend/src/metabase/routes.jsx b/frontend/src/metabase/routes.jsx
index c377a7963a3ae7ea967ec098460cf0029b81086e..5db1d0bdb2a73b2902bf3914fbfc7a42a53d637a 100644
--- a/frontend/src/metabase/routes.jsx
+++ b/frontend/src/metabase/routes.jsx
@@ -3,10 +3,10 @@
 import React from "react";
 
 import { Route } from "metabase/hoc/Title";
-import { Redirect, IndexRedirect, IndexRoute } from 'react-router';
-import { routerActions } from 'react-router-redux';
-import { UserAuthWrapper } from 'redux-auth-wrapper';
-import { t } from 'c-3po'
+import { Redirect, IndexRedirect, IndexRoute } from "react-router";
+import { routerActions } from "react-router-redux";
+import { UserAuthWrapper } from "redux-auth-wrapper";
+import { t } from "c-3po";
 
 import { loadCurrentUser } from "metabase/redux/user";
 import MetabaseSettings from "metabase/lib/settings";
@@ -43,7 +43,10 @@ import SetupApp from "metabase/setup/containers/SetupApp.jsx";
 import UserSettingsApp from "metabase/user/containers/UserSettingsApp.jsx";
 
 // new question
-import { NewQuestionStart, NewQuestionMetricSearch } from "metabase/new_query/router_wrappers";
+import {
+  NewQuestionStart,
+  NewQuestionMetricSearch,
+} from "metabase/new_query/router_wrappers";
 
 // admin containers
 import DatabaseListApp from "metabase/admin/databases/containers/DatabaseListApp.jsx";
@@ -54,7 +57,7 @@ import SegmentApp from "metabase/admin/datamodel/containers/SegmentApp.jsx";
 import RevisionHistoryApp from "metabase/admin/datamodel/containers/RevisionHistoryApp.jsx";
 import AdminPeopleApp from "metabase/admin/people/containers/AdminPeopleApp.jsx";
 import SettingsEditorApp from "metabase/admin/settings/containers/SettingsEditorApp.jsx";
-import FieldApp from "metabase/admin/datamodel/containers/FieldApp.jsx"
+import FieldApp from "metabase/admin/datamodel/containers/FieldApp.jsx";
 import TableSettingsApp from "metabase/admin/datamodel/containers/TableSettingsApp.jsx";
 
 import NotFound from "metabase/components/NotFound.jsx";
@@ -83,259 +86,379 @@ import TableQuestionsContainer from "metabase/reference/databases/TableQuestions
 import FieldListContainer from "metabase/reference/databases/FieldListContainer.jsx";
 import FieldDetailContainer from "metabase/reference/databases/FieldDetailContainer.jsx";
 
-
 /* XRay */
 import FieldXRay from "metabase/xray/containers/FieldXray.jsx";
 import TableXRay from "metabase/xray/containers/TableXRay.jsx";
 import SegmentXRay from "metabase/xray/containers/SegmentXRay.jsx";
 import CardXRay from "metabase/xray/containers/CardXRay.jsx";
-import { SharedTypeComparisonXRay, TwoTypesComparisonXRay } from "metabase/xray/containers/TableLikeComparison";
+import {
+  SharedTypeComparisonXRay,
+  TwoTypesComparisonXRay,
+} from "metabase/xray/containers/TableLikeComparison";
 
 import getAdminPermissionsRoutes from "metabase/admin/permissions/routes.jsx";
 
-
 import PeopleListingApp from "metabase/admin/people/containers/PeopleListingApp.jsx";
 import GroupsListingApp from "metabase/admin/people/containers/GroupsListingApp.jsx";
 import GroupDetailApp from "metabase/admin/people/containers/GroupDetailApp.jsx";
 
 import PublicQuestion from "metabase/public/containers/PublicQuestion.jsx";
 import PublicDashboard from "metabase/public/containers/PublicDashboard.jsx";
+import { DashboardHistoryModal } from "metabase/dashboard/components/DashboardHistoryModal";
+import { ModalRoute } from "metabase/hoc/ModalRoute";
 
 const MetabaseIsSetup = UserAuthWrapper({
-    predicate: authData => !authData.hasSetupToken,
-    failureRedirectPath: "/setup",
-    authSelector: state => ({ hasSetupToken: MetabaseSettings.hasSetupToken() }), // HACK
-    wrapperDisplayName: 'MetabaseIsSetup',
-    allowRedirectBack: false,
-    redirectAction: routerActions.replace,
+  predicate: authData => !authData.hasSetupToken,
+  failureRedirectPath: "/setup",
+  authSelector: state => ({ hasSetupToken: MetabaseSettings.hasSetupToken() }), // HACK
+  wrapperDisplayName: "MetabaseIsSetup",
+  allowRedirectBack: false,
+  redirectAction: routerActions.replace,
 });
 
 const UserIsAuthenticated = UserAuthWrapper({
-    failureRedirectPath: '/auth/login',
-    authSelector: state => state.currentUser,
-    wrapperDisplayName: 'UserIsAuthenticated',
-    redirectAction: (location) =>
-        // HACK: workaround for redux-auth-wrapper not including hash
-        // https://github.com/mjrussell/redux-auth-wrapper/issues/121
-        routerActions.replace({
-            ...location,
-            query: {
-                ...location.query,
-                redirect: location.query.redirect + (window.location.hash || "")
-            }
-        })
+  failureRedirectPath: "/auth/login",
+  authSelector: state => state.currentUser,
+  wrapperDisplayName: "UserIsAuthenticated",
+  redirectAction: location =>
+    // HACK: workaround for redux-auth-wrapper not including hash
+    // https://github.com/mjrussell/redux-auth-wrapper/issues/121
+    routerActions.replace({
+      ...location,
+      query: {
+        ...location.query,
+        redirect: location.query.redirect + (window.location.hash || ""),
+      },
+    }),
 });
 
 const UserIsAdmin = UserAuthWrapper({
-    predicate: currentUser => currentUser && currentUser.is_superuser,
-    failureRedirectPath: '/unauthorized',
-    authSelector: state => state.currentUser,
-    allowRedirectBack: false,
-    wrapperDisplayName: 'UserIsAdmin',
-    redirectAction: routerActions.replace,
+  predicate: currentUser => currentUser && currentUser.is_superuser,
+  failureRedirectPath: "/unauthorized",
+  authSelector: state => state.currentUser,
+  allowRedirectBack: false,
+  wrapperDisplayName: "UserIsAdmin",
+  redirectAction: routerActions.replace,
 });
 
 const UserIsNotAuthenticated = UserAuthWrapper({
-    predicate: currentUser => !currentUser,
-    failureRedirectPath: '/',
-    authSelector: state => state.currentUser,
-    allowRedirectBack: false,
-    wrapperDisplayName: 'UserIsNotAuthenticated',
-    redirectAction: routerActions.replace,
+  predicate: currentUser => !currentUser,
+  failureRedirectPath: "/",
+  authSelector: state => state.currentUser,
+  allowRedirectBack: false,
+  wrapperDisplayName: "UserIsNotAuthenticated",
+  redirectAction: routerActions.replace,
 });
 
-const IsAuthenticated = MetabaseIsSetup(UserIsAuthenticated(({ children }) => children));
-const IsAdmin = MetabaseIsSetup(UserIsAuthenticated(UserIsAdmin(({ children }) => children)));
-const IsNotAuthenticated = MetabaseIsSetup(UserIsNotAuthenticated(({ children }) => children));
-
-export const getRoutes = (store) =>
-    <Route title="Metabase" component={App}>
-        {/* SETUP */}
-        <Route path="/setup" component={SetupApp} onEnter={(nextState, replace) => {
-            if (!MetabaseSettings.hasSetupToken()) {
-                replace("/");
-            }
-        }} />
-
-        {/* PUBLICLY SHARED LINKS */}
-        <Route path="public">
-            <Route path="question/:uuid" component={PublicQuestion} />
-            <Route path="dashboard/:uuid" component={PublicDashboard} />
+const IsAuthenticated = MetabaseIsSetup(
+  UserIsAuthenticated(({ children }) => children),
+);
+const IsAdmin = MetabaseIsSetup(
+  UserIsAuthenticated(UserIsAdmin(({ children }) => children)),
+);
+const IsNotAuthenticated = MetabaseIsSetup(
+  UserIsNotAuthenticated(({ children }) => children),
+);
+
+export const getRoutes = store => (
+  <Route title="Metabase" component={App}>
+    {/* SETUP */}
+    <Route
+      path="/setup"
+      component={SetupApp}
+      onEnter={(nextState, replace) => {
+        if (!MetabaseSettings.hasSetupToken()) {
+          replace("/");
+        }
+      }}
+    />
+
+    {/* PUBLICLY SHARED LINKS */}
+    <Route path="public">
+      <Route path="question/:uuid" component={PublicQuestion} />
+      <Route path="dashboard/:uuid" component={PublicDashboard} />
+    </Route>
+
+    {/* APP */}
+    <Route
+      onEnter={async (nextState, replace, done) => {
+        await store.dispatch(loadCurrentUser());
+        done();
+      }}
+    >
+      {/* AUTH */}
+      <Route path="/auth">
+        <IndexRedirect to="/auth/login" />
+        <Route component={IsNotAuthenticated}>
+          <Route path="login" title={t`Login`} component={LoginApp} />
+        </Route>
+        <Route path="logout" component={LogoutApp} />
+        <Route path="forgot_password" component={ForgotPasswordApp} />
+        <Route path="reset_password/:token" component={PasswordResetApp} />
+        <Route path="google_no_mb_account" component={GoogleNoAccount} />
+      </Route>
+
+      {/* MAIN */}
+      <Route component={IsAuthenticated}>
+        {/* HOME */}
+        <Route path="/" component={HomepageApp} />
+
+        {/* DASHBOARD LIST */}
+        <Route
+          path="/dashboards"
+          title={t`Dashboards`}
+          component={Dashboards}
+        />
+        <Route
+          path="/dashboards/archive"
+          title={t`Dashboards`}
+          component={DashboardsArchive}
+        />
+
+        {/* INDIVIDUAL DASHBOARDS */}
+        <Route
+          path="/dashboard/:dashboardId"
+          title={t`Dashboard`}
+          component={DashboardApp}
+        >
+          <ModalRoute path="history" modal={DashboardHistoryModal} />
         </Route>
 
-        {/* APP */}
-        <Route onEnter={async (nextState, replace, done) => {
-            await store.dispatch(loadCurrentUser());
-            done();
-        }}>
-            {/* AUTH */}
-            <Route path="/auth">
-                <IndexRedirect to="/auth/login" />
-                <Route component={IsNotAuthenticated}>
-                    <Route path="login" title={t`Login`} component={LoginApp} />
-                </Route>
-                <Route path="logout" component={LogoutApp} />
-                <Route path="forgot_password" component={ForgotPasswordApp} />
-                <Route path="reset_password/:token" component={PasswordResetApp} />
-                <Route path="google_no_mb_account" component={GoogleNoAccount} />
-            </Route>
-
-            {/* MAIN */}
-            <Route component={IsAuthenticated}>
-                {/* HOME */}
-                <Route path="/" component={HomepageApp} />
-
-                {/* DASHBOARD LIST */}
-                <Route path="/dashboards" title={t`Dashboards`} component={Dashboards} />
-                <Route path="/dashboards/archive" title={t`Dashboards`} component={DashboardsArchive} />
-
-                {/* INDIVIDUAL DASHBOARDS */}
-                <Route path="/dashboard/:dashboardId" title={t`Dashboard`} component={DashboardApp} />
-
-                {/* QUERY BUILDER */}
-                <Route path="/question">
-                    <IndexRoute component={QueryBuilder} />
-                    { /* NEW QUESTION FLOW */ }
-                    <Route path="new" title={t`New Question`}>
-                        <IndexRoute component={NewQuestionStart} />
-                        <Route path="metric" title={t`Metrics`} component={NewQuestionMetricSearch} />
-                    </Route>
-                </Route>
-                <Route path="/question/:cardId" component={QueryBuilder} />
-
-                {/* QUESTIONS */}
-                <Route path="/questions" title={t`Questions`}>
-                    <IndexRoute component={QuestionIndex} />
-                    <Route path="search" title={({ location: { query: { q } }}) => t`Search` + ": " + q} component={SearchResults} />
-                    <Route path="archive" title={t`Archive`} component={Archive} />
-                    <Route path="collections/:collectionSlug" component={CollectionPage} />
-                </Route>
-
-                <Route path="/entities/:entityType" component={({ location, params }) =>
-                    <div className="p4">
-                        <EntityList entityType={params.entityType} entityQuery={location.query} />
-                    </div>
-                }/>
-
-                <Route path="/collections">
-                    <Route path="create" component={CollectionCreate} />
-                    <Route path="permissions" component={CollectionPermissions} />
-                    <Route path=":collectionId" component={CollectionEdit} />
-                </Route>
-
-                <Route path="/labels">
-                    <IndexRoute component={EditLabels} />
-                </Route>
-
-                {/* REFERENCE */}
-                <Route path="/reference" title={`Data Reference`}>
-                    <IndexRedirect to="/reference/guide" />
-                    <Route path="guide" title={`Getting Started`} component={GettingStartedGuideContainer} />
-                    <Route path="metrics" component={MetricListContainer} />
-                    <Route path="metrics/:metricId" component={MetricDetailContainer} />
-                    <Route path="metrics/:metricId/questions" component={MetricQuestionsContainer} />
-                    <Route path="metrics/:metricId/revisions" component={MetricRevisionsContainer} />
-                    <Route path="segments" component={SegmentListContainer} />
-                    <Route path="segments/:segmentId" component={SegmentDetailContainer} />
-                    <Route path="segments/:segmentId/fields" component={SegmentFieldListContainer} />
-                    <Route path="segments/:segmentId/fields/:fieldId" component={SegmentFieldDetailContainer} />
-                    <Route path="segments/:segmentId/questions" component={SegmentQuestionsContainer} />
-                    <Route path="segments/:segmentId/revisions" component={SegmentRevisionsContainer} />
-                    <Route path="databases" component={DatabaseListContainer} />
-                    <Route path="databases/:databaseId" component={DatabaseDetailContainer} />
-                    <Route path="databases/:databaseId/tables" component={TableListContainer} />
-                    <Route path="databases/:databaseId/tables/:tableId" component={TableDetailContainer} />
-                    <Route path="databases/:databaseId/tables/:tableId/fields" component={FieldListContainer} />
-                    <Route path="databases/:databaseId/tables/:tableId/fields/:fieldId" component={FieldDetailContainer} />
-                    <Route path="databases/:databaseId/tables/:tableId/questions" component={TableQuestionsContainer} />
-                </Route>
-
-                {/* XRAY */}
-                <Route path="/xray" title={t`XRay`}>
-                    <Route path="segment/:segmentId/:cost" component={SegmentXRay} />
-                    <Route path="table/:tableId/:cost" component={TableXRay} />
-                    <Route path="field/:fieldId/:cost" component={FieldXRay} />
-                    <Route path="card/:cardId/:cost" component={CardXRay} />
-                    <Route path="compare/:modelTypePlural/:modelId1/:modelId2/:cost" component={SharedTypeComparisonXRay} />
-                    <Route path="compare/:modelType1/:modelId1/:modelType2/:modelId2/:cost" component={TwoTypesComparisonXRay} />
-                </Route>
-
-                {/* PULSE */}
-                <Route path="/pulse" title={t`Pulses`}>
-                    <IndexRoute component={PulseListApp} />
-                    <Route path="create" component={PulseEditApp} />
-                    <Route path=":pulseId" component={PulseEditApp} />
-                </Route>
-
-                {/* USER */}
-                <Route path="/user/edit_current" component={UserSettingsApp} />
-            </Route>
-
-            {/* ADMIN */}
-            <Route path="/admin" title={t`Admin`} component={IsAdmin}>
-                <IndexRedirect to="/admin/settings" />
-
-                <Route path="databases" title={t`Databases`}>
-                    <IndexRoute component={DatabaseListApp} />
-                    <Route path="create" component={DatabaseEditApp} />
-                    <Route path=":databaseId" component={DatabaseEditApp} />
-                </Route>
-
-                <Route path="datamodel" title={t`Data Model`}>
-                    <IndexRedirect to="database" />
-                    <Route path="database" component={MetadataEditorApp} />
-                    <Route path="database/:databaseId" component={MetadataEditorApp} />
-                    <Route path="database/:databaseId/:mode" component={MetadataEditorApp} />
-                    <Route path="database/:databaseId/:mode/:tableId" component={MetadataEditorApp} />
-                    <Route path="database/:databaseId/:mode/:tableId/settings" component={TableSettingsApp} />
-                    <Route path="database/:databaseId/:mode/:tableId/:fieldId" component={FieldApp} />
-                    <Route path="metric/create" component={MetricApp} />
-                    <Route path="metric/:id" component={MetricApp} />
-                    <Route path="segment/create" component={SegmentApp} />
-                    <Route path="segment/:id" component={SegmentApp} />
-                    <Route path=":entity/:id/revisions" component={RevisionHistoryApp} />
-                </Route>
-
-                {/* PEOPLE */}
-                <Route path="people" title={t`People`} component={AdminPeopleApp}>
-                    <IndexRoute component={PeopleListingApp} />
-                    <Route path="groups" title={t`Groups`}>
-                        <IndexRoute component={GroupsListingApp} />
-                        <Route path=":groupId" component={GroupDetailApp} />
-                    </Route>
-                </Route>
-
-                {/* SETTINGS */}
-                <Route path="settings" title={t`Settings`}>
-                    <IndexRedirect to="/admin/settings/setup" />
-                    {/* <IndexRoute component={SettingsEditorApp} /> */}
-                    <Route path=":section/:authType" component={SettingsEditorApp} />
-                    <Route path=":section" component={SettingsEditorApp} />
-                </Route>
-
-                {getAdminPermissionsRoutes(store)}
-            </Route>
-
-            {/* INTERNAL */}
+        {/* QUERY BUILDER */}
+        <Route path="/question">
+          <IndexRoute component={QueryBuilder} />
+          {/* NEW QUESTION FLOW */}
+          <Route path="new" title={t`New Question`}>
+            <IndexRoute component={NewQuestionStart} />
             <Route
-                path="/_internal"
-                getChildRoutes={(partialNextState, callback) =>
-                    // $FlowFixMe: flow doesn't know about require.ensure
-                    require.ensure([], (require) => {
-                        callback(null, [require("metabase/internal/routes").default])
-                    })
-                }
-            >
-            </Route>
-
-            {/* DEPRECATED */}
-            {/* 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 */}
-            <Route path="/unauthorized" component={Unauthorized} />
-            <Route path="/*" component={NotFound} />
+              path="metric"
+              title={t`Metrics`}
+              component={NewQuestionMetricSearch}
+            />
+          </Route>
+        </Route>
+        <Route path="/question/:cardId" component={QueryBuilder} />
+
+        {/* QUESTIONS */}
+        <Route path="/questions" title={t`Questions`}>
+          <IndexRoute component={QuestionIndex} />
+          <Route
+            path="search"
+            title={({ location: { query: { q } } }) => t`Search` + ": " + q}
+            component={SearchResults}
+          />
+          <Route path="archive" title={t`Archive`} component={Archive} />
+          <Route
+            path="collections/:collectionSlug"
+            component={CollectionPage}
+          />
+        </Route>
+
+        <Route
+          path="/entities/:entityType"
+          component={({ location, params }) => (
+            <div className="p4">
+              <EntityList
+                entityType={params.entityType}
+                entityQuery={location.query}
+              />
+            </div>
+          )}
+        />
+
+        <Route path="/collections">
+          <Route path="create" component={CollectionCreate} />
+          <Route path="permissions" component={CollectionPermissions} />
+          <Route path=":collectionId" component={CollectionEdit} />
+        </Route>
+
+        <Route path="/labels">
+          <IndexRoute component={EditLabels} />
+        </Route>
+
+        {/* REFERENCE */}
+        <Route path="/reference" title={`Data Reference`}>
+          <IndexRedirect to="/reference/guide" />
+          <Route
+            path="guide"
+            title={`Getting Started`}
+            component={GettingStartedGuideContainer}
+          />
+          <Route path="metrics" component={MetricListContainer} />
+          <Route path="metrics/:metricId" component={MetricDetailContainer} />
+          <Route
+            path="metrics/:metricId/questions"
+            component={MetricQuestionsContainer}
+          />
+          <Route
+            path="metrics/:metricId/revisions"
+            component={MetricRevisionsContainer}
+          />
+          <Route path="segments" component={SegmentListContainer} />
+          <Route
+            path="segments/:segmentId"
+            component={SegmentDetailContainer}
+          />
+          <Route
+            path="segments/:segmentId/fields"
+            component={SegmentFieldListContainer}
+          />
+          <Route
+            path="segments/:segmentId/fields/:fieldId"
+            component={SegmentFieldDetailContainer}
+          />
+          <Route
+            path="segments/:segmentId/questions"
+            component={SegmentQuestionsContainer}
+          />
+          <Route
+            path="segments/:segmentId/revisions"
+            component={SegmentRevisionsContainer}
+          />
+          <Route path="databases" component={DatabaseListContainer} />
+          <Route
+            path="databases/:databaseId"
+            component={DatabaseDetailContainer}
+          />
+          <Route
+            path="databases/:databaseId/tables"
+            component={TableListContainer}
+          />
+          <Route
+            path="databases/:databaseId/tables/:tableId"
+            component={TableDetailContainer}
+          />
+          <Route
+            path="databases/:databaseId/tables/:tableId/fields"
+            component={FieldListContainer}
+          />
+          <Route
+            path="databases/:databaseId/tables/:tableId/fields/:fieldId"
+            component={FieldDetailContainer}
+          />
+          <Route
+            path="databases/:databaseId/tables/:tableId/questions"
+            component={TableQuestionsContainer}
+          />
+        </Route>
+
+        {/* XRAY */}
+        <Route path="/xray" title={t`XRay`}>
+          <Route path="segment/:segmentId/:cost" component={SegmentXRay} />
+          <Route path="table/:tableId/:cost" component={TableXRay} />
+          <Route path="field/:fieldId/:cost" component={FieldXRay} />
+          <Route path="card/:cardId/:cost" component={CardXRay} />
+          <Route
+            path="compare/:modelTypePlural/:modelId1/:modelId2/:cost"
+            component={SharedTypeComparisonXRay}
+          />
+          <Route
+            path="compare/:modelType1/:modelId1/:modelType2/:modelId2/:cost"
+            component={TwoTypesComparisonXRay}
+          />
         </Route>
+
+        {/* PULSE */}
+        <Route path="/pulse" title={t`Pulses`}>
+          <IndexRoute component={PulseListApp} />
+          <Route path="create" component={PulseEditApp} />
+          <Route path=":pulseId" component={PulseEditApp} />
+        </Route>
+
+        {/* USER */}
+        <Route path="/user/edit_current" component={UserSettingsApp} />
+      </Route>
+
+      {/* ADMIN */}
+      <Route path="/admin" title={t`Admin`} component={IsAdmin}>
+        <IndexRedirect to="/admin/settings" />
+
+        <Route path="databases" title={t`Databases`}>
+          <IndexRoute component={DatabaseListApp} />
+          <Route path="create" component={DatabaseEditApp} />
+          <Route path=":databaseId" component={DatabaseEditApp} />
+        </Route>
+
+        <Route path="datamodel" title={t`Data Model`}>
+          <IndexRedirect to="database" />
+          <Route path="database" component={MetadataEditorApp} />
+          <Route path="database/:databaseId" component={MetadataEditorApp} />
+          <Route
+            path="database/:databaseId/:mode"
+            component={MetadataEditorApp}
+          />
+          <Route
+            path="database/:databaseId/:mode/:tableId"
+            component={MetadataEditorApp}
+          />
+          <Route
+            path="database/:databaseId/:mode/:tableId/settings"
+            component={TableSettingsApp}
+          />
+          <Route
+            path="database/:databaseId/:mode/:tableId/:fieldId"
+            component={FieldApp}
+          />
+          <Route path="metric/create" component={MetricApp} />
+          <Route path="metric/:id" component={MetricApp} />
+          <Route path="segment/create" component={SegmentApp} />
+          <Route path="segment/:id" component={SegmentApp} />
+          <Route path=":entity/:id/revisions" component={RevisionHistoryApp} />
+        </Route>
+
+        {/* PEOPLE */}
+        <Route path="people" title={t`People`} component={AdminPeopleApp}>
+          <IndexRoute component={PeopleListingApp} />
+          <Route path="groups" title={t`Groups`}>
+            <IndexRoute component={GroupsListingApp} />
+            <Route path=":groupId" component={GroupDetailApp} />
+          </Route>
+        </Route>
+
+        {/* SETTINGS */}
+        <Route path="settings" title={t`Settings`}>
+          <IndexRedirect to="/admin/settings/setup" />
+          {/* <IndexRoute component={SettingsEditorApp} /> */}
+          <Route path=":section/:authType" component={SettingsEditorApp} />
+          <Route path=":section" component={SettingsEditorApp} />
+        </Route>
+
+        {getAdminPermissionsRoutes(store)}
+      </Route>
+
+      {/* INTERNAL */}
+      <Route
+        path="/_internal"
+        getChildRoutes={(partialNextState, callback) =>
+          // $FlowFixMe: flow doesn't know about require.ensure
+          require.ensure([], require => {
+            callback(null, [require("metabase/internal/routes").default]);
+          })
+        }
+      />
+
+      {/* DEPRECATED */}
+      {/* 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 */}
+      <Route path="/unauthorized" component={Unauthorized} />
+      <Route path="/*" component={NotFound} />
     </Route>
+  </Route>
+);
diff --git a/frontend/src/metabase/schema.js b/frontend/src/metabase/schema.js
index 70803fc5eea52ea842d07764b444974235be84a9..2a2b61874945e6cd547b04fcbb6bac8a471a16fe 100644
--- a/frontend/src/metabase/schema.js
+++ b/frontend/src/metabase/schema.js
@@ -1,34 +1,33 @@
-
 // normalizr schema for use in actions/reducers
 
 import { schema } from "normalizr";
 
-export const DatabaseSchema = new schema.Entity('databases');
-export const TableSchema = new schema.Entity('tables');
-export const FieldSchema = new schema.Entity('fields');
-export const SegmentSchema = new schema.Entity('segments');
-export const MetricSchema = new schema.Entity('metrics');
+export const DatabaseSchema = new schema.Entity("databases");
+export const TableSchema = new schema.Entity("tables");
+export const FieldSchema = new schema.Entity("fields");
+export const SegmentSchema = new schema.Entity("segments");
+export const MetricSchema = new schema.Entity("metrics");
 
 DatabaseSchema.define({
-    tables: [TableSchema]
+  tables: [TableSchema],
 });
 
 TableSchema.define({
-    db: DatabaseSchema,
-    fields: [FieldSchema],
-    segments: [SegmentSchema],
-    metrics: [MetricSchema]
+  db: DatabaseSchema,
+  fields: [FieldSchema],
+  segments: [SegmentSchema],
+  metrics: [MetricSchema],
 });
 
 FieldSchema.define({
-    target: FieldSchema,
-    table: TableSchema,
+  target: FieldSchema,
+  table: TableSchema,
 });
 
 SegmentSchema.define({
-    table: TableSchema,
+  table: TableSchema,
 });
 
 MetricSchema.define({
-    table: TableSchema,
+  table: TableSchema,
 });
diff --git a/frontend/src/metabase/selectors/app.js b/frontend/src/metabase/selectors/app.js
index 65f2802539e56d92fc0b8a733b1b748322d1e63c..1c29ae10eccdb761767bac6641a53a5e2ed101f9 100644
--- a/frontend/src/metabase/selectors/app.js
+++ b/frontend/src/metabase/selectors/app.js
@@ -1,3 +1,4 @@
-export const getErrorMessage = (state) =>
-    state.app.errorPage && state.app.errorPage.data &&
-    (state.app.errorPage.data.message || state.app.errorPage.data);
+export const getErrorMessage = state =>
+  state.app.errorPage &&
+  state.app.errorPage.data &&
+  (state.app.errorPage.data.message || state.app.errorPage.data);
diff --git a/frontend/src/metabase/selectors/metadata.js b/frontend/src/metabase/selectors/metadata.js
index 426d5d5e75ebf26b9661579b7b9bfd4e158765ca..acb3dce8da15f26be3c9d25b9def6748fe2678af 100644
--- a/frontend/src/metabase/selectors/metadata.js
+++ b/frontend/src/metabase/selectors/metadata.js
@@ -1,6 +1,10 @@
 /* @flow weak */
 
-import { createSelector, createSelectorCreator, defaultMemoize } from "reselect";
+import {
+  createSelector,
+  createSelectorCreator,
+  defaultMemoize,
+} from "reselect";
 
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 import Database from "metabase-lib/lib/metadata/Database";
@@ -11,12 +15,12 @@ import Segment from "metabase-lib/lib/metadata/Segment";
 
 import _ from "underscore";
 import { shallowEqual } from "recompose";
-import { getFieldValues } from "metabase/lib/query/field";
+import { getFieldValues, getRemappings } from "metabase/lib/query/field";
 
 import {
-    getOperators,
-    getBreakouts,
-    getAggregatorsWithFields
+  getOperators,
+  getBreakouts,
+  getAggregatorsWithFields,
 } from "metabase/lib/schema_metadata";
 import { getIn } from "icepick";
 
@@ -29,7 +33,8 @@ export const getNormalizedFields = state => state.metadata.fields;
 export const getNormalizedMetrics = state => state.metadata.metrics;
 export const getNormalizedSegments = state => state.metadata.segments;
 
-export const getMetadataFetched = state => state.requests.fetched.metadata || {}
+export const getMetadataFetched = state =>
+  state.requests.fetched.metadata || {};
 
 // TODO: these should be denomalized but non-cylical, and only to the same "depth" previous "tableMetadata" was, e.x.
 //
@@ -57,72 +62,74 @@ export const getShallowSegments = getNormalizedSegments;
 
 // fully connected graph of all databases, tables, fields, segments, and metrics
 export const getMetadata = createSelector(
-    [
-        getNormalizedDatabases,
-        getNormalizedTables,
-        getNormalizedFields,
-        getNormalizedSegments,
-        getNormalizedMetrics
-    ],
-    (databases, tables, fields, segments, metrics): Metadata => {
-        const meta = new Metadata();
-        meta.databases = copyObjects(meta, databases, Database)
-        meta.tables    = copyObjects(meta, tables, Table)
-        meta.fields    = copyObjects(meta, fields, Field)
-        meta.segments  = copyObjects(meta, segments, Segment)
-        meta.metrics   = copyObjects(meta, metrics, Metric)
-        // meta.loaded    = getLoadedStatuses(requestStates)
-
-        hydrateList(meta.databases, "tables", meta.tables);
-
-        hydrateList(meta.tables, "fields", meta.fields);
-        hydrateList(meta.tables, "segments", meta.segments);
-        hydrateList(meta.tables, "metrics", meta.metrics);
-
-        hydrate(meta.tables, "db", t => meta.databases[t.db_id || t.db]);
-
-        hydrate(meta.segments, "table", s => meta.tables[s.table_id]);
-        hydrate(meta.metrics, "table", m => meta.tables[m.table_id]);
-        hydrate(meta.fields, "table", f => meta.tables[f.table_id]);
-
-        hydrate(meta.fields, "target", f => meta.fields[f.fk_target_field_id]);
-
-        hydrate(meta.fields, "operators", f => getOperators(f, f.table));
-        hydrate(meta.tables, "aggregation_options", t =>
-            getAggregatorsWithFields(t));
-        hydrate(meta.tables, "breakout_options", t => getBreakouts(t.fields));
-
-        hydrate(meta.fields, "remapping", f => new Map(getFieldValues(f)));
-
-        hydrateLookup(meta.databases, "tables", "id");
-        hydrateLookup(meta.tables, "fields", "id");
-        hydrateLookup(meta.fields, "operators", "name");
-
-        return meta;
-    }
+  [
+    getNormalizedDatabases,
+    getNormalizedTables,
+    getNormalizedFields,
+    getNormalizedSegments,
+    getNormalizedMetrics,
+  ],
+  (databases, tables, fields, segments, metrics): Metadata => {
+    const meta = new Metadata();
+    meta.databases = copyObjects(meta, databases, Database);
+    meta.tables = copyObjects(meta, tables, Table);
+    meta.fields = copyObjects(meta, fields, Field);
+    meta.segments = copyObjects(meta, segments, Segment);
+    meta.metrics = copyObjects(meta, metrics, Metric);
+    // meta.loaded    = getLoadedStatuses(requestStates)
+
+    hydrateList(meta.databases, "tables", meta.tables);
+
+    hydrateList(meta.tables, "fields", meta.fields);
+    hydrateList(meta.tables, "segments", meta.segments);
+    hydrateList(meta.tables, "metrics", meta.metrics);
+
+    hydrate(meta.tables, "db", t => meta.databases[t.db_id || t.db]);
+
+    hydrate(meta.segments, "table", s => meta.tables[s.table_id]);
+    hydrate(meta.metrics, "table", m => meta.tables[m.table_id]);
+    hydrate(meta.fields, "table", f => meta.tables[f.table_id]);
+
+    hydrate(meta.fields, "target", f => meta.fields[f.fk_target_field_id]);
+
+    hydrate(meta.fields, "operators", f => getOperators(f, f.table));
+    hydrate(meta.tables, "aggregation_options", t =>
+      getAggregatorsWithFields(t),
+    );
+    hydrate(meta.tables, "breakout_options", t => getBreakouts(t.fields));
+
+    hydrate(meta.fields, "values", f => getFieldValues(f));
+    hydrate(meta.fields, "remapping", f => new Map(getRemappings(f)));
+
+    hydrateLookup(meta.databases, "tables", "id");
+    hydrateLookup(meta.tables, "fields", "id");
+    hydrateLookup(meta.fields, "operators", "name");
+
+    return meta;
+  },
 );
 
 export const getDatabases = createSelector(
-    [getMetadata],
-    ({ databases }) => databases
+  [getMetadata],
+  ({ databases }) => databases,
 );
 
 export const getDatabasesList = createSelector(
-    [getDatabases, state => state.metadata.databasesList],
-    (databases, ids) => ids.map(id => databases[id])
+  [getDatabases, state => state.metadata.databasesList],
+  (databases, ids) => ids.map(id => databases[id]),
 );
 
 export const getTables = createSelector([getMetadata], ({ tables }) => tables);
 
 export const getFields = createSelector([getMetadata], ({ fields }) => fields);
 export const getMetrics = createSelector(
-    [getMetadata],
-    ({ metrics }) => metrics
+  [getMetadata],
+  ({ metrics }) => metrics,
 );
 
 export const getSegments = createSelector(
-    [getMetadata],
-    ({ segments }) => segments
+  [getMetadata],
+  ({ segments }) => segments,
 );
 
 // FIELD VALUES FOR DASHBOARD FILTERS / SQL QUESTION PARAMETERS
@@ -131,35 +138,46 @@ export const getSegments = createSelector(
 // Currently this assumes that you are passing the props of <ParameterValueWidget> which contain the
 // `field_ids` array inside `parameter` prop.
 const getParameterFieldValuesByFieldId = (state, props) => {
-    // NOTE Atte Keinänen 9/14/17: Reading the state directly instead of using `getFields` selector
-    // because `getMetadata` doesn't currently work with fields of public dashboards
-    return _.chain(getIn(state, ["metadata", "fields"]))
-        // SQL template tags provide `field_id` instead of `field_ids`
-        .pick(...(props.parameter.field_ids || [props.parameter.field_id]))
-        .mapObject(getFieldValues)
-        .value()
-}
+  // NOTE Atte Keinänen 9/14/17: Reading the state directly instead of using `getFields` selector
+  // because `getMetadata` doesn't currently work with fields of public dashboards
+  return (
+    _.chain(getIn(state, ["metadata", "fields"]))
+      // SQL template tags provide `field_id` instead of `field_ids`
+      .pick(...(props.parameter.field_ids || [props.parameter.field_id]))
+      .mapObject(getFieldValues)
+      .value()
+  );
+};
 
 // Custom equality selector for checking if two field value dictionaries contain same fields and field values
 // Currently we simply check if fields match and the lengths of field value arrays are equal which makes the comparison fast
 // See https://github.com/reactjs/reselect#customize-equalitycheck-for-defaultmemoize
-const createFieldValuesEqualSelector = createSelectorCreator(defaultMemoize, (a, b) => {
-// TODO: Why can't we use plain shallowEqual, i.e. why the field value arrays change very often?
-    return shallowEqual(_.mapObject(a, (values) => values.length), _.mapObject(b, (values) => values.length));
-})
+const createFieldValuesEqualSelector = createSelectorCreator(
+  defaultMemoize,
+  (a, b) => {
+    // TODO: Why can't we use plain shallowEqual, i.e. why the field value arrays change very often?
+    return shallowEqual(
+      _.mapObject(a, values => values.length),
+      _.mapObject(b, values => values.length),
+    );
+  },
+);
 
 // HACK Atte Keinänen 7/27/17: Currently the field value analysis code only returns a single value for booleans,
 // this will be addressed in analysis sync refactor
-const patchBooleanFieldValues_HACK = (valueArray) => {
-    const isBooleanFieldValues =
-        valueArray && valueArray.length === 1 && valueArray[0] && typeof(valueArray[0][0]) === "boolean"
-
-    if (isBooleanFieldValues) {
-        return [[true], [false]];
-    } else {
-        return valueArray;
-    }
-}
+const patchBooleanFieldValues_HACK = valueArray => {
+  const isBooleanFieldValues =
+    valueArray &&
+    valueArray.length === 1 &&
+    valueArray[0] &&
+    typeof valueArray[0][0] === "boolean";
+
+  if (isBooleanFieldValues) {
+    return [[true], [false]];
+  } else {
+    return valueArray;
+  }
+};
 
 // Merges the field values of fields linked to a parameter and removes duplicates
 // We want that we have a distinct selector for each field id combination, and for that reason
@@ -167,72 +185,72 @@ const patchBooleanFieldValues_HACK = (valueArray) => {
 // https://github.com/reactjs/reselect#sharing-selectors-with-props-across-multiple-components
 // TODO Atte Keinänen 7/20/17: Should we have any thresholds if the count of field values is high or we have many (>2?) fields?
 export const makeGetMergedParameterFieldValues = () => {
-    return createFieldValuesEqualSelector(getParameterFieldValuesByFieldId, (fieldValues) => {
-        const fieldIds = Object.keys(fieldValues)
-
-        if (fieldIds.length === 0) {
-            // If we have no fields for the parameter, don't return any field values
-            return [];
-        } else if (fieldIds.length === 1) {
-            // We have just a single field so we can return the field values almost as-is,
-            // only address the boolean bug for now
-            const singleFieldValues = fieldValues[fieldIds[0]]
-            return patchBooleanFieldValues_HACK(singleFieldValues);
-        } else {
-            // We have multiple fields, so let's merge their values to a single array
-            const sortedMergedValues = _.chain(Object.values(fieldValues))
-                .flatten(true)
-                .sortBy(fieldValue => {
-                    const valueIsRemapped = fieldValue.length === 2
-                    return valueIsRemapped ? fieldValue[1] : fieldValue[0]
-                })
-                .value()
-
-            // run the uniqueness comparision always against a non-remapped value
-            return _.uniq(sortedMergedValues, false, (fieldValue) => fieldValue[0]);
-        }
-    });
-}
+  return createFieldValuesEqualSelector(
+    getParameterFieldValuesByFieldId,
+    fieldValues => {
+      const fieldIds = Object.keys(fieldValues);
+
+      if (fieldIds.length === 0) {
+        // If we have no fields for the parameter, don't return any field values
+        return [];
+      } else if (fieldIds.length === 1) {
+        // We have just a single field so we can return the field values almost as-is,
+        // only address the boolean bug for now
+        const singleFieldValues = fieldValues[fieldIds[0]];
+        return patchBooleanFieldValues_HACK(singleFieldValues);
+      } else {
+        // We have multiple fields, so let's merge their values to a single array
+        const sortedMergedValues = _.chain(Object.values(fieldValues))
+          .flatten(true)
+          .sortBy(fieldValue => {
+            const valueIsRemapped = fieldValue.length === 2;
+            return valueIsRemapped ? fieldValue[1] : fieldValue[0];
+          })
+          .value();
+
+        // run the uniqueness comparision always against a non-remapped value
+        return _.uniq(sortedMergedValues, false, fieldValue => fieldValue[0]);
+      }
+    },
+  );
+};
 
 // UTILS:
 
 // clone each object in the provided mapping of objects
 export function copyObjects(metadata, objects, Klass) {
-    let copies = {};
-    for (const object of Object.values(objects)) {
-        // $FlowFixMe
-        copies[object.id] = new Klass(object);
-        // $FlowFixMe
-        copies[object.id].metadata = metadata;
-    }
-    return copies;
+  let copies = {};
+  for (const object of Object.values(objects)) {
+    // $FlowFixMe
+    copies[object.id] = new Klass(object);
+    // $FlowFixMe
+    copies[object.id].metadata = metadata;
+  }
+  return copies;
 }
 
 // calls a function to derive the value of a property for every object
 function hydrate(objects, property, getPropertyValue) {
-    for (const object of Object.values(objects)) {
-        // $FlowFixMe
-        object[property] = getPropertyValue(object);
-    }
+  for (const object of Object.values(objects)) {
+    // $FlowFixMe
+    object[property] = getPropertyValue(object);
+  }
 }
 
 // replaces lists of ids with the actual objects
 function hydrateList(objects, property, targetObjects) {
-    hydrate(
-        objects,
-        property,
-        object =>
-            (object[property] || []).map(id => targetObjects[id])
-    );
+  hydrate(objects, property, object =>
+    (object[property] || []).map(id => targetObjects[id]),
+  );
 }
 
 // creates a *_lookup object for a previously hydrated list
 function hydrateLookup(objects, property, idProperty = "id") {
-    hydrate(objects, property + "_lookup", object => {
-        let lookup = {};
-        for (const item of object[property] || []) {
-            lookup[item[idProperty]] = item;
-        }
-        return lookup;
-    });
+  hydrate(objects, property + "_lookup", object => {
+    let lookup = {};
+    for (const item of object[property] || []) {
+      lookup[item[idProperty]] = item;
+    }
+    return lookup;
+  });
 }
diff --git a/frontend/src/metabase/selectors/settings.js b/frontend/src/metabase/selectors/settings.js
index f9c3743fab7503f9576ce3998a2f2a7e73a691ee..48f6ffc36c7f0d9e990d018e97b705da2f1e1e06 100644
--- a/frontend/src/metabase/selectors/settings.js
+++ b/frontend/src/metabase/selectors/settings.js
@@ -1,8 +1,10 @@
-
 // NOTE: these are "public" settings
-export const getIsPublicSharingEnabled = (state) => state.settings.values["public_sharing"];
-export const getIsApplicationEmbeddingEnabled = (state) => state.settings.values["embedding"];
+export const getIsPublicSharingEnabled = state =>
+  state.settings.values["public_sharing"];
+export const getIsApplicationEmbeddingEnabled = state =>
+  state.settings.values["embedding"];
 
 // NOTE: these are admin-only settings
-export const getSiteUrl = (state) => state.settings.values["site-url"];
-export const getEmbeddingSecretKey = (state) => state.settings.values["embedding-secret-key"];
+export const getSiteUrl = state => state.settings.values["site-url"];
+export const getEmbeddingSecretKey = state =>
+  state.settings.values["embedding-secret-key"];
diff --git a/frontend/src/metabase/selectors/undo.js b/frontend/src/metabase/selectors/undo.js
index 852127bf143ab3ccf62e128188c74476587c52b7..e7e15ca2a73cf82f99c41199f04faff886c44e26 100644
--- a/frontend/src/metabase/selectors/undo.js
+++ b/frontend/src/metabase/selectors/undo.js
@@ -1,2 +1 @@
-
 export const getUndos = (state, props) => state.undo;
diff --git a/frontend/src/metabase/selectors/user.js b/frontend/src/metabase/selectors/user.js
index 11bc1f12f3524608544dabed8b135bd11aa4d3cd..b515503e99cc141b701547458c893fdfc1be6e05 100644
--- a/frontend/src/metabase/selectors/user.js
+++ b/frontend/src/metabase/selectors/user.js
@@ -1,6 +1,4 @@
+export const getUser = state => state.currentUser;
 
-export const getUser = (state) =>
-    state.currentUser;
-
-export const getUserIsAdmin = (state) =>
-    (getUser(state) || {}).is_superuser || false;
+export const getUserIsAdmin = state =>
+  (getUser(state) || {}).is_superuser || false;
diff --git a/frontend/src/metabase/services.js b/frontend/src/metabase/services.js
index 6ed0b7d7b961384f122ca07ad632e93e9fcb3672..d1922d8d1098df3c62de49d8e33da304b74dbe8b 100644
--- a/frontend/src/metabase/services.js
+++ b/frontend/src/metabase/services.js
@@ -12,284 +12,307 @@ const embedBase = IS_EMBED_PREVIEW ? "/api/preview_embed" : "/api/embed";
 import getGAMetadata from "promise-loader?global!metabase/lib/ga-metadata"; // eslint-disable-line import/default
 
 export const ActivityApi = {
-    list:                        GET("/api/activity"),
-    recent_views:                GET("/api/activity/recent_views"),
+  list: GET("/api/activity"),
+  recent_views: GET("/api/activity/recent_views"),
 };
 
 export const CardApi = {
-    list:                        GET("/api/card", (cards, { data }) =>
-                                    // HACK: support for the "q" query param until backend implements it
-                                    cards.filter(card => !data.q || card.name.toLowerCase().indexOf(data.q.toLowerCase()) >= 0)
-                                 ),
-    create:                     POST("/api/card"),
-    get:                         GET("/api/card/:cardId"),
-    update:                      PUT("/api/card/:id"),
-    delete:                   DELETE("/api/card/:cardId"),
-    query:                      POST("/api/card/:cardId/query"),
-    // isfavorite:                  GET("/api/card/:cardId/favorite"),
-    favorite:                   POST("/api/card/:cardId/favorite"),
-    unfavorite:               DELETE("/api/card/:cardId/favorite"),
-    updateLabels:               POST("/api/card/:cardId/labels"),
-
-    listPublic:                  GET("/api/card/public"),
-    listEmbeddable:              GET("/api/card/embeddable"),
-    createPublicLink:           POST("/api/card/:id/public_link"),
-    deletePublicLink:         DELETE("/api/card/:id/public_link"),
+  list: GET("/api/card", (cards, { data }) =>
+    // HACK: support for the "q" query param until backend implements it
+    cards.filter(
+      card =>
+        !data.q || card.name.toLowerCase().indexOf(data.q.toLowerCase()) >= 0,
+    ),
+  ),
+  create: POST("/api/card"),
+  get: GET("/api/card/:cardId"),
+  update: PUT("/api/card/:id"),
+  delete: DELETE("/api/card/:cardId"),
+  query: POST("/api/card/:cardId/query"),
+  // isfavorite:                  GET("/api/card/:cardId/favorite"),
+  favorite: POST("/api/card/:cardId/favorite"),
+  unfavorite: DELETE("/api/card/:cardId/favorite"),
+  updateLabels: POST("/api/card/:cardId/labels"),
+
+  listPublic: GET("/api/card/public"),
+  listEmbeddable: GET("/api/card/embeddable"),
+  createPublicLink: POST("/api/card/:id/public_link"),
+  deletePublicLink: DELETE("/api/card/:id/public_link"),
 };
 
 export const DashboardApi = {
-    list:                        GET("/api/dashboard"),
-    create:                     POST("/api/dashboard"),
-    get:                         GET("/api/dashboard/:dashId"),
-    update:                      PUT("/api/dashboard/:id"),
-    delete:                   DELETE("/api/dashboard/:dashId"),
-    addcard:                    POST("/api/dashboard/:dashId/cards"),
-    removecard:               DELETE("/api/dashboard/:dashId/cards"),
-    reposition_cards:            PUT("/api/dashboard/:dashId/cards"),
-    favorite:                   POST("/api/dashboard/:dashId/favorite"),
-    unfavorite:               DELETE("/api/dashboard/:dashId/favorite"),
-
-    listPublic:                  GET("/api/dashboard/public"),
-    listEmbeddable:              GET("/api/dashboard/embeddable"),
-    createPublicLink:           POST("/api/dashboard/:id/public_link"),
-    deletePublicLink:         DELETE("/api/dashboard/:id/public_link"),
+  list: GET("/api/dashboard"),
+  create: POST("/api/dashboard"),
+  get: GET("/api/dashboard/:dashId"),
+  update: PUT("/api/dashboard/:id"),
+  delete: DELETE("/api/dashboard/:dashId"),
+  addcard: POST("/api/dashboard/:dashId/cards"),
+  removecard: DELETE("/api/dashboard/:dashId/cards"),
+  reposition_cards: PUT("/api/dashboard/:dashId/cards"),
+  favorite: POST("/api/dashboard/:dashId/favorite"),
+  unfavorite: DELETE("/api/dashboard/:dashId/favorite"),
+
+  listPublic: GET("/api/dashboard/public"),
+  listEmbeddable: GET("/api/dashboard/embeddable"),
+  createPublicLink: POST("/api/dashboard/:id/public_link"),
+  deletePublicLink: DELETE("/api/dashboard/:id/public_link"),
 };
 
 export const CollectionsApi = {
-    list:                        GET("/api/collection"),
-    create:                     POST("/api/collection"),
-    get:                         GET("/api/collection/:id"),
-    update:                      PUT("/api/collection/:id"),
-    delete:                   DELETE("/api/collection/:id"),
-    graph:                       GET("/api/collection/graph"),
-    updateGraph:                 PUT("/api/collection/graph"),
+  list: GET("/api/collection"),
+  create: POST("/api/collection"),
+  get: GET("/api/collection/:id"),
+  update: PUT("/api/collection/:id"),
+  delete: DELETE("/api/collection/:id"),
+  graph: GET("/api/collection/graph"),
+  updateGraph: PUT("/api/collection/graph"),
 };
 
 export const PublicApi = {
-    card:                        GET("/api/public/card/:uuid"),
-    cardQuery:                   GET("/api/public/card/:uuid/query"),
-    dashboard:                   GET("/api/public/dashboard/:uuid"),
-    dashboardCardQuery:          GET("/api/public/dashboard/:uuid/card/:cardId")
+  card: GET("/api/public/card/:uuid"),
+  cardQuery: GET("/api/public/card/:uuid/query"),
+  dashboard: GET("/api/public/dashboard/:uuid"),
+  dashboardCardQuery: GET("/api/public/dashboard/:uuid/card/:cardId"),
 };
 
 export const EmbedApi = {
-    card:                        GET(embedBase + "/card/:token"),
-    cardQuery:                   GET(embedBase + "/card/:token/query"),
-    dashboard:                   GET(embedBase + "/dashboard/:token"),
-    dashboardCardQuery:          GET(embedBase + "/dashboard/:token/dashcard/:dashcardId/card/:cardId")
+  card: GET(embedBase + "/card/:token"),
+  cardQuery: GET(embedBase + "/card/:token/query"),
+  dashboard: GET(embedBase + "/dashboard/:token"),
+  dashboardCardQuery: GET(
+    embedBase + "/dashboard/:token/dashcard/:dashcardId/card/:cardId",
+  ),
 };
 
 export const EmailApi = {
-    updateSettings:              PUT("/api/email"),
-    sendTest:                   POST("/api/email/test"),
+  updateSettings: PUT("/api/email"),
+  sendTest: POST("/api/email/test"),
 };
 
 export const SlackApi = {
-    updateSettings:              PUT("/api/slack/settings"),
+  updateSettings: PUT("/api/slack/settings"),
 };
 
 export const LdapApi = {
-    updateSettings:              PUT("/api/ldap/settings")
+  updateSettings: PUT("/api/ldap/settings"),
 };
 
 export const MetabaseApi = {
-    db_list:                     GET("/api/database"),
-    db_list_with_tables:         GET("/api/database?include_tables=true&include_cards=true"),
-    db_real_list_with_tables:    GET("/api/database?include_tables=true&include_cards=false"),
-    db_create:                  POST("/api/database"),
-    db_validate:                POST("/api/database/validate"),
-    db_add_sample_dataset:      POST("/api/database/sample_dataset"),
-    db_get:                      GET("/api/database/:dbId"),
-    db_update:                   PUT("/api/database/:id"),
-    db_delete:                DELETE("/api/database/:dbId"),
-    db_metadata:                 GET("/api/database/:dbId/metadata"),
-    // db_tables:                   GET("/api/database/:dbId/tables"),
-    db_fields:                   GET("/api/database/:dbId/fields"),
-    db_idfields:                 GET("/api/database/:dbId/idfields"),
-    db_autocomplete_suggestions: GET("/api/database/:dbId/autocomplete_suggestions?prefix=:prefix"),
-    db_sync_schema:             POST("/api/database/:dbId/sync_schema"),
-    db_rescan_values:           POST("/api/database/:dbId/rescan_values"),
-    db_discard_values:          POST("/api/database/:dbId/discard_values"),
-    table_list:                  GET("/api/table"),
-    // table_get:                   GET("/api/table/:tableId"),
-    table_update:                PUT("/api/table/:id"),
-    // table_fields:                GET("/api/table/:tableId/fields"),
-    table_fks:                   GET("/api/table/:tableId/fks"),
-    // table_reorder_fields:       POST("/api/table/:tableId/reorder"),
-    table_query_metadata:        GET("/api/table/:tableId/query_metadata", async (table) => {
-                                    // HACK: inject GA metadata that we don't have intergrated on the backend yet
-                                    if (table && table.db && table.db.engine === "googleanalytics") {
-                                        let GA = await getGAMetadata();
-                                        table.fields = table.fields.map(f => ({ ...f, ...GA.fields[f.name] }));
-                                        table.metrics.push(...GA.metrics);
-                                        table.segments.push(...GA.segments);
-                                    }
-
-                                    if (table && table.fields) {
-                                        // replace dimension_options IDs with objects
-                                        for (const field of table.fields) {
-                                            if (field.dimension_options) {
-                                                field.dimension_options = field.dimension_options.map(id => table.dimension_options[id])
-                                            }
-                                            if (field.default_dimension_option) {
-                                                field.default_dimension_option = table.dimension_options[field.default_dimension_option];
-                                            }
-                                        }
-                                    }
-
-                                    return table;
-                                 }),
-    // table_sync_metadata:        POST("/api/table/:tableId/sync"),
-    table_rescan_values:       POST("/api/table/:tableId/rescan_values"),
-    table_discard_values:      POST("/api/table/:tableId/discard_values"),
-    field_get:                   GET("/api/field/:fieldId"),
-    // field_summary:               GET("/api/field/:fieldId/summary"),
-    field_values:                GET("/api/field/:fieldId/values"),
-    field_values_update:        POST("/api/field/:fieldId/values"),
-    field_update:                PUT("/api/field/:id"),
-    field_dimension_update:     POST("/api/field/:fieldId/dimension"),
-    field_dimension_delete:   DELETE("/api/field/:fieldId/dimension"),
-    field_rescan_values:        POST("/api/field/:fieldId/rescan_values"),
-    field_discard_values:       POST("/api/field/:fieldId/discard_values"),
-    dataset:                    POST("/api/dataset"),
-    dataset_duration:           POST("/api/dataset/duration")
+  db_list: GET("/api/database"),
+  db_list_with_tables: GET(
+    "/api/database?include_tables=true&include_cards=true",
+  ),
+  db_real_list_with_tables: GET(
+    "/api/database?include_tables=true&include_cards=false",
+  ),
+  db_create: POST("/api/database"),
+  db_validate: POST("/api/database/validate"),
+  db_add_sample_dataset: POST("/api/database/sample_dataset"),
+  db_get: GET("/api/database/:dbId"),
+  db_update: PUT("/api/database/:id"),
+  db_delete: DELETE("/api/database/:dbId"),
+  db_metadata: GET("/api/database/:dbId/metadata"),
+  // db_tables:                   GET("/api/database/:dbId/tables"),
+  db_fields: GET("/api/database/:dbId/fields"),
+  db_idfields: GET("/api/database/:dbId/idfields"),
+  db_autocomplete_suggestions: GET(
+    "/api/database/:dbId/autocomplete_suggestions?prefix=:prefix",
+  ),
+  db_sync_schema: POST("/api/database/:dbId/sync_schema"),
+  db_rescan_values: POST("/api/database/:dbId/rescan_values"),
+  db_discard_values: POST("/api/database/:dbId/discard_values"),
+  table_list: GET("/api/table"),
+  // table_get:                   GET("/api/table/:tableId"),
+  table_update: PUT("/api/table/:id"),
+  // table_fields:                GET("/api/table/:tableId/fields"),
+  table_fks: GET("/api/table/:tableId/fks"),
+  // table_reorder_fields:       POST("/api/table/:tableId/reorder"),
+  table_query_metadata: GET(
+    "/api/table/:tableId/query_metadata",
+    async table => {
+      // HACK: inject GA metadata that we don't have intergrated on the backend yet
+      if (table && table.db && table.db.engine === "googleanalytics") {
+        let GA = await getGAMetadata();
+        table.fields = table.fields.map(f => ({ ...f, ...GA.fields[f.name] }));
+        table.metrics.push(...GA.metrics);
+        table.segments.push(...GA.segments);
+      }
+
+      if (table && table.fields) {
+        // replace dimension_options IDs with objects
+        for (const field of table.fields) {
+          if (field.dimension_options) {
+            field.dimension_options = field.dimension_options.map(
+              id => table.dimension_options[id],
+            );
+          }
+          if (field.default_dimension_option) {
+            field.default_dimension_option =
+              table.dimension_options[field.default_dimension_option];
+          }
+        }
+      }
+
+      return table;
+    },
+  ),
+  // table_sync_metadata:        POST("/api/table/:tableId/sync"),
+  table_rescan_values: POST("/api/table/:tableId/rescan_values"),
+  table_discard_values: POST("/api/table/:tableId/discard_values"),
+  field_get: GET("/api/field/:fieldId"),
+  // field_summary:               GET("/api/field/:fieldId/summary"),
+  field_values: GET("/api/field/:fieldId/values"),
+  field_values_update: POST("/api/field/:fieldId/values"),
+  field_update: PUT("/api/field/:id"),
+  field_dimension_update: POST("/api/field/:fieldId/dimension"),
+  field_dimension_delete: DELETE("/api/field/:fieldId/dimension"),
+  field_rescan_values: POST("/api/field/:fieldId/rescan_values"),
+  field_discard_values: POST("/api/field/:fieldId/discard_values"),
+  field_search: GET("/api/field/:fieldId/search/:searchFieldId"),
+  field_remapping: GET("/api/field/:fieldId/remapping/:remappedFieldId"),
+  dataset: POST("/api/dataset"),
+  dataset_duration: POST("/api/dataset/duration"),
 };
 
 export const AsyncApi = {
-    status:                     GET("/api/async/:jobId"),
-    // endpoints:                  GET("/api/async/running-jobs")
-}
+  status: GET("/api/async/:jobId"),
+  // endpoints:                  GET("/api/async/running-jobs")
+};
 
 export const XRayApi = {
-    // X-Rays
-    // NOTE Atte Keinänen 9/28/17: All xrays endpoints are asynchronous.
-    // You should use BackgroundJobRequest in `metabase/lib/promise` for invoking them.
-    field_xray:                  GET("/api/x-ray/field/:fieldId"),
-    table_xray:                  GET("/api/x-ray/table/:tableId"),
-    segment_xray:                GET("/api/x-ray/segment/:segmentId"),
-    card_xray:                   GET("/api/x-ray/card/:cardId"),
-
-    compare_shared_type:         GET("/api/x-ray/compare/:modelTypePlural/:modelId1/:modelId2"),
-    compare_two_types:           GET("/api/x-ray/compare/:modelType1/:modelId1/:modelType2/:modelId2"),
+  // X-Rays
+  // NOTE Atte Keinänen 9/28/17: All xrays endpoints are asynchronous.
+  // You should use BackgroundJobRequest in `metabase/lib/promise` for invoking them.
+  field_xray: GET("/api/x-ray/field/:fieldId"),
+  table_xray: GET("/api/x-ray/table/:tableId"),
+  segment_xray: GET("/api/x-ray/segment/:segmentId"),
+  card_xray: GET("/api/x-ray/card/:cardId"),
+
+  compare_shared_type: GET(
+    "/api/x-ray/compare/:modelTypePlural/:modelId1/:modelId2",
+  ),
+  compare_two_types: GET(
+    "/api/x-ray/compare/:modelType1/:modelId1/:modelType2/:modelId2",
+  ),
 };
 
 export const PulseApi = {
-    list:                        GET("/api/pulse"),
-    create:                     POST("/api/pulse"),
-    get:                         GET("/api/pulse/:pulseId"),
-    update:                      PUT("/api/pulse/:id"),
-    delete:                   DELETE("/api/pulse/:pulseId"),
-    test:                       POST("/api/pulse/test"),
-    form_input:                  GET("/api/pulse/form_input"),
-    preview_card:                GET("/api/pulse/preview_card_info/:id"),
+  list: GET("/api/pulse"),
+  create: POST("/api/pulse"),
+  get: GET("/api/pulse/:pulseId"),
+  update: PUT("/api/pulse/:id"),
+  delete: DELETE("/api/pulse/:pulseId"),
+  test: POST("/api/pulse/test"),
+  form_input: GET("/api/pulse/form_input"),
+  preview_card: GET("/api/pulse/preview_card_info/:id"),
 };
 
 export const AlertApi = {
-    list:                        GET("/api/alert"),
-    list_for_question:           GET("/api/alert/question/:questionId"),
-    create:                     POST("/api/alert"),
-    update:                      PUT("/api/alert/:id"),
-    delete:                   DELETE("/api/alert/:id"),
-    unsubscribe:                 PUT("/api/alert/:id/unsubscribe"),
+  list: GET("/api/alert"),
+  list_for_question: GET("/api/alert/question/:questionId"),
+  create: POST("/api/alert"),
+  update: PUT("/api/alert/:id"),
+  delete: DELETE("/api/alert/:id"),
+  unsubscribe: PUT("/api/alert/:id/unsubscribe"),
 };
 
 export const SegmentApi = {
-    list:                        GET("/api/segment"),
-    create:                     POST("/api/segment"),
-    get:                         GET("/api/segment/:segmentId"),
-    update:                      PUT("/api/segment/:id"),
-    delete:                   DELETE("/api/segment/:segmentId"),
+  list: GET("/api/segment"),
+  create: POST("/api/segment"),
+  get: GET("/api/segment/:segmentId"),
+  update: PUT("/api/segment/:id"),
+  delete: DELETE("/api/segment/:segmentId"),
 };
 
 export const MetricApi = {
-    list:                        GET("/api/metric"),
-    create:                     POST("/api/metric"),
-    get:                         GET("/api/metric/:metricId"),
-    update:                      PUT("/api/metric/:id"),
-    update_important_fields:     PUT("/api/metric/:metricId/important_fields"),
-    delete:                   DELETE("/api/metric/:metricId"),
+  list: GET("/api/metric"),
+  create: POST("/api/metric"),
+  get: GET("/api/metric/:metricId"),
+  update: PUT("/api/metric/:id"),
+  update_important_fields: PUT("/api/metric/:metricId/important_fields"),
+  delete: DELETE("/api/metric/:metricId"),
 };
 
 export const RevisionApi = {
-    list:                        GET("/api/revision"),
-    revert:                     POST("/api/revision/revert"),
+  list: GET("/api/revision"),
+  revert: POST("/api/revision/revert"),
 };
 
 export const RevisionsApi = {
-    get:                         GET("/api/:entity/:id/revisions"),
+  get: GET("/api/:entity/:id/revisions"),
 };
 
 export const LabelApi = {
-    list:                        GET("/api/label"),
-    create:                     POST("/api/label"),
-    update:                      PUT("/api/label/:id"),
-    delete:                   DELETE("/api/label/:id"),
+  list: GET("/api/label"),
+  create: POST("/api/label"),
+  update: PUT("/api/label/:id"),
+  delete: DELETE("/api/label/:id"),
 };
 
 export const SessionApi = {
-    create:                     POST("/api/session"),
-    createWithGoogleAuth:       POST("/api/session/google_auth"),
-    delete:                   DELETE("/api/session"),
-    properties:                  GET("/api/session/properties"),
-    forgot_password:            POST("/api/session/forgot_password"),
-    reset_password:             POST("/api/session/reset_password"),
-    password_reset_token_valid:  GET("/api/session/password_reset_token_valid"),
+  create: POST("/api/session"),
+  createWithGoogleAuth: POST("/api/session/google_auth"),
+  delete: DELETE("/api/session"),
+  properties: GET("/api/session/properties"),
+  forgot_password: POST("/api/session/forgot_password"),
+  reset_password: POST("/api/session/reset_password"),
+  password_reset_token_valid: GET("/api/session/password_reset_token_valid"),
 };
 
 export const SettingsApi = {
-    list:                        GET("/api/setting"),
-    put:                         PUT("/api/setting/:key"),
-    // setAll:                      PUT("/api/setting"),
-    // delete:                   DELETE("/api/setting/:key"),
+  list: GET("/api/setting"),
+  put: PUT("/api/setting/:key"),
+  // setAll:                      PUT("/api/setting"),
+  // delete:                   DELETE("/api/setting/:key"),
 };
 
 export const PermissionsApi = {
-    groups:                      GET("/api/permissions/group"),
-    groupDetails:                GET("/api/permissions/group/:id"),
-    graph:                       GET("/api/permissions/graph"),
-    updateGraph:                 PUT("/api/permissions/graph"),
-    createGroup:                POST("/api/permissions/group"),
-    memberships:                 GET("/api/permissions/membership"),
-    createMembership:           POST("/api/permissions/membership"),
-    deleteMembership:         DELETE("/api/permissions/membership/:id"),
-    updateGroup:                 PUT("/api/permissions/group/:id"),
-    deleteGroup:              DELETE("/api/permissions/group/:id"),
+  groups: GET("/api/permissions/group"),
+  groupDetails: GET("/api/permissions/group/:id"),
+  graph: GET("/api/permissions/graph"),
+  updateGraph: PUT("/api/permissions/graph"),
+  createGroup: POST("/api/permissions/group"),
+  memberships: GET("/api/permissions/membership"),
+  createMembership: POST("/api/permissions/membership"),
+  deleteMembership: DELETE("/api/permissions/membership/:id"),
+  updateGroup: PUT("/api/permissions/group/:id"),
+  deleteGroup: DELETE("/api/permissions/group/:id"),
 };
 
 export const GettingStartedApi = {
-    get:                         GET("/api/getting_started"),
+  get: GET("/api/getting_started"),
 };
 
 export const SetupApi = {
-    create:                     POST("/api/setup"),
-    validate_db:                POST("/api/setup/validate"),
-    admin_checklist:             GET("/api/setup/admin_checklist"),
+  create: POST("/api/setup"),
+  validate_db: POST("/api/setup/validate"),
+  admin_checklist: GET("/api/setup/admin_checklist"),
 };
 
 export const UserApi = {
-    create:                     POST("/api/user"),
-    list:                        GET("/api/user"),
-    current:                     GET("/api/user/current"),
-    // get:                         GET("/api/user/:userId"),
-    update:                      PUT("/api/user/:id"),
-    update_password:             PUT("/api/user/:id/password"),
-    update_qbnewb:               PUT("/api/user/:id/qbnewb"),
-    delete:                   DELETE("/api/user/:userId"),
-    send_invite:                POST("/api/user/:id/send_invite"),
+  create: POST("/api/user"),
+  list: GET("/api/user"),
+  current: GET("/api/user/current"),
+  // get:                         GET("/api/user/:userId"),
+  update: PUT("/api/user/:id"),
+  update_password: PUT("/api/user/:id/password"),
+  update_qbnewb: PUT("/api/user/:id/qbnewb"),
+  delete: DELETE("/api/user/:userId"),
+  send_invite: POST("/api/user/:id/send_invite"),
 };
 
 export const UtilApi = {
-    password_check:             POST("/api/util/password_check"),
-    random_token:                GET("/api/util/random_token"),
-    logs:                        GET("/api/util/logs"),
+  password_check: POST("/api/util/password_check"),
+  random_token: GET("/api/util/random_token"),
+  logs: GET("/api/util/logs"),
 };
 
 export const GeoJSONApi = {
-    get:                         GET("/api/geojson/:id"),
+  get: GET("/api/geojson/:id"),
 };
 
 export const I18NApi = {
-    locale:                      GET("/app/locales/:locale.json"),
-}
+  locale: GET("/app/locales/:locale.json"),
+};
 
 global.services = exports;
diff --git a/frontend/src/metabase/setup/actions.js b/frontend/src/metabase/setup/actions.js
index 423ba98fa4f89149d285e5dc0166be79d43eb01f..e8c5e527da2d2db0a74b421029288f2558bfd11a 100644
--- a/frontend/src/metabase/setup/actions.js
+++ b/frontend/src/metabase/setup/actions.js
@@ -8,17 +8,15 @@ import MetabaseSettings from "metabase/lib/settings";
 
 import { SetupApi, UtilApi } from "metabase/services";
 
-
 // action constants
-export const SET_ACTIVE_STEP = 'SET_ACTIVE_STEP';
-export const SET_USER_DETAILS = 'SET_USER_DETAILS';
-export const SET_DATABASE_DETAILS = 'SET_DATABASE_DETAILS';
-export const SET_ALLOW_TRACKING = 'SET_ALLOW_TRACKING';
-export const VALIDATE_DATABASE = 'VALIDATE_DATABASE';
-export const VALIDATE_PASSWORD = 'VALIDATE_PASSWORD';
-export const SUBMIT_SETUP = 'SUBMIT_SETUP';
-export const COMPLETE_SETUP = 'COMPLETE_SETUP';
-
+export const SET_ACTIVE_STEP = "SET_ACTIVE_STEP";
+export const SET_USER_DETAILS = "SET_USER_DETAILS";
+export const SET_DATABASE_DETAILS = "SET_DATABASE_DETAILS";
+export const SET_ALLOW_TRACKING = "SET_ALLOW_TRACKING";
+export const VALIDATE_DATABASE = "VALIDATE_DATABASE";
+export const VALIDATE_PASSWORD = "VALIDATE_PASSWORD";
+export const SUBMIT_SETUP = "SUBMIT_SETUP";
+export const COMPLETE_SETUP = "COMPLETE_SETUP";
 
 // action creators
 export const setActiveStep = createAction(SET_ACTIVE_STEP);
@@ -26,57 +24,62 @@ export const setUserDetails = createAction(SET_USER_DETAILS);
 export const setDatabaseDetails = createAction(SET_DATABASE_DETAILS);
 export const setAllowTracking = createAction(SET_ALLOW_TRACKING);
 
-
-export const validateDatabase = createThunkAction(VALIDATE_DATABASE, function(details) {
-    return async function(dispatch, getState) {
-        return await SetupApi.validate_db({
-            'token': MetabaseSettings.get('setup_token'),
-            'details': details
-        });
-    };
+export const validateDatabase = createThunkAction(VALIDATE_DATABASE, function(
+  details,
+) {
+  return async function(dispatch, getState) {
+    return await SetupApi.validate_db({
+      token: MetabaseSettings.get("setup_token"),
+      details: details,
+    });
+  };
 });
 
-export const validatePassword = createThunkAction(VALIDATE_PASSWORD, function(password) {
-    return async function(dispatch, getState) {
-        return await UtilApi.password_check({
-            'password': password
-        });
-    };
+export const validatePassword = createThunkAction(VALIDATE_PASSWORD, function(
+  password,
+) {
+  return async function(dispatch, getState) {
+    return await UtilApi.password_check({
+      password: password,
+    });
+  };
 });
 
 export const submitSetup = createThunkAction(SUBMIT_SETUP, function() {
-    return async function(dispatch, getState) {
-        let { setup: { allowTracking, databaseDetails, userDetails} } = getState();
-
-        try {
-            let response = await SetupApi.create({
-                'token': MetabaseSettings.get('setup_token'),
-                'prefs': {
-                    'site_name': userDetails.site_name,
-                    'allow_tracking': allowTracking.toString()
-                },
-                'database': databaseDetails,
-                'user': userDetails
-            });
-
-            // setup complete!
-            dispatch(completeSetup(response));
-
-            return null;
-        } catch (error) {
-            MetabaseAnalytics.trackEvent('Setup', 'Error', 'save');
-
-            return error;
-        }
-    };
+  return async function(dispatch, getState) {
+    let { setup: { allowTracking, databaseDetails, userDetails } } = getState();
+
+    try {
+      let response = await SetupApi.create({
+        token: MetabaseSettings.get("setup_token"),
+        prefs: {
+          site_name: userDetails.site_name,
+          allow_tracking: allowTracking.toString(),
+        },
+        database: databaseDetails,
+        user: userDetails,
+      });
+
+      // setup complete!
+      dispatch(completeSetup(response));
+
+      return null;
+    } catch (error) {
+      MetabaseAnalytics.trackEvent("Setup", "Error", "save");
+
+      return error;
+    }
+  };
 });
 
-export const completeSetup = createAction(COMPLETE_SETUP, function(apiResponse) {
-    // setup user session
-    MetabaseCookies.setSessionCookie(apiResponse.id);
+export const completeSetup = createAction(COMPLETE_SETUP, function(
+  apiResponse,
+) {
+  // setup user session
+  MetabaseCookies.setSessionCookie(apiResponse.id);
 
-    // clear setup token from settings
-    MetabaseSettings.setAll({'setup_token': null});
+  // clear setup token from settings
+  MetabaseSettings.setAll({ setup_token: null });
 
-    return true;
+  return true;
 });
diff --git a/frontend/src/metabase/setup/components/CollapsedStep.jsx b/frontend/src/metabase/setup/components/CollapsedStep.jsx
index f912bb9ae8303652f7d77553e452d5e2de5ff07c..0744cb05d470744f908929d41751b99f98a1c9f3 100644
--- a/frontend/src/metabase/setup/components/CollapsedStep.jsx
+++ b/frontend/src/metabase/setup/components/CollapsedStep.jsx
@@ -4,44 +4,48 @@ import PropTypes from "prop-types";
 import cx from "classnames";
 import Icon from "metabase/components/Icon.jsx";
 
-
 export default class CollapsedStep extends Component {
-    static propTypes = {
-        stepNumber: PropTypes.number.isRequired,
-        stepCircleText: PropTypes.string.isRequired,
-        stepText: PropTypes.string.isRequired,
-        setActiveStep: PropTypes.func.isRequired,
-        isCompleted: PropTypes.bool.isRequired,
-    }
+  static propTypes = {
+    stepNumber: PropTypes.number.isRequired,
+    stepCircleText: PropTypes.string.isRequired,
+    stepText: PropTypes.string.isRequired,
+    setActiveStep: PropTypes.func.isRequired,
+    isCompleted: PropTypes.bool.isRequired,
+  };
 
-    gotoStep() {
-        if (this.props.isCompleted) {
-            this.props.setActiveStep(this.props.stepNumber);
-        }
+  gotoStep() {
+    if (this.props.isCompleted) {
+      this.props.setActiveStep(this.props.stepNumber);
     }
+  }
 
-    render() {
-        let { isCompleted, stepCircleText, stepText } = this.props;
+  render() {
+    let { isCompleted, stepCircleText, stepText } = this.props;
 
-        const classes = cx({
-            'SetupStep': true,
-            'rounded': true,
-            'full': true,
-            'relative': true,
-            'SetupStep--completed shadowed': isCompleted,
-            'SetupStep--todo': !isCompleted
-        });
+    const classes = cx({
+      SetupStep: true,
+      rounded: true,
+      full: true,
+      relative: true,
+      "SetupStep--completed shadowed": isCompleted,
+      "SetupStep--todo": !isCompleted,
+    });
 
-        return (
-            <section className={classes}>
-                <div className="flex align-center py2">
-                    <span className="SetupStep-indicator flex layout-centered absolute bordered">
-                        <span className="SetupStep-number">{stepCircleText}</span>
-                        <Icon name={'check'} className="SetupStep-check" size={16}></Icon>
-                    </span>
-                    <h3 className="SetupStep-title ml4 my1" onClick={this.gotoStep.bind(this)}>{stepText}</h3>
-                </div>
-            </section>
-        );
-    }
+    return (
+      <section className={classes}>
+        <div className="flex align-center py2">
+          <span className="SetupStep-indicator flex layout-centered absolute bordered">
+            <span className="SetupStep-number">{stepCircleText}</span>
+            <Icon name={"check"} className="SetupStep-check" size={16} />
+          </span>
+          <h3
+            className="SetupStep-title ml4 my1"
+            onClick={this.gotoStep.bind(this)}
+          >
+            {stepText}
+          </h3>
+        </div>
+      </section>
+    );
+  }
 }
diff --git a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
index a9c9f1fd0486cbcd56f64f9c34b366d2c68ce48c..84185c292d019b0f5ace5c30699876c6ae31303d 100644
--- a/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseConnectionStep.jsx
@@ -1,8 +1,8 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
-import StepTitle from './StepTitle.jsx'
+import { t } from "c-3po";
+import StepTitle from "./StepTitle.jsx";
 import CollapsedStep from "./CollapsedStep.jsx";
 
 import DatabaseDetailsForm from "metabase/components/DatabaseDetailsForm.jsx";
@@ -14,167 +14,190 @@ import _ from "underscore";
 import { DEFAULT_SCHEDULES } from "metabase/admin/databases/database";
 
 export default class DatabaseConnectionStep extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = { 'engine': "", 'formError': null };
-    }
-
-    static propTypes = {
-        stepNumber: PropTypes.number.isRequired,
-        activeStep: PropTypes.number.isRequired,
-        setActiveStep: PropTypes.func.isRequired,
-
-        databaseDetails: PropTypes.object,
-        validateDatabase: PropTypes.func.isRequired,
-        setDatabaseDetails: PropTypes.func.isRequired,
-    }
-
-    chooseDatabaseEngine = (e) => {
-        let engine = e.target.value
-
-        this.setState({
-            'engine': engine
-        });
-
-        MetabaseAnalytics.trackEvent('Setup', 'Choose Database', engine);
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = { engine: "", formError: null };
+  }
+
+  static propTypes = {
+    stepNumber: PropTypes.number.isRequired,
+    activeStep: PropTypes.number.isRequired,
+    setActiveStep: PropTypes.func.isRequired,
+
+    databaseDetails: PropTypes.object,
+    validateDatabase: PropTypes.func.isRequired,
+    setDatabaseDetails: PropTypes.func.isRequired,
+  };
+
+  chooseDatabaseEngine = e => {
+    let engine = e.target.value;
+
+    this.setState({
+      engine: engine,
+    });
+
+    MetabaseAnalytics.trackEvent("Setup", "Choose Database", engine);
+  };
+
+  connectionDetailsCaptured = async database => {
+    this.setState({
+      formError: null,
+    });
+
+    // make sure that we are trying ssl db connections to start with
+    database.details.ssl = true;
+
+    try {
+      // validate the details before we move forward
+      await this.props.validateDatabase(database);
+    } catch (error) {
+      let formError = error;
+      database.details.ssl = false;
+
+      try {
+        // ssl connection failed, lets try non-ssl
+        await this.props.validateDatabase(database);
+
+        formError = null;
+      } catch (error2) {
+        formError = error2;
+      }
+
+      if (formError) {
+        MetabaseAnalytics.trackEvent(
+          "Setup",
+          "Error",
+          "database validation: " + this.state.engine,
+        );
 
-    connectionDetailsCaptured = async (database) => {
         this.setState({
-            'formError': null
+          formError: formError,
         });
 
-        // make sure that we are trying ssl db connections to start with
-        database.details.ssl = true;
-
-        try {
-            // validate the details before we move forward
-            await this.props.validateDatabase(database);
-
-        } catch (error) {
-            let formError = error;
-            database.details.ssl = false;
-
-            try {
-                // ssl connection failed, lets try non-ssl
-                await this.props.validateDatabase(database);
-
-                formError = null;
-
-            } catch (error2) {
-                formError = error2;
-            }
-
-            if (formError) {
-                MetabaseAnalytics.trackEvent('Setup', 'Error', 'database validation: '+this.state.engine);
-
-                this.setState({
-                    'formError': formError
-                });
-
-                return;
-            }
-        }
-
-        if (database.details["let-user-control-scheduling"]) {
-            // Show the scheduling step if user has chosen to control scheduling manually
-            // Add the default schedules because DatabaseSchedulingForm requires them and update the db state
-            this.props.setDatabaseDetails({
-                'nextStep': this.props.stepNumber + 1,
-                'details': {
-                    ...database,
-                    is_full_sync: true,
-                    schedules: DEFAULT_SCHEDULES
-                }
-            });
-        } else {
-            // now that they are good, store them
-            this.props.setDatabaseDetails({
-                // skip the scheduling step
-                'nextStep': this.props.stepNumber + 2,
-                'details': database
-            });
-
-            MetabaseAnalytics.trackEvent('Setup', 'Database Step', this.state.engine);
-        }
-
+        return;
+      }
     }
 
-    skipDatabase() {
-        this.setState({
-            'engine': ""
-        });
-
-        this.props.setDatabaseDetails({
-            'nextStep': this.props.stepNumber + 2,
-            'details': null
-        });
-
-        MetabaseAnalytics.trackEvent('Setup', 'Database Step');
+    if (database.details["let-user-control-scheduling"]) {
+      // Show the scheduling step if user has chosen to control scheduling manually
+      // Add the default schedules because DatabaseSchedulingForm requires them and update the db state
+      this.props.setDatabaseDetails({
+        nextStep: this.props.stepNumber + 1,
+        details: {
+          ...database,
+          is_full_sync: true,
+          schedules: DEFAULT_SCHEDULES,
+        },
+      });
+    } else {
+      // now that they are good, store them
+      this.props.setDatabaseDetails({
+        // skip the scheduling step
+        nextStep: this.props.stepNumber + 2,
+        details: database,
+      });
+
+      MetabaseAnalytics.trackEvent("Setup", "Database Step", this.state.engine);
     }
-
-    renderEngineSelect() {
-        let engines = MetabaseSettings.get('engines');
-        let { engine } = this.state,
-        engineNames = _.keys(engines).sort();
-
-        return (
-            <label className="Select Form-offset mt1">
-                <select defaultValue={engine} onChange={this.chooseDatabaseEngine}>
-                    <option value="">Select the type of Database you use</option>
-                    {engineNames.map(opt => <option key={opt} value={opt}>{engines[opt]['driver-name']}</option>)}
-                </select>
-            </label>
-        );
+  };
+
+  skipDatabase() {
+    this.setState({
+      engine: "",
+    });
+
+    this.props.setDatabaseDetails({
+      nextStep: this.props.stepNumber + 2,
+      details: null,
+    });
+
+    MetabaseAnalytics.trackEvent("Setup", "Database Step");
+  }
+
+  renderEngineSelect() {
+    let engines = MetabaseSettings.get("engines");
+    let { engine } = this.state,
+      engineNames = _.keys(engines).sort();
+
+    return (
+      <label className="Select Form-offset mt1">
+        <select defaultValue={engine} onChange={this.chooseDatabaseEngine}>
+          <option value="">Select the type of Database you use</option>
+          {engineNames.map(opt => (
+            <option key={opt} value={opt}>
+              {engines[opt]["driver-name"]}
+            </option>
+          ))}
+        </select>
+      </label>
+    );
+  }
+
+  render() {
+    let { activeStep, databaseDetails, setActiveStep, stepNumber } = this.props;
+    let { engine, formError } = this.state;
+    let engines = MetabaseSettings.get("engines");
+
+    let stepText = t`Add your data`;
+    if (activeStep > stepNumber) {
+      stepText =
+        databaseDetails === null
+          ? t`I'll add my own data later`
+          : t`Connecting to ${databaseDetails.name}`;
     }
 
-    render() {
-        let { activeStep, databaseDetails, setActiveStep, stepNumber } = this.props;
-        let { engine, formError } = this.state;
-        let engines = MetabaseSettings.get('engines');
-
-        let stepText = t`Add your data`;
-        if (activeStep > stepNumber) {
-            stepText = (databaseDetails === null) ? t`I'll add my own data later` : t`Connecting to ${databaseDetails.name}`;
-        }
-
-
-        if (activeStep !== stepNumber) {
-            return (<CollapsedStep stepNumber={stepNumber} stepCircleText="2" stepText={stepText} isCompleted={activeStep > stepNumber} setActiveStep={setActiveStep}></CollapsedStep>)
-        } else {
-            return (
-                <section className="SetupStep rounded full relative SetupStep--active">
-                    <StepTitle title={stepText} circleText={"2"} />
-                    <div className="mb4">
-                        <div style={{maxWidth: 600}} className="Form-field Form-offset">
-                            {t`You’ll need some info about your database, like the username and password. If you don’t have that right now, Metabase also comes with a sample dataset you can get started with.`}
-                        </div>
-
-                        <FormField fieldName="engine">
-                            {this.renderEngineSelect()}
-                        </FormField>
-
-                        { engine !== "" ?
-                          <DatabaseDetailsForm
-                              details={
-                                  (databaseDetails && 'details' in databaseDetails)
-                                      ? {...databaseDetails.details, name: databaseDetails.name, is_full_sync: databaseDetails.is_full_sync}
-                                      : null}
-                              engine={engine}
-                              engines={engines}
-                              formError={formError}
-                              hiddenFields={{ ssl: true }}
-                              submitFn={this.connectionDetailsCaptured}
-                              submitButtonText={'Next'}>
-                          </DatabaseDetailsForm>
-                          : null }
-
-                          <div className="Form-field Form-offset">
-                              <a className="link" onClick={this.skipDatabase.bind(this)}>{t`I'll add my data later`}</a>
-                          </div>
-                    </div>
-                </section>
-            );
-        }
+    if (activeStep !== stepNumber) {
+      return (
+        <CollapsedStep
+          stepNumber={stepNumber}
+          stepCircleText="2"
+          stepText={stepText}
+          isCompleted={activeStep > stepNumber}
+          setActiveStep={setActiveStep}
+        />
+      );
+    } else {
+      return (
+        <section className="SetupStep rounded full relative SetupStep--active">
+          <StepTitle title={stepText} circleText={"2"} />
+          <div className="mb4">
+            <div style={{ maxWidth: 600 }} className="Form-field Form-offset">
+              {t`You’ll need some info about your database, like the username and password. If you don’t have that right now, Metabase also comes with a sample dataset you can get started with.`}
+            </div>
+
+            <FormField fieldName="engine">
+              {this.renderEngineSelect()}
+            </FormField>
+
+            {engine !== "" ? (
+              <DatabaseDetailsForm
+                details={
+                  databaseDetails && "details" in databaseDetails
+                    ? {
+                        ...databaseDetails.details,
+                        name: databaseDetails.name,
+                        is_full_sync: databaseDetails.is_full_sync,
+                      }
+                    : null
+                }
+                engine={engine}
+                engines={engines}
+                formError={formError}
+                hiddenFields={{ ssl: true }}
+                submitFn={this.connectionDetailsCaptured}
+                submitButtonText={"Next"}
+              />
+            ) : null}
+
+            <div className="Form-field Form-offset">
+              <a
+                className="link"
+                onClick={this.skipDatabase.bind(this)}
+              >{t`I'll add my data later`}</a>
+            </div>
+          </div>
+        </section>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx b/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
index 716bf77142f75db4c853bc131e87c2469eb07986..f8ac341ae53717cba2672d74c53004c5ff501892 100644
--- a/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
+++ b/frontend/src/metabase/setup/components/DatabaseSchedulingStep.jsx
@@ -1,8 +1,8 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
-import StepTitle from './StepTitle.jsx'
+import { t } from "c-3po";
+import StepTitle from "./StepTitle.jsx";
 import CollapsedStep from "./CollapsedStep.jsx";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
@@ -11,61 +11,72 @@ import DatabaseSchedulingForm from "metabase/admin/databases/components/Database
 import Icon from "metabase/components/Icon";
 
 export default class DatabaseSchedulingStep extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = { 'engine': "", 'formError': null };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = { engine: "", formError: null };
+  }
 
-    static propTypes = {
-        stepNumber: PropTypes.number.isRequired,
-        activeStep: PropTypes.number.isRequired,
-        setActiveStep: PropTypes.func.isRequired,
+  static propTypes = {
+    stepNumber: PropTypes.number.isRequired,
+    activeStep: PropTypes.number.isRequired,
+    setActiveStep: PropTypes.func.isRequired,
 
-        databaseDetails: PropTypes.object,
-        setDatabaseDetails: PropTypes.func.isRequired,
-    }
+    databaseDetails: PropTypes.object,
+    setDatabaseDetails: PropTypes.func.isRequired,
+  };
 
-    schedulingDetailsCaptured = async (database) => {
-        this.props.setDatabaseDetails({
-            'nextStep': this.props.stepNumber + 1,
-            'details': database
-        });
+  schedulingDetailsCaptured = async database => {
+    this.props.setDatabaseDetails({
+      nextStep: this.props.stepNumber + 1,
+      details: database,
+    });
 
-        MetabaseAnalytics.trackEvent('Setup', 'Database Step', this.state.engine);
-    }
+    MetabaseAnalytics.trackEvent("Setup", "Database Step", this.state.engine);
+  };
 
-    render() {
-        let { activeStep, databaseDetails, setActiveStep, stepNumber } = this.props;
-        let { formError } = this.state;
+  render() {
+    let { activeStep, databaseDetails, setActiveStep, stepNumber } = this.props;
+    let { formError } = this.state;
 
-        let stepText = t`Control automatic scans`;
+    let stepText = t`Control automatic scans`;
 
-        const schedulingIcon =
-            <Icon
-                className="text-purple-hover cursor-pointer"
-                name='gear'
-                onClick={() => this.setState({ showCalendar: !this.state.showCalendar })}
-            />
-
-        if (activeStep !== stepNumber) {
-            return (<CollapsedStep stepNumber={stepNumber} stepCircleText={schedulingIcon} stepText={stepText} isCompleted={activeStep > stepNumber} setActiveStep={setActiveStep}></CollapsedStep>)
-        } else {
-            return (
-                <section className="SetupStep rounded full relative SetupStep--active">
-                    <StepTitle title={stepText} circleText={schedulingIcon} />
-                    <div className="mb4">
-                            <div className="text-default">
-                                <DatabaseSchedulingForm
-                                    database={databaseDetails}
-                                    formState={{ formError }}
-                                    // Use saveDatabase both for db creation and updating
-                                    save={this.schedulingDetailsCaptured}
-                                    submitButtonText={ t`Next` }
-                                />
-                            </div>
-                    </div>
-                </section>
-            );
+    const schedulingIcon = (
+      <Icon
+        className="text-purple-hover cursor-pointer"
+        name="gear"
+        onClick={() =>
+          this.setState({ showCalendar: !this.state.showCalendar })
         }
+      />
+    );
+
+    if (activeStep !== stepNumber) {
+      return (
+        <CollapsedStep
+          stepNumber={stepNumber}
+          stepCircleText={schedulingIcon}
+          stepText={stepText}
+          isCompleted={activeStep > stepNumber}
+          setActiveStep={setActiveStep}
+        />
+      );
+    } else {
+      return (
+        <section className="SetupStep rounded full relative SetupStep--active">
+          <StepTitle title={stepText} circleText={schedulingIcon} />
+          <div className="mb4">
+            <div className="text-default">
+              <DatabaseSchedulingForm
+                database={databaseDetails}
+                formState={{ formError }}
+                // Use saveDatabase both for db creation and updating
+                save={this.schedulingDetailsCaptured}
+                submitButtonText={t`Next`}
+              />
+            </div>
+          </div>
+        </section>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/setup/components/PreferencesStep.jsx b/frontend/src/metabase/setup/components/PreferencesStep.jsx
index e2c24889ca13df1b5be4c84f9157ea56c9a7e4b5..d6f78e486ef7772a5335467bcb6793e3565302e2 100644
--- a/frontend/src/metabase/setup/components/PreferencesStep.jsx
+++ b/frontend/src/metabase/setup/components/PreferencesStep.jsx
@@ -1,89 +1,124 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import Toggle from "metabase/components/Toggle.jsx";
 
-import StepTitle from './StepTitle.jsx';
+import StepTitle from "./StepTitle.jsx";
 import CollapsedStep from "./CollapsedStep.jsx";
 
-
 export default class PreferencesStep extends Component {
-
-    static propTypes = {
-        stepNumber: PropTypes.number.isRequired,
-        activeStep: PropTypes.number.isRequired,
-        setActiveStep: PropTypes.func.isRequired,
-
-        allowTracking: PropTypes.bool.isRequired,
-        setAllowTracking: PropTypes.func.isRequired,
-        setupComplete: PropTypes.bool.isRequired,
-        submitSetup: PropTypes.func.isRequired,
-    }
-
-    toggleTracking() {
-        let { allowTracking } = this.props;
-
-        this.props.setAllowTracking(!allowTracking);
+  static propTypes = {
+    stepNumber: PropTypes.number.isRequired,
+    activeStep: PropTypes.number.isRequired,
+    setActiveStep: PropTypes.func.isRequired,
+
+    allowTracking: PropTypes.bool.isRequired,
+    setAllowTracking: PropTypes.func.isRequired,
+    setupComplete: PropTypes.bool.isRequired,
+    submitSetup: PropTypes.func.isRequired,
+  };
+
+  toggleTracking() {
+    let { allowTracking } = this.props;
+
+    this.props.setAllowTracking(!allowTracking);
+  }
+
+  async formSubmitted(e) {
+    e.preventDefault();
+
+    // okay, this is the big one.  we actually submit everything to the api now and complete the process.
+    this.props.submitSetup();
+
+    MetabaseAnalytics.trackEvent(
+      "Setup",
+      "Preferences Step",
+      this.props.allowTracking,
+    );
+  }
+
+  render() {
+    let {
+      activeStep,
+      allowTracking,
+      setupComplete,
+      stepNumber,
+      setActiveStep,
+    } = this.props;
+    const { tag } = MetabaseSettings.get("version");
+
+    let stepText = t`Usage data preferences`;
+    if (setupComplete) {
+      stepText = allowTracking
+        ? t`Thanks for helping us improve`
+        : t`We won't collect any usage events`;
     }
 
-    async formSubmitted(e) {
-        e.preventDefault();
-
-        // okay, this is the big one.  we actually submit everything to the api now and complete the process.
-        this.props.submitSetup();
-
-        MetabaseAnalytics.trackEvent('Setup', 'Preferences Step', this.props.allowTracking);
-    }
-
-    render() {
-        let { activeStep, allowTracking, setupComplete, stepNumber, setActiveStep } = this.props;
-        const { tag } = MetabaseSettings.get('version');
-
-        let stepText = t`Usage data preferences`;
-        if (setupComplete) {
-            stepText = allowTracking ? t`Thanks for helping us improve` : t`We won't collect any usage events`;
-        }
-
-        if (activeStep !== stepNumber || setupComplete) {
-            return (<CollapsedStep stepNumber={stepNumber} stepCircleText="3" stepText={stepText} isCompleted={setupComplete} setActiveStep={setActiveStep}></CollapsedStep>)
-        } else {
-            return (
-                <section className="SetupStep rounded full relative SetupStep--active">
-                    <StepTitle title={stepText} circleText={"3"} />
-                    <form onSubmit={this.formSubmitted.bind(this)} noValidate>
-                        <div className="Form-field Form-offset">
-                            {t`In order to help us improve Metabase, we'd like to collect certain data about usage through Google Analytics.`} <a className="link" href={"http://www.metabase.com/docs/"+tag+"/information-collection.html"} target="_blank">{t`Here's a full list of everything we track and why.`}</a>
-                        </div>
-
-                        <div className="Form-field Form-offset mr4">
-                            <div style={{borderWidth: "2px"}} className="flex align-center bordered rounded p2">
-                                <Toggle value={allowTracking} onChange={this.toggleTracking.bind(this)} className="inline-block" />
-                                <span className="ml1">{t`Allow Metabase to anonymously collect usage events`}</span>
-                            </div>
-                        </div>
-
-                        { allowTracking ?
-                            <div className="Form-field Form-offset">
-                                <ul style={{listStyle: "disc inside", lineHeight: "200%"}}>
-                                    <li>{jt`Metabase ${<span style={{fontWeight: "bold"}}>never</span>} collects anything about your data or question results.`}</li>
-                                    <li>{t`All collection is completely anonymous.`}</li>
-                                    <li>{t`Collection can be turned off at any point in your admin settings.`}</li>
-                                </ul>
-                            </div>
-                        : null }
-
-                        <div className="Form-actions">
-                            <button className="Button Button--primary">
-                                {t`Next`}
-                            </button>
-                            { /* FIXME: <mb-form-message form="usageForm"></mb-form-message>*/ }
-                        </div>
-                    </form>
-                </section>
-            );
-        }
+    if (activeStep !== stepNumber || setupComplete) {
+      return (
+        <CollapsedStep
+          stepNumber={stepNumber}
+          stepCircleText="3"
+          stepText={stepText}
+          isCompleted={setupComplete}
+          setActiveStep={setActiveStep}
+        />
+      );
+    } else {
+      return (
+        <section className="SetupStep rounded full relative SetupStep--active">
+          <StepTitle title={stepText} circleText={"3"} />
+          <form onSubmit={this.formSubmitted.bind(this)} noValidate>
+            <div className="Form-field Form-offset">
+              {t`In order to help us improve Metabase, we'd like to collect certain data about usage through Google Analytics.`}{" "}
+              <a
+                className="link"
+                href={
+                  "http://www.metabase.com/docs/" +
+                  tag +
+                  "/information-collection.html"
+                }
+                target="_blank"
+              >{t`Here's a full list of everything we track and why.`}</a>
+            </div>
+
+            <div className="Form-field Form-offset mr4">
+              <div
+                style={{ borderWidth: "2px" }}
+                className="flex align-center bordered rounded p2"
+              >
+                <Toggle
+                  value={allowTracking}
+                  onChange={this.toggleTracking.bind(this)}
+                  className="inline-block"
+                />
+                <span className="ml1">{t`Allow Metabase to anonymously collect usage events`}</span>
+              </div>
+            </div>
+
+            {allowTracking ? (
+              <div className="Form-field Form-offset">
+                <ul style={{ listStyle: "disc inside", lineHeight: "200%" }}>
+                  <li>{jt`Metabase ${(
+                    <span style={{ fontWeight: "bold" }}>never</span>
+                  )} collects anything about your data or question results.`}</li>
+                  <li>{t`All collection is completely anonymous.`}</li>
+                  <li
+                  >{t`Collection can be turned off at any point in your admin settings.`}</li>
+                </ul>
+              </div>
+            ) : null}
+
+            <div className="Form-actions">
+              <button className="Button Button--primary">{t`Next`}</button>
+              {/* FIXME: <mb-form-message form="usageForm"></mb-form-message>*/}
+            </div>
+          </form>
+        </section>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/setup/components/Setup.jsx b/frontend/src/metabase/setup/components/Setup.jsx
index 994410db63e44799db370c544acea95e2189cb7c..15838ea9a5019f1b3bfec0f49d3de14acd232fd5 100644
--- a/frontend/src/metabase/setup/components/Setup.jsx
+++ b/frontend/src/metabase/setup/components/Setup.jsx
@@ -3,15 +3,15 @@ import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import PropTypes from "prop-types";
 import { Link } from "react-router";
-import { t } from 'c-3po';
-import LogoIcon from 'metabase/components/LogoIcon.jsx';
-import NewsletterForm from 'metabase/components/NewsletterForm.jsx';
+import { t } from "c-3po";
+import LogoIcon from "metabase/components/LogoIcon.jsx";
+import NewsletterForm from "metabase/components/NewsletterForm.jsx";
 import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 
-import UserStep from './UserStep.jsx';
-import DatabaseConnectionStep from './DatabaseConnectionStep.jsx';
-import PreferencesStep from './PreferencesStep.jsx';
+import UserStep from "./UserStep.jsx";
+import DatabaseConnectionStep from "./DatabaseConnectionStep.jsx";
+import PreferencesStep from "./PreferencesStep.jsx";
 import DatabaseSchedulingStep from "metabase/setup/components/DatabaseSchedulingStep";
 
 const WELCOME_STEP_NUMBER = 0;
@@ -21,105 +21,145 @@ const DATABASE_SCHEDULING_STEP_NUMBER = 3;
 const PREFERENCES_STEP_NUMBER = 4;
 
 export default class Setup extends Component {
-    static propTypes = {
-        activeStep: PropTypes.number.isRequired,
-        setupComplete: PropTypes.bool.isRequired,
-        userDetails: PropTypes.object,
-        setActiveStep: PropTypes.func.isRequired,
-        databaseDetails: PropTypes.object.isRequired
-    }
+  static propTypes = {
+    activeStep: PropTypes.number.isRequired,
+    setupComplete: PropTypes.bool.isRequired,
+    userDetails: PropTypes.object,
+    setActiveStep: PropTypes.func.isRequired,
+    databaseDetails: PropTypes.object.isRequired,
+  };
 
-    completeWelcome() {
-        this.props.setActiveStep(USER_STEP_NUMBER);
-        MetabaseAnalytics.trackEvent('Setup', 'Welcome');
-    }
+  completeWelcome() {
+    this.props.setActiveStep(USER_STEP_NUMBER);
+    MetabaseAnalytics.trackEvent("Setup", "Welcome");
+  }
 
-    completeSetup() {
-        MetabaseAnalytics.trackEvent('Setup', 'Complete');
-    }
+  completeSetup() {
+    MetabaseAnalytics.trackEvent("Setup", "Complete");
+  }
 
-    renderFooter() {
-        const { tag } = MetabaseSettings.get('version');
-        return (
-            <div className="SetupHelp bordered border-dashed p2 rounded mb4" >
-                {t`If you feel stuck`}, <a className="link" href={"http://www.metabase.com/docs/"+tag+"/setting-up-metabase"} target="_blank">{t`our getting started guide`}</a> {t`is just a click away.`}
-            </div>
-        );
-    }
+  renderFooter() {
+    const { tag } = MetabaseSettings.get("version");
+    return (
+      <div className="SetupHelp bordered border-dashed p2 rounded mb4">
+        {t`If you feel stuck`},{" "}
+        <a
+          className="link"
+          href={"http://www.metabase.com/docs/" + tag + "/setting-up-metabase"}
+          target="_blank"
+        >{t`our getting started guide`}</a>{" "}
+        {t`is just a click away.`}
+      </div>
+    );
+  }
 
-    componentWillReceiveProps(nextProps) {
-        // If we are entering the scheduling step, we need to scroll to the top of scheduling step container
-        if (this.props.activeStep !== nextProps.activeStep && nextProps.activeStep === 3) {
-            setTimeout(() => {
-                if (this.refs.databaseSchedulingStepContainer) {
-                    const node = ReactDOM.findDOMNode(this.refs.databaseSchedulingStepContainer);
-                    node && node.scrollIntoView && node.scrollIntoView()
-                }
-            }, 10)
+  componentWillReceiveProps(nextProps) {
+    // If we are entering the scheduling step, we need to scroll to the top of scheduling step container
+    if (
+      this.props.activeStep !== nextProps.activeStep &&
+      nextProps.activeStep === 3
+    ) {
+      setTimeout(() => {
+        if (this.refs.databaseSchedulingStepContainer) {
+          const node = ReactDOM.findDOMNode(
+            this.refs.databaseSchedulingStepContainer,
+          );
+          node && node.scrollIntoView && node.scrollIntoView();
         }
+      }, 10);
     }
+  }
 
-    render() {
-        let { activeStep, setupComplete, databaseDetails, userDetails } = this.props;
+  render() {
+    let {
+      activeStep,
+      setupComplete,
+      databaseDetails,
+      userDetails,
+    } = this.props;
 
-        if (activeStep === WELCOME_STEP_NUMBER) {
-            return (
-                <div className="relative full-height flex flex-full layout-centered">
-                    <div className="wrapper wrapper--trim text-centered">
-                        <LogoIcon className="text-brand mb4" width={89} height={118}></LogoIcon>
-                        <div className="relative z2 text-centered ml-auto mr-auto" style={{maxWidth: 550}}>
-                            <h1 style={{fontSize: '2.2rem'}} className="text-brand">{t`Welcome to Metabase`}</h1>
-                            <p className="text-body">{t`Looks like everything is working. Now let’s get to know you, connect to your data, and start finding you some answers!`}</p>
-                            <button className="Button Button--primary mt4" onClick={() => (this.completeWelcome())}>{t`Let's get started`}</button>
-                        </div>
-                        <div className="absolute z1 bottom left right">
-                            <div className="inline-block">
-                                {this.renderFooter()}
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            );
-        } else {
-            return (
-                <div>
-                    <nav className="SetupNav text-brand py2 flex layout-centered">
-                        <LogoIcon width={41} height={51}></LogoIcon>
-                    </nav>
-
-                    <div className="wrapper wrapper--small">
-                        <div className="SetupSteps full">
+    if (activeStep === WELCOME_STEP_NUMBER) {
+      return (
+        <div className="relative full-height flex flex-full layout-centered">
+          <div className="wrapper wrapper--trim text-centered">
+            <LogoIcon className="text-brand mb4" width={89} height={118} />
+            <div
+              className="relative z2 text-centered ml-auto mr-auto"
+              style={{ maxWidth: 550 }}
+            >
+              <h1
+                style={{ fontSize: "2.2rem" }}
+                className="text-brand"
+              >{t`Welcome to Metabase`}</h1>
+              <p className="text-body">{t`Looks like everything is working. Now let’s get to know you, connect to your data, and start finding you some answers!`}</p>
+              <button
+                className="Button Button--primary mt4"
+                onClick={() => this.completeWelcome()}
+              >{t`Let's get started`}</button>
+            </div>
+            <div className="absolute z1 bottom left right">
+              <div className="inline-block">{this.renderFooter()}</div>
+            </div>
+          </div>
+        </div>
+      );
+    } else {
+      return (
+        <div>
+          <nav className="SetupNav text-brand py2 flex layout-centered">
+            <LogoIcon width={41} height={51} />
+          </nav>
 
-                            <UserStep {...this.props} stepNumber={USER_STEP_NUMBER} />
-                            <DatabaseConnectionStep {...this.props} stepNumber={DATABASE_CONNECTION_STEP_NUMBER} />
+          <div className="wrapper wrapper--small">
+            <div className="SetupSteps full">
+              <UserStep {...this.props} stepNumber={USER_STEP_NUMBER} />
+              <DatabaseConnectionStep
+                {...this.props}
+                stepNumber={DATABASE_CONNECTION_STEP_NUMBER}
+              />
 
-                            { /* Have the ref for scrolling in componentWillReceiveProps */ }
-                            <div ref="databaseSchedulingStepContainer">
-                                { /* Show db scheduling step only if the user has explicitly set the "Let me choose when Metabase syncs and scans" toggle to true */ }
-                                { databaseDetails && databaseDetails.details && databaseDetails.details["let-user-control-scheduling"] &&
-                                    <DatabaseSchedulingStep {...this.props} stepNumber={DATABASE_SCHEDULING_STEP_NUMBER} />
-                                }
-                            </div>
-                            <PreferencesStep {...this.props} stepNumber={PREFERENCES_STEP_NUMBER} />
+              {/* Have the ref for scrolling in componentWillReceiveProps */}
+              <div ref="databaseSchedulingStepContainer">
+                {/* Show db scheduling step only if the user has explicitly set the "Let me choose when Metabase syncs and scans" toggle to true */}
+                {databaseDetails &&
+                  databaseDetails.details &&
+                  databaseDetails.details["let-user-control-scheduling"] && (
+                    <DatabaseSchedulingStep
+                      {...this.props}
+                      stepNumber={DATABASE_SCHEDULING_STEP_NUMBER}
+                    />
+                  )}
+              </div>
+              <PreferencesStep
+                {...this.props}
+                stepNumber={PREFERENCES_STEP_NUMBER}
+              />
 
-                            { setupComplete ?
-                                <section className="SetupStep rounded SetupStep--active flex flex-column layout-centered p4">
-                                    <h1 style={{fontSize: "xx-large"}} className="text-light pt2 pb2">{t`You're all set up!`}</h1>
-                                    <div className="pt4">
-                                        <NewsletterForm initialEmail={userDetails && userDetails.email} />
-                                    </div>
-                                    <div className="pt4 pb2">
-                                        <Link to="/?new" className="Button Button--primary" onClick={this.completeSetup.bind(this)}>{t`Take me to Metabase`}</Link>
-                                    </div>
-                                </section>
-                            : null }
-                            <div className="text-centered">
-                                {this.renderFooter()}
-                            </div>
-                        </div>
-                    </div>
-                </div>
-            );
-        }
+              {setupComplete ? (
+                <section className="SetupStep rounded SetupStep--active flex flex-column layout-centered p4">
+                  <h1
+                    style={{ fontSize: "xx-large" }}
+                    className="text-light pt2 pb2"
+                  >{t`You're all set up!`}</h1>
+                  <div className="pt4">
+                    <NewsletterForm
+                      initialEmail={userDetails && userDetails.email}
+                    />
+                  </div>
+                  <div className="pt4 pb2">
+                    <Link
+                      to="/?new"
+                      className="Button Button--primary"
+                      onClick={this.completeSetup.bind(this)}
+                    >{t`Take me to Metabase`}</Link>
+                  </div>
+                </section>
+              ) : null}
+              <div className="text-centered">{this.renderFooter()}</div>
+            </div>
+          </div>
+        </div>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/setup/components/StepTitle.jsx b/frontend/src/metabase/setup/components/StepTitle.jsx
index 1ddddc808a2f97434ea1cb2866658c337e0871f0..a361db6d2e7d054ad6008e5232deb66d37f51c1e 100644
--- a/frontend/src/metabase/setup/components/StepTitle.jsx
+++ b/frontend/src/metabase/setup/components/StepTitle.jsx
@@ -1,24 +1,26 @@
 /* eslint "react/prop-types": "warn" */
-import React, { Component } from 'react'
+import React, { Component } from "react";
 import PropTypes from "prop-types";
 import Icon from "metabase/components/Icon.jsx";
 
 export default class StepTitle extends Component {
-    static propTypes = {
-        circleText: PropTypes.string,
-        title: PropTypes.string.isRequired
-    };
+  static propTypes = {
+    circleText: PropTypes.string,
+    title: PropTypes.string.isRequired,
+  };
 
-    render() {
-        const { circleText, title } = this.props;
-        return (
-            <div className="flex align-center pt3 pb1">
-                <span className="SetupStep-indicator flex layout-centered absolute bordered">
-                    <span className="SetupStep-number">{circleText}</span>
-                    <Icon name={'check'} className="SetupStep-check" size={16}></Icon>
-                </span>
-                <h3 style={{marginTop: 10}} className="SetupStep-title Form-offset">{title}</h3>
-            </div>
-        );
-    }
+  render() {
+    const { circleText, title } = this.props;
+    return (
+      <div className="flex align-center pt3 pb1">
+        <span className="SetupStep-indicator flex layout-centered absolute bordered">
+          <span className="SetupStep-number">{circleText}</span>
+          <Icon name={"check"} className="SetupStep-check" size={16} />
+        </span>
+        <h3 style={{ marginTop: 10 }} className="SetupStep-title Form-offset">
+          {title}
+        </h3>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/setup/components/UserStep.jsx b/frontend/src/metabase/setup/components/UserStep.jsx
index 3608bfb0579774d1c970d9ceb331b9bce6ee6d66..aaef411eeb301bbcfc9814ebda31b20eff1f788d 100644
--- a/frontend/src/metabase/setup/components/UserStep.jsx
+++ b/frontend/src/metabase/setup/components/UserStep.jsx
@@ -1,7 +1,7 @@
 /* eslint "react/prop-types": "warn" */
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -9,194 +9,304 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 import MetabaseSettings from "metabase/lib/settings";
 import MetabaseUtils from "metabase/lib/utils";
 
-import StepTitle from './StepTitle.jsx'
+import StepTitle from "./StepTitle.jsx";
 import CollapsedStep from "./CollapsedStep.jsx";
 
 import _ from "underscore";
 import cx from "classnames";
 
 export default class UserStep extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            fieldValues: this.props.userDetails || {
-                first_name: "",
-                last_name: "",
-                email: "",
-                password: "",
-                site_name: ""
-            },
-            formError: null,
-            passwordError: null,
-            valid: false,
-            validPassword: false
-        }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      fieldValues: this.props.userDetails || {
+        first_name: "",
+        last_name: "",
+        email: "",
+        password: "",
+        site_name: "",
+      },
+      formError: null,
+      passwordError: null,
+      valid: false,
+      validPassword: false,
+    };
+  }
+
+  static propTypes = {
+    stepNumber: PropTypes.number.isRequired,
+    activeStep: PropTypes.number.isRequired,
+    setActiveStep: PropTypes.func.isRequired,
+
+    userDetails: PropTypes.object,
+    setUserDetails: PropTypes.func.isRequired,
+    validatePassword: PropTypes.func.isRequired,
+  };
+
+  validateForm = () => {
+    let { fieldValues, valid, validPassword } = this.state;
+    let isValid = true;
+
+    // required: first_name, last_name, email, password
+    Object.keys(fieldValues).forEach(fieldName => {
+      if (MetabaseUtils.isEmpty(fieldValues[fieldName])) isValid = false;
+    });
+
+    if (!validPassword) {
+      isValid = false;
     }
 
-    static propTypes = {
-        stepNumber: PropTypes.number.isRequired,
-        activeStep: PropTypes.number.isRequired,
-        setActiveStep: PropTypes.func.isRequired,
-
-        userDetails: PropTypes.object,
-        setUserDetails: PropTypes.func.isRequired,
-        validatePassword: PropTypes.func.isRequired,
+    if (isValid !== valid) {
+      this.setState({
+        valid: isValid,
+      });
     }
-
-    validateForm = () => {
-        let { fieldValues, valid, validPassword } = this.state;
-        let isValid = true;
-
-        // required: first_name, last_name, email, password
-        Object.keys(fieldValues).forEach((fieldName) => {
-            if (MetabaseUtils.isEmpty(fieldValues[fieldName])) isValid = false;
-        });
-
-        if (!validPassword) {
-            isValid = false;
-        }
-
-        if(isValid !== valid) {
-            this.setState({
-                'valid': isValid
-            });
-        }
-    }
-
-    onPasswordBlur = async (e) => {
-        try {
-            await this.props.validatePassword(this.state.fieldValues.password);
-
-            this.setState({
-                passwordError: null,
-                validPassword: true
-            }, this.validateForm);
-
-        } catch(error) {
-            this.setState({
-                passwordError: error.data.errors.password,
-                validPassword: false
-            });
-
-            MetabaseAnalytics.trackEvent('Setup', 'Error', 'password validation');
-        }
+  };
+
+  onPasswordBlur = async e => {
+    try {
+      await this.props.validatePassword(this.state.fieldValues.password);
+
+      this.setState(
+        {
+          passwordError: null,
+          validPassword: true,
+        },
+        this.validateForm,
+      );
+    } catch (error) {
+      this.setState({
+        passwordError: error.data.errors.password,
+        validPassword: false,
+      });
+
+      MetabaseAnalytics.trackEvent("Setup", "Error", "password validation");
     }
+  };
 
-    formSubmitted = (e) => {
-        const { fieldValues } = this.state
-
-        e.preventDefault();
+  formSubmitted = e => {
+    const { fieldValues } = this.state;
 
-        this.setState({
-            formError: null
-        });
+    e.preventDefault();
 
-        let formErrors = {data:{errors:{}}};
+    this.setState({
+      formError: null,
+    });
 
-        // validate email address
-        if (!MetabaseUtils.validEmail(fieldValues.email)) {
-            formErrors.data.errors.email = t`Not a valid formatted email address`;
-        }
+    let formErrors = { data: { errors: {} } };
 
-        // TODO - validate password complexity
-
-        // validate password match
-        if (fieldValues.password !== fieldValues.password_confirm) {
-            formErrors.data.errors.password_confirm = t`Passwords do not match`;
-        }
-
-        if (_.keys(formErrors.data.errors).length > 0) {
-            this.setState({
-                formError: formErrors
-            });
-            return;
-        }
+    // validate email address
+    if (!MetabaseUtils.validEmail(fieldValues.email)) {
+      formErrors.data.errors.email = t`Not a valid formatted email address`;
+    }
 
-        this.props.setUserDetails({
-            'nextStep': this.props.stepNumber + 1,
-            'details': _.omit(fieldValues, "password_confirm")
-        });
+    // TODO - validate password complexity
 
-        MetabaseAnalytics.trackEvent('Setup', 'User Details Step');
+    // validate password match
+    if (fieldValues.password !== fieldValues.password_confirm) {
+      formErrors.data.errors.password_confirm = t`Passwords do not match`;
     }
 
-    updateFieldValue = (fieldName, value) =>  {
-        this.setState({
-            fieldValues: {
-                ...this.state.fieldValues,
-                [fieldName]: value
-            }
-        }, this.validateForm);
+    if (_.keys(formErrors.data.errors).length > 0) {
+      this.setState({
+        formError: formErrors,
+      });
+      return;
     }
 
-    onFirstNameChange = (e) => this.updateFieldValue("first_name", e.target.value)
-    onLastNameChange = (e) => this.updateFieldValue("last_name", e.target.value)
-    onEmailChange = (e) => this.updateFieldValue("email", e.target.value)
-    onPasswordChange = (e) => this.updateFieldValue("password", e.target.value)
-    onPasswordConfirmChange = (e) => this.updateFieldValue("password_confirm", e.target.value)
-    onSiteNameChange = (e) => this.updateFieldValue("site_name", e.target.value)
-
-    render() {
-        let { activeStep, setActiveStep, stepNumber, userDetails } = this.props;
-        let { formError, passwordError, valid } = this.state;
-
-        const passwordComplexityDesc = MetabaseSettings.passwordComplexity();
-        const stepText = (activeStep <= stepNumber) ? t`What should we call you?` : t`Hi, ${userDetails.first_name}. nice to meet you!`;
-
-        if (activeStep !== stepNumber) {
-            return (<CollapsedStep stepNumber={stepNumber} stepCircleText="1" stepText={stepText} isCompleted={activeStep > stepNumber} setActiveStep={setActiveStep}></CollapsedStep>)
-        } else {
-            return (
-                <section className="SetupStep SetupStep--active rounded full relative">
-                    <StepTitle title={stepText} circleText={"1"} />
-                    <form name="userForm" onSubmit={this.formSubmitted} noValidate className="mt2">
-                        <FormField className="Grid mb3" fieldName="first_name" formError={formError}>
-                            <div>
-                                <FormLabel title={t`First name`} fieldName="first_name" formError={formError}></FormLabel>
-                                <input className="Form-input Form-offset full" name="first_name" defaultValue={(userDetails) ? userDetails.first_name : ""} placeholder="Johnny" required autoFocus={true} onChange={this.onFirstNameChange} />
-                                <span className="Form-charm"></span>
-                            </div>
-                            <div>
-                                <FormLabel title={t`Last name`} fieldName="last_name" formError={formError}></FormLabel>
-                                <input className="Form-input Form-offset" name="last_name" defaultValue={(userDetails) ? userDetails.last_name : ""} placeholder="Appleseed" required onChange={this.onLastNameChange} />
-                                <span className="Form-charm"></span>
-                            </div>
-                        </FormField>
-
-                        <FormField fieldName="email" formError={formError}>
-                            <FormLabel title={t`Email address`} fieldName="email" formError={formError}></FormLabel>
-                            <input className="Form-input Form-offset full" name="email" defaultValue={(userDetails) ? userDetails.email : ""} placeholder="youlooknicetoday@email.com" required onChange={this.onEmailChange} />
-                            <span className="Form-charm"></span>
-                        </FormField>
-
-                        <FormField fieldName="password" formError={formError} error={(passwordError !== null)}>
-                            <FormLabel title={t`Create a password`} fieldName="password" formError={formError} message={passwordError}></FormLabel>
-                            <span style={{fontWeight: "normal"}} className="Form-label Form-offset">{passwordComplexityDesc}</span>
-                            <input className="Form-input Form-offset full" name="password" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder={t`Shhh...`} required onChange={this.onPasswordChange} onBlur={this.onPasswordBlur}/>
-                            <span className="Form-charm"></span>
-                        </FormField>
-
-                        <FormField fieldName="password_confirm" formError={formError}>
-                            <FormLabel title={t`Confirm password`} fieldName="password_confirm" formError={formError}></FormLabel>
-                            <input className="Form-input Form-offset full" name="password_confirm" type="password" defaultValue={(userDetails) ? userDetails.password : ""} placeholder={t`Shhh... but one more time so we get it right`} required onChange={this.onPasswordConfirmChange} />
-                            <span className="Form-charm"></span>
-                        </FormField>
-
-                        <FormField fieldName="site_name" formError={formError}>
-                            <FormLabel title={t`Your company or team name`} fieldName="site_name" formError={formError}></FormLabel>
-                            <input className="Form-input Form-offset full" name="site_name" type="text" defaultValue={(userDetails) ? userDetails.site_name : ""} placeholder={t`Department of awesome`} required onChange={this.onSiteNameChange} />
-                            <span className="Form-charm"></span>
-                        </FormField>
-
-                        <div className="Form-actions">
-                            <button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
-                                {t`Next`}
-                            </button>
-                            <FormMessage></FormMessage>
-                        </div>
-                    </form>
-                </section>
-            );
-        }
+    this.props.setUserDetails({
+      nextStep: this.props.stepNumber + 1,
+      details: _.omit(fieldValues, "password_confirm"),
+    });
+
+    MetabaseAnalytics.trackEvent("Setup", "User Details Step");
+  };
+
+  updateFieldValue = (fieldName, value) => {
+    this.setState(
+      {
+        fieldValues: {
+          ...this.state.fieldValues,
+          [fieldName]: value,
+        },
+      },
+      this.validateForm,
+    );
+  };
+
+  onFirstNameChange = e => this.updateFieldValue("first_name", e.target.value);
+  onLastNameChange = e => this.updateFieldValue("last_name", e.target.value);
+  onEmailChange = e => this.updateFieldValue("email", e.target.value);
+  onPasswordChange = e => this.updateFieldValue("password", e.target.value);
+  onPasswordConfirmChange = e =>
+    this.updateFieldValue("password_confirm", e.target.value);
+  onSiteNameChange = e => this.updateFieldValue("site_name", e.target.value);
+
+  render() {
+    let { activeStep, setActiveStep, stepNumber, userDetails } = this.props;
+    let { formError, passwordError, valid } = this.state;
+
+    const passwordComplexityDesc = MetabaseSettings.passwordComplexity();
+    const stepText =
+      activeStep <= stepNumber
+        ? t`What should we call you?`
+        : t`Hi, ${userDetails.first_name}. nice to meet you!`;
+
+    if (activeStep !== stepNumber) {
+      return (
+        <CollapsedStep
+          stepNumber={stepNumber}
+          stepCircleText="1"
+          stepText={stepText}
+          isCompleted={activeStep > stepNumber}
+          setActiveStep={setActiveStep}
+        />
+      );
+    } else {
+      return (
+        <section className="SetupStep SetupStep--active rounded full relative">
+          <StepTitle title={stepText} circleText={"1"} />
+          <form
+            name="userForm"
+            onSubmit={this.formSubmitted}
+            noValidate
+            className="mt2"
+          >
+            <FormField
+              className="Grid mb3"
+              fieldName="first_name"
+              formError={formError}
+            >
+              <div>
+                <FormLabel
+                  title={t`First name`}
+                  fieldName="first_name"
+                  formError={formError}
+                />
+                <input
+                  className="Form-input Form-offset full"
+                  name="first_name"
+                  defaultValue={userDetails ? userDetails.first_name : ""}
+                  placeholder="Johnny"
+                  required
+                  autoFocus={true}
+                  onChange={this.onFirstNameChange}
+                />
+                <span className="Form-charm" />
+              </div>
+              <div>
+                <FormLabel
+                  title={t`Last name`}
+                  fieldName="last_name"
+                  formError={formError}
+                />
+                <input
+                  className="Form-input Form-offset"
+                  name="last_name"
+                  defaultValue={userDetails ? userDetails.last_name : ""}
+                  placeholder="Appleseed"
+                  required
+                  onChange={this.onLastNameChange}
+                />
+                <span className="Form-charm" />
+              </div>
+            </FormField>
+
+            <FormField fieldName="email" formError={formError}>
+              <FormLabel
+                title={t`Email address`}
+                fieldName="email"
+                formError={formError}
+              />
+              <input
+                className="Form-input Form-offset full"
+                name="email"
+                defaultValue={userDetails ? userDetails.email : ""}
+                placeholder="youlooknicetoday@email.com"
+                required
+                onChange={this.onEmailChange}
+              />
+              <span className="Form-charm" />
+            </FormField>
+
+            <FormField
+              fieldName="password"
+              formError={formError}
+              error={passwordError !== null}
+            >
+              <FormLabel
+                title={t`Create a password`}
+                fieldName="password"
+                formError={formError}
+                message={passwordError}
+              />
+              <span
+                style={{ fontWeight: "normal" }}
+                className="Form-label Form-offset"
+              >
+                {passwordComplexityDesc}
+              </span>
+              <input
+                className="Form-input Form-offset full"
+                name="password"
+                type="password"
+                defaultValue={userDetails ? userDetails.password : ""}
+                placeholder={t`Shhh...`}
+                required
+                onChange={this.onPasswordChange}
+                onBlur={this.onPasswordBlur}
+              />
+              <span className="Form-charm" />
+            </FormField>
+
+            <FormField fieldName="password_confirm" formError={formError}>
+              <FormLabel
+                title={t`Confirm password`}
+                fieldName="password_confirm"
+                formError={formError}
+              />
+              <input
+                className="Form-input Form-offset full"
+                name="password_confirm"
+                type="password"
+                defaultValue={userDetails ? userDetails.password : ""}
+                placeholder={t`Shhh... but one more time so we get it right`}
+                required
+                onChange={this.onPasswordConfirmChange}
+              />
+              <span className="Form-charm" />
+            </FormField>
+
+            <FormField fieldName="site_name" formError={formError}>
+              <FormLabel
+                title={t`Your company or team name`}
+                fieldName="site_name"
+                formError={formError}
+              />
+              <input
+                className="Form-input Form-offset full"
+                name="site_name"
+                type="text"
+                defaultValue={userDetails ? userDetails.site_name : ""}
+                placeholder={t`Department of awesome`}
+                required
+                onChange={this.onSiteNameChange}
+              />
+              <span className="Form-charm" />
+            </FormField>
+
+            <div className="Form-actions">
+              <button
+                className={cx("Button", { "Button--primary": valid })}
+                disabled={!valid}
+              >
+                {t`Next`}
+              </button>
+              <FormMessage />
+            </div>
+          </form>
+        </section>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/setup/containers/SetupApp.jsx b/frontend/src/metabase/setup/containers/SetupApp.jsx
index aa2f5617c9af0dc43021b64b5621a47d1d5b2968..f6d31350068bc054e3794ec44dbc794c7fc490fb 100644
--- a/frontend/src/metabase/setup/containers/SetupApp.jsx
+++ b/frontend/src/metabase/setup/containers/SetupApp.jsx
@@ -6,30 +6,30 @@ import Setup from "../components/Setup.jsx";
 
 import { setupSelectors } from "../selectors";
 import {
-    setUserDetails,
-    validatePassword,
-    setActiveStep,
-    validateDatabase,
-    setDatabaseDetails,
-    setAllowTracking,
-    submitSetup,
+  setUserDetails,
+  validatePassword,
+  setActiveStep,
+  validateDatabase,
+  setDatabaseDetails,
+  setAllowTracking,
+  submitSetup,
 } from "../actions";
 
 const mapStateToProps = setupSelectors;
 
 const mapDispatchToProps = {
-    setUserDetails,
-    validatePassword,
-    setActiveStep,
-    validateDatabase,
-    setDatabaseDetails,
-    setAllowTracking,
-    submitSetup
+  setUserDetails,
+  validatePassword,
+  setActiveStep,
+  validateDatabase,
+  setDatabaseDetails,
+  setAllowTracking,
+  submitSetup,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class SetupApp extends Component {
-    render() {
-        return <Setup {...this.props} />;
-    }
+  render() {
+    return <Setup {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/setup/reducers.js b/frontend/src/metabase/setup/reducers.js
index 7ce670c4587bef5d4137eee55975e01455b34e45..96f8b9e478c7811042ff3b7b8a6c3b01262f69a9 100644
--- a/frontend/src/metabase/setup/reducers.js
+++ b/frontend/src/metabase/setup/reducers.js
@@ -1,37 +1,54 @@
-import { handleActions } from 'redux-actions';
+import { handleActions } from "redux-actions";
 
 import {
-    SET_ACTIVE_STEP,
-    SET_USER_DETAILS,
-    SET_DATABASE_DETAILS,
-    SET_ALLOW_TRACKING,
-    SUBMIT_SETUP,
-    COMPLETE_SETUP
-} from './actions';
-
-
-export const activeStep = handleActions({
+  SET_ACTIVE_STEP,
+  SET_USER_DETAILS,
+  SET_DATABASE_DETAILS,
+  SET_ALLOW_TRACKING,
+  SUBMIT_SETUP,
+  COMPLETE_SETUP,
+} from "./actions";
+
+export const activeStep = handleActions(
+  {
     [SET_ACTIVE_STEP]: { next: (state, { payload }) => payload },
     [SET_USER_DETAILS]: { next: (state, { payload }) => payload.nextStep },
-    [SET_DATABASE_DETAILS]: { next: (state, { payload }) => payload.nextStep }
-}, 0);
-
-export const userDetails = handleActions({
-    [SET_USER_DETAILS]: { next: (state, { payload }) => payload.details }
-}, null);
-
-export const databaseDetails = handleActions({
-    [SET_DATABASE_DETAILS]: { next: (state, { payload }) => payload.details }
-}, null);
-
-export const allowTracking = handleActions({
-    [SET_ALLOW_TRACKING]: { next: (state, { payload }) => payload }
-}, true);
-
-export const setupError = handleActions({
-    [SUBMIT_SETUP]: { next: (state, { payload }) => payload }
-}, null);
-
-export const setupComplete = handleActions({
-    [COMPLETE_SETUP]: { next: (state, { payload }) => true }
-}, false);
+    [SET_DATABASE_DETAILS]: { next: (state, { payload }) => payload.nextStep },
+  },
+  0,
+);
+
+export const userDetails = handleActions(
+  {
+    [SET_USER_DETAILS]: { next: (state, { payload }) => payload.details },
+  },
+  null,
+);
+
+export const databaseDetails = handleActions(
+  {
+    [SET_DATABASE_DETAILS]: { next: (state, { payload }) => payload.details },
+  },
+  null,
+);
+
+export const allowTracking = handleActions(
+  {
+    [SET_ALLOW_TRACKING]: { next: (state, { payload }) => payload },
+  },
+  true,
+);
+
+export const setupError = handleActions(
+  {
+    [SUBMIT_SETUP]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
+
+export const setupComplete = handleActions(
+  {
+    [COMPLETE_SETUP]: { next: (state, { payload }) => true },
+  },
+  false,
+);
diff --git a/frontend/src/metabase/setup/selectors.js b/frontend/src/metabase/setup/selectors.js
index c99084fe6ea079e465496e85f79b6e83ba92a48e..b1b9a8e977db1d0fcf798c61a6fcbeabd9d593f6 100644
--- a/frontend/src/metabase/setup/selectors.js
+++ b/frontend/src/metabase/setup/selectors.js
@@ -1,16 +1,35 @@
-import { createSelector } from 'reselect';
-
-
-const activeStepSelector          = state => state.setup.activeStep;
-const userDetailsSelector         = state => state.setup.userDetails;
-const databaseDetailsSelector     = state => state.setup.databaseDetails;
-const allowTrackingSelector       = state => state.setup.allowTracking;
-const setupErrorSelector          = state => state.setup.setupError;
-const setupCompleteSelector       = state => state.setup.setupComplete;
+import { createSelector } from "reselect";
 
+const activeStepSelector = state => state.setup.activeStep;
+const userDetailsSelector = state => state.setup.userDetails;
+const databaseDetailsSelector = state => state.setup.databaseDetails;
+const allowTrackingSelector = state => state.setup.allowTracking;
+const setupErrorSelector = state => state.setup.setupError;
+const setupCompleteSelector = state => state.setup.setupComplete;
 
 // our master selector which combines all of our partial selectors above
 export const setupSelectors = createSelector(
-	[activeStepSelector, userDetailsSelector, databaseDetailsSelector, allowTrackingSelector, setupErrorSelector, setupCompleteSelector],
-	(activeStep, userDetails, databaseDetails, allowTracking, setupError, setupComplete) => ({activeStep, userDetails, databaseDetails, allowTracking, setupError, setupComplete})
+  [
+    activeStepSelector,
+    userDetailsSelector,
+    databaseDetailsSelector,
+    allowTrackingSelector,
+    setupErrorSelector,
+    setupCompleteSelector,
+  ],
+  (
+    activeStep,
+    userDetails,
+    databaseDetails,
+    allowTracking,
+    setupError,
+    setupComplete,
+  ) => ({
+    activeStep,
+    userDetails,
+    databaseDetails,
+    allowTracking,
+    setupError,
+    setupComplete,
+  }),
 );
diff --git a/frontend/src/metabase/store.js b/frontend/src/metabase/store.js
index 4b524a52cb747082f6b78a319f8c93f9bc1ef1d4..022146bf976938a7c68703aeb6c68d0ccb3706c9 100644
--- a/frontend/src/metabase/store.js
+++ b/frontend/src/metabase/store.js
@@ -1,8 +1,8 @@
 /* @flow weak */
 
-import { combineReducers, applyMiddleware, createStore, compose } from 'redux'
+import { combineReducers, applyMiddleware, createStore, compose } from "redux";
 import { reducer as form } from "redux-form";
-import { routerReducer as routing, routerMiddleware } from 'react-router-redux'
+import { routerReducer as routing, routerMiddleware } from "react-router-redux";
 
 import promise from "redux-promise";
 import logger from "redux-logger";
@@ -14,36 +14,37 @@ import { DEBUG } from "metabase/lib/debug";
  * `dispatch.action(type, payload)` which creates an action that adheres to Flux Standard Action format.
  */
 const thunkWithDispatchAction = ({ dispatch, getState }) => next => action => {
-    if (typeof action === 'function') {
-        const dispatchAugmented = Object.assign(dispatch, {
-            action: (type, payload) => dispatch({ type, payload })
-        });
-
-        return action(dispatchAugmented, getState);
-    }
-    return next(action);
-};
-
-const devToolsExtension = window.devToolsExtension ? window.devToolsExtension() : (f => f);
-
-export function getStore(reducers, history, intialState, enhancer = (a) => a) {
-
-    const reducer = combineReducers({
-        ...reducers,
-        form,
-        routing,
+  if (typeof action === "function") {
+    const dispatchAugmented = Object.assign(dispatch, {
+      action: (type, payload) => dispatch({ type, payload }),
     });
 
-    const middleware = [
-        thunkWithDispatchAction,
-        promise,
-        ...(DEBUG ? [logger] : []),
-        routerMiddleware(history)
-    ];
-
-    return createStore(reducer, intialState, compose(
-        applyMiddleware(...middleware),
-        devToolsExtension,
-        enhancer,
-    ));
-}
\ No newline at end of file
+    return action(dispatchAugmented, getState);
+  }
+  return next(action);
+};
+
+const devToolsExtension = window.devToolsExtension
+  ? window.devToolsExtension()
+  : f => f;
+
+export function getStore(reducers, history, intialState, enhancer = a => a) {
+  const reducer = combineReducers({
+    ...reducers,
+    form,
+    routing,
+  });
+
+  const middleware = [
+    thunkWithDispatchAction,
+    promise,
+    ...(DEBUG ? [logger] : []),
+    ...(history ? [routerMiddleware(history)] : []),
+  ];
+
+  return createStore(
+    reducer,
+    intialState,
+    compose(applyMiddleware(...middleware), devToolsExtension, enhancer),
+  );
+}
diff --git a/frontend/src/metabase/tutorial/PageFlag.css b/frontend/src/metabase/tutorial/PageFlag.css
index 42cfe58787659cec6ee4da31d91ff017ffea7346..146dfe199ff3984fda3d647cd6bd3f6c64f1beb4 100644
--- a/frontend/src/metabase/tutorial/PageFlag.css
+++ b/frontend/src/metabase/tutorial/PageFlag.css
@@ -4,13 +4,13 @@
   position: relative;
   min-width: 50px;
   height: 24px;
-  background-color: rgb(53,141,248);
+  background-color: rgb(53, 141, 248);
   box-sizing: content-box;
   border-top-right-radius: 8px;
   border-bottom-right-radius: 8px;
   border: 3px solid white;
   border-left: 1px solid white;
-  box-shadow: 2px 2px 6px rgba(0,0,0,0.5);
+  box-shadow: 2px 2px 6px rgba(0, 0, 0, 0.5);
 
   color: white;
   font-weight: bold;
@@ -35,7 +35,7 @@
   left: -12px;
   border-top: 12px solid transparent;
   border-bottom: 12px solid transparent;
-  border-right: 12px solid rgb(53,141,248);
+  border-right: 12px solid rgb(53, 141, 248);
 }
 
 .PageFlag:before {
@@ -44,7 +44,7 @@
   left: -16px;
   border-top: 15px solid transparent;
   border-bottom: 15px solid transparent;
-  border-right:15px solid white;
+  border-right: 15px solid white;
 }
 
 .PageFlag--large {
@@ -91,42 +91,42 @@
 
 @keyframes bounceleft {
   0% {
-    transform:translateX(50px);
+    transform: translateX(50px);
   }
   5% {
-    transform:translateX(50px);
+    transform: translateX(50px);
   }
   15% {
-    transform:translateX(0);
+    transform: translateX(0);
   }
   30% {
-    transform:translateX(25px);
+    transform: translateX(25px);
   }
   40% {
-    transform:translateX(0px);
+    transform: translateX(0px);
   }
   50% {
-    transform:translateX(15px);
+    transform: translateX(15px);
   }
   70% {
-    transform:translateX(0px);
+    transform: translateX(0px);
   }
   80% {
-    transform:translateX(7px);
+    transform: translateX(7px);
   }
   90% {
-    transform:translateX(0px);
+    transform: translateX(0px);
   }
   95% {
-    transform:translateX(3px);
+    transform: translateX(3px);
   }
   97% {
-    transform:translateX(0px);
+    transform: translateX(0px);
   }
   99% {
-    transform:translateX(1px);
+    transform: translateX(1px);
   }
   100% {
-    transform:translateX(0);
+    transform: translateX(0);
   }
 }
diff --git a/frontend/src/metabase/tutorial/PageFlag.jsx b/frontend/src/metabase/tutorial/PageFlag.jsx
index 56c1ca977cf9a24f90a1cc2926f01393a6df9cca..e70bbe48052b84f735e35e2d3d75887e1a4de936 100644
--- a/frontend/src/metabase/tutorial/PageFlag.jsx
+++ b/frontend/src/metabase/tutorial/PageFlag.jsx
@@ -10,24 +10,37 @@ import cx from "classnames";
 
 @BodyComponent
 export default class PageFlag extends Component {
-    renderPageFlag() {
-        let position = this.props.target.getBoundingClientRect();
-        let isLarge = !!this.props.text;
-        let style = {
-            position: "absolute",
-            left: position.left + position.width,
-            top: position.top + position.height / 2 - (isLarge ? 21 : 12)
-        };
-        return (
-            <div key="flag" className={cx("PageFlag", { "PageFlag--large": isLarge, "bounce-left": this.props.bounce })} style={style}>{this.props.text}</div>
-        );
-    }
+  renderPageFlag() {
+    let position = this.props.target.getBoundingClientRect();
+    let isLarge = !!this.props.text;
+    let style = {
+      position: "absolute",
+      left: position.left + position.width,
+      top: position.top + position.height / 2 - (isLarge ? 21 : 12),
+    };
+    return (
+      <div
+        key="flag"
+        className={cx("PageFlag", {
+          "PageFlag--large": isLarge,
+          "bounce-left": this.props.bounce,
+        })}
+        style={style}
+      >
+        {this.props.text}
+      </div>
+    );
+  }
 
-    render() {
-        return (
-            <ReactCSSTransitionGroup transitionName="PageFlag" transitionEnterTimeout={250} transitionLeaveTimeout={250}>
-                { this.props.target ? [this.renderPageFlag()] : [] }
-            </ReactCSSTransitionGroup>
-        );
-    }
+  render() {
+    return (
+      <ReactCSSTransitionGroup
+        transitionName="PageFlag"
+        transitionEnterTimeout={250}
+        transitionLeaveTimeout={250}
+      >
+        {this.props.target ? [this.renderPageFlag()] : []}
+      </ReactCSSTransitionGroup>
+    );
+  }
 }
diff --git a/frontend/src/metabase/tutorial/Portal.jsx b/frontend/src/metabase/tutorial/Portal.jsx
index 150e19b27f15a8ab6e8bffa342f710c5b551636d..9318f9127195941d089bfbd2df8ab964f8892870 100644
--- a/frontend/src/metabase/tutorial/Portal.jsx
+++ b/frontend/src/metabase/tutorial/Portal.jsx
@@ -4,66 +4,80 @@ import BodyComponent from "metabase/components/BodyComponent.jsx";
 
 @BodyComponent
 export default class Portal extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            target: null,
-            position: { top: 0, left: 0, height: 0, width: 0 }
-        };
-    }
-
-    static defaultProps = {
-        padding: 10
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      target: null,
+      position: { top: 0, left: 0, height: 0, width: 0 },
     };
+  }
 
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
-    }
+  static defaultProps = {
+    padding: 10,
+  };
 
-    componentWillReceiveProps(newProps) {
-        if (newProps.target !== this.state.target) {
-            const { target, padding } = newProps;
-            let position;
-            if (target === true) {
-                position = {
-                    top: this.state.position.top + this.state.position.height / 2,
-                    left: this.state.position.left + this.state.position.width / 2,
-                    width: -padding * 2,
-                    height: -padding * 2
-                }
-            } else if (target) {
-                position = target.getBoundingClientRect();
-            }
-            this.setState({ target, position: position || { top: 0, left: 0, height: 0, width: 0 } });
-        }
-    }
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
 
-    getStyles(position) {
-        const { padding } = this.props;
-        return {
-            position: "absolute",
-            boxSizing: "content-box",
-            border: "10000px solid rgba(0,0,0,0.70)",
-            boxShadow: "inset 0px 0px 8px rgba(0,0,0,0.25)",
-            transform: "translate(-10000px, -10000px)",
-            borderRadius: "10010px",
-            pointerEvents: "none",
-            transition: position.width < 0 ? "all 0.25s ease-in-out" : "all 0.5s ease-in-out",
-            top: position.top - padding,
-            left: position.left - padding,
-            width: position.width + 2 * padding,
-            height: position.height + 2 * padding
+  componentWillReceiveProps(newProps) {
+    if (newProps.target !== this.state.target) {
+      const { target, padding } = newProps;
+      let position;
+      if (target === true) {
+        position = {
+          top: this.state.position.top + this.state.position.height / 2,
+          left: this.state.position.left + this.state.position.width / 2,
+          width: -padding * 2,
+          height: -padding * 2,
         };
+      } else if (target) {
+        position = target.getBoundingClientRect();
+      }
+      this.setState({
+        target,
+        position: position || { top: 0, left: 0, height: 0, width: 0 },
+      });
     }
+  }
+
+  getStyles(position) {
+    const { padding } = this.props;
+    return {
+      position: "absolute",
+      boxSizing: "content-box",
+      border: "10000px solid rgba(0,0,0,0.70)",
+      boxShadow: "inset 0px 0px 8px rgba(0,0,0,0.25)",
+      transform: "translate(-10000px, -10000px)",
+      borderRadius: "10010px",
+      pointerEvents: "none",
+      transition:
+        position.width < 0 ? "all 0.25s ease-in-out" : "all 0.5s ease-in-out",
+      top: position.top - padding,
+      left: position.left - padding,
+      width: position.width + 2 * padding,
+      height: position.height + 2 * padding,
+    };
+  }
 
-    render() {
-        if (!this.props.target) {
-            return <div className="hide" />;
-        }
-        return (
-            <div style={{ position: "fixed", top: 0, left: 0, right: 0, bottom: 0, overflow: "hidden", pointerEvents: "none" }}>
-                <div style={this.getStyles(this.state.position)}/>
-            </div>
-        );
+  render() {
+    if (!this.props.target) {
+      return <div className="hide" />;
     }
+    return (
+      <div
+        style={{
+          position: "fixed",
+          top: 0,
+          left: 0,
+          right: 0,
+          bottom: 0,
+          overflow: "hidden",
+          pointerEvents: "none",
+        }}
+      >
+        <div style={this.getStyles(this.state.position)} />
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
index 6fea7453924b3e2eaa8a3f92f009c664c7939a90..d89ddaea58ab79c73af7cf19004ec335101be932 100644
--- a/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
+++ b/frontend/src/metabase/tutorial/QueryBuilderTutorial.jsx
@@ -1,194 +1,251 @@
 /* eslint-disable react/display-name */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Tutorial, { qs, qsWithContent } from "./Tutorial.jsx";
 
 import RetinaImage from "react-retina-image";
 
 const QUERY_BUILDER_STEPS = [
-    {
-        getPortalTarget: () => qs(".GuiBuilder"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage className="mb2" forceOriginalDimensions={false} src="app/assets/img/qb_tutorial/question_builder.png" width={186} />
-                <h3>{t`Welcome to the Query Builder!`}</h3>
-                <p>{t`The Query Builder lets you assemble questions (or "queries") to ask about your data.`}</p>
-                <a className="Button Button--primary" onClick={props.onNext}>{t`Tell me more`}</a>
-            </div>
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-data"),
-        getModalTarget: () => qs(".GuiBuilder-data"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage id="QB-TutorialTableImg" className="mb2" forceOriginalDimensions={false} src="app/assets/img/qb_tutorial/table.png" width={157} />
-                <h3>{t`Start by picking the table with the data that you have a question about.`}</h3>
-                <p>{t`Go ahead and select the "Orders" table from the dropdown menu.`}</p>
-            </div>,
-        shouldAllowEvent: (e) => qs(".GuiBuilder-data a").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-data"),
-        getPageFlagTarget: () => qsWithContent(".List-section-header", "Sample Dataset"),
-        shouldAllowEvent: (e) => qsWithContent(".List-section-header", "Sample Dataset").contains(e.target),
-        optional: true
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-data"),
-        getPageFlagTarget: () => qsWithContent(".List-item", "Orders"),
-        shouldAllowEvent: (e) => qsWithContent(".List-item > a", "Orders").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
-        getModalTarget: () => qs(".GuiBuilder-filtered-by"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage
-                    className="mb2"
-                    forceOriginalDimensions={false}
-                    id="QB-TutorialFunnelImg"
-                    src="app/assets/img/qb_tutorial/funnel.png"
-                    width={135}
-                />
-                <h3>{t`Filter your data to get just what you want.`}</h3>
-                <p>{t`Click the plus button and select the "Created At" field.`}</p>
-            </div>,
-        shouldAllowEvent: (e) => qs(".GuiBuilder-filtered-by a").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
-        getPageFlagTarget: () => qsWithContent(".List-item", "Created At"),
-        shouldAllowEvent: (e) => qsWithContent(".List-item > a", "Created At").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
-        getPageFlagText: () => t`Here we can pick how many days we want to see data for, try 10`,
-        getPageFlagTarget: () => qs('[data-ui-tag="relative-date-input"]'),
-        shouldAllowEvent: (e) => qs('[data-ui-tag="relative-date-input"]').contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
-        getPageFlagTarget: () => qs('[data-ui-tag="add-filter"]'),
-        shouldAllowEvent: (e) => qs('[data-ui-tag="add-filter"]').contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".Query-section-aggregation"),
-        getModalTarget: () => qs(".Query-section-aggregation"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage
-                    className="mb2"
-                    forceOriginalDimensions={false}
-                    id="QB-TutorialCalculatorImg"
-                    src="app/assets/img/qb_tutorial/calculator.png"
-                    width={115}
-                />
-                <h3>{t`Here's where you can choose to add or average your data, count the number of rows in the table, or just view the raw data.`}</h3>
-                <p>{t`Try it: click on <strong>Raw Data</strong> to change it to <strong>Count of rows</strong> so we can count how many orders there are in this table.`}</p>
-            </div>,
-        shouldAllowEvent: (e) => qs('.View-section-aggregation').contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".Query-section-aggregation"),
-        getPageFlagTarget: () => qsWithContent(".List-item", "Count of rows"),
-        shouldAllowEvent: (e) => qsWithContent(".List-item > a", "Count of rows").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".Query-section-breakout"),
-        getModalTarget: () => qs(".Query-section-breakout"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage
-                    className="mb2"
-                    forceOriginalDimensions={false}
-                    id="QB-TutorialBananaImg"
-                    src="app/assets/img/qb_tutorial/banana.png"
-                    width={232}
-                />
-                <h3>{t`Add a grouping to break out your results by category, day, month, and more.`}</h3>
-                <p>{t`Let's do it: click on <strong>Add a grouping</strong>, and choose <strong>Created At: by Week</strong>.`}</p>
-            </div>,
-        shouldAllowEvent: (e) => qs('.Query-section-breakout').contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".Query-section-breakout"),
-        getPageFlagTarget: () => qs(".FieldList-grouping-trigger"),
-        getPageFlagText: () => t`Click on "by day" to change it to "Week."`,
-        shouldAllowEvent: (e) => qs(".FieldList-grouping-trigger").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".Query-section-breakout"),
-        getPageFlagTarget: () => qsWithContent(".List-item", "Week"),
-        shouldAllowEvent: (e) => qsWithContent(".List-item > a", "Week").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".RunButton"),
-        getModalTarget: () => qs(".RunButton"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage
-                    className="mb2"
-                    forceOriginalDimensions={false}
-                    id="QB-TutorialRocketImg"
-                    src="app/assets/img/qb_tutorial/rocket.png"
-                    width={217}
-                />
-                <h3>{t`Run Your Query.`}</h3>
-                <p>{t`You're doing so well! Click <strong>Run query</strong> to get your results!`}</p>
-            </div>,
-        shouldAllowEvent: (e) => qs(".RunButton").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".VisualizationSettings"),
-        getModalTarget: () => qs(".VisualizationSettings"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage
-                    className="mb2"
-                    forceOriginalDimensions={false}
-                    id="QB-TutorialChartImg"
-                    src="app/assets/img/qb_tutorial/chart.png"
-                    width={160}
-                />
-                <h3>{t`You can view your results as a chart instead of a table.`}</h3>
-                <p>{t`Everbody likes charts! Click the <strong>Visualization</strong> dropdown and select <strong>Line</strong>.`}</p>
-            </div>,
-        shouldAllowEvent: (e) => qs(".VisualizationSettings a").contains(e.target)
-    },
-    {
-        getPortalTarget: () => qs(".VisualizationSettings"),
-        getPageFlagTarget: () => qsWithContent(".ChartType-popover li", "Line"),
-        shouldAllowEvent: (e) => qsWithContent(".ChartType-popover li", "Line").contains(e.target)
-    },
-    {
-        getPortalTarget: () => true,
-        getModal: (props) =>
-            <div className="text-centered">
-                <RetinaImage
-                    className="mb2"
-                    forceOriginalDimensions={false}
-                    id="QB-TutorialBoatImg"
-                    src="app/assets/img/qb_tutorial/boat.png" width={190}
-                />
-                <h3>{t`Well done!`}</h3>
-                <p>{t`That's all! If you still have questions, check out our`} <a className="link" target="_blank" href="http://www.metabase.com/docs/latest/users-guide/start.html">{t`User's Guide`}</a>. {t`Have fun exploring your data!`}</p>
-                <a className="Button Button--primary" onClick={props.onNext}>{t`Thanks`}!</a>
-            </div>
-    },
-    {
-        getModalTarget: () => qsWithContent(".Header-buttonSection a", "Save"),
-        getModal: (props) =>
-            <div className="text-centered">
-                <h3>{t`Save Your Questions`}!</h3>
-                <p>{t`By the way, you can save your questions so you can refer to them later. Saved Questions can also be put into dashboards or Pulses.`}</p>
-                <a className="Button Button--primary" onClick={props.onClose}>{t`Sounds good`}</a>
-            </div>
-    }
-]
+  {
+    getPortalTarget: () => qs(".GuiBuilder"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          src="app/assets/img/qb_tutorial/question_builder.png"
+          width={186}
+        />
+        <h3>{t`Welcome to the Query Builder!`}</h3>
+        <p
+        >{t`The Query Builder lets you assemble questions (or "queries") to ask about your data.`}</p>
+        <a
+          className="Button Button--primary"
+          onClick={props.onNext}
+        >{t`Tell me more`}</a>
+      </div>
+    ),
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-data"),
+    getModalTarget: () => qs(".GuiBuilder-data"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          id="QB-TutorialTableImg"
+          className="mb2"
+          forceOriginalDimensions={false}
+          src="app/assets/img/qb_tutorial/table.png"
+          width={157}
+        />
+        <h3
+        >{t`Start by picking the table with the data that you have a question about.`}</h3>
+        <p
+        >{t`Go ahead and select the "Orders" table from the dropdown menu.`}</p>
+      </div>
+    ),
+    shouldAllowEvent: e => qs(".GuiBuilder-data a").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-data"),
+    getPageFlagTarget: () =>
+      qsWithContent(".List-section-header", "Sample Dataset"),
+    shouldAllowEvent: e =>
+      qsWithContent(".List-section-header", "Sample Dataset").contains(
+        e.target,
+      ),
+    optional: true,
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-data"),
+    getPageFlagTarget: () => qsWithContent(".List-item", "Orders"),
+    shouldAllowEvent: e =>
+      qsWithContent(".List-item > a", "Orders").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
+    getModalTarget: () => qs(".GuiBuilder-filtered-by"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          id="QB-TutorialFunnelImg"
+          src="app/assets/img/qb_tutorial/funnel.png"
+          width={135}
+        />
+        <h3>{t`Filter your data to get just what you want.`}</h3>
+        <p>{t`Click the plus button and select the "Created At" field.`}</p>
+      </div>
+    ),
+    shouldAllowEvent: e => qs(".GuiBuilder-filtered-by a").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
+    getPageFlagTarget: () => qsWithContent(".List-item", "Created At"),
+    shouldAllowEvent: e =>
+      qsWithContent(".List-item > a", "Created At").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
+    getPageFlagText: () =>
+      t`Here we can pick how many days we want to see data for, try 10`,
+    getPageFlagTarget: () => qs('[data-ui-tag="relative-date-input"]'),
+    shouldAllowEvent: e =>
+      qs('[data-ui-tag="relative-date-input"]').contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".GuiBuilder-filtered-by"),
+    getPageFlagTarget: () => qs('[data-ui-tag="add-filter"]'),
+    shouldAllowEvent: e => qs('[data-ui-tag="add-filter"]').contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".Query-section-aggregation"),
+    getModalTarget: () => qs(".Query-section-aggregation"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          id="QB-TutorialCalculatorImg"
+          src="app/assets/img/qb_tutorial/calculator.png"
+          width={115}
+        />
+        <h3
+        >{t`Here's where you can choose to add or average your data, count the number of rows in the table, or just view the raw data.`}</h3>
+        <p
+        >{t`Try it: click on <strong>Raw Data</strong> to change it to <strong>Count of rows</strong> so we can count how many orders there are in this table.`}</p>
+      </div>
+    ),
+    shouldAllowEvent: e => qs(".View-section-aggregation").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".Query-section-aggregation"),
+    getPageFlagTarget: () => qsWithContent(".List-item", "Count of rows"),
+    shouldAllowEvent: e =>
+      qsWithContent(".List-item > a", "Count of rows").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".Query-section-breakout"),
+    getModalTarget: () => qs(".Query-section-breakout"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          id="QB-TutorialBananaImg"
+          src="app/assets/img/qb_tutorial/banana.png"
+          width={232}
+        />
+        <h3
+        >{t`Add a grouping to break out your results by category, day, month, and more.`}</h3>
+        <p
+        >{t`Let's do it: click on <strong>Add a grouping</strong>, and choose <strong>Created At: by Week</strong>.`}</p>
+      </div>
+    ),
+    shouldAllowEvent: e => qs(".Query-section-breakout").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".Query-section-breakout"),
+    getPageFlagTarget: () => qs(".FieldList-grouping-trigger"),
+    getPageFlagText: () => t`Click on "by day" to change it to "Week."`,
+    shouldAllowEvent: e => qs(".FieldList-grouping-trigger").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".Query-section-breakout"),
+    getPageFlagTarget: () => qsWithContent(".List-item", "Week"),
+    shouldAllowEvent: e =>
+      qsWithContent(".List-item > a", "Week").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".RunButton"),
+    getModalTarget: () => qs(".RunButton"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          id="QB-TutorialRocketImg"
+          src="app/assets/img/qb_tutorial/rocket.png"
+          width={217}
+        />
+        <h3>{t`Run Your Query.`}</h3>
+        <p
+        >{t`You're doing so well! Click <strong>Run query</strong> to get your results!`}</p>
+      </div>
+    ),
+    shouldAllowEvent: e => qs(".RunButton").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".VisualizationSettings"),
+    getModalTarget: () => qs(".VisualizationSettings"),
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          id="QB-TutorialChartImg"
+          src="app/assets/img/qb_tutorial/chart.png"
+          width={160}
+        />
+        <h3>{t`You can view your results as a chart instead of a table.`}</h3>
+        <p
+        >{t`Everbody likes charts! Click the <strong>Visualization</strong> dropdown and select <strong>Line</strong>.`}</p>
+      </div>
+    ),
+    shouldAllowEvent: e => qs(".VisualizationSettings a").contains(e.target),
+  },
+  {
+    getPortalTarget: () => qs(".VisualizationSettings"),
+    getPageFlagTarget: () => qsWithContent(".ChartType-popover li", "Line"),
+    shouldAllowEvent: e =>
+      qsWithContent(".ChartType-popover li", "Line").contains(e.target),
+  },
+  {
+    getPortalTarget: () => true,
+    getModal: props => (
+      <div className="text-centered">
+        <RetinaImage
+          className="mb2"
+          forceOriginalDimensions={false}
+          id="QB-TutorialBoatImg"
+          src="app/assets/img/qb_tutorial/boat.png"
+          width={190}
+        />
+        <h3>{t`Well done!`}</h3>
+        <p>
+          {t`That's all! If you still have questions, check out our`}{" "}
+          <a
+            className="link"
+            target="_blank"
+            href="http://www.metabase.com/docs/latest/users-guide/start.html"
+          >{t`User's Guide`}</a>. {t`Have fun exploring your data!`}
+        </p>
+        <a className="Button Button--primary" onClick={props.onNext}>
+          {t`Thanks`}!
+        </a>
+      </div>
+    ),
+  },
+  {
+    getModalTarget: () => qsWithContent(".Header-buttonSection a", "Save"),
+    getModal: props => (
+      <div className="text-centered">
+        <h3>{t`Save Your Questions`}!</h3>
+        <p
+        >{t`By the way, you can save your questions so you can refer to them later. Saved Questions can also be put into dashboards or Pulses.`}</p>
+        <a
+          className="Button Button--primary"
+          onClick={props.onClose}
+        >{t`Sounds good`}</a>
+      </div>
+    ),
+  },
+];
 
 export default class QueryBuilderTutorial extends Component {
-    render() {
-        return <Tutorial steps={QUERY_BUILDER_STEPS} {...this.props} />;
-    }
+  render() {
+    return <Tutorial steps={QUERY_BUILDER_STEPS} {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/tutorial/Tutorial.jsx b/frontend/src/metabase/tutorial/Tutorial.jsx
index c112bb52d8fe05094080217384b4d26988be029e..bded7bfb6a7ce47c4bc3128247ebc9f5f748d331 100644
--- a/frontend/src/metabase/tutorial/Tutorial.jsx
+++ b/frontend/src/metabase/tutorial/Tutorial.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Modal from "metabase/components/Modal.jsx";
 import Popover from "metabase/components/Popover.jsx";
 
@@ -12,257 +12,301 @@ import MetabaseAnalytics from "metabase/lib/analytics";
 import _ from "underscore";
 
 export function qs(selector) {
-    return document.querySelector(selector);
+  return document.querySelector(selector);
 }
 
 export function qsWithContent(selector, content) {
-    for (let element of document.querySelectorAll(selector)) {
-        if (element.textContent === content) {
-            return element;
-        }
+  for (let element of document.querySelectorAll(selector)) {
+    if (element.textContent === content) {
+      return element;
     }
+  }
 }
 
 const STEP_WARNING_TIMEOUT = 60 * 1000; // 60 seconds
 const STEP_SKIP_TIMEOUT = 500; // 500 ms
 
 export default class Tutorial extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            step: 0,
-            bouncePageFlag: false
-        };
-
-        _.bindAll(this, "close", "next", "back", "nextModal", "backModal", "mouseEventInterceptHandler");
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      step: 0,
+      bouncePageFlag: false,
+    };
+
+    _.bindAll(
+      this,
+      "close",
+      "next",
+      "back",
+      "nextModal",
+      "backModal",
+      "mouseEventInterceptHandler",
+    );
+  }
+
+  componentWillMount() {
+    ["mousedown", "mouseup", "mousemove", "click"].forEach(event => {
+      document.addEventListener(event, this.mouseEventInterceptHandler, true);
+    });
+  }
+
+  componentWillUnmount() {
+    ["mousedown", "mouseup", "mousemove", "click"].forEach(event => {
+      document.removeEventListener(
+        event,
+        this.mouseEventInterceptHandler,
+        true,
+      );
+    });
+  }
+
+  mouseEventInterceptHandler(e) {
+    let step = this.props.steps[this.state.step];
+
+    // don't intercept if we've somehow gotten into a weird state
+    if (!step) {
+      return;
     }
 
-    componentWillMount() {
-        ["mousedown", "mouseup", "mousemove", "click"].forEach((event) => {
-            document.addEventListener(event, this.mouseEventInterceptHandler, true);
-        });
+    // don't intercept on the last step
+    if (this.state.step === this.props.steps.length - 1) {
+      return;
     }
 
-    componentWillUnmount() {
-        ["mousedown", "mouseup", "mousemove", "click"].forEach((event) => {
-            document.removeEventListener(event, this.mouseEventInterceptHandler, true);
-        });
+    // don't intercept events within the modal screens
+    for (let modal of document.querySelectorAll(".TutorialModalContent")) {
+      if (modal.contains(e.target)) {
+        return;
+      }
     }
 
-    mouseEventInterceptHandler(e) {
-        let step = this.props.steps[this.state.step];
-
-        // don't intercept if we've somehow gotten into a weird state
-        if (!step) {
-            return;
-        }
-
-        // don't intercept on the last step
-        if (this.state.step === this.props.steps.length - 1) {
-            return;
-        }
-
-        // don't intercept events within the modal screens
-        for (let modal of document.querySelectorAll(".TutorialModalContent")) {
-            if (modal.contains(e.target)) {
-                return;
-            }
+    if (step.shouldAllowEvent) {
+      try {
+        if (step.shouldAllowEvent(e)) {
+          if (e.type === "click") {
+            setTimeout(this.next, 100);
+          }
+          return;
         }
+      } catch (e) {}
+    }
 
-        if (step.shouldAllowEvent) {
-            try {
-                if (step.shouldAllowEvent(e)) {
-                    if (e.type === "click") {
-                        setTimeout(this.next, 100);
-                    }
-                    return;
-                }
-            } catch (e) {
-            }
-        }
+    if (e.type === "click" && this.refs.pageflag) {
+      this.setState({ bouncePageFlag: true });
+      setTimeout(() => this.setState({ bouncePageFlag: false }), 1500);
+    }
 
-        if (e.type === "click" && this.refs.pageflag) {
-            this.setState({ bouncePageFlag: true });
-            setTimeout(() => this.setState({ bouncePageFlag: false }), 1500);
-        }
+    e.stopPropagation();
+    e.preventDefault();
+  }
 
-        e.stopPropagation();
-        e.preventDefault();
+  next() {
+    if (this.state.step + 1 === this.props.steps.length) {
+      this.close();
+      MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Finish");
+    } else {
+      this.setStep(this.state.step + 1);
     }
-
-    next() {
-        if (this.state.step + 1 === this.props.steps.length) {
-            this.close();
-            MetabaseAnalytics.trackEvent('QueryBuilder', 'Tutorial Finish');
-        } else {
-            this.setStep(this.state.step + 1);
-        }
+  }
+
+  back() {
+    this.setStep(Math.max(0, this.state.step - 1));
+  }
+
+  nextModal() {
+    let step = this.state.step;
+    while (++step < this.props.steps.length) {
+      if (this.props.steps[step].getModal) {
+        this.setStep(step);
+        return;
+      }
     }
-
-    back() {
-        this.setStep(Math.max(0, this.state.step - 1));
+    this.close();
+  }
+
+  backModal() {
+    let step = this.state.step;
+    while (--step >= 0) {
+      if (this.props.steps[step].getModal) {
+        this.setStep(step);
+        return;
+      }
     }
+    this.setStep(0);
+  }
 
-    nextModal() {
-        let step = this.state.step;
-        while (++step < this.props.steps.length) {
-            if (this.props.steps[step].getModal) {
-                this.setStep(step);
-                return;
-            }
-        }
-        this.close();
+  setStep(step) {
+    if (this.state.stepTimeout != null) {
+      clearTimeout(this.state.stepTimeout);
     }
-
-    backModal() {
-        let step = this.state.step;
-        while (--step >= 0) {
-            if (this.props.steps[step].getModal) {
-                this.setStep(step);
-                return;
-            }
-        }
-        this.setStep(0);
+    if (this.state.skipTimeout != null) {
+      clearTimeout(this.state.skipTimeout);
     }
-
-    setStep(step) {
-        if (this.state.stepTimeout != null) {
-            clearTimeout(this.state.stepTimeout);
-        }
-        if (this.state.skipTimeout != null) {
-            clearTimeout(this.state.skipTimeout);
+    this.setState({
+      step,
+      stepTimeout: setTimeout(() => {
+        this.setState({ stepTimeout: null });
+      }, STEP_WARNING_TIMEOUT),
+      skipTimeout: setTimeout(() => {
+        if (
+          this.props.steps[step].optional &&
+          this.getTargets(this.props.steps[step]).missingTarget
+        ) {
+          this.next();
         }
-        this.setState({
-            step,
-            stepTimeout: setTimeout(() => {
-                this.setState({ stepTimeout: null })
-            }, STEP_WARNING_TIMEOUT),
-            skipTimeout: setTimeout(() => {
-                if (this.props.steps[step].optional && this.getTargets(this.props.steps[step]).missingTarget) {
-                    this.next();
-                }
-            }, STEP_SKIP_TIMEOUT)
-        });
-        MetabaseAnalytics.trackEvent('QueryBuilder', 'Tutorial Step', step);
+      }, STEP_SKIP_TIMEOUT),
+    });
+    MetabaseAnalytics.trackEvent("QueryBuilder", "Tutorial Step", step);
+  }
+
+  close() {
+    this.props.onClose();
+  }
+
+  getTargets(step) {
+    let missingTarget = false;
+
+    let pageFlagTarget;
+    if (step.getPageFlagTarget) {
+      try {
+        pageFlagTarget = step.getPageFlagTarget();
+      } catch (e) {}
+      if (pageFlagTarget == undefined) {
+        missingTarget = missingTarget || true;
+      }
     }
 
-    close() {
-        this.props.onClose();
+    let portalTarget;
+    if (step.getPortalTarget) {
+      try {
+        portalTarget = step.getPortalTarget();
+      } catch (e) {}
+      if (portalTarget == undefined) {
+        missingTarget = missingTarget || true;
+      }
     }
 
-    getTargets(step) {
-        let missingTarget = false;
-
-        let pageFlagTarget;
-        if (step.getPageFlagTarget) {
-            try { pageFlagTarget = step.getPageFlagTarget(); } catch (e) {}
-            if (pageFlagTarget == undefined) {
-                missingTarget = missingTarget || true;
-            }
-        }
-
-        let portalTarget;
-        if (step.getPortalTarget) {
-            try { portalTarget = step.getPortalTarget(); } catch (e) {}
-            if (portalTarget == undefined) {
-                missingTarget = missingTarget || true;
-            }
-        }
-
-        let modalTarget;
-        if (step.getModalTarget) {
-            try { modalTarget = step.getModalTarget(); } catch (e) {}
-            if (modalTarget == undefined) {
-                missingTarget = missingTarget || true;
-            }
-        }
-
-        return {
-            missingTarget,
-            pageFlagTarget,
-            portalTarget,
-            modalTarget
-        };
+    let modalTarget;
+    if (step.getModalTarget) {
+      try {
+        modalTarget = step.getModalTarget();
+      } catch (e) {}
+      if (modalTarget == undefined) {
+        missingTarget = missingTarget || true;
+      }
     }
 
-    // HACK: Ensure we render twice so that getTargets can get the rendered DOM elements
-    componentWillReceiveProps() {
-        this.setState({ rendered: false });
-    }
-    componentDidMount() {
-        this.componentDidUpdate();
-    }
-    componentDidUpdate() {
-        if (!this.state.rendered) {
-            this.setState({ rendered: true });
-        }
+    return {
+      missingTarget,
+      pageFlagTarget,
+      portalTarget,
+      modalTarget,
+    };
+  }
+
+  // HACK: Ensure we render twice so that getTargets can get the rendered DOM elements
+  componentWillReceiveProps() {
+    this.setState({ rendered: false });
+  }
+  componentDidMount() {
+    this.componentDidUpdate();
+  }
+  componentDidUpdate() {
+    if (!this.state.rendered) {
+      this.setState({ rendered: true });
     }
+  }
 
-    render() {
-        let step = this.props.steps[this.state.step];
+  render() {
+    let step = this.props.steps[this.state.step];
 
-        if (!step) {
-            return null;
-        }
-
-        const { missingTarget, pageFlagTarget, portalTarget, modalTarget } = this.getTargets(step);
-
-        if (missingTarget && this.state.stepTimeout === null) {
-            return (
-                <Modal className="Modal TutorialModal">
-                    <TutorialModal
-                        onBack={this.backModal}
-                        onClose={this.close}
-                    >
-                        <div className="text-centered">
-                            <h2>{t`Whoops!`}</h2>
-                            <p className="my2">{t`Sorry, it looks like something went wrong. Please try restarting the tutorial in a minute.`}</p>
-                            <button className="Button Button--primary" onClick={this.close}>{t`Okay`}</button>
-                        </div>
-                    </TutorialModal>
-                </Modal>
-            );
-        }
+    if (!step) {
+      return null;
+    }
 
-        let modal;
-        if (step.getModal) {
-            let modalSteps = this.props.steps.filter((s) => !!s.getModal);
-            let modalStepIndex = modalSteps.indexOf(step);
-            modal = (
-                <TutorialModal
-                    modalStepIndex={modalStepIndex}
-                    modalStepCount={modalSteps.length}
-                    onBack={this.backModal}
-                    onClose={this.close}
-                >
-                    {step.getModal({
-                        onNext: this.next,
-                        onClose: this.close
-                    })}
-                </TutorialModal>
-            )
-        }
+    const {
+      missingTarget,
+      pageFlagTarget,
+      portalTarget,
+      modalTarget,
+    } = this.getTargets(step);
+
+    if (missingTarget && this.state.stepTimeout === null) {
+      return (
+        <Modal className="Modal TutorialModal">
+          <TutorialModal onBack={this.backModal} onClose={this.close}>
+            <div className="text-centered">
+              <h2>{t`Whoops!`}</h2>
+              <p className="my2">{t`Sorry, it looks like something went wrong. Please try restarting the tutorial in a minute.`}</p>
+              <button
+                className="Button Button--primary"
+                onClick={this.close}
+              >{t`Okay`}</button>
+            </div>
+          </TutorialModal>
+        </Modal>
+      );
+    }
 
-        let pageFlagText;
-        if (step.getPageFlagText) {
-            pageFlagText = step.getPageFlagText();
-        }
+    let modal;
+    if (step.getModal) {
+      let modalSteps = this.props.steps.filter(s => !!s.getModal);
+      let modalStepIndex = modalSteps.indexOf(step);
+      modal = (
+        <TutorialModal
+          modalStepIndex={modalStepIndex}
+          modalStepCount={modalSteps.length}
+          onBack={this.backModal}
+          onClose={this.close}
+        >
+          {step.getModal({
+            onNext: this.next,
+            onClose: this.close,
+          })}
+        </TutorialModal>
+      );
+    }
 
-        // only pass onClose to modal/popover if we're on the last step
-        let onClose;
-        if (this.state.step === this.props.steps.length - 1) {
-            onClose = this.close;
-        }
+    let pageFlagText;
+    if (step.getPageFlagText) {
+      pageFlagText = step.getPageFlagText();
+    }
 
-        return (
-            <div>
-                <PageFlag ref="pageflag" className="z5" target={pageFlagTarget} text={pageFlagText} bounce={this.state.bouncePageFlag} />
-                { portalTarget &&
-                    <Portal className="z2" target={portalTarget} />
-                }
-                <Modal isOpen={!!(modal && !step.getModalTarget)} style={{ backgroundColor: "transparent" }} className="Modal TutorialModal" onClose={onClose}>{modal}</Modal>
-                <Popover isOpen={!!(modal && step.getModalTarget && modalTarget)} target={step.getModalTarget} targetOffsetY={25} onClose={onClose} className="TutorialModal">{modal}</Popover>
-            </div>
-        );
+    // only pass onClose to modal/popover if we're on the last step
+    let onClose;
+    if (this.state.step === this.props.steps.length - 1) {
+      onClose = this.close;
     }
+
+    return (
+      <div>
+        <PageFlag
+          ref="pageflag"
+          className="z5"
+          target={pageFlagTarget}
+          text={pageFlagText}
+          bounce={this.state.bouncePageFlag}
+        />
+        {portalTarget && <Portal className="z2" target={portalTarget} />}
+        <Modal
+          isOpen={!!(modal && !step.getModalTarget)}
+          style={{ backgroundColor: "transparent" }}
+          className="Modal TutorialModal"
+          onClose={onClose}
+        >
+          {modal}
+        </Modal>
+        <Popover
+          isOpen={!!(modal && step.getModalTarget && modalTarget)}
+          target={step.getModalTarget}
+          targetOffsetY={25}
+          onClose={onClose}
+          className="TutorialModal"
+        >
+          {modal}
+        </Popover>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/tutorial/TutorialModal.jsx b/frontend/src/metabase/tutorial/TutorialModal.jsx
index 03a1849550e1c3c33ce359534e1f50aaef2aff7a..f0f08f863354595b11858ab0aa5431bcf55793a7 100644
--- a/frontend/src/metabase/tutorial/TutorialModal.jsx
+++ b/frontend/src/metabase/tutorial/TutorialModal.jsx
@@ -1,29 +1,41 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 
 const ENABLE_BACK_BUTTON = false; // disabled due to possibility of getting in inconsistent states
 
 export default class TutorialModal extends Component {
-    render() {
-        const { modalStepIndex, modalStepCount } = this.props;
-        let showStepCount = modalStepIndex != null;
-        let showBackButton = (ENABLE_BACK_BUTTON && modalStepIndex > 0);
-        return (
-            <div className="TutorialModalContent p2">
-                <div className="flex">
-                    <a className="text-grey-4 p1 cursor-pointer flex-align-right" onClick={this.props.onClose}>
-                        <Icon name='close' size={16}/>
-                    </a>
-                </div>
-                <div className="px4">
-                    {this.props.children}
-                </div>
-                <div className="flex">
-                    { showBackButton && <a className="text-grey-4 cursor-pointer" onClick={this.props.onBack}>back</a> }
-                    { showStepCount && <span className="text-grey-4 flex-align-right">{modalStepIndex + 1} {t`of`} {modalStepCount}</span> }
-                </div>
-            </div>
-        );
-    }
+  render() {
+    const { modalStepIndex, modalStepCount } = this.props;
+    let showStepCount = modalStepIndex != null;
+    let showBackButton = ENABLE_BACK_BUTTON && modalStepIndex > 0;
+    return (
+      <div className="TutorialModalContent p2">
+        <div className="flex">
+          <a
+            className="text-grey-4 p1 cursor-pointer flex-align-right"
+            onClick={this.props.onClose}
+          >
+            <Icon name="close" size={16} />
+          </a>
+        </div>
+        <div className="px4">{this.props.children}</div>
+        <div className="flex">
+          {showBackButton && (
+            <a
+              className="text-grey-4 cursor-pointer"
+              onClick={this.props.onBack}
+            >
+              back
+            </a>
+          )}
+          {showStepCount && (
+            <span className="text-grey-4 flex-align-right">
+              {modalStepIndex + 1} {t`of`} {modalStepCount}
+            </span>
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/user/actions.js b/frontend/src/metabase/user/actions.js
index 5d4aac8d8cce0ab204e3858ab4f83c167b27b140..2b9820646a142e60409c2b2aa53e6fe90fa1425b 100644
--- a/frontend/src/metabase/user/actions.js
+++ b/frontend/src/metabase/user/actions.js
@@ -1,5 +1,5 @@
 import { createAction } from "redux-actions";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { createThunkAction } from "metabase/lib/redux";
 
 import { UserApi } from "metabase/services";
@@ -7,53 +7,54 @@ import { UserApi } from "metabase/services";
 import { refreshCurrentUser } from "metabase/redux/user";
 
 // action constants
-export const CHANGE_TAB = 'CHANGE_TAB';
-export const UPDATE_PASSWORD = 'UPDATE_PASSWORD';
-export const UPDATE_USER = 'UPDATE_USER';
-
+export const CHANGE_TAB = "CHANGE_TAB";
+export const UPDATE_PASSWORD = "UPDATE_PASSWORD";
+export const UPDATE_USER = "UPDATE_USER";
 
 // action creators
 
 export const setTab = createAction(CHANGE_TAB);
 
-export const updatePassword = createThunkAction(UPDATE_PASSWORD, function(user_id, new_password, current_password) {
-    return async function(dispatch, getState) {
-        try {
-            await UserApi.update_password({
-                id: user_id,
-                password: new_password,
-                old_password: current_password
-            });
-
-            return {
-                success: true,
-                data:{
-                    message: t`Password updated successfully!`
-                }
-            };
-
-        } catch(error) {
-            return error;
-        }
-    };
+export const updatePassword = createThunkAction(UPDATE_PASSWORD, function(
+  user_id,
+  new_password,
+  current_password,
+) {
+  return async function(dispatch, getState) {
+    try {
+      await UserApi.update_password({
+        id: user_id,
+        password: new_password,
+        old_password: current_password,
+      });
+
+      return {
+        success: true,
+        data: {
+          message: t`Password updated successfully!`,
+        },
+      };
+    } catch (error) {
+      return error;
+    }
+  };
 });
 
 export const updateUser = createThunkAction(UPDATE_USER, function(user) {
-    return async function(dispatch, getState) {
-        try {
-            await UserApi.update(user);
-
-            dispatch(refreshCurrentUser());
-
-            return {
-                success: true,
-                data:{
-                    message: t`Account updated successfully!`
-                }
-            };
-
-        } catch(error) {
-            return error;
-        }
-    };
+  return async function(dispatch, getState) {
+    try {
+      await UserApi.update(user);
+
+      dispatch(refreshCurrentUser());
+
+      return {
+        success: true,
+        data: {
+          message: t`Account updated successfully!`,
+        },
+      };
+    } catch (error) {
+      return error;
+    }
+  };
 });
diff --git a/frontend/src/metabase/user/components/SetUserPassword.jsx b/frontend/src/metabase/user/components/SetUserPassword.jsx
index 84f193ecea25cfa6c664ba8b4db5cc7aa5db2625..6369684a99300e0700d0a1e1477f4e92d1ad6e0b 100644
--- a/frontend/src/metabase/user/components/SetUserPassword.jsx
+++ b/frontend/src/metabase/user/components/SetUserPassword.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -14,110 +14,175 @@ import _ from "underscore";
 import cx from "classnames";
 
 export default class SetUserPassword extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-        this.state = { formError: null, valid: false }
-    }
-
-    static propTypes = {
-        submitFn: PropTypes.func.isRequired,
-        user: PropTypes.object,
-        updatePasswordResult: PropTypes.object.isRequired
-    };
-
-    componentDidMount() {
-        this.validateForm();
-    }
-
-    validateForm() {
-        let { valid } = this.state;
-        let isValid = true;
-
-        // required: first_name, last_name, email
-        for (var fieldName in this.refs) {
-            let node = ReactDOM.findDOMNode(this.refs[fieldName]);
-            if (node.required && MetabaseUtils.isEmpty(node.value)) isValid = false;
-        }
-
-        if(isValid !== valid) {
-            this.setState({
-                'valid': isValid
-            });
-        }
+  constructor(props, context) {
+    super(props, context);
+    this.state = { formError: null, valid: false };
+  }
+
+  static propTypes = {
+    submitFn: PropTypes.func.isRequired,
+    user: PropTypes.object,
+    updatePasswordResult: PropTypes.object.isRequired,
+  };
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  validateForm() {
+    let { valid } = this.state;
+    let isValid = true;
+
+    // required: first_name, last_name, email
+    for (var fieldName in this.refs) {
+      let node = ReactDOM.findDOMNode(this.refs[fieldName]);
+      if (node.required && MetabaseUtils.isEmpty(node.value)) isValid = false;
     }
 
-    onChange() {
-        this.validateForm();
+    if (isValid !== valid) {
+      this.setState({
+        valid: isValid,
+      });
     }
+  }
 
-    formSubmitted(e) {
-        e.preventDefault();
-
-        this.setState({
-            formError: null
-        });
-
-        let formErrors = {data:{errors:{}}};
+  onChange() {
+    this.validateForm();
+  }
 
-        // make sure new passwords match
-        if (ReactDOM.findDOMNode(this.refs.password).value !== ReactDOM.findDOMNode(this.refs.password2).value) {
-            formErrors.data.errors.password2 = t`Passwords do not match`;
-        }
+  formSubmitted(e) {
+    e.preventDefault();
 
-        if (_.keys(formErrors.data.errors).length > 0) {
-            this.setState({
-                formError: formErrors
-            });
-            return;
-        }
+    this.setState({
+      formError: null,
+    });
 
-        let details = {};
+    let formErrors = { data: { errors: {} } };
 
-        details.user_id = this.props.user.id;
-        details.old_password = ReactDOM.findDOMNode(this.refs.oldPassword).value;
-        details.password = ReactDOM.findDOMNode(this.refs.password).value;
-
-        this.props.submitFn(details);
+    // make sure new passwords match
+    if (
+      ReactDOM.findDOMNode(this.refs.password).value !==
+      ReactDOM.findDOMNode(this.refs.password2).value
+    ) {
+      formErrors.data.errors.password2 = t`Passwords do not match`;
     }
 
-    render() {
-        const { updatePasswordResult } = this.props;
-        let { formError, valid } = this.state;
-        const passwordComplexity = MetabaseSettings.passwordComplexity(true);
-
-        formError = (updatePasswordResult && !formError) ? updatePasswordResult : formError;
-
-        return (
-            <div>
-                <form className="Form-new bordered rounded shadowed" onSubmit={this.formSubmitted.bind(this)} noValidate>
-                    <FormField fieldName="old_password" formError={formError}>
-                        <FormLabel title={t`Current password`} fieldName="old_password" formError={formError}></FormLabel>
-                        <input ref="oldPassword" type="password" className="Form-input Form-offset full" name="old_password" placeholder={t`Shhh...`} onChange={this.onChange.bind(this)} autoFocus={true} required />
-                        <span className="Form-charm"></span>
-                    </FormField>
-
-                    <FormField fieldName="password" formError={formError}>
-                        <FormLabel title={t`New password`} fieldName="password" formError={formError} ></FormLabel>
-                        <span style={{fontWeight: "400"}} className="Form-label Form-offset">{passwordComplexity}</span>
-                        <input ref="password" type="password" className="Form-input Form-offset full" name="password" placeholder={t`Make sure its secure like the instructions above`} onChange={this.onChange.bind(this)} required />
-                        <span className="Form-charm"></span>
-                    </FormField>
-
-                    <FormField fieldName="password2" formError={formError}>
-                        <FormLabel title={t`Confirm new password`} fieldName="password2" formError={formError} ></FormLabel>
-                        <input ref="password2" type="password" className="Form-input Form-offset full" name="password" placeholder={t`Make sure it matches the one you just entered`} required onChange={this.onChange.bind(this)} />
-                        <span className="Form-charm"></span>
-                    </FormField>
-
-                    <div className="Form-actions">
-                        <button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
-                            {t`Save`}
-                        </button>
-                        <FormMessage formError={(updatePasswordResult && !updatePasswordResult.success && !formError) ? updatePasswordResult : undefined} formSuccess={(updatePasswordResult && updatePasswordResult.success) ? updatePasswordResult : undefined} />
-                    </div>
-                </form>
-            </div>
-        );
+    if (_.keys(formErrors.data.errors).length > 0) {
+      this.setState({
+        formError: formErrors,
+      });
+      return;
     }
+
+    let details = {};
+
+    details.user_id = this.props.user.id;
+    details.old_password = ReactDOM.findDOMNode(this.refs.oldPassword).value;
+    details.password = ReactDOM.findDOMNode(this.refs.password).value;
+
+    this.props.submitFn(details);
+  }
+
+  render() {
+    const { updatePasswordResult } = this.props;
+    let { formError, valid } = this.state;
+    const passwordComplexity = MetabaseSettings.passwordComplexity(true);
+
+    formError =
+      updatePasswordResult && !formError ? updatePasswordResult : formError;
+
+    return (
+      <div>
+        <form
+          className="Form-new bordered rounded shadowed"
+          onSubmit={this.formSubmitted.bind(this)}
+          noValidate
+        >
+          <FormField fieldName="old_password" formError={formError}>
+            <FormLabel
+              title={t`Current password`}
+              fieldName="old_password"
+              formError={formError}
+            />
+            <input
+              ref="oldPassword"
+              type="password"
+              className="Form-input Form-offset full"
+              name="old_password"
+              placeholder={t`Shhh...`}
+              onChange={this.onChange.bind(this)}
+              autoFocus={true}
+              required
+            />
+            <span className="Form-charm" />
+          </FormField>
+
+          <FormField fieldName="password" formError={formError}>
+            <FormLabel
+              title={t`New password`}
+              fieldName="password"
+              formError={formError}
+            />
+            <span
+              style={{ fontWeight: "400" }}
+              className="Form-label Form-offset"
+            >
+              {passwordComplexity}
+            </span>
+            <input
+              ref="password"
+              type="password"
+              className="Form-input Form-offset full"
+              name="password"
+              placeholder={t`Make sure its secure like the instructions above`}
+              onChange={this.onChange.bind(this)}
+              required
+            />
+            <span className="Form-charm" />
+          </FormField>
+
+          <FormField fieldName="password2" formError={formError}>
+            <FormLabel
+              title={t`Confirm new password`}
+              fieldName="password2"
+              formError={formError}
+            />
+            <input
+              ref="password2"
+              type="password"
+              className="Form-input Form-offset full"
+              name="password"
+              placeholder={t`Make sure it matches the one you just entered`}
+              required
+              onChange={this.onChange.bind(this)}
+            />
+            <span className="Form-charm" />
+          </FormField>
+
+          <div className="Form-actions">
+            <button
+              className={cx("Button", { "Button--primary": valid })}
+              disabled={!valid}
+            >
+              {t`Save`}
+            </button>
+            <FormMessage
+              formError={
+                updatePasswordResult &&
+                !updatePasswordResult.success &&
+                !formError
+                  ? updatePasswordResult
+                  : undefined
+              }
+              formSuccess={
+                updatePasswordResult && updatePasswordResult.success
+                  ? updatePasswordResult
+                  : undefined
+              }
+            />
+          </div>
+        </form>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/user/components/UpdateUserDetails.jsx b/frontend/src/metabase/user/components/UpdateUserDetails.jsx
index 7442b7e8a70cf0a71b59be8bd0c80d4e11180537..ca8e1c870a328cb46e1f2b553e9d6b15dfd6bce6 100644
--- a/frontend/src/metabase/user/components/UpdateUserDetails.jsx
+++ b/frontend/src/metabase/user/components/UpdateUserDetails.jsx
@@ -2,7 +2,7 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import FormField from "metabase/components/form/FormField.jsx";
 import FormLabel from "metabase/components/form/FormLabel.jsx";
 import FormMessage from "metabase/components/form/FormMessage.jsx";
@@ -13,121 +13,169 @@ import _ from "underscore";
 import cx from "classnames";
 
 export default class UpdateUserDetails extends Component {
-
-    constructor(props, context) {
-        super(props, context);
-        this.state = { formError: null, valid: false }
-    }
-
-    static propTypes = {
-        submitFn: PropTypes.func.isRequired,
-        user: PropTypes.object,
-        updateUserResult: PropTypes.object.isRequired
-    };
-
-    componentDidMount() {
-        this.validateForm();
-    }
-
-    validateForm() {
-        let { valid } = this.state;
-        let isValid = true;
-
-        // required: first_name, last_name, email
-        for (var fieldName in this.refs) {
-            let node = ReactDOM.findDOMNode(this.refs[fieldName]);
-            if (node.required && MetabaseUtils.isEmpty(node.value)) isValid = false;
-        }
-
-        if(isValid !== valid) {
-            this.setState({
-                'valid': isValid
-            });
-        }
+  constructor(props, context) {
+    super(props, context);
+    this.state = { formError: null, valid: false };
+  }
+
+  static propTypes = {
+    submitFn: PropTypes.func.isRequired,
+    user: PropTypes.object,
+    updateUserResult: PropTypes.object.isRequired,
+  };
+
+  componentDidMount() {
+    this.validateForm();
+  }
+
+  validateForm() {
+    let { valid } = this.state;
+    let isValid = true;
+
+    // required: first_name, last_name, email
+    for (var fieldName in this.refs) {
+      let node = ReactDOM.findDOMNode(this.refs[fieldName]);
+      if (node.required && MetabaseUtils.isEmpty(node.value)) isValid = false;
     }
 
-    onChange() {
-        this.validateForm();
+    if (isValid !== valid) {
+      this.setState({
+        valid: isValid,
+      });
     }
+  }
 
-    formSubmitted(e) {
-        e.preventDefault();
-
-        this.setState({
-            formError: null
-        });
-
-        let formErrors = {data:{errors:{}}};
+  onChange() {
+    this.validateForm();
+  }
 
-        // validate email address
-        if (!MetabaseUtils.validEmail(ReactDOM.findDOMNode(this.refs.email).value)) {
-            formErrors.data.errors.email = t`Not a valid formatted email address`;
-        }
+  formSubmitted(e) {
+    e.preventDefault();
 
-        if (_.keys(formErrors.data.errors).length > 0) {
-            this.setState({
-                formError: formErrors
-            });
-            return;
-        }
+    this.setState({
+      formError: null,
+    });
 
-        let user = (this.props.user) ? _.clone(this.props.user) : {};
+    let formErrors = { data: { errors: {} } };
 
-        user.first_name = ReactDOM.findDOMNode(this.refs.firstName).value;
-        user.last_name = ReactDOM.findDOMNode(this.refs.lastName).value;
-        user.email = ReactDOM.findDOMNode(this.refs.email).value;
-
-        this.props.submitFn(user);
+    // validate email address
+    if (
+      !MetabaseUtils.validEmail(ReactDOM.findDOMNode(this.refs.email).value)
+    ) {
+      formErrors.data.errors.email = t`Not a valid formatted email address`;
     }
 
-    render() {
-        const { updateUserResult, user } = this.props;
-        const { formError, valid } = this.state;
-        const managed = user.google_auth || user.ldap_auth;
-
-        return (
-            <div>
-                <form className="Form-new bordered rounded shadowed" onSubmit={this.formSubmitted.bind(this)} noValidate>
-                    <FormField fieldName="first_name" formError={formError}>
-                        <FormLabel title={t`First name`} fieldName="first_name" formError={formError}></FormLabel>
-                        <input ref="firstName" className="Form-input Form-offset full" name="name" defaultValue={(user) ? user.first_name : null} placeholder="Johnny" onChange={this.onChange.bind(this)} />
-                        <span className="Form-charm"></span>
-                    </FormField>
-
-                    <FormField fieldName="last_name" formError={formError}>
-                        <FormLabel title={t`Last name`} fieldName="last_name" formError={formError} ></FormLabel>
-                        <input ref="lastName" className="Form-input Form-offset full" name="name" defaultValue={(user) ? user.last_name : null} placeholder="Appleseed" required onChange={this.onChange.bind(this)} />
-                        <span className="Form-charm"></span>
-                    </FormField>
-
-                    <FormField fieldName="email" formError={formError}>
-                        <FormLabel title={ user.google_auth ? t`Sign in with Google Email address` : t`Email address`} fieldName="email" formError={formError} ></FormLabel>
-                        <input
-                            ref="email"
-                            className={
-                              cx("Form-offset full", {
-                                "Form-input" : !managed,
-                                "text-grey-2 h1 borderless mt1": managed
-                              })
-                            }
-                            name="email"
-                            defaultValue={(user) ? user.email : null}
-                            placeholder="youlooknicetoday@email.com"
-                            required
-                            onChange={this.onChange.bind(this)}
-                            disabled={managed}
-                        />
-                        { !managed && <span className="Form-charm"></span>}
-                    </FormField>
-
-                    <div className="Form-actions">
-                        <button className={cx("Button", {"Button--primary": valid})} disabled={!valid}>
-                            {t`Save`}
-                        </button>
-                        <FormMessage formError={(updateUserResult && !updateUserResult.success) ? updateUserResult : undefined} formSuccess={(updateUserResult && updateUserResult.success) ? updateUserResult : undefined} />
-                    </div>
-                </form>
-            </div>
-        );
+    if (_.keys(formErrors.data.errors).length > 0) {
+      this.setState({
+        formError: formErrors,
+      });
+      return;
     }
+
+    let user = this.props.user ? _.clone(this.props.user) : {};
+
+    user.first_name = ReactDOM.findDOMNode(this.refs.firstName).value;
+    user.last_name = ReactDOM.findDOMNode(this.refs.lastName).value;
+    user.email = ReactDOM.findDOMNode(this.refs.email).value;
+
+    this.props.submitFn(user);
+  }
+
+  render() {
+    const { updateUserResult, user } = this.props;
+    const { formError, valid } = this.state;
+    const managed = user.google_auth || user.ldap_auth;
+
+    return (
+      <div>
+        <form
+          className="Form-new bordered rounded shadowed"
+          onSubmit={this.formSubmitted.bind(this)}
+          noValidate
+        >
+          <FormField fieldName="first_name" formError={formError}>
+            <FormLabel
+              title={t`First name`}
+              fieldName="first_name"
+              formError={formError}
+            />
+            <input
+              ref="firstName"
+              className="Form-input Form-offset full"
+              name="name"
+              defaultValue={user ? user.first_name : null}
+              placeholder="Johnny"
+              onChange={this.onChange.bind(this)}
+            />
+            <span className="Form-charm" />
+          </FormField>
+
+          <FormField fieldName="last_name" formError={formError}>
+            <FormLabel
+              title={t`Last name`}
+              fieldName="last_name"
+              formError={formError}
+            />
+            <input
+              ref="lastName"
+              className="Form-input Form-offset full"
+              name="name"
+              defaultValue={user ? user.last_name : null}
+              placeholder="Appleseed"
+              required
+              onChange={this.onChange.bind(this)}
+            />
+            <span className="Form-charm" />
+          </FormField>
+
+          <FormField fieldName="email" formError={formError}>
+            <FormLabel
+              title={
+                user.google_auth
+                  ? t`Sign in with Google Email address`
+                  : t`Email address`
+              }
+              fieldName="email"
+              formError={formError}
+            />
+            <input
+              ref="email"
+              className={cx("Form-offset full", {
+                "Form-input": !managed,
+                "text-grey-2 h1 borderless mt1": managed,
+              })}
+              name="email"
+              defaultValue={user ? user.email : null}
+              placeholder="youlooknicetoday@email.com"
+              required
+              onChange={this.onChange.bind(this)}
+              disabled={managed}
+            />
+            {!managed && <span className="Form-charm" />}
+          </FormField>
+
+          <div className="Form-actions">
+            <button
+              className={cx("Button", { "Button--primary": valid })}
+              disabled={!valid}
+            >
+              {t`Save`}
+            </button>
+            <FormMessage
+              formError={
+                updateUserResult && !updateUserResult.success
+                  ? updateUserResult
+                  : undefined
+              }
+              formSuccess={
+                updateUserResult && updateUserResult.success
+                  ? updateUserResult
+                  : undefined
+              }
+            />
+          </div>
+        </form>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/user/components/UserSettings.jsx b/frontend/src/metabase/user/components/UserSettings.jsx
index 97235318600d6312e11b38cad13cb8e585efb434..443b02fb749e6c7f2220733a8dfa785dad717e49 100644
--- a/frontend/src/metabase/user/components/UserSettings.jsx
+++ b/frontend/src/metabase/user/components/UserSettings.jsx
@@ -2,76 +2,93 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import cx from "classnames";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import SetUserPassword from "./SetUserPassword.jsx";
 import UpdateUserDetails from "./UpdateUserDetails.jsx";
 
-
 export default class UserSettings extends Component {
+  static propTypes = {
+    tab: PropTypes.string.isRequired,
+    user: PropTypes.object.isRequired,
+    setTab: PropTypes.func.isRequired,
+    updateUser: PropTypes.func.isRequired,
+    updatePassword: PropTypes.func.isRequired,
+  };
 
-    static propTypes = {
-        tab: PropTypes.string.isRequired,
-        user: PropTypes.object.isRequired,
-        setTab: PropTypes.func.isRequired,
-        updateUser: PropTypes.func.isRequired,
-        updatePassword: PropTypes.func.isRequired,
-    };
-
-    onSetTab(tab) {
-        this.props.setTab(tab);
-    }
+  onSetTab(tab) {
+    this.props.setTab(tab);
+  }
 
-    onUpdatePassword(details) {
-        this.props.updatePassword(details.user_id, details.password, details.old_password);
-    }
+  onUpdatePassword(details) {
+    this.props.updatePassword(
+      details.user_id,
+      details.password,
+      details.old_password,
+    );
+  }
 
-    onUpdateDetails(user) {
-        this.props.updateUser(user);
-    }
+  onUpdateDetails(user) {
+    this.props.updateUser(user);
+  }
 
-    render() {
-        let { tab } = this.props;
-        const nonSSOManagedAccount = !this.props.user.google_auth && !this.props.user.ldap_auth;
+  render() {
+    let { tab } = this.props;
+    const nonSSOManagedAccount =
+      !this.props.user.google_auth && !this.props.user.ldap_auth;
 
-        let allClasses = "Grid-cell md-no-flex md-mt1 text-brand-hover bordered border-brand-hover rounded p1 md-p3 block cursor-pointer text-centered md-text-left",
-            tabClasses = {};
+    let allClasses =
+        "Grid-cell md-no-flex md-mt1 text-brand-hover bordered border-brand-hover rounded p1 md-p3 block cursor-pointer text-centered md-text-left",
+      tabClasses = {};
 
-        ['details', 'password'].forEach(function(t) {
-            tabClasses[t] = (t === tab) ? allClasses + " bg-brand text-white text-white-hover" : allClasses;
-        });
+    ["details", "password"].forEach(function(t) {
+      tabClasses[t] =
+        t === tab
+          ? allClasses + " bg-brand text-white text-white-hover"
+          : allClasses;
+    });
 
-        return (
-            <div>
-                <div className="py4 border-bottom">
-                    <div className="wrapper wrapper--trim">
-                        <h2 className="text-grey-4">{t`Account settings`}</h2>
-                    </div>
-                </div>
-                <div className="mt2 md-mt4 wrapper wrapper--trim">
-                    <div className="Grid Grid--gutters Grid--full md-Grid--normal md-flex-reverse">
-                        { nonSSOManagedAccount && (
-                            <div className="Grid-cell Grid Grid--fit md-flex-column md-Cell--1of3">
-                              <a className={cx(tabClasses['details'])}
-                                onClick={this.onSetTab.bind(this, 'details')}>
-                                {t`User Details`}
-                              </a>
+    return (
+      <div>
+        <div className="py4 border-bottom">
+          <div className="wrapper wrapper--trim">
+            <h2 className="text-grey-4">{t`Account settings`}</h2>
+          </div>
+        </div>
+        <div className="mt2 md-mt4 wrapper wrapper--trim">
+          <div className="Grid Grid--gutters Grid--full md-Grid--normal md-flex-reverse">
+            {nonSSOManagedAccount && (
+              <div className="Grid-cell Grid Grid--fit md-flex-column md-Cell--1of3">
+                <a
+                  className={cx(tabClasses["details"])}
+                  onClick={this.onSetTab.bind(this, "details")}
+                >
+                  {t`User Details`}
+                </a>
 
-                              <a className={cx(tabClasses['password'])}
-                                onClick={this.onSetTab.bind(this, 'password')}>
-                                {t`Password`}
-                              </a>
-                            </div>
-                        )}
-                        <div className="Grid-cell">
-                            { tab === 'details' ?
-                                <UpdateUserDetails submitFn={this.onUpdateDetails.bind(this)} {...this.props} />
-                            : tab === 'password' ?
-                                <SetUserPassword submitFn={this.onUpdatePassword.bind(this)} {...this.props} />
-                            : null }
-                        </div>
-                    </div>
-                </div>
+                <a
+                  className={cx(tabClasses["password"])}
+                  onClick={this.onSetTab.bind(this, "password")}
+                >
+                  {t`Password`}
+                </a>
+              </div>
+            )}
+            <div className="Grid-cell">
+              {tab === "details" ? (
+                <UpdateUserDetails
+                  submitFn={this.onUpdateDetails.bind(this)}
+                  {...this.props}
+                />
+              ) : tab === "password" ? (
+                <SetUserPassword
+                  submitFn={this.onUpdatePassword.bind(this)}
+                  {...this.props}
+                />
+              ) : null}
             </div>
-        );
-    }
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/user/containers/UserSettingsApp.jsx b/frontend/src/metabase/user/containers/UserSettingsApp.jsx
index 839412015a88510ecb1a1e01e21df1eadea18d58..d24640dc2a03cd5edb9089acf9e7d6fac6675dd0 100644
--- a/frontend/src/metabase/user/containers/UserSettingsApp.jsx
+++ b/frontend/src/metabase/user/containers/UserSettingsApp.jsx
@@ -8,21 +8,21 @@ import { selectors } from "../selectors";
 import { setTab, updatePassword, updateUser } from "../actions";
 
 const mapStateToProps = (state, props) => {
-    return {
-        ...selectors(state),
-        user: state.currentUser
-    }
-}
+  return {
+    ...selectors(state),
+    user: state.currentUser,
+  };
+};
 
 const mapDispatchToProps = {
-    setTab,
-    updatePassword,
-    updateUser
+  setTab,
+  updatePassword,
+  updateUser,
 };
 
 @connect(mapStateToProps, mapDispatchToProps)
 export default class UserSettingsApp extends Component {
-    render() {
-        return <UserSettings {...this.props} />;
-    }
+  render() {
+    return <UserSettings {...this.props} />;
+  }
 }
diff --git a/frontend/src/metabase/user/reducers.js b/frontend/src/metabase/user/reducers.js
index 6499385aa61ab970056da76f610e52a9fb2453eb..778fc2bf55e89a0664379e876c998f091eee6136 100644
--- a/frontend/src/metabase/user/reducers.js
+++ b/frontend/src/metabase/user/reducers.js
@@ -1,22 +1,26 @@
-import { handleActions } from 'redux-actions';
+import { handleActions } from "redux-actions";
 
-import {
-    CHANGE_TAB,
-    UPDATE_PASSWORD,
-    UPDATE_USER
-} from './actions';
+import { CHANGE_TAB, UPDATE_PASSWORD, UPDATE_USER } from "./actions";
 
+export const tab = handleActions(
+  {
+    [CHANGE_TAB]: { next: (state, { payload }) => payload },
+  },
+  "details",
+);
 
-export const tab = handleActions({
-    [CHANGE_TAB]: { next: (state, { payload }) => payload }
-}, 'details');
-
-export const updatePasswordResult = handleActions({
+export const updatePasswordResult = handleActions(
+  {
     [CHANGE_TAB]: { next: (state, { payload }) => null },
-    [UPDATE_PASSWORD]: { next: (state, { payload }) => payload }
-}, null);
+    [UPDATE_PASSWORD]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
 
-export const updateUserResult = handleActions({
+export const updateUserResult = handleActions(
+  {
     [CHANGE_TAB]: { next: (state, { payload }) => null },
-    [UPDATE_USER]: { next: (state, { payload }) => payload }
-}, null);
+    [UPDATE_USER]: { next: (state, { payload }) => payload },
+  },
+  null,
+);
diff --git a/frontend/src/metabase/user/selectors.js b/frontend/src/metabase/user/selectors.js
index 7c31874743bf20706294668438c87e69e54647e3..0eb96ba56e7726218566c6f748f259070dd23b86 100644
--- a/frontend/src/metabase/user/selectors.js
+++ b/frontend/src/metabase/user/selectors.js
@@ -1,7 +1,15 @@
-import { createSelector } from 'reselect';
+import { createSelector } from "reselect";
 
 // our master selector which combines all of our partial selectors above
 export const selectors = createSelector(
-	[state => state.user.tab, state => state.user.updatePasswordResult, state => state.user.updateUserResult],
-	(tab, updatePasswordResult, updateUserResult) => ({tab, updatePasswordResult, updateUserResult})
+  [
+    state => state.user.tab,
+    state => state.user.updatePasswordResult,
+    state => state.user.updateUserResult,
+  ],
+  (tab, updatePasswordResult, updateUserResult) => ({
+    tab,
+    updatePasswordResult,
+    updateUserResult,
+  }),
 );
diff --git a/frontend/src/metabase/visualizations/components/CardRenderer.jsx b/frontend/src/metabase/visualizations/components/CardRenderer.jsx
index 545881c62ee9fd4451bd97e1d2533339b0e1b11e..f74b1a0973506d9ded998c89c9203c51062b8627 100644
--- a/frontend/src/metabase/visualizations/components/CardRenderer.jsx
+++ b/frontend/src/metabase/visualizations/components/CardRenderer.jsx
@@ -12,73 +12,73 @@ import dc from "dc";
 
 @ExplicitSize
 export default class CardRenderer extends Component {
-    static propTypes = {
-        series: PropTypes.array.isRequired,
-        width: PropTypes.number,
-        height: PropTypes.number,
-        renderer: PropTypes.func.isRequired,
-        onRenderError: PropTypes.func.isRequired,
-        className: PropTypes.string
-    };
-
-    shouldComponentUpdate(nextProps, nextState) {
-        // a chart only needs re-rendering when the result itself changes OR the chart type is different
-        let sameSize = (this.props.width === nextProps.width && this.props.height === nextProps.height);
-        let sameSeries = isSameSeries(this.props.series, nextProps.series);
-        return !(sameSize && sameSeries);
+  static propTypes = {
+    series: PropTypes.array.isRequired,
+    width: PropTypes.number,
+    height: PropTypes.number,
+    renderer: PropTypes.func.isRequired,
+    onRenderError: PropTypes.func.isRequired,
+    className: PropTypes.string,
+  };
+
+  shouldComponentUpdate(nextProps, nextState) {
+    // a chart only needs re-rendering when the result itself changes OR the chart type is different
+    let sameSize =
+      this.props.width === nextProps.width &&
+      this.props.height === nextProps.height;
+    let sameSeries = isSameSeries(this.props.series, nextProps.series);
+    return !(sameSize && sameSeries);
+  }
+
+  componentDidMount() {
+    this.renderChart();
+  }
+
+  componentDidUpdate() {
+    this.renderChart();
+  }
+
+  componentWillUnmount() {
+    this._deregisterChart();
+  }
+
+  _deregisterChart() {
+    if (this._chart) {
+      // Prevents memory leak
+      dc.chartRegistry.deregister(this._chart);
+      delete this._chart;
     }
+  }
 
-    componentDidMount() {
-        this.renderChart();
+  renderChart() {
+    if (this.props.width == null || this.props.height == null) {
+      return;
     }
 
-    componentDidUpdate() {
-        this.renderChart();
-    }
+    let parent = ReactDOM.findDOMNode(this);
 
-    componentWillUnmount() {
-        this._deregisterChart();
-    }
+    // deregister previous chart:
+    this._deregisterChart();
 
-    _deregisterChart() {
-        if (this._chart) {
-            // Prevents memory leak
-            dc.chartRegistry.deregister(this._chart);
-            delete this._chart;
-        }
+    // reset the DOM:
+    let element = parent.firstChild;
+    if (element) {
+      parent.removeChild(element);
     }
 
-    renderChart() {
-        if (this.props.width == null || this.props.height == null) {
-            return;
-        }
-
-        let parent = ReactDOM.findDOMNode(this);
-
-        // deregister previous chart:
-        this._deregisterChart();
+    // create a new container element
+    element = document.createElement("div");
+    parent.appendChild(element);
 
-        // reset the DOM:
-        let element = parent.firstChild;
-        if (element) {
-            parent.removeChild(element);
-        }
-
-        // create a new container element
-        element = document.createElement("div");
-        parent.appendChild(element);
-
-        try {
-            this._chart = this.props.renderer(element, this.props);
-        } catch (err) {
-            console.error(err);
-            this.props.onRenderError(err.message || err);
-        }
+    try {
+      this._chart = this.props.renderer(element, this.props);
+    } catch (err) {
+      console.error(err);
+      this.props.onRenderError(err.message || err);
     }
+  }
 
-    render() {
-        return (
-            <div className={this.props.className}></div>
-        );
-    }
+  render() {
+    return <div className={this.props.className} />;
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
index 45a5bb3b6ec6a5a36398db3fd2cc9d91bd2fb4ac..72781527387aebc99fb3738c09cb99cfb25ef14c 100644
--- a/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartClickActions.jsx
@@ -1,167 +1,200 @@
 /* @flow */
 
 import React, { Component } from "react";
-import cx from 'classnames'
+import cx from "classnames";
 
 import Icon from "metabase/components/Icon";
 import Popover from "metabase/components/Popover";
 
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-import type { ClickObject, ClickAction } from "metabase/meta/types/Visualization";
+import type {
+  ClickObject,
+  ClickAction,
+} from "metabase/meta/types/Visualization";
 
 import _ from "underscore";
 
 const SECTIONS = {
-    zoom: {
-        icon: "zoom"
-    },
-    records: {
-        icon: "table2"
-    },
-    details: {
-        icon: "document"
-    },
-    sort: {
-        icon: "sort"
-    },
-    breakout: {
-        icon: "breakout"
-    },
-    sum: {
-        icon: "sum"
-    },
-    averages: {
-        icon: "curve"
-    },
-    filter: {
-        icon: "funneloutline"
-    },
-    dashboard: {
-        icon: "dashboard"
-    },
-    distribution: {
-        icon: "bar"
-    }
-}
+  zoom: {
+    icon: "zoom",
+  },
+  records: {
+    icon: "table2",
+  },
+  details: {
+    icon: "document",
+  },
+  sort: {
+    icon: "sort",
+  },
+  breakout: {
+    icon: "breakout",
+  },
+  sum: {
+    icon: "sum",
+  },
+  averages: {
+    icon: "curve",
+  },
+  filter: {
+    icon: "funneloutline",
+  },
+  dashboard: {
+    icon: "dashboard",
+  },
+  distribution: {
+    icon: "bar",
+  },
+};
 // give them indexes so we can sort the sections by the above ordering (JS objects are ordered)
 Object.values(SECTIONS).map((section, index) => {
-    // $FlowFixMe
-    section.index = index;
+  // $FlowFixMe
+  section.index = index;
 });
 
 type Props = {
-    clicked: ?ClickObject,
-    clickActions: ?ClickAction[],
-    onChangeCardAndRun: (Object) => void,
-    onClose: () => void
+  clicked: ?ClickObject,
+  clickActions: ?(ClickAction[]),
+  onChangeCardAndRun: Object => void,
+  onClose: () => void,
 };
 
 type State = {
-    popoverAction: ?ClickAction;
-}
+  popoverAction: ?ClickAction,
+};
 
 export default class ChartClickActions extends Component {
-    props: Props;
-    state: State = {
-        popoverAction: null
-    };
-
-    close = () => {
-        this.setState({ popoverAction: null });
-        if (this.props.onClose) {
-            this.props.onClose();
-        }
+  props: Props;
+  state: State = {
+    popoverAction: null,
+  };
+
+  close = () => {
+    this.setState({ popoverAction: null });
+    if (this.props.onClose) {
+      this.props.onClose();
+    }
+  };
+
+  handleClickAction = (action: ClickAction) => {
+    const { onChangeCardAndRun } = this.props;
+    if (action.popover) {
+      this.setState({ popoverAction: action });
+    } else if (action.question) {
+      const nextQuestion = action.question();
+      if (nextQuestion) {
+        MetabaseAnalytics.trackEvent(
+          "Actions",
+          "Executed Click Action",
+          `${action.section || ""}:${action.name || ""}`,
+        );
+        onChangeCardAndRun({ nextCard: nextQuestion.card() });
+      }
+      this.close();
+    }
+  };
+
+  render() {
+    const { clicked, clickActions, onChangeCardAndRun } = this.props;
+
+    if (!clicked || !clickActions || clickActions.length === 0) {
+      return null;
     }
 
-    handleClickAction = (action: ClickAction) => {
-        const { onChangeCardAndRun } = this.props;
-        if (action.popover) {
-            this.setState({ popoverAction: action });
-        } else if (action.question) {
-            const nextQuestion = action.question();
-            if (nextQuestion) {
-                MetabaseAnalytics.trackEvent("Actions", "Executed Click Action", `${action.section||""}:${action.name||""}`);
-                onChangeCardAndRun({ nextCard: nextQuestion.card() });
+    let { popoverAction } = this.state;
+    let popover;
+    if (popoverAction && popoverAction.popover) {
+      const PopoverContent = popoverAction.popover;
+      popover = (
+        <PopoverContent
+          onChangeCardAndRun={({ nextCard }) => {
+            if (popoverAction) {
+              MetabaseAnalytics.trackEvent(
+                "Action",
+                "Executed Click Action",
+                `${popoverAction.section || ""}:${popoverAction.name || ""}`,
+              );
             }
-            this.close();
-        }
-    };
-
-    render() {
-        const { clicked, clickActions, onChangeCardAndRun } = this.props;
-
-        if (!clicked || !clickActions || clickActions.length === 0) {
-            return null;
-        }
-
-        let { popoverAction } = this.state;
-        let popover;
-        if (popoverAction && popoverAction.popover) {
-            const PopoverContent = popoverAction.popover;
-            popover = (
-                <PopoverContent
-                    onChangeCardAndRun={({ nextCard }) => {
-                        if (popoverAction) {
-                            MetabaseAnalytics.trackEvent("Action", "Executed Click Action", `${popoverAction.section||""}:${popoverAction.name||""}`);
-                        }
-                        onChangeCardAndRun({ nextCard });
-                    }}
-                    onClose={() => {
-                        MetabaseAnalytics.trackEvent("Action", "Dismissed Click Action Menu");
-                        this.close();
-                    }}
-                />
+            onChangeCardAndRun({ nextCard });
+          }}
+          onClose={() => {
+            MetabaseAnalytics.trackEvent(
+              "Action",
+              "Dismissed Click Action Menu",
             );
-        }
-
-        const sections = _.chain(clickActions)
-            .groupBy("section")
-            .pairs()
-            .sortBy(([key]) => SECTIONS[key] ? SECTIONS[key].index : 99)
-            .value();
-
-        return (
-            <Popover
-                target={clicked.element}
-                targetEvent={clicked.event}
-                onClose={() => {
-                    MetabaseAnalytics.trackEvent("Action", "Dismissed Click Action Menu");
-                    this.close();
-                }}
-                verticalAttachments={["top", "bottom"]}
-                horizontalAttachments={["left", "center", "right"]}
-                sizeToFit
-                pinInitialAttachment
-            >
-                { popover ?
-                    popover
-                :
-                    <div className="text-bold text-grey-3">
-                        {sections.map(([key, actions]) =>
-                            <div key={key} className="border-row-divider p2 flex align-center text-default-hover">
-                                <Icon name={SECTIONS[key] && SECTIONS[key].icon || "unknown"} className="mr3" size={16} />
-                                { actions.map((action, index) =>
-                                    <ChartClickAction
-                                        index={index}
-                                        action={action}
-                                        isLastItem={index === actions.length - 1}
-                                        handleClickAction={this.handleClickAction}
-                                    />
-                                )}
-                            </div>
-                        )}
-                    </div>
-                }
-            </Popover>
-        );
+            this.close();
+          }}
+        />
+      );
     }
+
+    const sections = _.chain(clickActions)
+      .groupBy("section")
+      .pairs()
+      .sortBy(([key]) => (SECTIONS[key] ? SECTIONS[key].index : 99))
+      .value();
+
+    return (
+      <Popover
+        target={clicked.element}
+        targetEvent={clicked.event}
+        onClose={() => {
+          MetabaseAnalytics.trackEvent("Action", "Dismissed Click Action Menu");
+          this.close();
+        }}
+        verticalAttachments={["top", "bottom"]}
+        horizontalAttachments={["left", "center", "right"]}
+        sizeToFit
+        pinInitialAttachment
+      >
+        {popover ? (
+          popover
+        ) : (
+          <div className="text-bold text-grey-3">
+            {sections.map(([key, actions]) => (
+              <div
+                key={key}
+                className="border-row-divider p2 flex align-center text-default-hover"
+              >
+                <Icon
+                  name={(SECTIONS[key] && SECTIONS[key].icon) || "unknown"}
+                  className="mr3"
+                  size={16}
+                />
+                {actions.map((action, index) => (
+                  <ChartClickAction
+                    index={index}
+                    action={action}
+                    isLastItem={index === actions.length - 1}
+                    handleClickAction={this.handleClickAction}
+                  />
+                ))}
+              </div>
+            ))}
+          </div>
+        )}
+      </Popover>
+    );
+  }
 }
 
-export const ChartClickAction = ({ action, isLastItem, handleClickAction }: { action: any, isLastItem: any, handleClickAction: any }) =>
-    <div
-        className={cx("text-brand-hover cursor-pointer", { "pr2": isLastItem, "pr4": !isLastItem})}
-        onClick={() => handleClickAction(action)}
-    >
-        { action.title }
-    </div>
+export const ChartClickAction = ({
+  action,
+  isLastItem,
+  handleClickAction,
+}: {
+  action: any,
+  isLastItem: any,
+  handleClickAction: any,
+}) => (
+  <div
+    className={cx("text-brand-hover cursor-pointer", {
+      pr2: isLastItem,
+      pr4: !isLastItem,
+    })}
+    onClick={() => handleClickAction(action)}
+  >
+    {action.title}
+  </div>
+);
diff --git a/frontend/src/metabase/visualizations/components/ChartSettings.jsx b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
index 443ff02219273761371af9fb52b43289db17ac93..22aa9b87704dc0e014ca938d7e8a2b40b9e5e47c 100644
--- a/frontend/src/metabase/visualizations/components/ChartSettings.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartSettings.jsx
@@ -2,168 +2,212 @@ import React, { Component } from "react";
 import cx from "classnames";
 import { assocIn } from "icepick";
 import _ from "underscore";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Warnings from "metabase/query_builder/components/Warnings.jsx";
 
-import Visualization from "metabase/visualizations/components/Visualization.jsx"
+import Visualization from "metabase/visualizations/components/Visualization.jsx";
 import { getSettingsWidgets } from "metabase/visualizations/lib/settings";
 import MetabaseAnalytics from "metabase/lib/analytics";
-import { getVisualizationTransformed, extractRemappings } from "metabase/visualizations";
+import {
+  getVisualizationTransformed,
+  extractRemappings,
+} from "metabase/visualizations";
 
-const ChartSettingsTab = ({name, active, onClick}) =>
+const ChartSettingsTab = ({ name, active, onClick }) => (
   <a
-    className={cx('block text-brand py1 text-centered', { 'bg-brand text-white' : active})}
-    onClick={() => onClick(name) }
+    className={cx("block text-brand py1 text-centered", {
+      "bg-brand text-white": active,
+    })}
+    onClick={() => onClick(name)}
   >
     {name.toUpperCase()}
   </a>
+);
 
-const ChartSettingsTabs = ({ tabs, selectTab, activeTab}) =>
+const ChartSettingsTabs = ({ tabs, selectTab, activeTab }) => (
   <ul className="bordered rounded flex justify-around overflow-hidden">
-    { tabs.map((tab, index) =>
-        <li className="flex-full border-left" key={index}>
-          <ChartSettingsTab name={tab} active={tab === activeTab} onClick={selectTab} />
-        </li>
-    )}
+    {tabs.map((tab, index) => (
+      <li className="flex-full border-left" key={index}>
+        <ChartSettingsTab
+          name={tab}
+          active={tab === activeTab}
+          onClick={selectTab}
+        />
+      </li>
+    ))}
   </ul>
-
-const Widget = ({ title, hidden, disabled, widget, value, onChange, props }) => {
-    const W = widget;
-    return (
-        <div className={cx("mb2", { hide: hidden, disable: disabled })}>
-            { title && <h4 className="mb1">{title}</h4> }
-            { W && <W value={value} onChange={onChange} {...props}/> }
-        </div>
-    );
-}
-
+);
+
+const Widget = ({
+  title,
+  hidden,
+  disabled,
+  widget,
+  value,
+  onChange,
+  props,
+}) => {
+  const W = widget;
+  return (
+    <div className={cx("mb2", { hide: hidden, disable: disabled })}>
+      {title && <h4 className="mb1">{title}</h4>}
+      {W && <W value={value} onChange={onChange} {...props} />}
+    </div>
+  );
+};
 
 class ChartSettings extends Component {
-    constructor (props) {
-        super(props);
-        const initialSettings = props.series[0].card.visualization_settings;
-        this.state = {
-          currentTab: null,
-          settings: initialSettings,
-          series: this._getSeries(props.series, initialSettings)
-      };
+  constructor(props) {
+    super(props);
+    const initialSettings = props.series[0].card.visualization_settings;
+    this.state = {
+      currentTab: null,
+      settings: initialSettings,
+      series: this._getSeries(props.series, initialSettings),
+    };
+  }
+
+  selectTab = tab => {
+    this.setState({ currentTab: tab });
+  };
+
+  _getSeries(series, settings) {
+    if (settings) {
+      series = assocIn(series, [0, "card", "visualization_settings"], settings);
     }
-
-    selectTab = (tab) => {
-        this.setState({ currentTab: tab });
+    const transformed = getVisualizationTransformed(extractRemappings(series));
+    return transformed.series;
+  }
+
+  onResetSettings = () => {
+    MetabaseAnalytics.trackEvent("Chart Settings", "Reset Settings");
+    this.setState({
+      settings: {},
+      series: this._getSeries(this.props.series, {}),
+    });
+  };
+
+  onChangeSettings = newSettings => {
+    for (const key of Object.keys(newSettings)) {
+      MetabaseAnalytics.trackEvent("Chart Settings", "Change Setting", key);
     }
-
-    _getSeries(series, settings) {
-        if (settings) {
-            series = assocIn(series, [0, "card", "visualization_settings"], settings);
-        }
-        const transformed = getVisualizationTransformed(extractRemappings(series));
-        return transformed.series;
+    const settings = {
+      ...this.state.settings,
+      ...newSettings,
+    };
+    this.setState({
+      settings: settings,
+      series: this._getSeries(this.props.series, settings),
+    });
+  };
+
+  onDone() {
+    this.props.onChange(this.state.settings);
+    this.props.onClose();
+  }
+
+  getChartTypeName() {
+    let { CardVisualization } = getVisualizationTransformed(this.props.series);
+    switch (CardVisualization.identifier) {
+      case "table":
+        return "table";
+      case "scalar":
+        return "number";
+      case "funnel":
+        return "funnel";
+      default:
+        return "chart";
     }
-
-    onResetSettings = () => {
-        MetabaseAnalytics.trackEvent("Chart Settings", "Reset Settings");
-        this.setState({
-            settings: {},
-            series: this._getSeries(this.props.series, {})
-        });
+  }
+
+  render() {
+    const { onClose, isDashboard } = this.props;
+    const { series } = this.state;
+
+    const tabs = {};
+    for (const widget of getSettingsWidgets(
+      series,
+      this.onChangeSettings,
+      isDashboard,
+    )) {
+      tabs[widget.section] = tabs[widget.section] || [];
+      tabs[widget.section].push(widget);
     }
 
-    onChangeSettings = (newSettings) => {
-        for (const key of Object.keys(newSettings)) {
-            MetabaseAnalytics.trackEvent("Chart Settings", "Change Setting", key);
-        }
-        const settings = {
-            ...this.state.settings,
-            ...newSettings
-        }
-        this.setState({
-            settings: settings,
-            series: this._getSeries(this.props.series, settings)
-        });
+    // Move settings from the "undefined" section in the first tab
+    if (tabs["undefined"] && Object.values(tabs).length > 1) {
+      let extra = tabs["undefined"];
+      delete tabs["undefined"];
+      Object.values(tabs)[0].unshift(...extra);
     }
 
-    onDone() {
-        this.props.onChange(this.state.settings);
-        this.props.onClose();
-    }
+    const tabNames = Object.keys(tabs);
+    const currentTab = this.state.currentTab || tabNames[0];
+    const widgets = tabs[currentTab];
 
-    getChartTypeName() {
-        let { CardVisualization } = getVisualizationTransformed(this.props.series);
-        switch (CardVisualization.identifier) {
-            case "table": return "table";
-            case "scalar": return "number";
-            case "funnel": return "funnel";
-            default: return "chart";
-        }
-    }
-
-    render () {
-        const { onClose, isDashboard } = this.props;
-        const { series } = this.state;
-
-        const tabs = {};
-        for (const widget of getSettingsWidgets(series, this.onChangeSettings, isDashboard)) {
-            tabs[widget.section] = tabs[widget.section] || [];
-            tabs[widget.section].push(widget);
-        }
-
-        // Move settings from the "undefined" section in the first tab
-        if (tabs["undefined"] && Object.values(tabs).length > 1) {
-            let extra = tabs["undefined"];
-            delete tabs["undefined"];
-            Object.values(tabs)[0].unshift(...extra);
-        }
-
-        const tabNames = Object.keys(tabs);
-        const currentTab = this.state.currentTab || tabNames[0];
-        const widgets = tabs[currentTab];
-
-        return (
-            <div className="flex flex-column spread p4">
-                <h2 className="my2">{t`Customize this ${this.getChartTypeName()}`}</h2>
-
-                { tabNames.length > 1 &&
-                    <ChartSettingsTabs tabs={tabNames} selectTab={this.selectTab} activeTab={currentTab}/>
-                }
-                <div className="Grid flex-full mt3">
-                    <div className="Grid-cell Cell--1of3 scroll-y p1">
-                        { widgets && widgets.map((widget) =>
-                            <Widget key={widget.id} {...widget} />
-                        )}
-                    </div>
-                    <div className="Grid-cell flex flex-column">
-                        <div className="flex flex-column">
-                            <Warnings className="mx2 align-self-end text-gold" warnings={this.state.warnings} size={20} />
-                        </div>
-                        <div className="flex-full relative">
-                            <Visualization
-                                className="spread"
-                                rawSeries={series}
-                                isEditing
-                                showTitle
-                                isDashboard
-                                showWarnings
-                                onUpdateVisualizationSettings={this.onChangeSettings}
-                                onUpdateWarnings={(warnings) => this.setState({ warnings })}
-                            />
-                        </div>
-                    </div>
-                </div>
-                <div className="pt1">
-                    { !_.isEqual(this.state.settings, {}) &&
-                        <a className="Button Button--danger float-right" onClick={this.onResetSettings} data-metabase-event="Chart Settings;Reset">{t`Reset to defaults`}</a>
-                    }
-
-                    <div className="float-left">
-                      <a className="Button Button--primary ml2" onClick={() => this.onDone()} data-metabase-event="Chart Settings;Done">{t`Done`}</a>
-                      <a className="Button ml2" onClick={onClose} data-metabase-event="Chart Settings;Cancel">{t`Cancel`}</a>
-                    </div>
-                </div>
+    return (
+      <div className="flex flex-column spread p4">
+        <h2 className="my2">{t`Customize this ${this.getChartTypeName()}`}</h2>
+
+        {tabNames.length > 1 && (
+          <ChartSettingsTabs
+            tabs={tabNames}
+            selectTab={this.selectTab}
+            activeTab={currentTab}
+          />
+        )}
+        <div className="Grid flex-full mt3">
+          <div className="Grid-cell Cell--1of3 scroll-y p1">
+            {widgets &&
+              widgets.map(widget => <Widget key={widget.id} {...widget} />)}
+          </div>
+          <div className="Grid-cell flex flex-column">
+            <div className="flex flex-column">
+              <Warnings
+                className="mx2 align-self-end text-gold"
+                warnings={this.state.warnings}
+                size={20}
+              />
             </div>
-        );
-    }
+            <div className="flex-full relative">
+              <Visualization
+                className="spread"
+                rawSeries={series}
+                isEditing
+                showTitle
+                isDashboard
+                showWarnings
+                onUpdateVisualizationSettings={this.onChangeSettings}
+                onUpdateWarnings={warnings => this.setState({ warnings })}
+              />
+            </div>
+          </div>
+        </div>
+        <div className="pt1">
+          {!_.isEqual(this.state.settings, {}) && (
+            <a
+              className="Button Button--danger float-right"
+              onClick={this.onResetSettings}
+              data-metabase-event="Chart Settings;Reset"
+            >{t`Reset to defaults`}</a>
+          )}
+
+          <div className="float-left">
+            <a
+              className="Button Button--primary ml2"
+              onClick={() => this.onDone()}
+              data-metabase-event="Chart Settings;Done"
+            >{t`Done`}</a>
+            <a
+              className="Button ml2"
+              onClick={onClose}
+              data-metabase-event="Chart Settings;Cancel"
+            >{t`Cancel`}</a>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
 
-
-export default ChartSettings
+export default ChartSettings;
diff --git a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
index 266271be3e806080acc2877b81285ba1ba39565e..b0ff14dfb07cef5f6f2f780a9f2b355f9f7784d2 100644
--- a/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartTooltip.jsx
@@ -1,103 +1,98 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 
-import TooltipPopover from "metabase/components/TooltipPopover.jsx"
+import TooltipPopover from "metabase/components/TooltipPopover.jsx";
 import Value from "metabase/components/Value.jsx";
 
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
 
 export default class ChartTooltip extends Component {
-    static propTypes = {
-        series: PropTypes.array.isRequired,
-        hovered: PropTypes.object
-    };
+  static propTypes = {
+    series: PropTypes.array.isRequired,
+    hovered: PropTypes.object,
+  };
 
-    _getRows() {
-        const { series, hovered } = this.props;
-        if (!hovered) {
-            return [];
-        }
-        // Array of key, value, col: { data: [{ key, value, col }], element, event }
-        if (Array.isArray(hovered.data)) {
-            return hovered.data;
-        }
-        // ClickObject: { value, column, dimensions: [{ value, column }], element, event }
-        else if (hovered.value !== undefined || hovered.dimensions) {
-            const dimensions = [];
-            if (hovered.value !== undefined) {
-                dimensions.push({ value: hovered.value, column: hovered.column });
-            }
-            if (hovered.dimensions) {
-                dimensions.push(...hovered.dimensions);
-            }
-            return dimensions.map(({ value, column }) => ({
-                key: getFriendlyName(column),
-                value: value,
-                col: column
-            }))
-        }
-        // DEPRECATED: { key, value }
-        else if (hovered.data) {
-            console.warn("hovered should be a ClickObject or hovered.data should be an array of { key, value, col }", hovered.data);
-            let s = series[hovered.index] || series[0];
-            return [
-                {
-                    key: getFriendlyName(s.data.cols[0]),
-                    value: hovered.data.key,
-                    col: s.data.cols[0]
-                },
-                {
-                    key: getFriendlyName(s.data.cols[1]),
-                    value: hovered.data.value,
-                    col: s.data.cols[1]
-                },
-            ]
-        }
-        return [];
+  _getRows() {
+    const { series, hovered } = this.props;
+    if (!hovered) {
+      return [];
     }
-
-    render() {
-        const { hovered } = this.props;
-        const rows = this._getRows();
-        const hasEventOrElement = hovered && ((hovered.element && document.contains(hovered.element)) || hovered.event);
-        const isOpen = rows.length > 0 && !!hasEventOrElement;
-        return (
-            <TooltipPopover
-                target={hovered && hovered.element}
-                targetEvent={hovered && hovered.event}
-                verticalAttachments={["bottom", "top"]}
-                isOpen={isOpen}
-            >
-                <table className="py1 px2">
-                    <tbody>
-                        { rows.map(({ key, value, col }, index) =>
-                            <TooltipRow
-                                key={index}
-                                name={key}
-                                value={value}
-                                column={col}
-                            />
-                        ) }
-                    </tbody>
-                </table>
-            </TooltipPopover>
-        );
+    // Array of key, value, col: { data: [{ key, value, col }], element, event }
+    if (Array.isArray(hovered.data)) {
+      return hovered.data;
+    } else if (hovered.value !== undefined || hovered.dimensions) {
+      // ClickObject: { value, column, dimensions: [{ value, column }], element, event }
+      const dimensions = [];
+      if (hovered.value !== undefined) {
+        dimensions.push({ value: hovered.value, column: hovered.column });
+      }
+      if (hovered.dimensions) {
+        dimensions.push(...hovered.dimensions);
+      }
+      return dimensions.map(({ value, column }) => ({
+        key: getFriendlyName(column),
+        value: value,
+        col: column,
+      }));
+    } else if (hovered.data) {
+      // DEPRECATED: { key, value }
+      console.warn(
+        "hovered should be a ClickObject or hovered.data should be an array of { key, value, col }",
+        hovered.data,
+      );
+      let s = series[hovered.index] || series[0];
+      return [
+        {
+          key: getFriendlyName(s.data.cols[0]),
+          value: hovered.data.key,
+          col: s.data.cols[0],
+        },
+        {
+          key: getFriendlyName(s.data.cols[1]),
+          value: hovered.data.value,
+          col: s.data.cols[1],
+        },
+      ];
     }
+    return [];
+  }
+
+  render() {
+    const { hovered } = this.props;
+    const rows = this._getRows();
+    const hasEventOrElement =
+      hovered &&
+      ((hovered.element && document.contains(hovered.element)) ||
+        hovered.event);
+    const isOpen = rows.length > 0 && !!hasEventOrElement;
+    return (
+      <TooltipPopover
+        target={hovered && hovered.element}
+        targetEvent={hovered && hovered.event}
+        verticalAttachments={["bottom", "top"]}
+        isOpen={isOpen}
+      >
+        <table className="py1 px2">
+          <tbody>
+            {rows.map(({ key, value, col }, index) => (
+              <TooltipRow key={index} name={key} value={value} column={col} />
+            ))}
+          </tbody>
+        </table>
+      </TooltipPopover>
+    );
+  }
 }
 
-const TooltipRow = ({ name, value, column }) =>
-    <tr>
-        <td className="text-light text-right">{name}:</td>
-        <td className="pl1 text-bold text-left">
-            { React.isValidElement(value) ?
-                value
-            :
-                <Value
-                    type="tooltip"
-                    value={value}
-                    column={column}
-                    majorWidth={0}
-                />
-            }
-        </td>
-    </tr>
+const TooltipRow = ({ name, value, column }) => (
+  <tr>
+    <td className="text-light text-right">{name}:</td>
+    <td className="pl1 text-bold text-left">
+      {React.isValidElement(value) ? (
+        value
+      ) : (
+        <Value type="tooltip" value={value} column={column} majorWidth={0} />
+      )}
+    </td>
+  </tr>
+);
diff --git a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
index 4cb2efaf16edf22f8c1dfce215957276335f149d..1391ccb93e5a2957dc0519c2b5379c1cf94bc666 100644
--- a/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
+++ b/frontend/src/metabase/visualizations/components/ChartWithLegend.jsx
@@ -8,71 +8,113 @@ import ExplicitSize from "metabase/components/ExplicitSize.jsx";
 
 import cx from "classnames";
 
-const GRID_ASPECT_RATIO = (4 / 3);
+const GRID_ASPECT_RATIO = 4 / 3;
 const PADDING = 14;
 
 @ExplicitSize
 export default class ChartWithLegend extends Component {
-    static defaultProps = {
-        aspectRatio: 1
-    };
+  static defaultProps = {
+    aspectRatio: 1,
+  };
 
-    render() {
-        let { children, legendTitles, legendColors, hovered, onHoverChange, className, gridSize, aspectRatio, height, width, showLegend } = this.props;
+  render() {
+    let {
+      children,
+      legendTitles,
+      legendColors,
+      hovered,
+      onHoverChange,
+      className,
+      gridSize,
+      aspectRatio,
+      height,
+      width,
+      showLegend,
+    } = this.props;
 
-        // padding
-        width -= PADDING * 2
-        height -= PADDING;
+    // padding
+    width -= PADDING * 2;
+    height -= PADDING;
 
-        let chartWidth, chartHeight, flexChart = false;
-        let type, LegendComponent;
-        let isHorizontal = gridSize && gridSize.width > gridSize.height / GRID_ASPECT_RATIO;
-        if (showLegend === false) {
-            type = "small";
-        } else if (!gridSize || (isHorizontal && (showLegend || gridSize.width > 4 || gridSize.height > 4))) {
-            type = "horizontal";
-            LegendComponent = LegendVertical;
-            if (gridSize && gridSize.width < 6) {
-                legendTitles = legendTitles.map(title => Array.isArray(title) ? title.slice(0,1) : title);
-            }
-            let desiredWidth = height * aspectRatio;
-            if (desiredWidth > width * (2 / 3)) {
-                flexChart = true;
-            } else {
-                chartWidth = desiredWidth;
-            }
-            chartHeight = height;
-        } else if (!isHorizontal && (showLegend || (gridSize.height > 3 && gridSize.width > 2))) {
-            type = "vertical";
-            LegendComponent = LegendHorizontal;
-            legendTitles = legendTitles.map(title => Array.isArray(title) ? title[0] : title);
-            let desiredHeight = width * (1 / aspectRatio);
-            if (desiredHeight > height * (3 / 4)) {
-                // chartHeight = height * (3 / 4);
-                flexChart = true;
-            } else {
-                chartHeight = desiredHeight;
-            }
-            chartWidth = width;
-        } else {
-            type = "small";
-        }
-
-        return (
-            <div className={cx(className, 'fullscreen-text-small fullscreen-normal-text fullscreen-night-text', styles.ChartWithLegend, styles[type], flexChart && styles.flexChart)} style={{ paddingBottom: PADDING, paddingLeft: PADDING, paddingRight: PADDING }}>
-                { LegendComponent ?
-                    <LegendComponent
-                        className={styles.Legend}
-                        titles={legendTitles}
-                        colors={legendColors}
-                        hovered={hovered}
-                        onHoverChange={onHoverChange}
-                    />
-                : null }
-                <div className={cx(styles.Chart)} style={{ width: chartWidth, height: chartHeight }}>
-                    {children}
-                </div>
-            </div>
-        )
+    let chartWidth,
+      chartHeight,
+      flexChart = false;
+    let type, LegendComponent;
+    let isHorizontal =
+      gridSize && gridSize.width > gridSize.height / GRID_ASPECT_RATIO;
+    if (showLegend === false) {
+      type = "small";
+    } else if (
+      !gridSize ||
+      (isHorizontal &&
+        (showLegend || gridSize.width > 4 || gridSize.height > 4))
+    ) {
+      type = "horizontal";
+      LegendComponent = LegendVertical;
+      if (gridSize && gridSize.width < 6) {
+        legendTitles = legendTitles.map(
+          title => (Array.isArray(title) ? title.slice(0, 1) : title),
+        );
+      }
+      let desiredWidth = height * aspectRatio;
+      if (desiredWidth > width * (2 / 3)) {
+        flexChart = true;
+      } else {
+        chartWidth = desiredWidth;
+      }
+      chartHeight = height;
+    } else if (
+      !isHorizontal &&
+      (showLegend || (gridSize.height > 3 && gridSize.width > 2))
+    ) {
+      type = "vertical";
+      LegendComponent = LegendHorizontal;
+      legendTitles = legendTitles.map(
+        title => (Array.isArray(title) ? title[0] : title),
+      );
+      let desiredHeight = width * (1 / aspectRatio);
+      if (desiredHeight > height * (3 / 4)) {
+        // chartHeight = height * (3 / 4);
+        flexChart = true;
+      } else {
+        chartHeight = desiredHeight;
+      }
+      chartWidth = width;
+    } else {
+      type = "small";
     }
+
+    return (
+      <div
+        className={cx(
+          className,
+          "fullscreen-text-small fullscreen-normal-text fullscreen-night-text",
+          styles.ChartWithLegend,
+          styles[type],
+          flexChart && styles.flexChart,
+        )}
+        style={{
+          paddingBottom: PADDING,
+          paddingLeft: PADDING,
+          paddingRight: PADDING,
+        }}
+      >
+        {LegendComponent ? (
+          <LegendComponent
+            className={styles.Legend}
+            titles={legendTitles}
+            colors={legendColors}
+            hovered={hovered}
+            onHoverChange={onHoverChange}
+          />
+        ) : null}
+        <div
+          className={cx(styles.Chart)}
+          style={{ width: chartWidth, height: chartHeight }}
+        >
+          {children}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
index eef5eb7503bbd2390464a0fa503ef0dd2986e515..568b7055bac691b610beb207dcbf9c8ca399cb38 100644
--- a/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
+++ b/frontend/src/metabase/visualizations/components/ChoroplethMap.jsx
@@ -1,5 +1,5 @@
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 
 import { isString } from "metabase/lib/schema_metadata";
@@ -12,7 +12,10 @@ import ChartWithLegend from "./ChartWithLegend.jsx";
 import LegacyChoropleth from "./LegacyChoropleth.jsx";
 import LeafletChoropleth from "./LeafletChoropleth.jsx";
 
-import { computeMinimalBounds, getCanonicalRowKey } from "metabase/visualizations/lib/mapping";
+import {
+  computeMinimalBounds,
+  getCanonicalRowKey,
+} from "metabase/visualizations/lib/mapping";
 
 import d3 from "d3";
 import ss from "simple-statistics";
@@ -28,227 +31,264 @@ import _ from "underscore";
 // const HEAT_MAP_ZERO_COLOR = '#CCC';
 
 const HEAT_MAP_COLORS = [
-    // "#E2F2FF",
-    "#C4E4FF",
-    // "#9ED2FF",
-    "#81C5FF",
-    // "#6BBAFF",
-    "#51AEFF",
-    // "#36A2FF",
-    "#1E96FF",
-    // "#0089FF",
-    "#0061B5"
+  // "#E2F2FF",
+  "#C4E4FF",
+  // "#9ED2FF",
+  "#81C5FF",
+  // "#6BBAFF",
+  "#51AEFF",
+  // "#36A2FF",
+  "#1E96FF",
+  // "#0089FF",
+  "#0061B5",
 ];
-const HEAT_MAP_ZERO_COLOR = '#CCC';
+const HEAT_MAP_ZERO_COLOR = "#CCC";
 
 const geoJsonCache = new Map();
 function loadGeoJson(geoJsonPath, callback) {
-    if (geoJsonCache.has(geoJsonPath)) {
-        setTimeout(() =>
-            callback(geoJsonCache.get(geoJsonPath))
-        , 0);
-    } else {
-        d3.json(geoJsonPath, (json) => {
-            geoJsonCache.set(geoJsonPath, json)
-            callback(json);
-        });
-    }
+  if (geoJsonCache.has(geoJsonPath)) {
+    setTimeout(() => callback(geoJsonCache.get(geoJsonPath)), 0);
+  } else {
+    d3.json(geoJsonPath, json => {
+      geoJsonCache.set(geoJsonPath, json);
+      callback(json);
+    });
+  }
 }
 
 export default class ChoroplethMap extends Component {
-    static propTypes = {
-    };
+  static propTypes = {};
 
-    static minSize = { width: 4, height: 4 };
+  static minSize = { width: 4, height: 4 };
 
-    static isSensible(cols, rows) {
-        return cols.length > 1 && isString(cols[0]);
-    }
+  static isSensible(cols, rows) {
+    return cols.length > 1 && isString(cols[0]);
+  }
 
-    static checkRenderable([{ data: { cols, rows} }]) {
-        if (cols.length < 2) { throw new MinColumnsError(2, cols.length); }
+  static checkRenderable([{ data: { cols, rows } }]) {
+    if (cols.length < 2) {
+      throw new MinColumnsError(2, cols.length);
     }
+  }
 
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            geoJson: null,
-            geoJsonPath: null
-        };
-    }
-
-    componentWillMount() {
-        this.componentWillReceiveProps(this.props);
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      geoJson: null,
+      geoJsonPath: null,
+    };
+  }
+
+  componentWillMount() {
+    this.componentWillReceiveProps(this.props);
+  }
+
+  _getDetails(props) {
+    return MetabaseSettings.get("custom_geojson", {})[
+      props.settings["map.region"]
+    ];
+  }
+
+  componentWillReceiveProps(nextProps) {
+    const details = this._getDetails(nextProps);
+    if (details) {
+      let geoJsonPath;
+      if (details.builtin) {
+        geoJsonPath = details.url;
+      } else {
+        geoJsonPath = "api/geojson/" + nextProps.settings["map.region"];
+      }
+      if (this.state.geoJsonPath !== geoJsonPath) {
+        this.setState({
+          geoJson: null,
+          geoJsonPath: geoJsonPath,
+        });
+        loadGeoJson(geoJsonPath, geoJson => {
+          this.setState({
+            geoJson: geoJson,
+            geoJsonPath: geoJsonPath,
+            minimalBounds: computeMinimalBounds(geoJson.features),
+          });
+        });
+      }
     }
+  }
 
-    _getDetails(props) {
-        return MetabaseSettings.get("custom_geojson", {})[props.settings["map.region"]];
+  render() {
+    const details = this._getDetails(this.props);
+    if (!details) {
+      return <div>{t`unknown map`}</div>;
     }
 
-    componentWillReceiveProps(nextProps) {
-        const details = this._getDetails(nextProps)
-        if (details) {
-            let geoJsonPath;
-            if (details.builtin) {
-                geoJsonPath = details.url;
-            } else {
-                geoJsonPath = "api/geojson/" + nextProps.settings["map.region"]
-            }
-            if (this.state.geoJsonPath !== geoJsonPath) {
-                this.setState({
-                    geoJson: null,
-                    geoJsonPath: geoJsonPath
-                });
-                loadGeoJson(geoJsonPath, (geoJson) => {
-                    this.setState({
-                        geoJson: geoJson,
-                        geoJsonPath: geoJsonPath,
-                        minimalBounds: computeMinimalBounds(geoJson.features)
-                    });
-                });
-            }
-        }
+    const {
+      series,
+      className,
+      gridSize,
+      hovered,
+      onHoverChange,
+      visualizationIsClickable,
+      onVisualizationClick,
+      settings,
+    } = this.props;
+    let { geoJson, minimalBounds } = this.state;
+
+    // special case builtin maps to use legacy choropleth map
+    let projection;
+    if (settings["map.region"] === "us_states") {
+      projection = d3.geo.albersUsa();
+    } else if (settings["map.region"] === "world_countries") {
+      projection = d3.geo.mercator();
+    } else {
+      projection = null;
     }
 
-    render() {
-        const details = this._getDetails(this.props);
-        if (!details) {
-            return (
-                <div>{t`unknown map`}</div>
-            );
-        }
-
-        const { series, className, gridSize, hovered, onHoverChange, visualizationIsClickable, onVisualizationClick, settings } = this.props;
-        let { geoJson, minimalBounds } = this.state;
-
-        // special case builtin maps to use legacy choropleth map
-        let projection;
-        if (settings["map.region"] === "us_states") {
-            projection = d3.geo.albersUsa();
-        } else if (settings["map.region"] === "world_countries") {
-            projection = d3.geo.mercator();
-        } else {
-            projection = null;
-        }
-
-        const nameProperty = details.region_name;
-        const keyProperty = details.region_key;
-
-        if (!geoJson) {
-            return (
-                <div className={className + " flex layout-centered"}>
-                    <LoadingSpinner />
-                </div>
-            );
-        }
-
-        const [{ data: { cols, rows }}] = series;
-        const dimensionIndex = _.findIndex(cols, (col) => col.name === settings["map.dimension"]);
-        const metricIndex = _.findIndex(cols, (col) => col.name === settings["map.metric"]);
-
-        const getRowKey       = (row) => getCanonicalRowKey(row[dimensionIndex], settings["map.region"]);
-        const getRowValue     = (row) => row[metricIndex] || 0;
-        const getFeatureName  = (feature) => String(feature.properties[nameProperty]);
-        const getFeatureKey   = (feature) => String(feature.properties[keyProperty]).toLowerCase();
-        const getFeatureValue = (feature) => valuesMap[getFeatureKey(feature)];
-
-        const heatMapColors = HEAT_MAP_COLORS.slice(0, Math.min(HEAT_MAP_COLORS.length, rows.length))
-
-        const onHoverFeature = (hover) => {
-            onHoverChange && onHoverChange(hover && {
-                index: heatMapColors.indexOf(getColor(hover.feature)),
-                event: hover.event,
-                data: { key: getFeatureName(hover.feature), value: getFeatureValue(hover.feature)
-            } })
-        }
+    const nameProperty = details.region_name;
+    const keyProperty = details.region_key;
 
-        const getFeatureClickObject = (row) => ({
-            value: row[metricIndex],
-            column: cols[metricIndex],
-            dimensions: [{
-                value: row[dimensionIndex],
-                column: cols[dimensionIndex]
-            }]
-        })
-
-        const isClickable = onVisualizationClick && visualizationIsClickable(getFeatureClickObject(rows[0]))
-
-        const onClickFeature = isClickable && ((click) => {
-            const featureKey = getFeatureKey(click.feature);
-            const row = _.find(rows, row => getRowKey(row) === featureKey);
-            if (onVisualizationClick && row !== undefined) {
-                onVisualizationClick({
-                    ...getFeatureClickObject(row),
-                    event: click.event
-                });
-            }
-        })
-
-        const valuesMap = {};
-        const domain = []
-        for (const row of rows) {
-            valuesMap[getRowKey(row)] = (valuesMap[getRowKey(row)] || 0) + getRowValue(row);
-            domain.push(getRowValue(row));
-        }
-
-        const groups = ss.ckmeans(domain, heatMapColors.length);
-
-        var colorScale = d3.scale.quantile().domain(groups.map((cluster) => cluster[0])).range(heatMapColors);
+    if (!geoJson) {
+      return (
+        <div className={className + " flex layout-centered"}>
+          <LoadingSpinner />
+        </div>
+      );
+    }
 
-        let legendColors = heatMapColors.slice();
-        let legendTitles = heatMapColors.map((color, index) => {
-            const min = groups[index][0];
-            const max = groups[index].slice(-1)[0];
-            return index === heatMapColors.length - 1 ?
-                formatNumber(min) + " +" :
-                formatNumber(min) + " - " + formatNumber(max)
-        });
+    const [{ data: { cols, rows } }] = series;
+    const dimensionIndex = _.findIndex(
+      cols,
+      col => col.name === settings["map.dimension"],
+    );
+    const metricIndex = _.findIndex(
+      cols,
+      col => col.name === settings["map.metric"],
+    );
+
+    const getRowKey = row =>
+      getCanonicalRowKey(row[dimensionIndex], settings["map.region"]);
+    const getRowValue = row => row[metricIndex] || 0;
+    const getFeatureName = feature => String(feature.properties[nameProperty]);
+    const getFeatureKey = feature =>
+      String(feature.properties[keyProperty]).toLowerCase();
+    const getFeatureValue = feature => valuesMap[getFeatureKey(feature)];
+
+    const heatMapColors = HEAT_MAP_COLORS.slice(
+      0,
+      Math.min(HEAT_MAP_COLORS.length, rows.length),
+    );
+
+    const onHoverFeature = hover => {
+      onHoverChange &&
+        onHoverChange(
+          hover && {
+            index: heatMapColors.indexOf(getColor(hover.feature)),
+            event: hover.event,
+            data: {
+              key: getFeatureName(hover.feature),
+              value: getFeatureValue(hover.feature),
+            },
+          },
+        );
+    };
 
-        const getColor = (feature) => {
-            let value = getFeatureValue(feature);
-            return value == null ? HEAT_MAP_ZERO_COLOR : colorScale(value);
+    const getFeatureClickObject = row => ({
+      value: row[metricIndex],
+      column: cols[metricIndex],
+      dimensions: [
+        {
+          value: row[dimensionIndex],
+          column: cols[dimensionIndex],
+        },
+      ],
+    });
+
+    const isClickable =
+      onVisualizationClick &&
+      visualizationIsClickable(getFeatureClickObject(rows[0]));
+
+    const onClickFeature =
+      isClickable &&
+      (click => {
+        const featureKey = getFeatureKey(click.feature);
+        const row = _.find(rows, row => getRowKey(row) === featureKey);
+        if (onVisualizationClick && row !== undefined) {
+          onVisualizationClick({
+            ...getFeatureClickObject(row),
+            event: click.event,
+          });
         }
+      });
+
+    const valuesMap = {};
+    const domain = [];
+    for (const row of rows) {
+      valuesMap[getRowKey(row)] =
+        (valuesMap[getRowKey(row)] || 0) + getRowValue(row);
+      domain.push(getRowValue(row));
+    }
 
-        let aspectRatio;
-        if (projection) {
-            let translate = projection.translate();
-            let width = translate[0] * 2;
-            let height = translate[1] * 2;
-            aspectRatio = width / height;
-        } else {
-            aspectRatio =
-                (minimalBounds.getEast() - minimalBounds.getWest()) /
-                (minimalBounds.getNorth() - minimalBounds.getSouth());
-        }
+    const groups = ss.ckmeans(domain, heatMapColors.length);
+
+    var colorScale = d3.scale
+      .quantile()
+      .domain(groups.map(cluster => cluster[0]))
+      .range(heatMapColors);
+
+    let legendColors = heatMapColors.slice();
+    let legendTitles = heatMapColors.map((color, index) => {
+      const min = groups[index][0];
+      const max = groups[index].slice(-1)[0];
+      return index === heatMapColors.length - 1
+        ? formatNumber(min) + " +"
+        : formatNumber(min) + " - " + formatNumber(max);
+    });
+
+    const getColor = feature => {
+      let value = getFeatureValue(feature);
+      return value == null ? HEAT_MAP_ZERO_COLOR : colorScale(value);
+    };
 
-        return (
-            <ChartWithLegend
-                className={className}
-                aspectRatio={aspectRatio}
-                legendTitles={legendTitles} legendColors={legendColors}
-                gridSize={gridSize}
-                hovered={hovered} onHoverChange={onHoverChange}
-            >
-                { projection ?
-                    <LegacyChoropleth
-                        series={series}
-                        geoJson={geoJson}
-                        getColor={getColor}
-                        onHoverFeature={onHoverFeature}
-                        onClickFeature={onClickFeature}
-                        projection={projection}
-                    />
-                :
-                    <LeafletChoropleth
-                        series={series}
-                        geoJson={geoJson}
-                        getColor={getColor}
-                        onHoverFeature={onHoverFeature}
-                        onClickFeature={onClickFeature}
-                        minimalBounds={minimalBounds}
-                    />
-                }
-            </ChartWithLegend>
-        );
+    let aspectRatio;
+    if (projection) {
+      let translate = projection.translate();
+      let width = translate[0] * 2;
+      let height = translate[1] * 2;
+      aspectRatio = width / height;
+    } else {
+      aspectRatio =
+        (minimalBounds.getEast() - minimalBounds.getWest()) /
+        (minimalBounds.getNorth() - minimalBounds.getSouth());
     }
+
+    return (
+      <ChartWithLegend
+        className={className}
+        aspectRatio={aspectRatio}
+        legendTitles={legendTitles}
+        legendColors={legendColors}
+        gridSize={gridSize}
+        hovered={hovered}
+        onHoverChange={onHoverChange}
+      >
+        {projection ? (
+          <LegacyChoropleth
+            series={series}
+            geoJson={geoJson}
+            getColor={getColor}
+            onHoverFeature={onHoverFeature}
+            onClickFeature={onClickFeature}
+            projection={projection}
+          />
+        ) : (
+          <LeafletChoropleth
+            series={series}
+            geoJson={geoJson}
+            getColor={getColor}
+            onHoverFeature={onHoverFeature}
+            onClickFeature={onClickFeature}
+            minimalBounds={minimalBounds}
+          />
+        )}
+      </ChartWithLegend>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/FunnelBar.jsx b/frontend/src/metabase/visualizations/components/FunnelBar.jsx
index 50eb5245c746322a06cb708d86ff5489e219b47c..cecd21533f28d89cc4e99ee0c3e1f42c67365c6f 100644
--- a/frontend/src/metabase/visualizations/components/FunnelBar.jsx
+++ b/frontend/src/metabase/visualizations/components/FunnelBar.jsx
@@ -10,19 +10,21 @@ import { assocIn } from "icepick";
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class BarFunnel extends Component {
-    props: VisualizationProps;
+  props: VisualizationProps;
 
-    render() {
-        return (
-            <BarChart
-                 {...this.props}
-                 isScalarSeries={true}
-                 settings={{
-                     ...this.props.settings,
-                     ...getSettings(assocIn(this.props.series, [0, "card", "display"], "bar")),
-                     "bar.scalar_series": true
-                 }}
-             />
-        );
-    }
+  render() {
+    return (
+      <BarChart
+        {...this.props}
+        isScalarSeries={true}
+        settings={{
+          ...this.props.settings,
+          ...getSettings(
+            assocIn(this.props.series, [0, "card", "display"], "bar"),
+          ),
+          "bar.scalar_series": true,
+        }}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.css b/frontend/src/metabase/visualizations/components/FunnelNormal.css
index 6b736bc61380c2fe7114875dae0f576d594fe40c..c1497d0458e4eba82475a1fc6ce0faf74714a363 100644
--- a/frontend/src/metabase/visualizations/components/FunnelNormal.css
+++ b/frontend/src/metabase/visualizations/components/FunnelNormal.css
@@ -1,73 +1,72 @@
 :local .Funnel {
-    color: #A2A2A2;
-    height: 100%;
+  color: #a2a2a2;
+  height: 100%;
 }
 
 :local .FunnelStep {
-    width: 100%;
-    min-width: 20px;
-    border-right: 1px solid #E2E2E2;
+  width: 100%;
+  min-width: 20px;
+  border-right: 1px solid #e2e2e2;
 }
 
 :local .FunnelStep.Initial {
-    min-width: auto;
+  min-width: auto;
 }
 
 /* Display information for the initial blox */
 :local .Start {
-    display: flex;
-    justify-content: center;
-    flex-direction: column;
-    text-align: right;
-    flex-grow: 1;
+  display: flex;
+  justify-content: center;
+  flex-direction: column;
+  text-align: right;
+  flex-grow: 1;
 
-    padding-right: 0.5em;
-    font-size: 24px;
+  padding-right: 0.5em;
+  font-size: 24px;
 }
 
 :local .Start .Title {
-    font-weight: bold;
-    color: black;
+  font-weight: bold;
+  color: black;
 }
 
 :local .Start .Subtitle {
-    font-size: 0.6875em;
+  font-size: 0.6875em;
 }
 
 /* Head information */
 :local .Head {
-    text-align: right;
-    padding: 0.5em;
-    min-width: 0;
+  text-align: right;
+  padding: 0.5em;
+  min-width: 0;
 }
 
 /* Plot graph element */
 :local .Graph {
-    flex-grow: 1;
+  flex-grow: 1;
 }
 
 /* Information at the end of the step */
 :local .Infos {
-    text-align: right;
-    padding: 0.5em 0.5em 0  0.5em;
-    font-size: 16px;
+  text-align: right;
+  padding: 0.5em 0.5em 0 0.5em;
+  font-size: 16px;
 }
 
 :local .Infos .Title {
-
 }
 
 :local .Infos .Subtitle {
-    font-size: 0.6875em;
-    margin-top: 1em;
+  font-size: 0.6875em;
+  margin-top: 1em;
 }
 
 /* Small version */
 :local .Small .Head,
 :local .Small .Infos {
-    display: none;
+  display: none;
 }
 
 :local .Small .FunnelStep {
-    border-color: white;
+  border-color: white;
 }
diff --git a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
index 17f43b109c86cebd433956f7d684327c97099528..4e3efe93b30c059d472082bb55bff05000e9c988 100644
--- a/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
+++ b/frontend/src/metabase/visualizations/components/FunnelNormal.jsx
@@ -13,189 +13,234 @@ import { normal } from "metabase/lib/colors";
 
 const DEFAULT_COLORS = Object.values(normal);
 
-import type { VisualizationProps, HoverObject, ClickObject } from "metabase/meta/types/Visualization";
+import type {
+  VisualizationProps,
+  HoverObject,
+  ClickObject,
+} from "metabase/meta/types/Visualization";
 
 type StepInfo = {
-    value: number,
-    graph: {
-        startBottom: number,
-        startTop: number,
-        endBottom: number,
-        endTop: number
-    },
-    hovered?: HoverObject,
-    clicked?: ClickObject,
+  value: number,
+  graph: {
+    startBottom: number,
+    startTop: number,
+    endBottom: number,
+    endTop: number,
+  },
+  hovered?: HoverObject,
+  clicked?: ClickObject,
 };
 
 export default class Funnel extends Component {
-    props: VisualizationProps;
-
-    render() {
-        const { className, series, gridSize, hovered, onHoverChange, onVisualizationClick, visualizationIsClickable } = this.props;
-
-        const dimensionIndex = 0;
-        const metricIndex = 1;
-        const cols = series[0].data.cols;
-        // $FlowFixMe
-        const rows: number[][] = series.map(s => s.data.rows[0]);
-
-        const funnelSmallSize = gridSize && (gridSize.width < 7 || gridSize.height <= 5);
-
-        const formatDimension = (dimension, jsx = true) => formatValue(dimension, { column: cols[dimensionIndex], jsx, majorWidth: 0 })
-        const formatMetric    =    (metric, jsx = true) => formatValue(metric, { column: cols[metricIndex], jsx, majorWidth: 0 , comma: true})
-        const formatPercent   =               (percent) => `${(100 * percent).toFixed(2)} %`
-
-        // Initial infos (required for step calculation)
-        var infos: StepInfo[] = [{
-            value: rows[0][metricIndex],
-            graph: {
-                startBottom: 0.0,
-                startTop: 1.0,
-                endBottom: 0.0,
-                endTop: 1.0,
-            }
-        }];
-
-        var remaining: number = rows[0][metricIndex];
-
-        rows.map((row, rowIndex) => {
-            remaining -= (infos[rowIndex].value - row[metricIndex]);
-
-            infos[rowIndex + 1] = {
-                value: row[metricIndex],
-
-                graph: {
-                    startBottom: infos[rowIndex].graph.endBottom,
-                    startTop: infos[rowIndex].graph.endTop,
-                    endTop: 0.5 + ((remaining / infos[0].value) / 2),
-                    endBottom: 0.5 - ((remaining / infos[0].value) / 2),
-                },
-
-                hovered: {
-                    index: rowIndex,
-                    data: [
-                        {
-                            key: 'Step',
-                            value: formatDimension(row[dimensionIndex]),
-                        },
-                        {
-                            key: getFriendlyName(cols[metricIndex]),
-                            value: formatMetric(row[metricIndex]),
-                        },
-                        {
-                            key: 'Retained',
-                            value: formatPercent(row[metricIndex] / infos[0].value),
-                        },
-                    ]
-                },
-
-                clicked: {
-                    value: row[metricIndex],
-                    column: cols[metricIndex],
-                    dimensions: [{
-                        value: row[dimensionIndex],
-                        column: cols[dimensionIndex],
-                    }]
-                }
-            };
-        });
-
-        // Remove initial setup
-        infos = infos.slice(1);
-
-        let initial = infos[0];
-
-        const isClickable = visualizationIsClickable(infos[0].clicked);
-
-        return (
-            <div className={cx(className, styles.Funnel, 'flex', {
-                [styles.Small]: funnelSmallSize,
-                "p1": funnelSmallSize,
-                "p2": !funnelSmallSize
-            })}>
-                <div className={cx(styles.FunnelStep, styles.Initial, 'flex flex-column')}>
-                    <Ellipsified className={styles.Head}>{formatDimension(rows[0][dimensionIndex])}</Ellipsified>
-                    <div className={styles.Start}>
-                        <div className={styles.Title}>{formatMetric(rows[0][metricIndex])}</div>
-                        <div className={styles.Subtitle}>{getFriendlyName(cols[dimensionIndex])}</div>
-                    </div>
-                    {/* This part of code in used only to share height between .Start and .Graph columns. */}
-                    <div className={styles.Infos}>
-                        <div className={styles.Title}>&nbsp;</div>
-                        <div className={styles.Subtitle}>&nbsp;</div>
-                    </div>
-                </div>
-                {infos.slice(1).map((info, index) =>
-                    <div key={index} className={cx(styles.FunnelStep, 'flex flex-column')}>
-                        <Ellipsified className={styles.Head}>{formatDimension(rows[index + 1][dimensionIndex])}</Ellipsified>
-                        <GraphSection
-                            className={cx({ "cursor-pointer": isClickable })}
-                            index={index}
-                            info={info}
-                            infos={infos}
-                            hovered={hovered}
-                            onHoverChange={onHoverChange}
-                            onVisualizationClick={isClickable ? onVisualizationClick : null}
-                        />
-                        <div className={styles.Infos}>
-                            <div className={styles.Title}>{formatPercent(info.value / initial.value)}</div>
-                            <div className={styles.Subtitle}>{formatMetric(rows[index + 1][metricIndex])}</div>
-                        </div>
-                    </div>
-                )}
-            </div>
-        );
-    }
-}
-const GraphSection = (
-    {
-        index,
-        info,
-        infos,
-        hovered,
-        onHoverChange,
-        onVisualizationClick,
-        className,
-    }: {
-        className?: string,
-        index: number,
-        info: StepInfo,
-        infos: StepInfo[],
-        hovered: ?HoverObject,
-        onVisualizationClick: ?((clicked: ?ClickObject) => void),
-        onHoverChange: (hovered: ?HoverObject) => void
-    }
-) => {
+  props: VisualizationProps;
+
+  render() {
+    const {
+      className,
+      series,
+      gridSize,
+      hovered,
+      onHoverChange,
+      onVisualizationClick,
+      visualizationIsClickable,
+    } = this.props;
+
+    const dimensionIndex = 0;
+    const metricIndex = 1;
+    const cols = series[0].data.cols;
+    // $FlowFixMe
+    const rows: number[][] = series.map(s => s.data.rows[0]);
+
+    const funnelSmallSize =
+      gridSize && (gridSize.width < 7 || gridSize.height <= 5);
+
+    const formatDimension = (dimension, jsx = true) =>
+      formatValue(dimension, {
+        column: cols[dimensionIndex],
+        jsx,
+        majorWidth: 0,
+      });
+    const formatMetric = (metric, jsx = true) =>
+      formatValue(metric, {
+        column: cols[metricIndex],
+        jsx,
+        majorWidth: 0,
+        comma: true,
+      });
+    const formatPercent = percent => `${(100 * percent).toFixed(2)} %`;
+
+    // Initial infos (required for step calculation)
+    var infos: StepInfo[] = [
+      {
+        value: rows[0][metricIndex],
+        graph: {
+          startBottom: 0.0,
+          startTop: 1.0,
+          endBottom: 0.0,
+          endTop: 1.0,
+        },
+      },
+    ];
+
+    var remaining: number = rows[0][metricIndex];
+
+    rows.map((row, rowIndex) => {
+      remaining -= infos[rowIndex].value - row[metricIndex];
+
+      infos[rowIndex + 1] = {
+        value: row[metricIndex],
+
+        graph: {
+          startBottom: infos[rowIndex].graph.endBottom,
+          startTop: infos[rowIndex].graph.endTop,
+          endTop: 0.5 + remaining / infos[0].value / 2,
+          endBottom: 0.5 - remaining / infos[0].value / 2,
+        },
+
+        hovered: {
+          index: rowIndex,
+          data: [
+            {
+              key: "Step",
+              value: formatDimension(row[dimensionIndex]),
+            },
+            {
+              key: getFriendlyName(cols[metricIndex]),
+              value: formatMetric(row[metricIndex]),
+            },
+            {
+              key: "Retained",
+              value: formatPercent(row[metricIndex] / infos[0].value),
+            },
+          ],
+        },
+
+        clicked: {
+          value: row[metricIndex],
+          column: cols[metricIndex],
+          dimensions: [
+            {
+              value: row[dimensionIndex],
+              column: cols[dimensionIndex],
+            },
+          ],
+        },
+      };
+    });
+
+    // Remove initial setup
+    infos = infos.slice(1);
+
+    let initial = infos[0];
+
+    const isClickable = visualizationIsClickable(infos[0].clicked);
+
     return (
-        <svg
-            className={cx(className, styles.Graph)}
-            onMouseMove={e => {
-                if (onHoverChange && info.hovered) {
-                    onHoverChange({
-                        ...info.hovered,
-                        event: e.nativeEvent
-                    })
-                }
-            }}
-            onMouseLeave={() => onHoverChange && onHoverChange(null)}
-            onClick={e => {
-                if (onVisualizationClick && info.clicked) {
-                    onVisualizationClick({
-                        ...info.clicked,
-                        event: e.nativeEvent
-                    })
-                }
-            }}
-            viewBox="0 0 1 1"
-            preserveAspectRatio="none"
+      <div
+        className={cx(className, styles.Funnel, "flex", {
+          [styles.Small]: funnelSmallSize,
+          p1: funnelSmallSize,
+          p2: !funnelSmallSize,
+        })}
+      >
+        <div
+          className={cx(styles.FunnelStep, styles.Initial, "flex flex-column")}
         >
-            <polygon
-                opacity={1 - index * (0.9 / (infos.length + 1))}
-                fill={DEFAULT_COLORS[0]}
-                points={
-                    `0 ${info.graph.startBottom}, 0 ${info.graph.startTop}, 1 ${info.graph.endTop}, 1 ${info.graph.endBottom}`
-                }
+          <Ellipsified className={styles.Head}>
+            {formatDimension(rows[0][dimensionIndex])}
+          </Ellipsified>
+          <div className={styles.Start}>
+            <div className={styles.Title}>
+              {formatMetric(rows[0][metricIndex])}
+            </div>
+            <div className={styles.Subtitle}>
+              {getFriendlyName(cols[dimensionIndex])}
+            </div>
+          </div>
+          {/* This part of code in used only to share height between .Start and .Graph columns. */}
+          <div className={styles.Infos}>
+            <div className={styles.Title}>&nbsp;</div>
+            <div className={styles.Subtitle}>&nbsp;</div>
+          </div>
+        </div>
+        {infos.slice(1).map((info, index) => (
+          <div
+            key={index}
+            className={cx(styles.FunnelStep, "flex flex-column")}
+          >
+            <Ellipsified className={styles.Head}>
+              {formatDimension(rows[index + 1][dimensionIndex])}
+            </Ellipsified>
+            <GraphSection
+              className={cx({ "cursor-pointer": isClickable })}
+              index={index}
+              info={info}
+              infos={infos}
+              hovered={hovered}
+              onHoverChange={onHoverChange}
+              onVisualizationClick={isClickable ? onVisualizationClick : null}
             />
-        </svg>
+            <div className={styles.Infos}>
+              <div className={styles.Title}>
+                {formatPercent(info.value / initial.value)}
+              </div>
+              <div className={styles.Subtitle}>
+                {formatMetric(rows[index + 1][metricIndex])}
+              </div>
+            </div>
+          </div>
+        ))}
+      </div>
     );
+  }
+}
+const GraphSection = ({
+  index,
+  info,
+  infos,
+  hovered,
+  onHoverChange,
+  onVisualizationClick,
+  className,
+}: {
+  className?: string,
+  index: number,
+  info: StepInfo,
+  infos: StepInfo[],
+  hovered: ?HoverObject,
+  onVisualizationClick: ?(clicked: ?ClickObject) => void,
+  onHoverChange: (hovered: ?HoverObject) => void,
+}) => {
+  return (
+    <svg
+      className={cx(className, styles.Graph)}
+      onMouseMove={e => {
+        if (onHoverChange && info.hovered) {
+          onHoverChange({
+            ...info.hovered,
+            event: e.nativeEvent,
+          });
+        }
+      }}
+      onMouseLeave={() => onHoverChange && onHoverChange(null)}
+      onClick={e => {
+        if (onVisualizationClick && info.clicked) {
+          onVisualizationClick({
+            ...info.clicked,
+            event: e.nativeEvent,
+          });
+        }
+      }}
+      viewBox="0 0 1 1"
+      preserveAspectRatio="none"
+    >
+      <polygon
+        opacity={1 - index * (0.9 / (infos.length + 1))}
+        fill={DEFAULT_COLORS[0]}
+        points={`0 ${info.graph.startBottom}, 0 ${info.graph.startTop}, 1 ${
+          info.graph.endTop
+        }, 1 ${info.graph.endBottom}`}
+      />
+    </svg>
+  );
 };
diff --git a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
index 6099b9eeeec27cd5841fe10dda0396ca91dec170..181bbcb6518c3fd08515fa7a31c85f7fa6657c76 100644
--- a/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletChoropleth.jsx
@@ -11,93 +11,96 @@ import L from "leaflet";
 import { computeMinimalBounds } from "metabase/visualizations/lib/mapping";
 
 const LeafletChoropleth = ({
-    series,
-    geoJson,
-    minimalBounds = computeMinimalBounds(geoJson.features),
-    getColor = () => normal.blue,
-    onHoverFeature = () => {},
-    onClickFeature = () => {},
-}) =>
-    <CardRenderer
-        series={series}
-        className="spread"
-        renderer={(element, props) => {
-            element.className = "spread";
-            element.style.backgroundColor = "transparent";
+  series,
+  geoJson,
+  minimalBounds = computeMinimalBounds(geoJson.features),
+  getColor = () => normal.blue,
+  onHoverFeature = () => {},
+  onClickFeature = () => {},
+}) => (
+  <CardRenderer
+    series={series}
+    className="spread"
+    renderer={(element, props) => {
+      element.className = "spread";
+      element.style.backgroundColor = "transparent";
 
-            const map = L.map(element, {
-                zoomSnap: 0,
-                worldCopyJump: true,
-                attributionControl: false,
+      const map = L.map(element, {
+        zoomSnap: 0,
+        worldCopyJump: true,
+        attributionControl: false,
 
-                // disable zoom controls
-                dragging: false,
-                tap: false,
-                zoomControl: false,
-                touchZoom: false,
-                doubleClickZoom: false,
-                scrollWheelZoom: false,
-                boxZoom: false,
-                keyboard: false,
-            });
+        // disable zoom controls
+        dragging: false,
+        tap: false,
+        zoomControl: false,
+        touchZoom: false,
+        doubleClickZoom: false,
+        scrollWheelZoom: false,
+        boxZoom: false,
+        keyboard: false,
+      });
 
-            // L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
-            //     attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'
-            // }).addTo(map);
+      // L.tileLayer("http://{s}.tile.openstreetmap.org/{z}/{x}/{y}.png", {
+      //     attribution: 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'
+      // }).addTo(map);
 
-            const style = (feature) => ({
-                fillColor: getColor(feature),
-                weight: 1,
-                opacity: 1,
-                color: "white",
-                fillOpacity: 1
-            });
+      const style = feature => ({
+        fillColor: getColor(feature),
+        weight: 1,
+        opacity: 1,
+        color: "white",
+        fillOpacity: 1,
+      });
 
-            const onEachFeature = (feature, layer) => {
-                layer.on({
-                    mousemove: (e) => {
-                        onHoverFeature({
-                            feature: feature,
-                            event: e.originalEvent
-                        })
-                    },
-                    mouseout: (e) => {
-                        onHoverFeature(null)
-                    },
-                    click: (e) => {
-                        onClickFeature({
-                            feature: feature,
-                            event: e.originalEvent
-                        })
-                    },
-                });
-            }
+      const onEachFeature = (feature, layer) => {
+        layer.on({
+          mousemove: e => {
+            onHoverFeature({
+              feature: feature,
+              event: e.originalEvent,
+            });
+          },
+          mouseout: e => {
+            onHoverFeature(null);
+          },
+          click: e => {
+            onClickFeature({
+              feature: feature,
+              event: e.originalEvent,
+            });
+          },
+        });
+      };
 
-            // main layer
-            L.featureGroup([
-                L.geoJson(geoJson, {
-                    style: style,
-                    onEachFeature: onEachFeature
-                })
-            ]).addTo(map);
+      // main layer
+      L.featureGroup([
+        L.geoJson(geoJson, {
+          style: style,
+          onEachFeature: onEachFeature,
+        }),
+      ]).addTo(map);
 
-            // left and right duplicates so we can pan a bit
-            L.featureGroup([
-                L.geoJson(geoJson, {
-                    style: style,
-                    onEachFeature: onEachFeature,
-                    coordsToLatLng: ([longitude, latitude]) => L.latLng(latitude, longitude - 360)
-                }),
-                L.geoJson(geoJson, {
-                    style: style,
-                    onEachFeature: onEachFeature,
-                    coordsToLatLng: ([longitude, latitude]) => L.latLng(latitude, longitude + 360)
-                })
-            ]).addTo(map);
+      // left and right duplicates so we can pan a bit
+      L.featureGroup([
+        L.geoJson(geoJson, {
+          style: style,
+          onEachFeature: onEachFeature,
+          coordsToLatLng: ([longitude, latitude]) =>
+            L.latLng(latitude, longitude - 360),
+        }),
+        L.geoJson(geoJson, {
+          style: style,
+          onEachFeature: onEachFeature,
+          coordsToLatLng: ([longitude, latitude]) =>
+            L.latLng(latitude, longitude + 360),
+        }),
+      ]).addTo(map);
 
-            map.fitBounds(minimalBounds);
-            // map.fitBounds(geoFeatureGroup.getBounds());
-        }}
-    />
+      map.fitBounds(minimalBounds);
+      // map.fitBounds(geoFeatureGroup.getBounds());
+    }}
+  />
+);
 
 export default LeafletChoropleth;
diff --git a/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx b/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx
index d4f122ceb18b5b9c4cca6d087f8d9591099fd57d..96b66add16440248b9ad63e3a0b4248083c57c4c 100644
--- a/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletGridHeatMap.jsx
@@ -1,114 +1,113 @@
 import LeafletMap from "./LeafletMap.jsx";
 import L from "leaflet";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import d3 from "d3";
 
 import { rangeForValue } from "metabase/lib/dataset";
 
 export default class LeafletGridHeatMap extends LeafletMap {
-    componentDidMount() {
-        super.componentDidMount();
+  componentDidMount() {
+    super.componentDidMount();
 
-        this.gridLayer = L.layerGroup([]).addTo(this.map);
-        this.componentDidUpdate({}, {});
-    }
+    this.gridLayer = L.layerGroup([]).addTo(this.map);
+    this.componentDidUpdate({}, {});
+  }
 
-    componentDidUpdate(prevProps, prevState) {
-        super.componentDidUpdate(prevProps, prevState);
+  componentDidUpdate(prevProps, prevState) {
+    super.componentDidUpdate(prevProps, prevState);
 
-        try {
-            const { gridLayer } = this;
-            const { points, min, max } = this.props;
+    try {
+      const { gridLayer } = this;
+      const { points, min, max } = this.props;
 
-            const { latitudeColumn, longitudeColumn } = this._getLatLonColumns();
-            if (!latitudeColumn.binning_info || !longitudeColumn.binning_info) {
-                throw new Error(t`Grid map requires binned longitude/latitude.`);
-            }
+      const { latitudeColumn, longitudeColumn } = this._getLatLonColumns();
+      if (!latitudeColumn.binning_info || !longitudeColumn.binning_info) {
+        throw new Error(t`Grid map requires binned longitude/latitude.`);
+      }
 
-            const color = d3.scale.linear().domain([min,max])
-                .interpolate(d3.interpolateHcl)
-                .range([d3.rgb("#00FF00"), d3.rgb('#FF0000')]);
+      const color = d3.scale
+        .linear()
+        .domain([min, max])
+        .interpolate(d3.interpolateHcl)
+        .range([d3.rgb("#00FF00"), d3.rgb("#FF0000")]);
 
-            let gridSquares = gridLayer.getLayers();
-            let totalSquares = Math.max(points.length, gridSquares.length);
-            for (let i = 0; i < totalSquares; i++) {
-                if (i >= points.length) {
-                    gridLayer.removeLayer(gridSquares[i]);
-                }
-                if (i >= gridSquares.length) {
-                    const gridSquare = this._createGridSquare(i);
-                    gridLayer.addLayer(gridSquare);
-                    gridSquares.push(gridSquare);
-                }
+      let gridSquares = gridLayer.getLayers();
+      let totalSquares = Math.max(points.length, gridSquares.length);
+      for (let i = 0; i < totalSquares; i++) {
+        if (i >= points.length) {
+          gridLayer.removeLayer(gridSquares[i]);
+        }
+        if (i >= gridSquares.length) {
+          const gridSquare = this._createGridSquare(i);
+          gridLayer.addLayer(gridSquare);
+          gridSquares.push(gridSquare);
+        }
 
-                if (i < points.length) {
-                    gridSquares[i].setStyle({ color: color(points[i][2]) });
-                    const [latMin, latMax] = rangeForValue(points[i][0], latitudeColumn);
-                    const [lonMin, lonMax] = rangeForValue(points[i][1], longitudeColumn);
-                    gridSquares[i].setBounds([
-                        [latMin, lonMin],
-                        [latMax, lonMax]
-                    ]);
-                }
-            }
-        } catch (err) {
-            console.error(err);
-            this.props.onRenderError(err.message || err);
+        if (i < points.length) {
+          gridSquares[i].setStyle({ color: color(points[i][2]) });
+          const [latMin, latMax] = rangeForValue(points[i][0], latitudeColumn);
+          const [lonMin, lonMax] = rangeForValue(points[i][1], longitudeColumn);
+          gridSquares[i].setBounds([[latMin, lonMin], [latMax, lonMax]]);
         }
+      }
+    } catch (err) {
+      console.error(err);
+      this.props.onRenderError(err.message || err);
     }
+  }
 
-    _createGridSquare = (index) => {
-        const bounds = [[54.559322, -5.767822], [56.1210604, -3.021240]];
-        const gridSquare = L.rectangle(bounds, {
-            color: "red",
-            weight: 1,
-            stroke: true,
-            fillOpacity: 0.5,
-            strokeOpacity: 1.0
-        });
-        gridSquare.on("click", this._onVisualizationClick.bind(this, index));
-        gridSquare.on("mousemove", this._onHoverChange.bind(this, index));
-        gridSquare.on("mouseout", this._onHoverChange.bind(this, null));
-        return gridSquare;
-    }
+  _createGridSquare = index => {
+    const bounds = [[54.559322, -5.767822], [56.1210604, -3.02124]];
+    const gridSquare = L.rectangle(bounds, {
+      color: "red",
+      weight: 1,
+      stroke: true,
+      fillOpacity: 0.5,
+      strokeOpacity: 1.0,
+    });
+    gridSquare.on("click", this._onVisualizationClick.bind(this, index));
+    gridSquare.on("mousemove", this._onHoverChange.bind(this, index));
+    gridSquare.on("mouseout", this._onHoverChange.bind(this, null));
+    return gridSquare;
+  };
 
-    _clickForPoint(index, e) {
-        const { points } = this.props;
-        const point = points[index];
-        const metricColumn = this._getMetricColumn();
-        const { latitudeColumn, longitudeColumn } = this._getLatLonColumns();
-        return {
-            value: point[2],
-            column: metricColumn,
-            dimensions: [
-                {
-                    value: point[0],
-                    column: latitudeColumn,
-                },
-                {
-                    value: point[1],
-                    column: longitudeColumn,
-                }
-            ],
-            event: e.originalEvent
-        }
-    }
+  _clickForPoint(index, e) {
+    const { points } = this.props;
+    const point = points[index];
+    const metricColumn = this._getMetricColumn();
+    const { latitudeColumn, longitudeColumn } = this._getLatLonColumns();
+    return {
+      value: point[2],
+      column: metricColumn,
+      dimensions: [
+        {
+          value: point[0],
+          column: latitudeColumn,
+        },
+        {
+          value: point[1],
+          column: longitudeColumn,
+        },
+      ],
+      event: e.originalEvent,
+    };
+  }
 
-    _onVisualizationClick(index, e) {
-        const { onVisualizationClick } = this.props;
-        if (onVisualizationClick) {
-            onVisualizationClick(this._clickForPoint(index, e));
-        }
+  _onVisualizationClick(index, e) {
+    const { onVisualizationClick } = this.props;
+    if (onVisualizationClick) {
+      onVisualizationClick(this._clickForPoint(index, e));
     }
+  }
 
-    _onHoverChange(index, e) {
-        const { onHoverChange } = this.props;
-        if (onHoverChange) {
-            if (index == null) {
-                onHoverChange(null);
-            } else {
-                onHoverChange(this._clickForPoint(index, e));
-            }
-        }
+  _onHoverChange(index, e) {
+    const { onHoverChange } = this.props;
+    if (onHoverChange) {
+      if (index == null) {
+        onHoverChange(null);
+      } else {
+        onHoverChange(this._clickForPoint(index, e));
+      }
     }
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LeafletHeatMap.jsx b/frontend/src/metabase/visualizations/components/LeafletHeatMap.jsx
index 68c2dfc41b91325788243b539f93bdbf4b9160dc..184efccd68ae046b8b23d765cbcdf8b77d4fb4f8 100644
--- a/frontend/src/metabase/visualizations/components/LeafletHeatMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletHeatMap.jsx
@@ -4,36 +4,36 @@ import L from "leaflet";
 import "leaflet.heat";
 
 export default class LeafletHeatMap extends LeafletMap {
-    componentDidMount() {
-        super.componentDidMount();
+  componentDidMount() {
+    super.componentDidMount();
 
-        // Leaflet map may not be fully initialized
-        // https://stackoverflow.com/a/28903337/113
-        setTimeout(() => {
-            this.pinMarkerLayer = L.layerGroup([]).addTo(this.map);
-            this.heatLayer = L.heatLayer([], { radius: 25 }).addTo(this.map);
-            this.componentDidUpdate({}, {});
-        });
-    }
+    // Leaflet map may not be fully initialized
+    // https://stackoverflow.com/a/28903337/113
+    setTimeout(() => {
+      this.pinMarkerLayer = L.layerGroup([]).addTo(this.map);
+      this.heatLayer = L.heatLayer([], { radius: 25 }).addTo(this.map);
+      this.componentDidUpdate({}, {});
+    });
+  }
 
-    componentDidUpdate(prevProps, prevState) {
-        super.componentDidUpdate(prevProps, prevState);
+  componentDidUpdate(prevProps, prevState) {
+    super.componentDidUpdate(prevProps, prevState);
 
-        try {
-            const { heatLayer } = this;
-            const { points, max, settings } = this.props;
+    try {
+      const { heatLayer } = this;
+      const { points, max, settings } = this.props;
 
-            heatLayer.setOptions({
-                max: max,
-                maxZoom: settings["map.heat.max-zoom"],
-                minOpacity: settings["map.heat.min-opacity"],
-                radius:  settings["map.heat.radius"],
-                blur: settings["map.heat.blur"],
-            });
-            heatLayer.setLatLngs(points);
-        } catch (err) {
-            console.error(err);
-            this.props.onRenderError(err.message || err);
-        }
+      heatLayer.setOptions({
+        max: max,
+        maxZoom: settings["map.heat.max-zoom"],
+        minOpacity: settings["map.heat.min-opacity"],
+        radius: settings["map.heat.radius"],
+        blur: settings["map.heat.blur"],
+      });
+      heatLayer.setLatLngs(points);
+    } catch (err) {
+      console.error(err);
+      this.props.onRenderError(err.message || err);
     }
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LeafletMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMap.jsx
index aea44ae2ed6fa49bffa4d3c850d69582c1ae7a6d..2f22994e8b5cbe7cf3cd87db1fbee0f19ab872f8 100644
--- a/frontend/src/metabase/visualizations/components/LeafletMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletMap.jsx
@@ -12,128 +12,161 @@ import _ from "underscore";
 import { updateLatLonFilter } from "metabase/qb/lib/actions";
 
 export default class LeafletMap extends Component {
-    componentDidMount() {
-        try {
-            const element = ReactDOM.findDOMNode(this.refs.map);
-
-            const map = this.map = L.map(element, {
-                scrollWheelZoom: false,
-                minZoom: 2,
-                drawControlTooltips: false,
-                zoomSnap: false
-            });
-
-            const drawnItems = new L.FeatureGroup();
-            map.addLayer(drawnItems);
-            const drawControl = this.drawControl = new L.Control.Draw({
-                draw: {
-                    rectangle: false,
-                    polyline: false,
-                    polygon: false,
-                    circle: false,
-                    marker: false
-                },
-                edit: {
-                    featureGroup: drawnItems,
-                    edit: false,
-                    remove: false
-                }
-            });
-            map.addControl(drawControl);
-            map.on("draw:created", this.onFilter);
-
-            map.setView([0,0], 8);
-
-            const mapTileUrl = MetabaseSettings.get("map_tile_server_url");
-            const mapTileAttribution = mapTileUrl.indexOf("openstreetmap.org") >= 0 ? 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors' : null;
-
-            L.tileLayer(mapTileUrl, { attribution: mapTileAttribution }).addTo(map);
-
-            map.on("moveend", () => {
-                const { lat, lng } = map.getCenter();
-                this.props.onMapCenterChange(lat, lng);
-            });
-            map.on("zoomend", () => {
-                const zoom = map.getZoom();
-                this.props.onMapZoomChange(zoom);
-            });
-        } catch (err) {
-            console.error(err);
-            this.props.onRenderError(err.message || err);
-        }
+  componentDidMount() {
+    try {
+      const element = ReactDOM.findDOMNode(this.refs.map);
+
+      const map = (this.map = L.map(element, {
+        scrollWheelZoom: false,
+        minZoom: 2,
+        drawControlTooltips: false,
+        zoomSnap: false,
+      }));
+
+      const drawnItems = new L.FeatureGroup();
+      map.addLayer(drawnItems);
+      const drawControl = (this.drawControl = new L.Control.Draw({
+        draw: {
+          rectangle: false,
+          polyline: false,
+          polygon: false,
+          circle: false,
+          marker: false,
+        },
+        edit: {
+          featureGroup: drawnItems,
+          edit: false,
+          remove: false,
+        },
+      }));
+      map.addControl(drawControl);
+      map.on("draw:created", this.onFilter);
+
+      map.setView([0, 0], 8);
+
+      const mapTileUrl = MetabaseSettings.get("map_tile_server_url");
+      const mapTileAttribution =
+        mapTileUrl.indexOf("openstreetmap.org") >= 0
+          ? 'Map data © <a href="http://openstreetmap.org">OpenStreetMap</a> contributors'
+          : null;
+
+      L.tileLayer(mapTileUrl, { attribution: mapTileAttribution }).addTo(map);
+
+      map.on("moveend", () => {
+        const { lat, lng } = map.getCenter();
+        this.props.onMapCenterChange(lat, lng);
+      });
+      map.on("zoomend", () => {
+        const zoom = map.getZoom();
+        this.props.onMapZoomChange(zoom);
+      });
+    } catch (err) {
+      console.error(err);
+      this.props.onRenderError(err.message || err);
     }
-
-    componentDidUpdate(prevProps) {
-        const { bounds, settings } = this.props;
-        if (!prevProps || prevProps.points !== this.props.points || prevProps.width !== this.props.width || prevProps.height !== this.props.height) {
-            this.map.invalidateSize();
-
-            if (settings["map.center_latitude"] != null || settings["map.center_longitude"] != null || settings["map.zoom"] != null) {
-                this.map.setView([
-                    settings["map.center_latitude"],
-                    settings["map.center_longitude"]
-                ], settings["map.zoom"]);
-            } else {
-                // compute ideal lat and lon zoom separately and use the lesser zoom to ensure the bounds are visible
-                const latZoom = this.map.getBoundsZoom(L.latLngBounds([[bounds.getSouth(), 0], [bounds.getNorth(), 0]]))
-                const lonZoom = this.map.getBoundsZoom(L.latLngBounds([[0, bounds.getWest()], [0, bounds.getEast()]]))
-                const zoom = Math.min(latZoom, lonZoom);
-                // NOTE: unclear why calling `fitBounds` twice is sometimes required to get it to work 
-                this.map.fitBounds(bounds);
-                this.map.setZoom(zoom);
-                this.map.fitBounds(bounds);
-            }
-        }
-    }
-
-    startFilter() {
-        this._filter = new L.Draw.Rectangle(this.map, this.drawControl.options.rectangle);
-        this._filter.enable();
-        this.props.onFiltering(true);
-    }
-    stopFilter() {
-        this._filter && this._filter.disable();
-        this.props.onFiltering(false);
-    }
-    onFilter = (e) => {
-        const bounds = e.layer.getBounds();
-
-        const { series: [{ card, data: { cols } }], settings, setCardAndRun } = this.props;
-
-        const latitudeColumn = _.findWhere(cols, { name: settings["map.latitude_column"] });
-        const longitudeColumn = _.findWhere(cols, { name: settings["map.longitude_column"] });
-
-        setCardAndRun(updateLatLonFilter(card, latitudeColumn, longitudeColumn, bounds));
-
-        this.props.onFiltering(false);
-    }
-
-    render() {
-        const { className } = this.props;
-        return (
-            <div className={className} ref="map"></div>
+  }
+
+  componentDidUpdate(prevProps) {
+    const { bounds, settings } = this.props;
+    if (
+      !prevProps ||
+      prevProps.points !== this.props.points ||
+      prevProps.width !== this.props.width ||
+      prevProps.height !== this.props.height
+    ) {
+      this.map.invalidateSize();
+
+      if (
+        settings["map.center_latitude"] != null ||
+        settings["map.center_longitude"] != null ||
+        settings["map.zoom"] != null
+      ) {
+        this.map.setView(
+          [settings["map.center_latitude"], settings["map.center_longitude"]],
+          settings["map.zoom"],
         );
+      } else {
+        // compute ideal lat and lon zoom separately and use the lesser zoom to ensure the bounds are visible
+        const latZoom = this.map.getBoundsZoom(
+          L.latLngBounds([[bounds.getSouth(), 0], [bounds.getNorth(), 0]]),
+        );
+        const lonZoom = this.map.getBoundsZoom(
+          L.latLngBounds([[0, bounds.getWest()], [0, bounds.getEast()]]),
+        );
+        const zoom = Math.min(latZoom, lonZoom);
+        // NOTE: unclear why calling `fitBounds` twice is sometimes required to get it to work
+        this.map.fitBounds(bounds);
+        this.map.setZoom(zoom);
+        this.map.fitBounds(bounds);
+      }
     }
-
-    _getLatLonIndexes() {
-        const { settings, series: [{ data: { cols }}] } = this.props;
-        return {
-            latitudeIndex: _.findIndex(cols, (col) => col.name === settings["map.latitude_column"]),
-            longitudeIndex: _.findIndex(cols, (col) => col.name === settings["map.longitude_column"])
-        };
-    }
-
-    _getLatLonColumns() {
-        const { series: [{ data: { cols }}] } = this.props;
-        const { latitudeIndex, longitudeIndex } = this._getLatLonIndexes();
-        return {
-            latitudeColumn: cols[latitudeIndex],
-            longitudeColumn: cols[longitudeIndex]
-        };
-    }
-
-    _getMetricColumn() {
-        const { settings, series: [{ data: { cols }}] } = this.props;
-        return _.findWhere(cols, { name: settings["map.metric_column"] });
-    }
+  }
+
+  startFilter() {
+    this._filter = new L.Draw.Rectangle(
+      this.map,
+      this.drawControl.options.rectangle,
+    );
+    this._filter.enable();
+    this.props.onFiltering(true);
+  }
+  stopFilter() {
+    this._filter && this._filter.disable();
+    this.props.onFiltering(false);
+  }
+  onFilter = e => {
+    const bounds = e.layer.getBounds();
+
+    const {
+      series: [{ card, data: { cols } }],
+      settings,
+      setCardAndRun,
+    } = this.props;
+
+    const latitudeColumn = _.findWhere(cols, {
+      name: settings["map.latitude_column"],
+    });
+    const longitudeColumn = _.findWhere(cols, {
+      name: settings["map.longitude_column"],
+    });
+
+    setCardAndRun(
+      updateLatLonFilter(card, latitudeColumn, longitudeColumn, bounds),
+    );
+
+    this.props.onFiltering(false);
+  };
+
+  render() {
+    const { className } = this.props;
+    return <div className={className} ref="map" />;
+  }
+
+  _getLatLonIndexes() {
+    const { settings, series: [{ data: { cols } }] } = this.props;
+    return {
+      latitudeIndex: _.findIndex(
+        cols,
+        col => col.name === settings["map.latitude_column"],
+      ),
+      longitudeIndex: _.findIndex(
+        cols,
+        col => col.name === settings["map.longitude_column"],
+      ),
+    };
+  }
+
+  _getLatLonColumns() {
+    const { series: [{ data: { cols } }] } = this.props;
+    const { latitudeIndex, longitudeIndex } = this._getLatLonIndexes();
+    return {
+      latitudeColumn: cols[latitudeIndex],
+      longitudeColumn: cols[longitudeIndex],
+    };
+  }
+
+  _getMetricColumn() {
+    const { settings, series: [{ data: { cols } }] } = this.props;
+    return _.findWhere(cols, { name: settings["map.metric_column"] });
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
index 3518c697998b14156dee653bb525f5181569196c..9a525e89fd2f4167ec94754346e6504cea6e197a 100644
--- a/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletMarkerPinMap.jsx
@@ -7,75 +7,79 @@ import L from "leaflet";
 import { formatValue } from "metabase/lib/formatting";
 
 const MARKER_ICON = L.icon({
-    iconUrl: "app/assets/img/pin.png",
-    iconSize: [28, 32],
-    iconAnchor: [15, 24],
-    popupAnchor: [0, -13]
+  iconUrl: "app/assets/img/pin.png",
+  iconSize: [28, 32],
+  iconAnchor: [15, 24],
+  popupAnchor: [0, -13],
 });
 
 export default class LeafletMarkerPinMap extends LeafletMap {
-    componentDidMount() {
-        super.componentDidMount();
+  componentDidMount() {
+    super.componentDidMount();
 
-        this.pinMarkerLayer = L.layerGroup([]).addTo(this.map);
-        this.componentDidUpdate({}, {});
-    }
+    this.pinMarkerLayer = L.layerGroup([]).addTo(this.map);
+    this.componentDidUpdate({}, {});
+  }
 
-    componentDidUpdate(prevProps, prevState) {
-        super.componentDidUpdate(prevProps, prevState);
+  componentDidUpdate(prevProps, prevState) {
+    super.componentDidUpdate(prevProps, prevState);
 
-        try {
-            const { pinMarkerLayer } = this;
-            const { points } = this.props;
+    try {
+      const { pinMarkerLayer } = this;
+      const { points } = this.props;
 
-            let markers = pinMarkerLayer.getLayers();
-            let max = Math.max(points.length, markers.length);
-            for (let i = 0; i < max; i++) {
-                if (i >= points.length) {
-                    pinMarkerLayer.removeLayer(markers[i]);
-                }
-                if (i >= markers.length) {
-                    const marker = this._createMarker(i);
-                    pinMarkerLayer.addLayer(marker);
-                    markers.push(marker);
-                }
+      let markers = pinMarkerLayer.getLayers();
+      let max = Math.max(points.length, markers.length);
+      for (let i = 0; i < max; i++) {
+        if (i >= points.length) {
+          pinMarkerLayer.removeLayer(markers[i]);
+        }
+        if (i >= markers.length) {
+          const marker = this._createMarker(i);
+          pinMarkerLayer.addLayer(marker);
+          markers.push(marker);
+        }
 
-                if (i < points.length) {
-                    const { lat, lng } = markers[i].getLatLng();
-                    if (lng !== points[i][0] || lat !== points[i][1]) {
-                        markers[i].setLatLng(points[i]);
-                    }
-                }
-            }
-        } catch (err) {
-            console.error(err);
-            this.props.onRenderError(err.message || err);
+        if (i < points.length) {
+          const { lat, lng } = markers[i].getLatLng();
+          if (lng !== points[i][0] || lat !== points[i][1]) {
+            markers[i].setLatLng(points[i]);
+          }
         }
+      }
+    } catch (err) {
+      console.error(err);
+      this.props.onRenderError(err.message || err);
     }
+  }
 
-    _createMarker = (index) => {
-        const marker = L.marker([0,0], { icon: MARKER_ICON });
-        marker.on("click", () => {
-            const { series: [{ data }] } = this.props;
-            const { popup } = this;
-            const el = document.createElement("div");
-            ReactDOM.render(<ObjectDetailTooltip row={data.rows[index]} cols={data.cols} />, el);
-            marker.unbindPopup();
-            marker.bindPopup(el, popup);
-            marker.openPopup();
-        });
-        return marker;
-    }
+  _createMarker = index => {
+    const marker = L.marker([0, 0], { icon: MARKER_ICON });
+    marker.on("click", () => {
+      const { series: [{ data }] } = this.props;
+      const { popup } = this;
+      const el = document.createElement("div");
+      ReactDOM.render(
+        <ObjectDetailTooltip row={data.rows[index]} cols={data.cols} />,
+        el,
+      );
+      marker.unbindPopup();
+      marker.bindPopup(el, popup);
+      marker.openPopup();
+    });
+    return marker;
+  };
 }
 
-const ObjectDetailTooltip = ({ row, cols }) =>
-    <table>
-        <tbody>
-            { cols.map((col, index) =>
-                <tr>
-                    <td className="pr1">{col.display_name}:</td>
-                    <td>{formatValue(row[index], { column: col, jsx: true })}</td>
-                </tr>
-            )}
-        </tbody>
-    </table>
+const ObjectDetailTooltip = ({ row, cols }) => (
+  <table>
+    <tbody>
+      {cols.map((col, index) => (
+        <tr>
+          <td className="pr1">{col.display_name}:</td>
+          <td>{formatValue(row[index], { column: col, jsx: true })}</td>
+        </tr>
+      ))}
+    </tbody>
+  </table>
+);
diff --git a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
index caf810183203d163048e2dcf5324cda42ce09876..7b9f65037694959701501a40b7948b5646741e06 100644
--- a/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/LeafletTilePinMap.jsx
@@ -1,44 +1,58 @@
-
 import LeafletMap from "./LeafletMap.jsx";
 import L from "leaflet";
 
 export default class LeafletTilePinMap extends LeafletMap {
-    componentDidMount() {
-        super.componentDidMount();
-
-        this.pinTileLayer = L.tileLayer("", {}).addTo(this.map);
-        this.componentDidUpdate({}, {});
-    }
-
-    componentDidUpdate(prevProps, prevState) {
-        super.componentDidUpdate(prevProps, prevState);
-
-        try {
-            const { pinTileLayer } = this;
-            const newUrl = this._getTileUrl({ x: "{x}", y: "{y}"}, "{z}");
-            if (newUrl !== pinTileLayer._url) {
-                pinTileLayer.setUrl(newUrl)
-            }
-        } catch (err) {
-            console.error(err);
-            this.props.onRenderError(err.message || err);
-        }
+  componentDidMount() {
+    super.componentDidMount();
+
+    this.pinTileLayer = L.tileLayer("", {}).addTo(this.map);
+    this.componentDidUpdate({}, {});
+  }
+
+  componentDidUpdate(prevProps, prevState) {
+    super.componentDidUpdate(prevProps, prevState);
+
+    try {
+      const { pinTileLayer } = this;
+      const newUrl = this._getTileUrl({ x: "{x}", y: "{y}" }, "{z}");
+      if (newUrl !== pinTileLayer._url) {
+        pinTileLayer.setUrl(newUrl);
+      }
+    } catch (err) {
+      console.error(err);
+      this.props.onRenderError(err.message || err);
     }
+  }
 
-    _getTileUrl = (coord, zoom) => {
-        const [{ card: { dataset_query }, data: { cols }}] = this.props.series;
-
-        const { latitudeIndex, longitudeIndex } = this._getLatLonIndexes();
-        const latitudeField = cols[latitudeIndex];
-        const longitudeField = cols[longitudeIndex];
+  _getTileUrl = (coord, zoom) => {
+    const [{ card: { dataset_query }, data: { cols } }] = this.props.series;
 
-        if (!latitudeField || !longitudeField) {
-            return;
-        }
+    const { latitudeIndex, longitudeIndex } = this._getLatLonIndexes();
+    const latitudeField = cols[latitudeIndex];
+    const longitudeField = cols[longitudeIndex];
 
-        return 'api/tiles/' + zoom + '/' + coord.x + '/' + coord.y + '/' +
-            latitudeField.id + '/' + longitudeField.id + '/' +
-            latitudeIndex + '/' + longitudeIndex + '/' +
-            '?query=' + encodeURIComponent(JSON.stringify(dataset_query))
+    if (!latitudeField || !longitudeField) {
+      return;
     }
+
+    return (
+      "api/tiles/" +
+      zoom +
+      "/" +
+      coord.x +
+      "/" +
+      coord.y +
+      "/" +
+      latitudeField.id +
+      "/" +
+      longitudeField.id +
+      "/" +
+      latitudeIndex +
+      "/" +
+      longitudeIndex +
+      "/" +
+      "?query=" +
+      encodeURIComponent(JSON.stringify(dataset_query))
+    );
+  };
 }
diff --git a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
index 2c296d640f67c174bde72cca4c2452fae1dc8479..7e14561faa19a420f720a8b27718612164e5bd6c 100644
--- a/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
+++ b/frontend/src/metabase/visualizations/components/LegacyChoropleth.jsx
@@ -4,61 +4,77 @@ import { isSameSeries } from "metabase/visualizations/lib/utils";
 import d3 from "d3";
 import cx from "classnames";
 
-const LegacyChoropleth = ({ series, geoJson, projection, getColor, onHoverFeature, onClickFeature }) => {
-    let geo = d3.geo.path()
-        .projection(projection);
+const LegacyChoropleth = ({
+  series,
+  geoJson,
+  projection,
+  getColor,
+  onHoverFeature,
+  onClickFeature,
+}) => {
+  let geo = d3.geo.path().projection(projection);
 
-    let translate = projection.translate();
-    let width = translate[0] * 2;
-    let height = translate[1] * 2;
+  let translate = projection.translate();
+  let width = translate[0] * 2;
+  let height = translate[1] * 2;
 
-    return (
-        <div className="absolute top bottom left right flex layout-centered">
-            <ShouldUpdate series={series} shouldUpdate={(props, nextProps) => !isSameSeries(props.series, nextProps.series)}>
-                { () =>  // eslint-disable-line react/display-name
-                    <svg className="flex-full m1" viewBox={`0 0 ${width} ${height}`}>
-                    {geoJson.features.map((feature, index) =>
-                        <path
-                            d={geo(feature, index)}
-                            stroke="white"
-                            strokeWidth={1}
-                            fill={getColor(feature)}
-                            onMouseMove={(e) => onHoverFeature({
-                                feature: feature,
-                                event: e.nativeEvent
-                            })}
-                            onMouseLeave={() => onHoverFeature(null)}
-                            className={cx({ "cursor-pointer": !!onClickFeature })}
-                            onClick={onClickFeature && ((e) => onClickFeature({
-                                feature: feature,
-                                event: e.nativeEvent
-                            }))}
-                        />
-                    )}
-                    </svg>
+  return (
+    <div className="absolute top bottom left right flex layout-centered">
+      <ShouldUpdate
+        series={series}
+        shouldUpdate={(props, nextProps) =>
+          !isSameSeries(props.series, nextProps.series)
+        }
+      >
+        {() => (
+          // eslint-disable-line react/display-name
+          <svg className="flex-full m1" viewBox={`0 0 ${width} ${height}`}>
+            {geoJson.features.map((feature, index) => (
+              <path
+                d={geo(feature, index)}
+                stroke="white"
+                strokeWidth={1}
+                fill={getColor(feature)}
+                onMouseMove={e =>
+                  onHoverFeature({
+                    feature: feature,
+                    event: e.nativeEvent,
+                  })
                 }
-            </ShouldUpdate>
-        </div>
-    );
-}
-
+                onMouseLeave={() => onHoverFeature(null)}
+                className={cx({ "cursor-pointer": !!onClickFeature })}
+                onClick={
+                  onClickFeature &&
+                  (e =>
+                    onClickFeature({
+                      feature: feature,
+                      event: e.nativeEvent,
+                    }))
+                }
+              />
+            ))}
+          </svg>
+        )}
+      </ShouldUpdate>
+    </div>
+  );
+};
 
 class ShouldUpdate extends Component {
-    shouldComponentUpdate(nextProps) {
-        if (nextProps.shouldUpdate) {
-            return nextProps.shouldUpdate(this.props, nextProps);
-        }
-        return true;
+  shouldComponentUpdate(nextProps) {
+    if (nextProps.shouldUpdate) {
+      return nextProps.shouldUpdate(this.props, nextProps);
     }
-    render() {
-        const { children } = this.props;
-        if (typeof children === "function") {
-            return children();
-        } else {
-            return children;
-        }
+    return true;
+  }
+  render() {
+    const { children } = this.props;
+    if (typeof children === "function") {
+      return children();
+    } else {
+      return children;
     }
+  }
 }
 
-
 export default LegacyChoropleth;
diff --git a/frontend/src/metabase/visualizations/components/Legend.css b/frontend/src/metabase/visualizations/components/Legend.css
index dfffbe9c6e03a32c244f5a20018846403d055d9f..5c18933e09c5b56971516eed80a1bcfd77890533 100644
--- a/frontend/src/metabase/visualizations/components/Legend.css
+++ b/frontend/src/metabase/visualizations/components/Legend.css
@@ -1,4 +1,3 @@
-
 /*
 legend item needs to be in the scope in order to control the font size in
 fullscreen dashboard mode
diff --git a/frontend/src/metabase/visualizations/components/LegendHeader.jsx b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
index 7f5c6e9ae92d5cfc5ca61feec821199ccdbec0c7..51f9253b50f1ad6511f1cb41df0e79085d6362d0 100644
--- a/frontend/src/metabase/visualizations/components/LegendHeader.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendHeader.jsx
@@ -13,82 +13,116 @@ import { normal } from "metabase/lib/colors";
 const DEFAULT_COLORS = Object.values(normal);
 
 export default class LegendHeader extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            width: 0
-        };
-    }
-
-    static propTypes = {
-        series: PropTypes.array.isRequired,
-        hovered: PropTypes.object,
-        onHoverChange: PropTypes.func,
-        onRemoveSeries: PropTypes.func,
-        onChangeCardAndRun: PropTypes.func,
-        actionButtons: PropTypes.node,
-        description: PropTypes.string
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      width: 0,
     };
+  }
 
-    static defaultProps = {
-        series: [],
-        settings: {},
-        visualizationIsClickable: () => false
-    };
+  static propTypes = {
+    series: PropTypes.array.isRequired,
+    hovered: PropTypes.object,
+    onHoverChange: PropTypes.func,
+    onRemoveSeries: PropTypes.func,
+    onChangeCardAndRun: PropTypes.func,
+    actionButtons: PropTypes.node,
+    description: PropTypes.string,
+  };
 
-    componentDidMount() {
-        this.componentDidUpdate();
-    }
+  static defaultProps = {
+    series: [],
+    settings: {},
+    visualizationIsClickable: () => false,
+  };
+
+  componentDidMount() {
+    this.componentDidUpdate();
+  }
 
-    componentDidUpdate() {
-        let width = ReactDOM.findDOMNode(this).offsetWidth;
-        if (width !== this.state.width) {
-            this.setState({ width });
-        }
+  componentDidUpdate() {
+    let width = ReactDOM.findDOMNode(this).offsetWidth;
+    if (width !== this.state.width) {
+      this.setState({ width });
     }
+  }
 
-    render() {
-        const { series, hovered, onRemoveSeries, actionButtons, onHoverChange, onChangeCardAndRun, settings, description, onVisualizationClick, visualizationIsClickable } = this.props;
-        const showDots     = series.length > 1;
-        const isNarrow     = this.state.width < 150;
-        const showTitles   = !showDots || !isNarrow;
-        const colors       = settings["graph.colors"] || DEFAULT_COLORS;
-        const customTitles = settings["graph.series_labels"];
-        const titles       = (customTitles && customTitles.length === series.length) ? customTitles : series.map((thisSeries) => thisSeries.card.name);
+  render() {
+    const {
+      series,
+      hovered,
+      onRemoveSeries,
+      actionButtons,
+      onHoverChange,
+      onChangeCardAndRun,
+      settings,
+      description,
+      onVisualizationClick,
+      visualizationIsClickable,
+    } = this.props;
+    const showDots = series.length > 1;
+    const isNarrow = this.state.width < 150;
+    const showTitles = !showDots || !isNarrow;
+    const colors = settings["graph.colors"] || DEFAULT_COLORS;
+    const customTitles = settings["graph.series_labels"];
+    const titles =
+      customTitles && customTitles.length === series.length
+        ? customTitles
+        : series.map(thisSeries => thisSeries.card.name);
 
-        return (
-            <div  className={cx(styles.LegendHeader, "Card-title mx1 flex flex-no-shrink flex-row align-center")}>
-                { series.map((s, index) => [
-                    <LegendItem
-                        key={index}
-                        title={titles[index]}
-                        description={description}
-                        color={colors[index % colors.length]}
-                        showDot={showDots}
-                        showTitle={showTitles}
-                        isMuted={hovered && hovered.index != null && index !== hovered.index}
-                        onMouseEnter={() => onHoverChange && onHoverChange({ index })}
-                        onMouseLeave={() => onHoverChange && onHoverChange(null) }
-                        onClick={s.clicked && visualizationIsClickable(s.clicked) ?
-                            ((e) => onVisualizationClick({ ...s.clicked, element: e.currentTarget }))
-                        : onChangeCardAndRun ?
-                            (() => onChangeCardAndRun({ nextCard: s.card, seriesIndex: index }))
-                        : null }
-                    />,
-                    onRemoveSeries && index > 0 &&
-                      <Icon
-                          name="close"
-                          className="text-grey-2 flex-no-shrink mr1 cursor-pointer"
-                          width={12} height={12}
-                          onClick={() => onRemoveSeries(s.card)}
-                      />
-                ])}
-                { actionButtons &&
-                  <span className="flex-no-shrink flex-align-right relative">
-                      {actionButtons}
-                  </span>
-                }
-            </div>
-        );
-    }
+    return (
+      <div
+        className={cx(
+          styles.LegendHeader,
+          "Card-title mx1 flex flex-no-shrink flex-row align-center",
+        )}
+      >
+        {series.map((s, index) => [
+          <LegendItem
+            key={index}
+            title={titles[index]}
+            description={description}
+            color={colors[index % colors.length]}
+            showDot={showDots}
+            showTitle={showTitles}
+            isMuted={
+              hovered && hovered.index != null && index !== hovered.index
+            }
+            onMouseEnter={() => onHoverChange && onHoverChange({ index })}
+            onMouseLeave={() => onHoverChange && onHoverChange(null)}
+            onClick={
+              s.clicked && visualizationIsClickable(s.clicked)
+                ? e =>
+                    onVisualizationClick({
+                      ...s.clicked,
+                      element: e.currentTarget,
+                    })
+                : onChangeCardAndRun
+                  ? () =>
+                      onChangeCardAndRun({
+                        nextCard: s.card,
+                        seriesIndex: index,
+                      })
+                  : null
+            }
+          />,
+          onRemoveSeries &&
+            index > 0 && (
+              <Icon
+                name="close"
+                className="text-grey-2 flex-no-shrink mr1 cursor-pointer"
+                width={12}
+                height={12}
+                onClick={() => onRemoveSeries(s.card)}
+              />
+            ),
+        ])}
+        {actionButtons && (
+          <span className="flex-no-shrink flex-align-right relative">
+            {actionButtons}
+          </span>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx b/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx
index 10600a508438ebd76db5a0329210b1ce1d1cb77b..2594968b122c49c8d961370d0e73f70f0c47c0bf 100644
--- a/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendHorizontal.jsx
@@ -7,27 +7,37 @@ import LegendItem from "./LegendItem.jsx";
 import cx from "classnames";
 
 export default class LegendHorizontal extends Component {
-    render() {
-        const { className, titles, colors, hovered, onHoverChange } = this.props;
-        return (
-            <ol ref="container" className={cx(className, styles.Legend, styles.horizontal)}>
-                {titles.map((title, index) =>
-                    <li key={index}>
-                        <LegendItem
-                            ref={"legendItem"+index}
-                            title={title}
-                            color={colors[index % colors.length]}
-                            isMuted={hovered && hovered.index != null && index !== hovered.index}
-                            showTooltip={false}
-                            onMouseEnter={() => onHoverChange && onHoverChange({
-                                index,
-                                element: ReactDOM.findDOMNode(this.refs["legendItem"+index])
-                            })}
-                            onMouseLeave={() => onHoverChange && onHoverChange(null) }
-                        />
-                    </li>
-                )}
-            </ol>
-        );
-    }
+  render() {
+    const { className, titles, colors, hovered, onHoverChange } = this.props;
+    return (
+      <ol
+        ref="container"
+        className={cx(className, styles.Legend, styles.horizontal)}
+      >
+        {titles.map((title, index) => (
+          <li key={index}>
+            <LegendItem
+              ref={"legendItem" + index}
+              title={title}
+              color={colors[index % colors.length]}
+              isMuted={
+                hovered && hovered.index != null && index !== hovered.index
+              }
+              showTooltip={false}
+              onMouseEnter={() =>
+                onHoverChange &&
+                onHoverChange({
+                  index,
+                  element: ReactDOM.findDOMNode(
+                    this.refs["legendItem" + index],
+                  ),
+                })
+              }
+              onMouseLeave={() => onHoverChange && onHoverChange(null)}
+            />
+          </li>
+        ))}
+      </ol>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LegendItem.jsx b/frontend/src/metabase/visualizations/components/LegendItem.jsx
index 65965db18c88de3ebe09d0231ac885b248ae4de4..4edacdabbbb1d8d032452380fc6d27a883563910 100644
--- a/frontend/src/metabase/visualizations/components/LegendItem.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendItem.jsx
@@ -7,60 +7,85 @@ import Ellipsified from "metabase/components/Ellipsified.jsx";
 import cx from "classnames";
 
 // Don't use a <a> tag if there's no href
-const LegendLink = (props) =>
-    props.href ? <a {...props} /> : <span {...props} />
+const LegendLink = props =>
+  props.href ? <a {...props} /> : <span {...props} />;
 
 export default class LegendItem extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {};
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {};
+  }
 
-    static propTypes = {};
-    static defaultProps = {
-        showDot: true,
-        showTitle: true,
-        isMuted: false,
-        showTooltip: true,
-        showDotTooltip: true
-    };
+  static propTypes = {};
+  static defaultProps = {
+    showDot: true,
+    showTitle: true,
+    isMuted: false,
+    showTooltip: true,
+    showDotTooltip: true,
+  };
 
-    render() {
-        const { title, color, showDot, showTitle, isMuted, showTooltip, showDotTooltip, onMouseEnter, onMouseLeave, className, description, onClick } = this.props;
-        return (
-            <LegendLink
-                className={cx(className, "LegendItem", "no-decoration flex align-center fullscreen-normal-text fullscreen-night-text", {
-                    mr1: showTitle,
-                    muted: isMuted,
-                    "cursor-pointer": onClick
-                })}
-                style={{ overflowX: "hidden", flex: "0 1 auto" }}
-                onMouseEnter={onMouseEnter}
-                onMouseLeave={onMouseLeave}
-                onClick={onClick}
-            >
-                { showDot &&
-                    <Tooltip tooltip={title} isEnabled={showTooltip && showDotTooltip}>
-                        <div
-                            className={cx("flex-no-shrink", "inline-block circular")}
-                            style={{width: 13, height: 13, margin: 4, marginRight: 8, backgroundColor: color }}
-                        />
-                    </Tooltip>
-                }
-                { showTitle &&
-                  <div className="flex align-center">
-                      <span className="mr1"><Ellipsified showTooltip={showTooltip}>{title}</Ellipsified></span>
-                      { description &&
-                          <div className="hover-child">
-                              <Tooltip tooltip={description} maxWidth={'22em'}>
-                                  <Icon name='info' />
-                              </Tooltip>
-                          </div>
-                      }
-                  </div>
-                }
-
-            </LegendLink>
-        );
-    }
+  render() {
+    const {
+      title,
+      color,
+      showDot,
+      showTitle,
+      isMuted,
+      showTooltip,
+      showDotTooltip,
+      onMouseEnter,
+      onMouseLeave,
+      className,
+      description,
+      onClick,
+    } = this.props;
+    return (
+      <LegendLink
+        className={cx(
+          className,
+          "LegendItem",
+          "no-decoration flex align-center fullscreen-normal-text fullscreen-night-text",
+          {
+            mr1: showTitle,
+            muted: isMuted,
+            "cursor-pointer": onClick,
+          },
+        )}
+        style={{ overflowX: "hidden", flex: "0 1 auto" }}
+        onMouseEnter={onMouseEnter}
+        onMouseLeave={onMouseLeave}
+        onClick={onClick}
+      >
+        {showDot && (
+          <Tooltip tooltip={title} isEnabled={showTooltip && showDotTooltip}>
+            <div
+              className={cx("flex-no-shrink", "inline-block circular")}
+              style={{
+                width: 13,
+                height: 13,
+                margin: 4,
+                marginRight: 8,
+                backgroundColor: color,
+              }}
+            />
+          </Tooltip>
+        )}
+        {showTitle && (
+          <div className="flex align-center">
+            <span className="mr1">
+              <Ellipsified showTooltip={showTooltip}>{title}</Ellipsified>
+            </span>
+            {description && (
+              <div className="hover-child">
+                <Tooltip tooltip={description} maxWidth={"22em"}>
+                  <Icon name="info" />
+                </Tooltip>
+              </div>
+            )}
+          </div>
+        )}
+      </LegendLink>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LegendVertical.jsx b/frontend/src/metabase/visualizations/components/LegendVertical.jsx
index 41b027628bcf1a6aae95f4e9049b31e3a73d24b8..c3230164b429fe73021fa50eab5072527b50829e 100644
--- a/frontend/src/metabase/visualizations/components/LegendVertical.jsx
+++ b/frontend/src/metabase/visualizations/components/LegendVertical.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./Legend.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Tooltip from "metabase/components/Tooltip.jsx";
 
 import LegendItem from "./LegendItem.jsx";
@@ -9,87 +9,114 @@ import LegendItem from "./LegendItem.jsx";
 import cx from "classnames";
 
 export default class LegendVertical extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            overflowCount: 0,
-            size: null
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      overflowCount: 0,
+      size: null,
+    };
+  }
 
-    static propTypes = {};
-    static defaultProps = {};
+  static propTypes = {};
+  static defaultProps = {};
 
-    componentDidUpdate() {
-        // Get the bounding rectangle of the chart widget to determine if
-        // legend items will overflow the widget area
-        let size = ReactDOM.findDOMNode(this).getBoundingClientRect();
+  componentDidUpdate() {
+    // Get the bounding rectangle of the chart widget to determine if
+    // legend items will overflow the widget area
+    let size = ReactDOM.findDOMNode(this).getBoundingClientRect();
 
-        // only check the height. width may flucatuate depending on the browser causing an infinite loop
-        if (this.state.size && size.height !== this.state.size.height) {
-            this.setState({ overflowCount: 0, size });
-        } else if (this.state.overflowCount === 0) {
-            let overflowCount = 0;
-            for (var i = 0; i < this.props.titles.length; i++) {
-                let itemSize = ReactDOM.findDOMNode(this.refs["item"+i]).getBoundingClientRect();
-                if (size.top > itemSize.top || size.bottom < itemSize.bottom) {
-                    overflowCount++;
-                }
-            }
-            if (this.state.overflowCount !== overflowCount) {
-                this.setState({ overflowCount, size });
-            }
+    // only check the height. width may flucatuate depending on the browser causing an infinite loop
+    if (this.state.size && size.height !== this.state.size.height) {
+      this.setState({ overflowCount: 0, size });
+    } else if (this.state.overflowCount === 0) {
+      let overflowCount = 0;
+      for (var i = 0; i < this.props.titles.length; i++) {
+        let itemSize = ReactDOM.findDOMNode(
+          this.refs["item" + i],
+        ).getBoundingClientRect();
+        if (size.top > itemSize.top || size.bottom < itemSize.bottom) {
+          overflowCount++;
         }
+      }
+      if (this.state.overflowCount !== overflowCount) {
+        this.setState({ overflowCount, size });
+      }
     }
+  }
 
-    render() {
-        const { className, titles, colors, hovered, onHoverChange } = this.props;
-        const { overflowCount } = this.state;
-        let items, extraItems, extraColors;
-        if (overflowCount > 0) {
-            items = titles.slice(0, -overflowCount - 1);
-            extraItems = titles.slice(-overflowCount - 1);
-            extraColors = colors.slice(-overflowCount - 1).concat(colors.slice(0, -overflowCount - 1));
-        } else {
-            items = titles;
-        }
-        return (
-            <ol ref="container" className={cx(className, styles.Legend, styles.vertical)}>
-                {items.map((title, index) =>
-                    <li
-                        key={index}
-                        ref={"item"+index}
-                        className="flex flex-no-shrink"
-                        onMouseEnter={(e) => onHoverChange && onHoverChange({
-                            index,
-                            element: ReactDOM.findDOMNode(this.refs["legendItem"+index])
-                        })}
-                        onMouseLeave={(e) => onHoverChange && onHoverChange()}
-                    >
-                        <LegendItem
-                            ref={"legendItem"+index}
-                            title={Array.isArray(title) ? title[0] : title}
-                            color={colors[index % colors.length]}
-                            isMuted={hovered && hovered.index != null && index !== hovered.index}
-                            showTooltip={false}
-                        />
-                        { Array.isArray(title) &&
-                            <span className={cx("LegendItem","flex-align-right pl1", { muted: hovered && hovered.index != null && index !== hovered.index })}>{title[1]}</span>
-                        }
-                    </li>
-                )}
-                {overflowCount > 0 ?
-                    <li key="extra" className="flex flex-no-shrink" >
-                        <Tooltip tooltip={<LegendVertical className="p2" titles={extraItems} colors={extraColors} />}>
-                            <LegendItem
-                                title={(overflowCount + 1) + " " + t`more`}
-                                color="gray"
-                                showTooltip={false}
-                            />
-                        </Tooltip>
-                    </li>
-                : null }
-            </ol>
-        );
+  render() {
+    const { className, titles, colors, hovered, onHoverChange } = this.props;
+    const { overflowCount } = this.state;
+    let items, extraItems, extraColors;
+    if (overflowCount > 0) {
+      items = titles.slice(0, -overflowCount - 1);
+      extraItems = titles.slice(-overflowCount - 1);
+      extraColors = colors
+        .slice(-overflowCount - 1)
+        .concat(colors.slice(0, -overflowCount - 1));
+    } else {
+      items = titles;
     }
+    return (
+      <ol
+        ref="container"
+        className={cx(className, styles.Legend, styles.vertical)}
+      >
+        {items.map((title, index) => (
+          <li
+            key={index}
+            ref={"item" + index}
+            className="flex flex-no-shrink"
+            onMouseEnter={e =>
+              onHoverChange &&
+              onHoverChange({
+                index,
+                element: ReactDOM.findDOMNode(this.refs["legendItem" + index]),
+              })
+            }
+            onMouseLeave={e => onHoverChange && onHoverChange()}
+          >
+            <LegendItem
+              ref={"legendItem" + index}
+              title={Array.isArray(title) ? title[0] : title}
+              color={colors[index % colors.length]}
+              isMuted={
+                hovered && hovered.index != null && index !== hovered.index
+              }
+              showTooltip={false}
+            />
+            {Array.isArray(title) && (
+              <span
+                className={cx("LegendItem", "flex-align-right pl1", {
+                  muted:
+                    hovered && hovered.index != null && index !== hovered.index,
+                })}
+              >
+                {title[1]}
+              </span>
+            )}
+          </li>
+        ))}
+        {overflowCount > 0 ? (
+          <li key="extra" className="flex flex-no-shrink">
+            <Tooltip
+              tooltip={
+                <LegendVertical
+                  className="p2"
+                  titles={extraItems}
+                  colors={extraColors}
+                />
+              }
+            >
+              <LegendItem
+                title={overflowCount + 1 + " " + t`more`}
+                color="gray"
+                showTooltip={false}
+              />
+            </Tooltip>
+          </li>
+        ) : null}
+      </ol>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.css b/frontend/src/metabase/visualizations/components/LineAreaBarChart.css
index 9b50a126147423079016174c76fa1cb4aa180658..689a80609c43a0e2857c829d494878f764361a50 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.css
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.css
@@ -8,7 +8,7 @@
 
 .LineAreaBarChart .dc-chart .grid-line.horizontal {
   stroke: rgba(151, 151, 151, 0.2);
-  stroke-dasharray: 5,5;
+  stroke-dasharray: 5, 5;
 }
 
 .LineAreaBarChart .dc-chart .axis {
@@ -23,15 +23,15 @@
 
 .LineAreaBarChart .dc-chart .axis .domain,
 .LineAreaBarChart .dc-chart .axis .tick line {
-  stroke: #DCE1E4;
+  stroke: #dce1e4;
 }
 
 .LineAreaBarChart .dc-chart .axis .tick text {
-  fill: #93A1AB;
+  fill: #93a1ab;
 }
 
 .LineAreaBarChart .dc-chart g.row text.outside {
-  fill: #C5C6C8;
+  fill: #c5c6c8;
 }
 .LineAreaBarChart .dc-chart g.row text.inside {
   fill: white;
@@ -97,9 +97,9 @@
 }
 
 .LineAreaBarChart .dc-chart circle.bubble {
-    fill-opacity: 0.80;
-    stroke-width: 1;
-    stroke: white;
+  fill-opacity: 0.8;
+  stroke-width: 1;
+  stroke: white;
 }
 
 .LineAreaBarChart .dc-chart .enable-dots .dc-tooltip .dot:hover,
@@ -168,6 +168,6 @@
 
 /* brush handles */
 .LineAreaBarChart .dc-chart .brush .resize path {
-  fill: #F9FBFC;
-  stroke: #9BA5B1;
+  fill: #f9fbfc;
+  stroke: #9ba5b1;
 }
diff --git a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
index 0d7c4a910935757aa0ec86c8af670388569b45c5..b6c22eb1bc38a81f74f57abf277e286428a24dc6 100644
--- a/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
+++ b/frontend/src/metabase/visualizations/components/LineAreaBarChart.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import PropTypes from "prop-types";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import CardRenderer from "./CardRenderer.jsx";
 import LegendHeader from "./LegendHeader.jsx";
 import { TitleLegendHeader } from "./TitleLegendHeader.jsx";
@@ -11,329 +11,407 @@ import "./LineAreaBarChart.css";
 
 import { isNumeric, isDate } from "metabase/lib/schema_metadata";
 import {
-    getChartTypeFromData,
-    getFriendlyName
+  getChartTypeFromData,
+  getFriendlyName,
 } from "metabase/visualizations/lib/utils";
 import { addCSSRule } from "metabase/lib/dom";
 import { formatValue } from "metabase/lib/formatting";
 
 import { getSettings } from "metabase/visualizations/lib/settings";
 
-import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
+import {
+  MinRowsError,
+  ChartSettingsError,
+} from "metabase/visualizations/lib/errors";
 
 import _ from "underscore";
 import cx from "classnames";
 
 const MAX_SERIES = 20;
 
-const MUTE_STYLE = "opacity: 0.25;"
+const MUTE_STYLE = "opacity: 0.25;";
 for (let i = 0; i < MAX_SERIES; i++) {
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .area`,       MUTE_STYLE);
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .line`,       MUTE_STYLE);
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .bar`,        MUTE_STYLE);
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg.stacked .dc-tooltip._${i} .dot`,   MUTE_STYLE);
-
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .bar`,    MUTE_STYLE);
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .line`,   MUTE_STYLE);
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .dot`,    MUTE_STYLE);
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .bubble`, MUTE_STYLE);
-
-    // row charts don't support multiseries
-    addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .row`, MUTE_STYLE);
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .area`,
+    MUTE_STYLE,
+  );
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .line`,
+    MUTE_STYLE,
+  );
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg.stacked .stack._${i} .bar`,
+    MUTE_STYLE,
+  );
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg.stacked .dc-tooltip._${i} .dot`,
+    MUTE_STYLE,
+  );
+
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .bar`,
+    MUTE_STYLE,
+  );
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .line`,
+    MUTE_STYLE,
+  );
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .dot`,
+    MUTE_STYLE,
+  );
+  addCSSRule(
+    `.LineAreaBarChart.mute-${i} svg:not(.stacked) .sub._${i} .bubble`,
+    MUTE_STYLE,
+  );
+
+  // row charts don't support multiseries
+  addCSSRule(`.LineAreaBarChart.mute-${i} svg:not(.stacked) .row`, MUTE_STYLE);
 }
 
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class LineAreaBarChart extends Component {
-    props: VisualizationProps;
+  props: VisualizationProps;
 
-    static identifier: string;
-    static renderer: (element: Element, props: VisualizationProps) => any;
+  static identifier: string;
+  static renderer: (element: Element, props: VisualizationProps) => any;
 
-    static noHeader = true;
-    static supportsSeries = true;
+  static noHeader = true;
+  static supportsSeries = true;
 
-    static minSize = { width: 4, height: 3 };
+  static minSize = { width: 4, height: 3 };
 
-    static isSensible(cols, rows) {
-        return getChartTypeFromData(cols, rows, false) != null;
-    }
+  static isSensible(cols, rows) {
+    return getChartTypeFromData(cols, rows, false) != null;
+  }
 
-    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) {
-            throw new ChartSettingsError(t`Which fields do you want to use for the X and Y axes?`, t`Data`, t`Choose fields`);
-        }
+  static checkRenderable(series, settings) {
+    const singleSeriesHasNoRows = ({ data: { cols, rows } }) => rows.length < 1;
+    if (_.every(series, singleSeriesHasNoRows)) {
+      throw new MinRowsError(1, 0);
     }
 
-    static seriesAreCompatible(initialSeries, newSeries) {
-        let initialSettings = getSettings([initialSeries]);
-        let newSettings = getSettings([newSeries]);
-
-        let initialDimensions = getColumnsFromNames(initialSeries.data.cols, initialSettings["graph.dimensions"]);
-        let newDimensions = getColumnsFromNames(newSeries.data.cols, newSettings["graph.dimensions"]);
-        let newMetrics = getColumnsFromNames(newSeries.data.cols, newSettings["graph.metrics"]);
-
-        // must have at least one dimension and one metric
-        if (newDimensions.length === 0 || newMetrics.length === 0) {
-            return false;
-        }
-
-        // all metrics must be numeric
-        if (!_.all(newMetrics, isNumeric)) {
-            return false;
-        }
-
-        // both or neither primary dimension must be dates
-        if (isDate(initialDimensions[0]) !== isDate(newDimensions[0])) {
-            return false;
-        }
-
-        // both or neither primary dimension must be numeric
-        // a timestamp field is both date and number so don't enforce the condition if both fields are dates; see #2811
-        if ((isNumeric(initialDimensions[0]) !== isNumeric(newDimensions[0])) &&
-            !(isDate(initialDimensions[0]) && isDate(newDimensions[0]))) {
-            return false;
-        }
-
-        return true;
+    const dimensions = (settings["graph.dimensions"] || []).filter(
+      name => name,
+    );
+    const metrics = (settings["graph.metrics"] || []).filter(name => name);
+    if (dimensions.length < 1 || metrics.length < 1) {
+      throw new ChartSettingsError(
+        t`Which fields do you want to use for the X and Y axes?`,
+        t`Data`,
+        t`Choose fields`,
+      );
     }
+  }
+
+  static seriesAreCompatible(initialSeries, newSeries) {
+    let initialSettings = getSettings([initialSeries]);
+    let newSettings = getSettings([newSeries]);
+
+    let initialDimensions = getColumnsFromNames(
+      initialSeries.data.cols,
+      initialSettings["graph.dimensions"],
+    );
+    let newDimensions = getColumnsFromNames(
+      newSeries.data.cols,
+      newSettings["graph.dimensions"],
+    );
+    let newMetrics = getColumnsFromNames(
+      newSeries.data.cols,
+      newSettings["graph.metrics"],
+    );
 
-    static transformSeries(series) {
-        let newSeries = [].concat(...series.map((s, seriesIndex) => transformSingleSeries(s, series, seriesIndex)));
-        if (_.isEqual(series, newSeries) || newSeries.length === 0) {
-            return series;
-        } else {
-            return newSeries;
-        }
+    // must have at least one dimension and one metric
+    if (newDimensions.length === 0 || newMetrics.length === 0) {
+      return false;
     }
 
-    static propTypes = {
-        series: PropTypes.array.isRequired,
-        actionButtons: PropTypes.node,
-        showTitle: PropTypes.bool,
-        isDashboard: PropTypes.bool
-    };
+    // all metrics must be numeric
+    if (!_.all(newMetrics, isNumeric)) {
+      return false;
+    }
 
-    static defaultProps = {
-    };
+    // both or neither primary dimension must be dates
+    if (isDate(initialDimensions[0]) !== isDate(newDimensions[0])) {
+      return false;
+    }
 
-    getHoverClasses() {
-        const { hovered } = this.props;
-        if (hovered && hovered.index != null) {
-            let seriesClasses = _.range(0, MAX_SERIES).filter(n => n !== hovered.index).map(n => "mute-"+n);
-            let axisClasses =
-                hovered.axisIndex === 0 ? "mute-yr" :
-                hovered.axisIndex === 1 ? "mute-yl" :
-                null;
-            return seriesClasses.concat(axisClasses);
-        } else {
-            return null;
-        }
+    // both or neither primary dimension must be numeric
+    // a timestamp field is both date and number so don't enforce the condition if both fields are dates; see #2811
+    if (
+      isNumeric(initialDimensions[0]) !== isNumeric(newDimensions[0]) &&
+      !(isDate(initialDimensions[0]) && isDate(newDimensions[0]))
+    ) {
+      return false;
     }
 
-    getFidelity() {
-        let fidelity = { x: 0, y: 0 };
-        let size = this.props.gridSize ||  { width: Infinity, height: Infinity };
-        if (size.width >= 5) {
-            fidelity.x = 2;
-        } else if (size.width >= 4) {
-            fidelity.x = 1;
-        }
-        if (size.height >= 5) {
-            fidelity.y = 2;
-        } else if (size.height >= 4) {
-            fidelity.y = 1;
-        }
-
-        return fidelity;
+    return true;
+  }
+
+  static transformSeries(series) {
+    let newSeries = [].concat(
+      ...series.map((s, seriesIndex) =>
+        transformSingleSeries(s, series, seriesIndex),
+      ),
+    );
+    if (_.isEqual(series, newSeries) || newSeries.length === 0) {
+      return series;
+    } else {
+      return newSeries;
+    }
+  }
+
+  static propTypes = {
+    series: PropTypes.array.isRequired,
+    actionButtons: PropTypes.node,
+    showTitle: PropTypes.bool,
+    isDashboard: PropTypes.bool,
+  };
+
+  static defaultProps = {};
+
+  getHoverClasses() {
+    const { hovered } = this.props;
+    if (hovered && hovered.index != null) {
+      let seriesClasses = _.range(0, MAX_SERIES)
+        .filter(n => n !== hovered.index)
+        .map(n => "mute-" + n);
+      let axisClasses =
+        hovered.axisIndex === 0
+          ? "mute-yr"
+          : hovered.axisIndex === 1 ? "mute-yl" : null;
+      return seriesClasses.concat(axisClasses);
+    } else {
+      return null;
+    }
+  }
+
+  getFidelity() {
+    let fidelity = { x: 0, y: 0 };
+    let size = this.props.gridSize || { width: Infinity, height: Infinity };
+    if (size.width >= 5) {
+      fidelity.x = 2;
+    } else if (size.width >= 4) {
+      fidelity.x = 1;
+    }
+    if (size.height >= 5) {
+      fidelity.y = 2;
+    } else if (size.height >= 4) {
+      fidelity.y = 1;
     }
 
-    getSettings() {
-        let fidelity = this.getFidelity();
+    return fidelity;
+  }
 
-        let settings = { ...this.props.settings };
+  getSettings() {
+    let fidelity = this.getFidelity();
 
-        // smooth interpolation at smallest x/y fidelity
-        if (fidelity.x === 0 && fidelity.y === 0) {
-            settings["line.interpolate"] = "cardinal";
-        }
+    let settings = { ...this.props.settings };
 
-        // no axis in < 1 fidelity
-        if (fidelity.x < 1 || fidelity.y < 1) {
-            settings["graph.y_axis.axis_enabled"] = false;
-        }
+    // smooth interpolation at smallest x/y fidelity
+    if (fidelity.x === 0 && fidelity.y === 0) {
+      settings["line.interpolate"] = "cardinal";
+    }
 
-        // no labels in < 2 fidelity
-        if (fidelity.x < 2 || fidelity.y < 2) {
-            settings["graph.y_axis.labels_enabled"] = false;
-        }
+    // no axis in < 1 fidelity
+    if (fidelity.x < 1 || fidelity.y < 1) {
+      settings["graph.y_axis.axis_enabled"] = false;
+    }
 
-        return settings;
+    // no labels in < 2 fidelity
+    if (fidelity.x < 2 || fidelity.y < 2) {
+      settings["graph.y_axis.labels_enabled"] = false;
     }
 
-    render() {
-        const { series, hovered, showTitle, actionButtons, onChangeCardAndRun, onVisualizationClick, visualizationIsClickable } = this.props;
-
-        const settings = this.getSettings();
-
-        let multiseriesHeaderSeries;
-        if (series.length > 1) {
-            multiseriesHeaderSeries = series;
-        }
-
-        const hasTitle = showTitle && settings["card.title"];
-
-        return (
-            <div className={cx("LineAreaBarChart flex flex-column p1", this.getHoverClasses(), this.props.className)}>
-                { hasTitle &&
-                    <TitleLegendHeader
-                        series={series}
-                        settings={settings}
-                        onChangeCardAndRun={onChangeCardAndRun}
-                        actionButtons={actionButtons}
-                    />
-                }
-                { multiseriesHeaderSeries || (!hasTitle && actionButtons) ? // always show action buttons if we have them
-                    <LegendHeader
-                        className="flex-no-shrink"
-                        series={multiseriesHeaderSeries}
-                        settings={settings}
-                        hovered={hovered}
-                        onHoverChange={this.props.onHoverChange}
-                        actionButtons={!hasTitle ? actionButtons : null}
-                        onChangeCardAndRun={onChangeCardAndRun}
-                        onVisualizationClick={onVisualizationClick}
-                        visualizationIsClickable={visualizationIsClickable}
-                    />
-                : null }
-                <CardRenderer
-                    {...this.props}
-                    series={series}
-                    settings={settings}
-                    className="renderer flex-full"
-                    maxSeries={MAX_SERIES}
-                    renderer={this.constructor.renderer}
-                />
-            </div>
-        );
+    return settings;
+  }
+
+  render() {
+    const {
+      series,
+      hovered,
+      showTitle,
+      actionButtons,
+      onChangeCardAndRun,
+      onVisualizationClick,
+      visualizationIsClickable,
+    } = this.props;
+
+    const settings = this.getSettings();
+
+    let multiseriesHeaderSeries;
+    if (series.length > 1) {
+      multiseriesHeaderSeries = series;
     }
+
+    const hasTitle = showTitle && settings["card.title"];
+
+    return (
+      <div
+        className={cx(
+          "LineAreaBarChart flex flex-column p1",
+          this.getHoverClasses(),
+          this.props.className,
+        )}
+      >
+        {hasTitle && (
+          <TitleLegendHeader
+            series={series}
+            settings={settings}
+            onChangeCardAndRun={onChangeCardAndRun}
+            actionButtons={actionButtons}
+          />
+        )}
+        {multiseriesHeaderSeries || (!hasTitle && actionButtons) ? ( // always show action buttons if we have them
+          <LegendHeader
+            className="flex-no-shrink"
+            series={multiseriesHeaderSeries}
+            settings={settings}
+            hovered={hovered}
+            onHoverChange={this.props.onHoverChange}
+            actionButtons={!hasTitle ? actionButtons : null}
+            onChangeCardAndRun={onChangeCardAndRun}
+            onVisualizationClick={onVisualizationClick}
+            visualizationIsClickable={visualizationIsClickable}
+          />
+        ) : null}
+        <CardRenderer
+          {...this.props}
+          series={series}
+          settings={settings}
+          className="renderer flex-full"
+          maxSeries={MAX_SERIES}
+          renderer={this.constructor.renderer}
+        />
+      </div>
+    );
+  }
 }
 
 function getColumnsFromNames(cols, names) {
-    if (!names) {
-        return [];
-    }
-    return names.map(name => _.findWhere(cols, { name }));
+  if (!names) {
+    return [];
+  }
+  return names.map(name => _.findWhere(cols, { name }));
 }
 
 function transformSingleSeries(s, series, seriesIndex) {
-    const { card, data } = s;
+  const { card, data } = s;
+
+  // HACK: prevents cards from being transformed too many times
+  if (card._transformed) {
+    return [s];
+  }
+
+  const { cols, rows } = data;
+  const settings = getSettings([s]);
+
+  const dimensions = settings["graph.dimensions"].filter(d => d != null);
+  const metrics = settings["graph.metrics"].filter(d => d != null);
+  const dimensionColumnIndexes = dimensions.map(dimensionName =>
+    _.findIndex(cols, col => col.name === dimensionName),
+  );
+  const metricColumnIndexes = metrics.map(metricName =>
+    _.findIndex(cols, col => col.name === metricName),
+  );
+  const bubbleColumnIndex =
+    settings["scatter.bubble"] &&
+    _.findIndex(cols, col => col.name === settings["scatter.bubble"]);
+  const extraColumnIndexes =
+    bubbleColumnIndex && bubbleColumnIndex >= 0 ? [bubbleColumnIndex] : [];
+
+  if (dimensions.length > 1) {
+    const [dimensionColumnIndex, seriesColumnIndex] = dimensionColumnIndexes;
+    const rowColumnIndexes = [dimensionColumnIndex].concat(
+      metricColumnIndexes,
+      extraColumnIndexes,
+    );
 
-    // HACK: prevents cards from being transformed too many times
-    if (card._transformed) {
-        return [s];
-    }
+    const breakoutValues = [];
+    const breakoutRowsByValue = new Map();
 
-    const { cols, rows } = data;
-    const settings = getSettings([s]);
+    for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
+      const row = rows[rowIndex];
+      const seriesValue = row[seriesColumnIndex];
 
-    const dimensions = settings["graph.dimensions"].filter(d => d != null);
-    const metrics = settings["graph.metrics"].filter(d => d != null);
-    const dimensionColumnIndexes = dimensions.map(dimensionName =>
-        _.findIndex(cols, (col) => col.name === dimensionName)
-    );
-    const metricColumnIndexes = metrics.map(metricName =>
-        _.findIndex(cols, (col) => col.name === metricName)
-    );
-    const bubbleColumnIndex = settings["scatter.bubble"] && _.findIndex(cols, (col) => col.name === settings["scatter.bubble"]);
-    const extraColumnIndexes = bubbleColumnIndex && bubbleColumnIndex >= 0 ? [bubbleColumnIndex] : [];
-
-    if (dimensions.length > 1) {
-        const [dimensionColumnIndex, seriesColumnIndex] = dimensionColumnIndexes;
-        const rowColumnIndexes = [dimensionColumnIndex].concat(metricColumnIndexes, extraColumnIndexes);
-
-        const breakoutValues = [];
-        const breakoutRowsByValue = new Map;
-
-        for (let rowIndex = 0; rowIndex < rows.length; rowIndex++) {
-            const row = rows[rowIndex];
-            const seriesValue = row[seriesColumnIndex];
-
-            let seriesRows = breakoutRowsByValue.get(seriesValue);
-            if (!seriesRows) {
-                breakoutRowsByValue.set(seriesValue, seriesRows = []);
-                breakoutValues.push(seriesValue);
-            }
-
-            let newRow = rowColumnIndexes.map(columnIndex => row[columnIndex]);
-            // $FlowFixMe: _origin not typed
-            newRow._origin = { seriesIndex, rowIndex, row, cols };
-            seriesRows.push(newRow);
-        }
-
-        return breakoutValues.map((breakoutValue) => ({
-            card: {
-                ...card,
-                // if multiseries include the card title as well as the breakout value
-                name: [
-                    // show series title if it's multiseries
-                    series.length > 1 && card.name,
-                    // always show grouping value
-                    formatValue(breakoutValue, { column: cols[seriesColumnIndex] })
-                ].filter(n => n).join(": "),
-                _transformed: true,
-                _breakoutValue: breakoutValue,
-                _breakoutColumn: cols[seriesColumnIndex],
-            },
-            data: {
-                rows: breakoutRowsByValue.get(breakoutValue),
-                cols: rowColumnIndexes.map(i => cols[i]),
-                _rawCols: cols
-            },
-            // for when the legend header for the breakout is clicked
-            clicked: {
-                dimensions: [{
-                    value: breakoutValue,
-                    column: cols[seriesColumnIndex]
-                }]
-            }
-        }));
+      let seriesRows = breakoutRowsByValue.get(seriesValue);
+      if (!seriesRows) {
+        breakoutRowsByValue.set(seriesValue, (seriesRows = []));
+        breakoutValues.push(seriesValue);
+      }
+
+      let newRow = rowColumnIndexes.map(columnIndex => row[columnIndex]);
+      // $FlowFixMe: _origin not typed
+      newRow._origin = { seriesIndex, rowIndex, row, cols };
+      seriesRows.push(newRow);
     }
 
-    // dimensions.length <= 1
-    const dimensionColumnIndex = dimensionColumnIndexes[0];
-    return metricColumnIndexes.map(metricColumnIndex => {
-        const col = cols[metricColumnIndex];
-        const rowColumnIndexes = [dimensionColumnIndex].concat(metricColumnIndex, extraColumnIndexes);
-        return {
-            card: {
-                ...card,
-                name: [
-                    // show series title if it's multiseries
-                    series.length > 1 && card.name,
-                    // show column name if there are multiple metrics
-                    metricColumnIndexes.length > 1 && getFriendlyName(col)
-                ].filter(n => n).join(": "),
-                _transformed: true,
-                _seriesIndex: seriesIndex,
-            },
-            data: {
-                rows: rows.map((row, rowIndex) => {
-                    const newRow = rowColumnIndexes.map(i => row[i]);
-                    // $FlowFixMe: _origin not typed
-                    newRow._origin = { seriesIndex, rowIndex, row, cols };
-                    return newRow;
-                }),
-                cols: rowColumnIndexes.map(i => cols[i]),
-                _rawCols: cols
-            }
-        };
-    });
+    return breakoutValues.map(breakoutValue => ({
+      card: {
+        ...card,
+        // if multiseries include the card title as well as the breakout value
+        name: [
+          // show series title if it's multiseries
+          series.length > 1 && card.name,
+          // always show grouping value
+          formatValue(breakoutValue, { column: cols[seriesColumnIndex] }),
+        ]
+          .filter(n => n)
+          .join(": "),
+        _transformed: true,
+        _breakoutValue: breakoutValue,
+        _breakoutColumn: cols[seriesColumnIndex],
+      },
+      data: {
+        rows: breakoutRowsByValue.get(breakoutValue),
+        cols: rowColumnIndexes.map(i => cols[i]),
+        _rawCols: cols,
+      },
+      // for when the legend header for the breakout is clicked
+      clicked: {
+        dimensions: [
+          {
+            value: breakoutValue,
+            column: cols[seriesColumnIndex],
+          },
+        ],
+      },
+    }));
+  }
+
+  // dimensions.length <= 1
+  const dimensionColumnIndex = dimensionColumnIndexes[0];
+  return metricColumnIndexes.map(metricColumnIndex => {
+    const col = cols[metricColumnIndex];
+    const rowColumnIndexes = [dimensionColumnIndex].concat(
+      metricColumnIndex,
+      extraColumnIndexes,
+    );
+    return {
+      card: {
+        ...card,
+        name: [
+          // show series title if it's multiseries
+          series.length > 1 && card.name,
+          // show column name if there are multiple metrics
+          metricColumnIndexes.length > 1 && getFriendlyName(col),
+        ]
+          .filter(n => n)
+          .join(": "),
+        _transformed: true,
+        _seriesIndex: seriesIndex,
+      },
+      data: {
+        rows: rows.map((row, rowIndex) => {
+          const newRow = rowColumnIndexes.map(i => row[i]);
+          // $FlowFixMe: _origin not typed
+          newRow._origin = { seriesIndex, rowIndex, row, cols };
+          return newRow;
+        }),
+        cols: rowColumnIndexes.map(i => cols[i]),
+        _rawCols: cols,
+      },
+    };
+  });
 }
diff --git a/frontend/src/metabase/visualizations/components/PinMap.jsx b/frontend/src/metabase/visualizations/components/PinMap.jsx
index 220626d05b94011396b71aec25ec8bd38c156c52..b700af59d562a470eaf11b683ae351df39e695a3 100644
--- a/frontend/src/metabase/visualizations/components/PinMap.jsx
+++ b/frontend/src/metabase/visualizations/components/PinMap.jsx
@@ -1,7 +1,7 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { hasLatitudeAndLongitudeColumns } from "metabase/lib/schema_metadata";
 import { LatitudeLongitudeError } from "metabase/visualizations/lib/errors";
 
@@ -21,173 +21,215 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization";
 type Props = VisualizationProps;
 
 type State = {
-    lat: ?number,
-    lng: ?number,
-    min: ?number,
-    max: ?number,
-    binHeight: ?number,
-    binWidth: ?number,
-    zoom: ?number,
-    points: L.Point[],
-    bounds: L.Bounds,
-    filtering: boolean,
+  lat: ?number,
+  lng: ?number,
+  min: ?number,
+  max: ?number,
+  binHeight: ?number,
+  binWidth: ?number,
+  zoom: ?number,
+  points: L.Point[],
+  bounds: L.Bounds,
+  filtering: boolean,
 };
 
 const MAP_COMPONENTS_BY_TYPE = {
-    "markers": LeafletMarkerPinMap,
-    "tiles": LeafletTilePinMap,
-    "heat": LeafletHeatMap,
-    "grid": LeafletGridHeatMap,
-}
+  markers: LeafletMarkerPinMap,
+  tiles: LeafletTilePinMap,
+  heat: LeafletHeatMap,
+  grid: LeafletGridHeatMap,
+};
 
 export default class PinMap extends Component {
-    props: Props;
-    state: State;
+  props: Props;
+  state: State;
 
-    static uiName = t`Pin Map`;
-    static identifier = "pin_map";
-    static iconName = "pinmap";
+  static uiName = t`Pin Map`;
+  static identifier = "pin_map";
+  static iconName = "pinmap";
 
-    static isSensible(cols, rows) {
-        return hasLatitudeAndLongitudeColumns(cols);
-    }
+  static isSensible(cols, rows) {
+    return hasLatitudeAndLongitudeColumns(cols);
+  }
 
-    static checkRenderable([{ data: { cols, rows} }]) {
-        if (!hasLatitudeAndLongitudeColumns(cols)) { throw new LatitudeLongitudeError(); }
+  static checkRenderable([{ data: { cols, rows } }]) {
+    if (!hasLatitudeAndLongitudeColumns(cols)) {
+      throw new LatitudeLongitudeError();
     }
-
-    state: State;
-    _map: ?(LeafletMarkerPinMap|LeafletTilePinMap) = null;
-
-    constructor(props: Props) {
-        super(props);
-        this.state = {
-            lat: null,
-            lng: null,
-            zoom: null,
-            filtering: false,
-            ...this._getPoints(props)
-        };
+  }
+
+  state: State;
+  _map: ?(LeafletMarkerPinMap | LeafletTilePinMap) = null;
+
+  constructor(props: Props) {
+    super(props);
+    this.state = {
+      lat: null,
+      lng: null,
+      zoom: null,
+      filtering: false,
+      ...this._getPoints(props),
+    };
+  }
+
+  componentWillReceiveProps(newProps: Props) {
+    const SETTINGS_KEYS = [
+      "map.latitude_column",
+      "map.longitude_column",
+      "map.metric_column",
+    ];
+    if (
+      newProps.series[0].data !== this.props.series[0].data ||
+      !_.isEqual(
+        // $FlowFixMe
+        _.pick(newProps.settings, ...SETTINGS_KEYS),
+        // $FlowFixMe
+        _.pick(this.props.settings, ...SETTINGS_KEYS),
+      )
+    ) {
+      this.setState(this._getPoints(newProps));
     }
+  }
 
-    componentWillReceiveProps(newProps: Props) {
-        const SETTINGS_KEYS = ["map.latitude_column", "map.longitude_column", "map.metric_column"];
-        if (newProps.series[0].data !== this.props.series[0].data ||
-            !_.isEqual(
-                // $FlowFixMe
-                _.pick(newProps.settings, ...SETTINGS_KEYS),
-                // $FlowFixMe
-                _.pick(this.props.settings, ...SETTINGS_KEYS))
-        ) {
-            this.setState(this._getPoints(newProps))
-        }
+  updateSettings = () => {
+    let newSettings = {};
+    if (this.state.lat != null) {
+      newSettings["map.center_latitude"] = this.state.lat;
     }
-
-    updateSettings = () => {
-        let newSettings = {};
-        if (this.state.lat != null) {
-            newSettings["map.center_latitude"] = this.state.lat;
-        }
-        if (this.state.lng != null) {
-            newSettings["map.center_longitude"] = this.state.lng;
-        }
-        if (this.state.zoom != null) {
-            newSettings["map.zoom"] = this.state.zoom;
-        }
-        this.props.onUpdateVisualizationSettings(newSettings);
-        this.setState({ lat: null, lng: null, zoom: null });
+    if (this.state.lng != null) {
+      newSettings["map.center_longitude"] = this.state.lng;
     }
-
-    onMapCenterChange = (lat: number, lng: number) => {
-        this.setState({ lat, lng });
+    if (this.state.zoom != null) {
+      newSettings["map.zoom"] = this.state.zoom;
     }
-
-    onMapZoomChange = (zoom: number) => {
-        this.setState({ zoom });
+    this.props.onUpdateVisualizationSettings(newSettings);
+    this.setState({ lat: null, lng: null, zoom: null });
+  };
+
+  onMapCenterChange = (lat: number, lng: number) => {
+    this.setState({ lat, lng });
+  };
+
+  onMapZoomChange = (zoom: number) => {
+    this.setState({ zoom });
+  };
+
+  _getPoints(props: Props) {
+    const { settings, series: [{ data: { cols, rows } }] } = props;
+    const latitudeIndex = _.findIndex(
+      cols,
+      col => col.name === settings["map.latitude_column"],
+    );
+    const longitudeIndex = _.findIndex(
+      cols,
+      col => col.name === settings["map.longitude_column"],
+    );
+    const metricIndex = _.findIndex(
+      cols,
+      col => col.name === settings["map.metric_column"],
+    );
+
+    const points = rows.map(row => [
+      row[latitudeIndex],
+      row[longitudeIndex],
+      metricIndex >= 0 ? row[metricIndex] : 1,
+    ]);
+
+    const bounds = L.latLngBounds(points);
+
+    const min = d3.min(points, point => point[2]);
+    const max = d3.max(points, point => point[2]);
+
+    const binWidth =
+      cols[longitudeIndex] &&
+      cols[longitudeIndex].binning_info &&
+      cols[longitudeIndex].binning_info.bin_width;
+    const binHeight =
+      cols[latitudeIndex] &&
+      cols[latitudeIndex].binning_info &&
+      cols[latitudeIndex].binning_info.bin_width;
+
+    if (binWidth != null) {
+      bounds._northEast.lng += binWidth;
     }
-
-    _getPoints(props: Props) {
-        const { settings, series: [{ data: { cols, rows }}] } = props;
-        const latitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.latitude_column"]);
-        const longitudeIndex = _.findIndex(cols, (col) => col.name === settings["map.longitude_column"]);
-        const metricIndex = _.findIndex(cols, (col) => col.name === settings["map.metric_column"]);
-
-        const points = rows.map(row => [
-            row[latitudeIndex],
-            row[longitudeIndex],
-            metricIndex >= 0 ? row[metricIndex] : 1
-        ]);
-
-        const bounds = L.latLngBounds(points);
-
-        const min = d3.min(points, point => point[2]);
-        const max = d3.max(points, point => point[2]);
-
-        const binWidth = cols[longitudeIndex] && cols[longitudeIndex].binning_info && cols[longitudeIndex].binning_info.bin_width;
-        const binHeight = cols[latitudeIndex] && cols[latitudeIndex].binning_info && cols[latitudeIndex].binning_info.bin_width;
-
-        if (binWidth != null) {
-            bounds._northEast.lng += binWidth;
-        }
-        if (binHeight != null) {
-            bounds._northEast.lat += binHeight;
-        }
-
-        return { points, bounds, min, max, binWidth, binHeight };
+    if (binHeight != null) {
+      bounds._northEast.lat += binHeight;
     }
 
-    render() {
-        const { className, settings, isEditing, isDashboard } = this.props;
-        let { lat, lng, zoom } = this.state;
-        const disableUpdateButton = lat == null && lng == null && zoom == null;
-
-        const Map = MAP_COMPONENTS_BY_TYPE[settings["map.pin_type"]];
-
-        const { points, bounds, min, max, binHeight, binWidth } = this.state;
-
-        return (
-            <div className={cx(className, "PinMap relative hover-parent hover--visibility")} onMouseDownCapture={(e) =>e.stopPropagation() /* prevent dragging */}>
-                { Map ?
-                    <Map
-                        {...this.props}
-                        ref={map => this._map = map}
-                        className="absolute top left bottom right z1"
-                        onMapCenterChange={this.onMapCenterChange}
-                        onMapZoomChange={this.onMapZoomChange}
-                        lat={lat}
-                        lng={lng}
-                        zoom={zoom}
-                        points={points}
-                        bounds={bounds}
-                        min={min}
-                        max={max}
-                        binWidth={binWidth}
-                        binHeight={binHeight}
-                        onFiltering={(filtering) => this.setState({ filtering })}
-                    />
-                : null }
-                <div className="absolute top right m1 z2 flex flex-column hover-child">
-                    { isEditing || !isDashboard ?
-                        <div className={cx("PinMapUpdateButton Button Button--small mb1", { "PinMapUpdateButton--disabled": disableUpdateButton })} onClick={this.updateSettings}>
-                            {t`Save as default view`}
-                        </div>
-                    : null }
-                    { !isDashboard &&
-                        <div
-                            className={cx("PinMapUpdateButton Button Button--small mb1")}
-                            onClick={() => {
-                                if (!this.state.filtering && this._map && this._map.startFilter) {
-                                    this._map.startFilter();
-                                } else if (this.state.filtering && this._map && this._map.stopFilter) {
-                                    this._map.stopFilter();
-                                }
-                            }}
-                        >
-                            { !this.state.filtering ? t`Draw box to filter` : t`Cancel filter` }
-                        </div>
-                    }
-                </div>
+    return { points, bounds, min, max, binWidth, binHeight };
+  }
+
+  render() {
+    const { className, settings, isEditing, isDashboard } = this.props;
+    let { lat, lng, zoom } = this.state;
+    const disableUpdateButton = lat == null && lng == null && zoom == null;
+
+    const Map = MAP_COMPONENTS_BY_TYPE[settings["map.pin_type"]];
+
+    const { points, bounds, min, max, binHeight, binWidth } = this.state;
+
+    return (
+      <div
+        className={cx(
+          className,
+          "PinMap relative hover-parent hover--visibility",
+        )}
+        onMouseDownCapture={e => e.stopPropagation() /* prevent dragging */}
+      >
+        {Map ? (
+          <Map
+            {...this.props}
+            ref={map => (this._map = map)}
+            className="absolute top left bottom right z1"
+            onMapCenterChange={this.onMapCenterChange}
+            onMapZoomChange={this.onMapZoomChange}
+            lat={lat}
+            lng={lng}
+            zoom={zoom}
+            points={points}
+            bounds={bounds}
+            min={min}
+            max={max}
+            binWidth={binWidth}
+            binHeight={binHeight}
+            onFiltering={filtering => this.setState({ filtering })}
+          />
+        ) : null}
+        <div className="absolute top right m1 z2 flex flex-column hover-child">
+          {isEditing || !isDashboard ? (
+            <div
+              className={cx("PinMapUpdateButton Button Button--small mb1", {
+                "PinMapUpdateButton--disabled": disableUpdateButton,
+              })}
+              onClick={this.updateSettings}
+            >
+              {t`Save as default view`}
             </div>
-        );
-    }
+          ) : null}
+          {!isDashboard && (
+            <div
+              className={cx("PinMapUpdateButton Button Button--small mb1")}
+              onClick={() => {
+                if (
+                  !this.state.filtering &&
+                  this._map &&
+                  this._map.startFilter
+                ) {
+                  this._map.startFilter();
+                } else if (
+                  this.state.filtering &&
+                  this._map &&
+                  this._map.stopFilter
+                ) {
+                  this._map.stopFilter();
+                }
+              }}
+            >
+              {!this.state.filtering ? t`Draw box to filter` : t`Cancel filter`}
+            </div>
+          )}
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/Table.css b/frontend/src/metabase/visualizations/components/Table.css
index a130993a092299d728a2932ea87e436684abae48..715c772707bd66f24c1e18aebf3f1ff90f20a05c 100644
--- a/frontend/src/metabase/visualizations/components/Table.css
+++ b/frontend/src/metabase/visualizations/components/Table.css
@@ -1,4 +1,3 @@
-
 :local(.Table) {
   /* standard table reset */
   border-collapse: collapse;
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.css b/frontend/src/metabase/visualizations/components/TableInteractive.css
index d65d7f5196a2314bb37b1fd313103fcd0f0845af..354c88e47eb0303d5660672353cf3f294b9a5cc3 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.css
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.css
@@ -1,5 +1,5 @@
 :root {
-    --table-border-radius: 6px;
+  --table-border-radius: 6px;
 }
 
 .TableInteractive {
@@ -25,7 +25,7 @@
 
 .TableInteractive-headerCellData--sorted .Icon {
   opacity: 1;
-  transition: opacity .3s linear;
+  transition: opacity 0.3s linear;
 }
 
 /* if the column is the one that is being sorted*/
@@ -33,7 +33,6 @@
   color: var(--brand-color);
 }
 
-
 .TableInteractive-header {
   box-sizing: border-box;
   border-bottom: 1px solid #e0e0e0;
@@ -51,7 +50,6 @@
   border-bottom: 1px solid var(--table-border-color);
 }
 
-
 .TableInteractive .TableInteractive-cellWrapper:hover {
   border-color: var(--brand-color);
   color: var(--brand-color);
@@ -77,7 +75,8 @@
 }
 
 /* pivot */
-.TableInteractive.TableInteractive--pivot .TableInteractive-cellWrapper--firstColumn {
+.TableInteractive.TableInteractive--pivot
+  .TableInteractive-cellWrapper--firstColumn {
   border-right: 1px solid rgb(205, 205, 205);
 }
 
diff --git a/frontend/src/metabase/visualizations/components/TableInteractive.jsx b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
index 04d4d6177bcde9b7f9764bef3a6ac7dfb846f3da..64fd902303af3236cb6d0ff13cb4c1aa4a8e32db 100644
--- a/frontend/src/metabase/visualizations/components/TableInteractive.jsx
+++ b/frontend/src/metabase/visualizations/components/TableInteractive.jsx
@@ -3,14 +3,17 @@
 import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import "./TableInteractive.css";
 
 import Icon from "metabase/components/Icon.jsx";
 
 import { formatValue, formatColumn } from "metabase/lib/formatting";
 import { isID } from "metabase/lib/schema_metadata";
-import { getTableCellClickedObject, isColumnRightAligned } from "metabase/visualizations/lib/table";
+import {
+  getTableCellClickedObject,
+  isColumnRightAligned,
+} from "metabase/visualizations/lib/table";
 
 import _ from "underscore";
 import cx from "classnames";
@@ -27,354 +30,471 @@ const RESIZE_HANDLE_WIDTH = 5;
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps & {
-    width: number,
-    height: number,
-    sort: any,
-    isPivoted: boolean,
-    onActionDismissal: () => void
-}
+  width: number,
+  height: number,
+  sort: any,
+  isPivoted: boolean,
+  onActionDismissal: () => void,
+};
 type State = {
-    columnWidths: number[],
-    contentWidths: ?number[]
-}
+  columnWidths: number[],
+  contentWidths: ?(number[]),
+};
 
 type CellRendererProps = {
-    key: string,
-    style: { [key:string]: any },
-    columnIndex: number,
-    rowIndex: number
-}
+  key: string,
+  style: { [key: string]: any },
+  columnIndex: number,
+  rowIndex: number,
+};
 
-type GridComponent = Component<void, void, void> & { recomputeGridSize: () => void }
+type GridComponent = Component<void, void, void> & {
+  recomputeGridSize: () => void,
+};
 
 @ExplicitSize
 export default class TableInteractive extends Component {
-    state: State;
-    props: Props;
+  state: State;
+  props: Props;
 
-    columnHasResized: { [key:number]: boolean };
-    columnNeedsResize: { [key:number]: boolean };
-    _div: HTMLElement;
+  columnHasResized: { [key: number]: boolean };
+  columnNeedsResize: { [key: number]: boolean };
+  _div: HTMLElement;
 
-    header: GridComponent;
-    grid: GridComponent;
+  header: GridComponent;
+  grid: GridComponent;
 
-    constructor(props: Props) {
-        super(props);
+  constructor(props: Props) {
+    super(props);
 
-        this.state = {
-            columnWidths: [],
-            contentWidths: null
-        };
-        this.columnHasResized = {};
-    }
-
-    static propTypes = {
-        data: PropTypes.object.isRequired,
-        isPivoted: PropTypes.bool.isRequired,
-        sort: PropTypes.array
+    this.state = {
+      columnWidths: [],
+      contentWidths: null,
     };
-
-    static defaultProps = {
-        isPivoted: false,
-    };
-
-    componentWillMount() {
-        // for measuring cells:
-        this._div = document.createElement("div");
-        this._div.className = "TableInteractive";
-        this._div.style.display = "inline-block"
-        this._div.style.position = "absolute"
-        this._div.style.visibility = "hidden"
-        this._div.style.zIndex = "-1"
-        document.body.appendChild(this._div);
-
-        this._measure();
-    }
-
-    componentWillUnmount() {
-        if (this._div && this._div.parentNode) {
-            this._div.parentNode.removeChild(this._div);
-        }
+    this.columnHasResized = {};
+  }
+
+  static propTypes = {
+    data: PropTypes.object.isRequired,
+    isPivoted: PropTypes.bool.isRequired,
+    sort: PropTypes.array,
+  };
+
+  static defaultProps = {
+    isPivoted: false,
+  };
+
+  componentWillMount() {
+    // for measuring cells:
+    this._div = document.createElement("div");
+    this._div.className = "TableInteractive";
+    this._div.style.display = "inline-block";
+    this._div.style.position = "absolute";
+    this._div.style.visibility = "hidden";
+    this._div.style.zIndex = "-1";
+    document.body.appendChild(this._div);
+
+    this._measure();
+  }
+
+  componentWillUnmount() {
+    if (this._div && this._div.parentNode) {
+      this._div.parentNode.removeChild(this._div);
     }
-
-    componentWillReceiveProps(newProps: Props) {
-        if (JSON.stringify(this.props.data && this.props.data.cols) !== JSON.stringify(newProps.data && newProps.data.cols)) {
-            this.resetColumnWidths();
-        }
+  }
+
+  componentWillReceiveProps(newProps: Props) {
+    if (
+      JSON.stringify(this.props.data && this.props.data.cols) !==
+      JSON.stringify(newProps.data && newProps.data.cols)
+    ) {
+      this.resetColumnWidths();
     }
-
-    shouldComponentUpdate(nextProps: Props, nextState: State) {
-        const PROP_KEYS: string[] = ["width", "height", "settings", "data"];
-        // compare specific props and state to determine if we should re-render
-        return (
-            !_.isEqual(_.pick(this.props, ...PROP_KEYS), _.pick(nextProps, ...PROP_KEYS)) ||
-            !_.isEqual(this.state, nextState)
-        );
+  }
+
+  shouldComponentUpdate(nextProps: Props, nextState: State) {
+    const PROP_KEYS: string[] = ["width", "height", "settings", "data"];
+    // compare specific props and state to determine if we should re-render
+    return (
+      !_.isEqual(
+        _.pick(this.props, ...PROP_KEYS),
+        _.pick(nextProps, ...PROP_KEYS),
+      ) || !_.isEqual(this.state, nextState)
+    );
+  }
+
+  componentDidUpdate() {
+    if (!this.state.contentWidths) {
+      this._measure();
     }
-
-    componentDidUpdate() {
-        if (!this.state.contentWidths) {
-            this._measure();
+  }
+
+  resetColumnWidths() {
+    this.setState({
+      columnWidths: [],
+      contentWidths: null,
+    });
+    this.columnHasResized = {};
+    this.props.onUpdateVisualizationSettings({
+      "table.column_widths": undefined,
+    });
+  }
+
+  _measure() {
+    const { data: { cols } } = this.props;
+
+    let contentWidths = cols.map((col, index) => this._measureColumn(index));
+
+    let columnWidths: number[] = cols.map((col, index) => {
+      if (this.columnNeedsResize) {
+        if (this.columnNeedsResize[index] && !this.columnHasResized[index]) {
+          this.columnHasResized[index] = true;
+          return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
+        } else if (this.state.columnWidths[index]) {
+          return this.state.columnWidths[index];
+        } else {
+          return 0;
         }
-    }
-
-    resetColumnWidths() {
-        this.setState({
-            columnWidths: [],
-            contentWidths: null
-        });
-        this.columnHasResized = {};
-        this.props.onUpdateVisualizationSettings({ "table.column_widths": undefined });
-    }
-
-    _measure() {
-        const { data: { cols } } = this.props;
-
-        let contentWidths = cols.map((col, index) =>
-            this._measureColumn(index)
+      } else {
+        return contentWidths[index] + 1;
+      }
+    });
+
+    delete this.columnNeedsResize;
+
+    this.setState({ contentWidths, columnWidths }, this.recomputeGridSize);
+  }
+
+  _measureColumn(columnIndex: number) {
+    const { data: { rows } } = this.props;
+    let width = MIN_COLUMN_WIDTH;
+
+    // measure column header
+    width = Math.max(
+      width,
+      this._measureCell(
+        this.tableHeaderRenderer({
+          columnIndex,
+          rowIndex: 0,
+          key: "",
+          style: {},
+        }),
+      ),
+    );
+
+    // measure up to 10 non-nil cells
+    let remaining = 10;
+    for (
+      let rowIndex = 0;
+      rowIndex < rows.length && remaining > 0;
+      rowIndex++
+    ) {
+      if (rows[rowIndex][columnIndex] != null) {
+        const cellWidth = this._measureCell(
+          this.cellRenderer({ rowIndex, columnIndex, key: "", style: {} }),
         );
-
-        let columnWidths: number[] = cols.map((col, index) => {
-            if (this.columnNeedsResize) {
-                if (this.columnNeedsResize[index] && !this.columnHasResized[index]) {
-                    this.columnHasResized[index] = true;
-                    return contentWidths[index] + 1; // + 1 to make sure it doen't wrap?
-                } else if (this.state.columnWidths[index]) {
-                    return this.state.columnWidths[index];
-                } else {
-                    return 0;
-                }
-            } else {
-                return contentWidths[index] + 1;
-            }
-        });
-
-        delete this.columnNeedsResize;
-
-        this.setState({ contentWidths, columnWidths }, this.recomputeGridSize);
+        width = Math.max(width, cellWidth);
+        remaining--;
+      }
     }
 
-    _measureColumn(columnIndex: number) {
-        const { data: { rows } } = this.props;
-        let width = MIN_COLUMN_WIDTH;
-
-        // measure column header
-        width = Math.max(width, this._measureCell(this.tableHeaderRenderer({ columnIndex, rowIndex: 0, key: "", style: {} })));
-
-        // measure up to 10 non-nil cells
-        let remaining = 10;
-        for (let rowIndex = 0; rowIndex < rows.length && remaining > 0; rowIndex++) {
-            if (rows[rowIndex][columnIndex] != null) {
-                const cellWidth = this._measureCell(this.cellRenderer({ rowIndex, columnIndex, key: "", style: {} }));
-                width = Math.max(width, cellWidth);
-                remaining--;
-            }
-        }
+    return width;
+  }
 
-        return width;
-    }
+  _measureCell(cell: React.Element<any>) {
+    ReactDOM.unstable_renderSubtreeIntoContainer(this, cell, this._div);
 
-    _measureCell(cell: React.Element<any>) {
-        ReactDOM.unstable_renderSubtreeIntoContainer(this, cell, this._div);
+    // 2px for border?
+    const width = this._div.clientWidth + 2;
 
-        // 2px for border?
-        const width = this._div.clientWidth + 2;
+    ReactDOM.unmountComponentAtNode(this._div);
 
-        ReactDOM.unmountComponentAtNode(this._div);
+    return width;
+  }
 
-        return width;
+  recomputeGridSize = () => {
+    if (this.header && this.grid) {
+      this.header.recomputeGridSize();
+      this.grid.recomputeGridSize();
     }
-
-    recomputeGridSize = () => {
-        if (this.header && this.grid) {
-            this.header.recomputeGridSize();
-            this.grid.recomputeGridSize();
+  };
+
+  recomputeColumnSizes = _.debounce(() => {
+    this.setState({ contentWidths: null });
+  }, 100);
+
+  onCellResize(columnIndex: number) {
+    this.columnNeedsResize = this.columnNeedsResize || {};
+    this.columnNeedsResize[columnIndex] = true;
+    this.recomputeColumnSizes();
+  }
+
+  onColumnResize(columnIndex: number, width: number) {
+    const { settings } = this.props;
+    let columnWidthsSetting = settings["table.column_widths"]
+      ? settings["table.column_widths"].slice()
+      : [];
+    columnWidthsSetting[columnIndex] = Math.max(MIN_COLUMN_WIDTH, width);
+    this.props.onUpdateVisualizationSettings({
+      "table.column_widths": columnWidthsSetting,
+    });
+    setTimeout(() => this.recomputeGridSize(), 1);
+  }
+
+  cellRenderer = ({ key, style, rowIndex, columnIndex }: CellRendererProps) => {
+    const {
+      data,
+      isPivoted,
+      onVisualizationClick,
+      visualizationIsClickable,
+    } = this.props;
+    const { rows, cols } = data;
+
+    const column = cols[columnIndex];
+    const row = rows[rowIndex];
+    const value = row[columnIndex];
+
+    const clicked = getTableCellClickedObject(
+      data,
+      rowIndex,
+      columnIndex,
+      isPivoted,
+    );
+    const isClickable =
+      onVisualizationClick && visualizationIsClickable(clicked);
+
+    return (
+      <div
+        key={key}
+        style={style}
+        className={cx("TableInteractive-cellWrapper", {
+          "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
+          "TableInteractive-cellWrapper--lastColumn":
+            columnIndex === cols.length - 1,
+          "cursor-pointer": isClickable,
+          "justify-end": isColumnRightAligned(column),
+          link: isClickable && isID(column),
+        })}
+        onClick={
+          isClickable &&
+          (e => {
+            onVisualizationClick({ ...clicked, element: e.currentTarget });
+          })
         }
+      >
+        <div className="cellData">
+          {/* using formatValue instead of <Value> here for performance. The later wraps in an extra <span> */}
+          {formatValue(value, {
+            column: column,
+            type: "cell",
+            jsx: true,
+          })}
+        </div>
+      </div>
+    );
+  };
+
+  tableHeaderRenderer = ({ key, style, columnIndex }: CellRendererProps) => {
+    const {
+      sort,
+      isPivoted,
+      onVisualizationClick,
+      visualizationIsClickable,
+    } = this.props;
+    // $FlowFixMe: not sure why flow has a problem with this
+    const { cols } = this.props.data;
+    const column = cols[columnIndex];
+
+    let columnTitle = formatColumn(column);
+    if (!columnTitle && this.props.isPivoted && columnIndex !== 0) {
+      columnTitle = t`Unset`;
     }
 
-    recomputeColumnSizes = _.debounce(() => {
-        this.setState({ contentWidths: null })
-    }, 100)
-
-    onCellResize(columnIndex: number) {
-        this.columnNeedsResize = this.columnNeedsResize || {}
-        this.columnNeedsResize[columnIndex] = true;
-        this.recomputeColumnSizes();
+    let clicked;
+    if (isPivoted) {
+      // if it's a pivot table, the first column is
+      if (columnIndex >= 0) {
+        clicked = column._dimension;
+      }
+    } else {
+      clicked = { column };
     }
 
-    onColumnResize(columnIndex: number, width: number) {
-        const { settings } = this.props;
-        let columnWidthsSetting = settings["table.column_widths"] ? settings["table.column_widths"].slice() : [];
-        columnWidthsSetting[columnIndex] = Math.max(MIN_COLUMN_WIDTH, width);
-        this.props.onUpdateVisualizationSettings({ "table.column_widths": columnWidthsSetting });
-        setTimeout(() => this.recomputeGridSize(), 1);
-    }
-
-    cellRenderer = ({ key, style, rowIndex, columnIndex }: CellRendererProps) => {
-        const { data, isPivoted, onVisualizationClick, visualizationIsClickable } = this.props;
-        const { rows, cols } = data;
-
-        const column = cols[columnIndex];
-        const row = rows[rowIndex];
-        const value = row[columnIndex];
-
-        const clicked = getTableCellClickedObject(data, rowIndex, columnIndex, isPivoted);
-        const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
-
-        return (
-            <div
-                key={key} style={style}
-                className={cx("TableInteractive-cellWrapper", {
-                    "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
-                    "TableInteractive-cellWrapper--lastColumn": columnIndex === cols.length - 1,
-                    "cursor-pointer": isClickable,
-                    "justify-end": isColumnRightAligned(column),
-                    "link": isClickable && isID(column)
-                })}
-                onClick={isClickable && ((e) => {
-                    onVisualizationClick({ ...clicked, element: e.currentTarget });
-                })}
-            >
-                <div className="cellData">
-                    {/* using formatValue instead of <Value> here for performance. The later wraps in an extra <span> */}
-                    {formatValue(value, {
-                        column: column,
-                        type: "cell",
-                        jsx: true
-                    })}
-                </div>
-            </div>
-        );
-    }
-
-    tableHeaderRenderer = ({ key, style, columnIndex }: CellRendererProps) => {
-        const { sort, isPivoted, onVisualizationClick, visualizationIsClickable } = this.props;
-        // $FlowFixMe: not sure why flow has a problem with this
-        const { cols } = this.props.data;
-        const column = cols[columnIndex];
-
-        let columnTitle = formatColumn(column);
-        if (!columnTitle && this.props.isPivoted && columnIndex !== 0) {
-            columnTitle = t`Unset`;
+    const isClickable =
+      onVisualizationClick && visualizationIsClickable(clicked);
+    const isSortable = isClickable && column.source;
+    const isRightAligned = isColumnRightAligned(column);
+
+    // the column id is in `["field-id", fieldId]` format
+    const isSorted =
+      sort && sort[0] && sort[0][0] && sort[0][0][1] === column.id;
+    const isAscending = sort && sort[0] && sort[0][1] === "ascending";
+
+    return (
+      <div
+        key={key}
+        style={{
+          ...style,
+          overflow: "visible" /* ensure resize handle is visible */,
+        }}
+        className={cx(
+          "TableInteractive-cellWrapper TableInteractive-headerCellData",
+          {
+            "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
+            "TableInteractive-cellWrapper--lastColumn":
+              columnIndex === cols.length - 1,
+            "TableInteractive-headerCellData--sorted": isSorted,
+            "cursor-pointer": isClickable,
+            "justify-end": isRightAligned,
+          },
+        )}
+        onClick={
+          isClickable &&
+          (e => {
+            onVisualizationClick({ ...clicked, element: e.currentTarget });
+          })
         }
-
-        let clicked;
-        if (isPivoted) {
-            // if it's a pivot table, the first column is
-            if (columnIndex >= 0) {
-                clicked = column._dimension;
-            }
-        } else {
-            clicked = { column };
-        }
-
-        const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
-        const isSortable = isClickable && column.source;
-        const isRightAligned = isColumnRightAligned(column);
-
-        // the column id is in `["field-id", fieldId]` format
-        const isSorted = sort && sort[0] && sort[0][0] && sort[0][0][1] === column.id;
-        const isAscending = sort && sort[0] && sort[0][1] === "ascending";
-
-        return (
-            <div
-                key={key}
-                style={{ ...style, overflow: "visible" /* ensure resize handle is visible */ }}
-                className={cx("TableInteractive-cellWrapper TableInteractive-headerCellData", {
-                    "TableInteractive-cellWrapper--firstColumn": columnIndex === 0,
-                    "TableInteractive-cellWrapper--lastColumn": columnIndex === cols.length - 1,
-                    "TableInteractive-headerCellData--sorted": isSorted,
-                    "cursor-pointer": isClickable,
-                    "justify-end": isRightAligned
-                })}
-                onClick={isClickable && ((e) => {
-                    onVisualizationClick({ ...clicked, element: e.currentTarget });
-                })}
-            >
-                <div className="cellData">
-                    {isSortable && isRightAligned &&
-                        <Icon className="Icon mr1" name={isAscending ? "chevronup" : "chevrondown"} size={8} />
-                    }
-                    {columnTitle}
-                    {isSortable && !isRightAligned &&
-                        <Icon className="Icon ml1" name={isAscending ? "chevronup" : "chevrondown"} size={8} />
-                    }
-                </div>
-                <Draggable
-                    axis="x"
-                    bounds={{ left: RESIZE_HANDLE_WIDTH }}
-                    position={{ x: this.getColumnWidth({ index: columnIndex }), y: 0 }}
-                    onStop={(e, { x }) => {
-                        this.onColumnResize(columnIndex, x)}
-                    }
-                >
-                    <div
-                        className="bg-brand-hover bg-brand-active"
-                        style={{ zIndex: 99, position: "absolute", width: RESIZE_HANDLE_WIDTH, top: 0, bottom: 0, left: -RESIZE_HANDLE_WIDTH - 1, cursor: "ew-resize" }}
-                    />
-                </Draggable>
-            </div>
-        )
-    }
-
-    getColumnWidth = ({ index }: { index: number }) => {
-        const { settings } = this.props;
-        const { columnWidths } = this.state;
-        const columnWidthsSetting = settings["table.column_widths"] || [];
-        return columnWidthsSetting[index] || columnWidths[index] || MIN_COLUMN_WIDTH;
+      >
+        <div className="cellData">
+          {isSortable &&
+            isRightAligned && (
+              <Icon
+                className="Icon mr1"
+                name={isAscending ? "chevronup" : "chevrondown"}
+                size={8}
+              />
+            )}
+          {columnTitle}
+          {isSortable &&
+            !isRightAligned && (
+              <Icon
+                className="Icon ml1"
+                name={isAscending ? "chevronup" : "chevrondown"}
+                size={8}
+              />
+            )}
+        </div>
+        <Draggable
+          axis="x"
+          bounds={{ left: RESIZE_HANDLE_WIDTH }}
+          position={{ x: this.getColumnWidth({ index: columnIndex }), y: 0 }}
+          onStop={(e, { x }) => {
+            this.onColumnResize(columnIndex, x);
+          }}
+        >
+          <div
+            className="bg-brand-hover bg-brand-active"
+            style={{
+              zIndex: 99,
+              position: "absolute",
+              width: RESIZE_HANDLE_WIDTH,
+              top: 0,
+              bottom: 0,
+              left: -RESIZE_HANDLE_WIDTH - 1,
+              cursor: "ew-resize",
+            }}
+          />
+        </Draggable>
+      </div>
+    );
+  };
+
+  getColumnWidth = ({ index }: { index: number }) => {
+    const { settings } = this.props;
+    const { columnWidths } = this.state;
+    const columnWidthsSetting = settings["table.column_widths"] || [];
+    return (
+      columnWidthsSetting[index] || columnWidths[index] || MIN_COLUMN_WIDTH
+    );
+  };
+
+  render() {
+    const { width, height, data: { cols, rows }, className } = this.props;
+
+    if (!width || !height) {
+      return <div className={className} />;
     }
 
-    render() {
-        const { width, height, data: { cols, rows }, className } = this.props;
-
-        if (!width || !height) {
-            return <div className={className} />;
-        }
-
-        return (
-            <ScrollSync>
-            {({ clientHeight, clientWidth, onScroll, scrollHeight, scrollLeft, scrollTop, scrollWidth }) =>
-                <div className={cx(className, 'TableInteractive relative', { 'TableInteractive--pivot': this.props.isPivoted, 'TableInteractive--ready': this.state.contentWidths })}>
-                    <canvas className="spread" style={{ pointerEvents: "none", zIndex: 999 }} width={width} height={height} />
-                    <Grid
-                        ref={(ref) => this.header = ref}
-                        style={{ top: 0, left: 0, right: 0, height: HEADER_HEIGHT, position: "absolute", overflow: "hidden" }}
-                        className="TableInteractive-header scroll-hide-all"
-                        width={width || 0}
-                        height={HEADER_HEIGHT}
-                        rowCount={1}
-                        rowHeight={HEADER_HEIGHT}
-                        // HACK: there might be a better way to do this, but add a phantom padding cell at the end to ensure scroll stays synced if main content scrollbars are visible
-                        columnCount={cols.length + 1}
-                        columnWidth={(props) => props.index < cols.length ? this.getColumnWidth(props) : 50}
-                        cellRenderer={(props) => props.columnIndex < cols.length ? this.tableHeaderRenderer(props) : null}
-                        onScroll={({ scrollLeft }) => onScroll({ scrollLeft })}
-                        scrollLeft={scrollLeft}
-                        tabIndex={null}
-                    />
-                    <Grid
-                        ref={(ref) => this.grid = ref}
-                        style={{ top: HEADER_HEIGHT, left: 0, right: 0, bottom: 0, position: "absolute" }}
-                        className=""
-                        width={width}
-                        height={height - HEADER_HEIGHT}
-                        columnCount={cols.length}
-                        columnWidth={this.getColumnWidth}
-                        rowCount={rows.length}
-                        rowHeight={ROW_HEIGHT}
-                        cellRenderer={this.cellRenderer}
-                        onScroll={({ scrollLeft }) => {
-                            this.props.onActionDismissal()
-                            return onScroll({ scrollLeft })}
-                        }
-                        scrollLeft={scrollLeft}
-                        tabIndex={null}
-                        overscanRowCount={20}
-                    />
-                </div>
-            }
-            </ScrollSync>
-        );
-    }
+    return (
+      <ScrollSync>
+        {({
+          clientHeight,
+          clientWidth,
+          onScroll,
+          scrollHeight,
+          scrollLeft,
+          scrollTop,
+          scrollWidth,
+        }) => (
+          <div
+            className={cx(className, "TableInteractive relative", {
+              "TableInteractive--pivot": this.props.isPivoted,
+              "TableInteractive--ready": this.state.contentWidths,
+            })}
+          >
+            <canvas
+              className="spread"
+              style={{ pointerEvents: "none", zIndex: 999 }}
+              width={width}
+              height={height}
+            />
+            <Grid
+              ref={ref => (this.header = ref)}
+              style={{
+                top: 0,
+                left: 0,
+                right: 0,
+                height: HEADER_HEIGHT,
+                position: "absolute",
+                overflow: "hidden",
+              }}
+              className="TableInteractive-header scroll-hide-all"
+              width={width || 0}
+              height={HEADER_HEIGHT}
+              rowCount={1}
+              rowHeight={HEADER_HEIGHT}
+              // HACK: there might be a better way to do this, but add a phantom padding cell at the end to ensure scroll stays synced if main content scrollbars are visible
+              columnCount={cols.length + 1}
+              columnWidth={props =>
+                props.index < cols.length ? this.getColumnWidth(props) : 50
+              }
+              cellRenderer={props =>
+                props.columnIndex < cols.length
+                  ? this.tableHeaderRenderer(props)
+                  : null
+              }
+              onScroll={({ scrollLeft }) => onScroll({ scrollLeft })}
+              scrollLeft={scrollLeft}
+              tabIndex={null}
+            />
+            <Grid
+              ref={ref => (this.grid = ref)}
+              style={{
+                top: HEADER_HEIGHT,
+                left: 0,
+                right: 0,
+                bottom: 0,
+                position: "absolute",
+              }}
+              className=""
+              width={width}
+              height={height - HEADER_HEIGHT}
+              columnCount={cols.length}
+              columnWidth={this.getColumnWidth}
+              rowCount={rows.length}
+              rowHeight={ROW_HEIGHT}
+              cellRenderer={this.cellRenderer}
+              onScroll={({ scrollLeft }) => {
+                this.props.onActionDismissal();
+                return onScroll({ scrollLeft });
+              }}
+              scrollLeft={scrollLeft}
+              tabIndex={null}
+              overscanRowCount={20}
+            />
+          </div>
+        )}
+      </ScrollSync>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/TableSimple.jsx b/frontend/src/metabase/visualizations/components/TableSimple.jsx
index d5d57d5d486ec1d198c87f6810e075878f6ddad6..397278eb2991465ff0966fea62c904b2f86ee3b0 100644
--- a/frontend/src/metabase/visualizations/components/TableSimple.jsx
+++ b/frontend/src/metabase/visualizations/components/TableSimple.jsx
@@ -4,13 +4,16 @@ import React, { Component } from "react";
 import PropTypes from "prop-types";
 import ReactDOM from "react-dom";
 import styles from "./Table.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ExplicitSize from "metabase/components/ExplicitSize.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
 import Icon from "metabase/components/Icon.jsx";
 
 import { formatColumn, formatValue } from "metabase/lib/formatting";
-import { getTableCellClickedObject, isColumnRightAligned } from "metabase/visualizations/lib/table";
+import {
+  getTableCellClickedObject,
+  isColumnRightAligned,
+} from "metabase/visualizations/lib/table";
 
 import cx from "classnames";
 import _ from "underscore";
@@ -18,146 +21,218 @@ import _ from "underscore";
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps & {
-    height: number,
-    className?: string,
-    isPivoted: boolean,
-}
+  height: number,
+  className?: string,
+  isPivoted: boolean,
+};
 
 type State = {
-    page: number,
-    pageSize: number,
-    sortColumn: ?number,
-    sortDescending: boolean
-}
+  page: number,
+  pageSize: number,
+  sortColumn: ?number,
+  sortDescending: boolean,
+};
 
 @ExplicitSize
 export default class TableSimple extends Component {
-    props: Props;
-    state: State;
-
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            page: 0,
-            pageSize: 1,
-            sortColumn: null,
-            sortDescending: false
-        }
-    }
+  props: Props;
+  state: State;
 
-    static propTypes = {
-        data: PropTypes.object.isRequired
-    };
+  constructor(props: Props) {
+    super(props);
 
-    static defaultProps = {
-        className: ""
+    this.state = {
+      page: 0,
+      pageSize: 1,
+      sortColumn: null,
+      sortDescending: false,
     };
+  }
 
-    setSort(colIndex: number) {
-        if (this.state.sortColumn === colIndex) {
-            this.setState({ sortDescending: !this.state.sortDescending });
-        } else {
-            this.setState({ sortColumn: colIndex });
-        }
-    }
+  static propTypes = {
+    data: PropTypes.object.isRequired,
+  };
 
-    componentDidUpdate() {
-        let headerHeight = ReactDOM.findDOMNode(this.refs.header).getBoundingClientRect().height;
-        let footerHeight = this.refs.footer ? ReactDOM.findDOMNode(this.refs.footer).getBoundingClientRect().height : 0;
-        let rowHeight = ReactDOM.findDOMNode(this.refs.firstRow).getBoundingClientRect().height + 1;
-        let pageSize = Math.max(1, Math.floor((this.props.height - headerHeight - footerHeight) / rowHeight));
-        if (this.state.pageSize !== pageSize) {
-            this.setState({ pageSize });
-        }
-    }
+  static defaultProps = {
+    className: "",
+  };
 
-    render() {
-        const { data, onVisualizationClick, visualizationIsClickable, isPivoted } = this.props;
-        const { rows, cols } = data;
-
-        const { page, pageSize, sortColumn, sortDescending } = this.state;
-
-        let start = pageSize * page;
-        let end = Math.min(rows.length - 1, pageSize * (page + 1) - 1);
-
-        let rowIndexes = _.range(0, rows.length);
-        if (sortColumn != null) {
-            rowIndexes = _.sortBy(rowIndexes, (rowIndex) => rows[rowIndex][sortColumn]);
-            if (sortDescending) {
-                rowIndexes.reverse();
-            }
-        }
-
-        return (
-            <div className={cx(this.props.className, "relative flex flex-column")}>
-                <div className="flex-full relative">
-                    <div className="absolute top bottom left right scroll-x scroll-show scroll-show--hover" style={{ overflowY: "hidden" }}>
-                        <table className={cx(styles.Table, styles.TableSimple, 'fullscreen-normal-text', 'fullscreen-night-text')}>
-                            <thead ref="header">
-                                <tr>
-                                    {cols.map((col, colIndex) =>
-                                        <th
-                                            key={colIndex}
-                                            className={cx("TableInteractive-headerCellData cellData text-brand-hover", {
-                                                "TableInteractive-headerCellData--sorted": sortColumn === colIndex,
-                                                "text-right": isColumnRightAligned(col)
-                                            })}
-                                            onClick={() => this.setSort(colIndex)}
-                                        >
-                                            <div className="relative">
-                                                <Icon
-                                                    name={sortDescending ? "chevrondown" : "chevronup"}
-                                                    width={8} height={8}
-                                                    style={{ position: "absolute", right: "100%", marginRight: 3 }}
-                                                />
-                                                <Ellipsified>{formatColumn(col)}</Ellipsified>
-                                            </div>
-                                        </th>
-                                    )}
-                                </tr>
-                            </thead>
-                            <tbody>
-                            {rowIndexes.slice(start, end + 1).map((rowIndex, index) =>
-                                <tr key={rowIndex} ref={index === 0 ? "firstRow" : null}>
-                                    {rows[rowIndex].map((cell, columnIndex) => {
-                                        const clicked = getTableCellClickedObject(data, rowIndex, columnIndex, isPivoted);
-                                        const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
-                                        return (
-                                            <td
-                                                key={columnIndex}
-                                                style={{ whiteSpace: "nowrap" }}
-                                                className={cx("px1 border-bottom", { "text-right": isColumnRightAligned(cols[columnIndex]) })}
-                                            >
-                                                <span
-                                                    className={cx({ "cursor-pointer text-brand-hover": isClickable })}
-                                                    onClick={isClickable && ((e) => {
-                                                        onVisualizationClick({ ...clicked, element: e.currentTarget });
-                                                    })}
-                                                >
-                                                    { cell == null ? "-" : formatValue(cell, { column: cols[columnIndex], jsx: true }) }
-                                                </span>
-                                            </td>
-                                        );
-                                    })}
-                                </tr>
-                            )}
-                            </tbody>
-                        </table>
-                    </div>
-                </div>
-                { pageSize < rows.length ?
-                    <div ref="footer" className="p1 flex flex-no-shrink flex-align-right fullscreen-normal-text fullscreen-night-text">
-                        <span className="text-bold">{t`Rows ${start + 1}-${end + 1} of ${rows.length}`}</span>
-                        <span className={cx("text-brand-hover px1 cursor-pointer", { disabled: start === 0 })} onClick={() => this.setState({ page: page - 1 })}>
-                            <Icon name="left" size={10} />
-                        </span>
-                        <span className={cx("text-brand-hover pr1 cursor-pointer", { disabled: end + 1 >= rows.length })} onClick={() => this.setState({ page: page + 1 })}>
-                            <Icon name="right" size={10} />
-                        </span>
-                    </div>
-                : null }
-            </div>
-        );
+  setSort(colIndex: number) {
+    if (this.state.sortColumn === colIndex) {
+      this.setState({ sortDescending: !this.state.sortDescending });
+    } else {
+      this.setState({ sortColumn: colIndex });
+    }
+  }
+
+  componentDidUpdate() {
+    let headerHeight = ReactDOM.findDOMNode(
+      this.refs.header,
+    ).getBoundingClientRect().height;
+    let footerHeight = this.refs.footer
+      ? ReactDOM.findDOMNode(this.refs.footer).getBoundingClientRect().height
+      : 0;
+    let rowHeight =
+      ReactDOM.findDOMNode(this.refs.firstRow).getBoundingClientRect().height +
+      1;
+    let pageSize = Math.max(
+      1,
+      Math.floor((this.props.height - headerHeight - footerHeight) / rowHeight),
+    );
+    if (this.state.pageSize !== pageSize) {
+      this.setState({ pageSize });
     }
+  }
+
+  render() {
+    const {
+      data,
+      onVisualizationClick,
+      visualizationIsClickable,
+      isPivoted,
+    } = this.props;
+    const { rows, cols } = data;
+
+    const { page, pageSize, sortColumn, sortDescending } = this.state;
+
+    let start = pageSize * page;
+    let end = Math.min(rows.length - 1, pageSize * (page + 1) - 1);
+
+    let rowIndexes = _.range(0, rows.length);
+    if (sortColumn != null) {
+      rowIndexes = _.sortBy(rowIndexes, rowIndex => rows[rowIndex][sortColumn]);
+      if (sortDescending) {
+        rowIndexes.reverse();
+      }
+    }
+
+    return (
+      <div className={cx(this.props.className, "relative flex flex-column")}>
+        <div className="flex-full relative">
+          <div
+            className="absolute top bottom left right scroll-x scroll-show scroll-show--hover"
+            style={{ overflowY: "hidden" }}
+          >
+            <table
+              className={cx(
+                styles.Table,
+                styles.TableSimple,
+                "fullscreen-normal-text",
+                "fullscreen-night-text",
+              )}
+            >
+              <thead ref="header">
+                <tr>
+                  {cols.map((col, colIndex) => (
+                    <th
+                      key={colIndex}
+                      className={cx(
+                        "TableInteractive-headerCellData cellData text-brand-hover",
+                        {
+                          "TableInteractive-headerCellData--sorted":
+                            sortColumn === colIndex,
+                          "text-right": isColumnRightAligned(col),
+                        },
+                      )}
+                      onClick={() => this.setSort(colIndex)}
+                    >
+                      <div className="relative">
+                        <Icon
+                          name={sortDescending ? "chevrondown" : "chevronup"}
+                          width={8}
+                          height={8}
+                          style={{
+                            position: "absolute",
+                            right: "100%",
+                            marginRight: 3,
+                          }}
+                        />
+                        <Ellipsified>{formatColumn(col)}</Ellipsified>
+                      </div>
+                    </th>
+                  ))}
+                </tr>
+              </thead>
+              <tbody>
+                {rowIndexes.slice(start, end + 1).map((rowIndex, index) => (
+                  <tr key={rowIndex} ref={index === 0 ? "firstRow" : null}>
+                    {rows[rowIndex].map((cell, columnIndex) => {
+                      const clicked = getTableCellClickedObject(
+                        data,
+                        rowIndex,
+                        columnIndex,
+                        isPivoted,
+                      );
+                      const isClickable =
+                        onVisualizationClick &&
+                        visualizationIsClickable(clicked);
+                      return (
+                        <td
+                          key={columnIndex}
+                          style={{ whiteSpace: "nowrap" }}
+                          className={cx("px1 border-bottom", {
+                            "text-right": isColumnRightAligned(
+                              cols[columnIndex],
+                            ),
+                          })}
+                        >
+                          <span
+                            className={cx({
+                              "cursor-pointer text-brand-hover": isClickable,
+                            })}
+                            onClick={
+                              isClickable &&
+                              (e => {
+                                onVisualizationClick({
+                                  ...clicked,
+                                  element: e.currentTarget,
+                                });
+                              })
+                            }
+                          >
+                            {cell == null
+                              ? "-"
+                              : formatValue(cell, {
+                                  column: cols[columnIndex],
+                                  jsx: true,
+                                })}
+                          </span>
+                        </td>
+                      );
+                    })}
+                  </tr>
+                ))}
+              </tbody>
+            </table>
+          </div>
+        </div>
+        {pageSize < rows.length ? (
+          <div
+            ref="footer"
+            className="p1 flex flex-no-shrink flex-align-right fullscreen-normal-text fullscreen-night-text"
+          >
+            <span className="text-bold">{t`Rows ${start + 1}-${end + 1} of ${
+              rows.length
+            }`}</span>
+            <span
+              className={cx("text-brand-hover px1 cursor-pointer", {
+                disabled: start === 0,
+              })}
+              onClick={() => this.setState({ page: page - 1 })}
+            >
+              <Icon name="left" size={10} />
+            </span>
+            <span
+              className={cx("text-brand-hover pr1 cursor-pointer", {
+                disabled: end + 1 >= rows.length,
+              })}
+              onClick={() => this.setState({ page: page + 1 })}
+            >
+              <Icon name="right" size={10} />
+            </span>
+          </div>
+        ) : null}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/TitleLegendHeader.jsx b/frontend/src/metabase/visualizations/components/TitleLegendHeader.jsx
index b6d4fe7f03e0ac6057ce81bb556529286975912a..b25d0fd5a967d78bf7d6fd9913ddd9a006a86ed1 100644
--- a/frontend/src/metabase/visualizations/components/TitleLegendHeader.jsx
+++ b/frontend/src/metabase/visualizations/components/TitleLegendHeader.jsx
@@ -2,35 +2,47 @@ import React from "react";
 import LegendHeader from "./LegendHeader.jsx";
 import _ from "underscore";
 
-export const TitleLegendHeader = ({ series, settings, onChangeCardAndRun, actionButtons }) => {
-    // $FlowFixMe
-    let originalSeries = series._raw || series;
-    let cardIds = _.uniq(originalSeries.map(s => s.card.id))
-    const isComposedOfMultipleQuestions = cardIds.length > 1;
+export const TitleLegendHeader = ({
+  series,
+  settings,
+  onChangeCardAndRun,
+  actionButtons,
+}) => {
+  // $FlowFixMe
+  let originalSeries = series._raw || series;
+  let cardIds = _.uniq(originalSeries.map(s => s.card.id));
+  const isComposedOfMultipleQuestions = cardIds.length > 1;
 
-    if (settings["card.title"]) {
-        const titleHeaderSeries = [{ card: {
-            name: settings["card.title"],
-            ...(isComposedOfMultipleQuestions ? {} : {
+  if (settings["card.title"]) {
+    const titleHeaderSeries = [
+      {
+        card: {
+          name: settings["card.title"],
+          ...(isComposedOfMultipleQuestions
+            ? {}
+            : {
                 id: cardIds[0],
-                dataset_query: originalSeries[0].card.dataset_query
-            }),
-        }}];
-
-        return (
-            <LegendHeader
-            className="flex-no-shrink"
-            series={titleHeaderSeries}
-            description={settings["card.description"]}
-            actionButtons={actionButtons}
-            // If a dashboard card is composed of multiple questions, its custom card title
-            // shouldn't act as a link as it's ambiguous that which question it should open
-            onChangeCardAndRun={ isComposedOfMultipleQuestions ? null : onChangeCardAndRun }
-        />
-        )
-    } else {
-        // If the title isn't provided in settings, render nothing
-        return null
-    }
-}
+                dataset_query: originalSeries[0].card.dataset_query,
+              }),
+        },
+      },
+    ];
 
+    return (
+      <LegendHeader
+        className="flex-no-shrink"
+        series={titleHeaderSeries}
+        description={settings["card.description"]}
+        actionButtons={actionButtons}
+        // If a dashboard card is composed of multiple questions, its custom card title
+        // shouldn't act as a link as it's ambiguous that which question it should open
+        onChangeCardAndRun={
+          isComposedOfMultipleQuestions ? null : onChangeCardAndRun
+        }
+      />
+    );
+  } else {
+    // If the title isn't provided in settings, render nothing
+    return null;
+  }
+};
diff --git a/frontend/src/metabase/visualizations/components/Visualization.jsx b/frontend/src/metabase/visualizations/components/Visualization.jsx
index 6dd16ae2963cafb20b6ed84583bd82fa848eab49..425ad081f9c84a890328dc8e1dd3300111f5a50d 100644
--- a/frontend/src/metabase/visualizations/components/Visualization.jsx
+++ b/frontend/src/metabase/visualizations/components/Visualization.jsx
@@ -9,426 +9,514 @@ import ChartClickActions from "metabase/visualizations/components/ChartClickActi
 import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
-import { t, jt } from 'c-3po';
+import { t, jt } from "c-3po";
 import { duration, formatNumber } from "metabase/lib/formatting";
 import MetabaseAnalytics from "metabase/lib/analytics";
 
-import { getVisualizationTransformed, extractRemappings } from "metabase/visualizations";
+import {
+  getVisualizationTransformed,
+  extractRemappings,
+} from "metabase/visualizations";
 import { getSettings } from "metabase/visualizations/lib/settings";
 import { isSameSeries } from "metabase/visualizations/lib/utils";
 
 import Utils from "metabase/lib/utils";
 import { datasetContainsNoResults } from "metabase/lib/dataset";
 
-import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
+import {
+  MinRowsError,
+  ChartSettingsError,
+} from "metabase/visualizations/lib/errors";
 
 import { assoc, setIn } from "icepick";
 import _ from "underscore";
 import cx from "classnames";
 
-export const ERROR_MESSAGE_GENERIC = "There was a problem displaying this chart.";
-export const ERROR_MESSAGE_PERMISSION = "Sorry, you don't have permission to see this card."
+export const ERROR_MESSAGE_GENERIC =
+  "There was a problem displaying this chart.";
+export const ERROR_MESSAGE_PERMISSION =
+  "Sorry, you don't have permission to see this card.";
 
 import Question from "metabase-lib/lib/Question";
-import type { Card as CardObject, VisualizationSettings } from "metabase/meta/types/Card";
-import type { HoverObject, ClickObject, Series, OnChangeCardAndRun } from "metabase/meta/types/Visualization";
+import type {
+  Card as CardObject,
+  VisualizationSettings,
+} from "metabase/meta/types/Card";
+import type {
+  HoverObject,
+  ClickObject,
+  Series,
+  OnChangeCardAndRun,
+} from "metabase/meta/types/Visualization";
 import Metadata from "metabase-lib/lib/metadata/Metadata";
 
 type Props = {
-    rawSeries: Series,
+  rawSeries: Series,
 
-    className: string,
+  className: string,
 
-    showTitle: boolean,
-    isDashboard: boolean,
-    isEditing: boolean,
+  showTitle: boolean,
+  isDashboard: boolean,
+  isEditing: boolean,
 
-    actionButtons: Element<any>,
+  actionButtons: Element<any>,
 
-    // errors
-    error: string,
-    errorIcon: string,
+  // errors
+  error: string,
+  errorIcon: string,
 
-    // slow card warnings
-    isSlow: boolean,
-    expectedDuration: number,
+  // slow card warnings
+  isSlow: boolean,
+  expectedDuration: number,
 
-    // injected by ExplicitSize
-    width: number,
-    height: number,
+  // injected by ExplicitSize
+  width: number,
+  height: number,
 
-    // settings overrides from settings panel
-    settings: VisualizationSettings,
+  // settings overrides from settings panel
+  settings: VisualizationSettings,
 
-    // for click actions
-    metadata: Metadata,
-    onChangeCardAndRun: OnChangeCardAndRun,
+  // for click actions
+  metadata: Metadata,
+  onChangeCardAndRun: OnChangeCardAndRun,
 
-    // used for showing content in place of visualization, e.x. dashcard filter mapping
-    replacementContent: Element<any>,
+  // used for showing content in place of visualization, e.x. dashcard filter mapping
+  replacementContent: Element<any>,
 
-    // misc
-    onUpdateWarnings: (string[]) => void,
-    onOpenChartSettings: () => void,
+  // misc
+  onUpdateWarnings: (string[]) => void,
+  onOpenChartSettings: () => void,
 
-    // number of grid cells wide and tall
-    gridSize?: { width: number, height: number },
-    // if gridSize isn't specified, compute using this gridSize (4x width, 3x height)
-    gridUnit?: number,
-}
+  // number of grid cells wide and tall
+  gridSize?: { width: number, height: number },
+  // if gridSize isn't specified, compute using this gridSize (4x width, 3x height)
+  gridUnit?: number,
+};
 
 type State = {
-    series: ?Series,
-    CardVisualization: ?(Component<void, VisualizationSettings, void> & {
-        checkRenderable: (any, any) => void,
-        noHeader: boolean
-    }),
-
-    hovered: ?HoverObject,
-    clicked: ?ClickObject,
-
-    error: ?Error,
-    warnings: string[],
-    yAxisSplit: ?number[][],
-}
+  series: ?Series,
+  CardVisualization: ?(Component<void, VisualizationSettings, void> & {
+    checkRenderable: (any, any) => void,
+    noHeader: boolean,
+  }),
+
+  hovered: ?HoverObject,
+  clicked: ?ClickObject,
+
+  error: ?Error,
+  warnings: string[],
+  yAxisSplit: ?(number[][]),
+};
 
 @ExplicitSize
 export default class Visualization extends Component {
-    state: State;
-    props: Props;
-
-    _resetHoverTimer: ?number;
-
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            hovered: null,
-            clicked: null,
-            error: null,
-            warnings: [],
-            yAxisSplit: null,
-            series: null,
-            CardVisualization: null
-        };
+  state: State;
+  props: Props;
+
+  _resetHoverTimer: ?number;
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      hovered: null,
+      clicked: null,
+      error: null,
+      warnings: [],
+      yAxisSplit: null,
+      series: null,
+      CardVisualization: null,
+    };
+  }
+
+  static defaultProps = {
+    showTitle: false,
+    isDashboard: false,
+    isEditing: false,
+    onUpdateVisualizationSettings: (...args) =>
+      console.warn("onUpdateVisualizationSettings", args),
+  };
+
+  componentWillMount() {
+    this.transform(this.props);
+  }
+
+  componentWillReceiveProps(newProps) {
+    if (
+      !isSameSeries(newProps.rawSeries, this.props.rawSeries) ||
+      !Utils.equals(newProps.settings, this.props.settings)
+    ) {
+      this.transform(newProps);
     }
+  }
 
-    static defaultProps = {
-        showTitle: false,
-        isDashboard: false,
-        isEditing: false,
-        onUpdateVisualizationSettings: (...args) => console.warn("onUpdateVisualizationSettings", args)
-    };
+  componentDidMount() {
+    this.updateWarnings();
+  }
 
-    componentWillMount() {
-        this.transform(this.props);
+  componentDidUpdate(prevProps, prevState) {
+    if (
+      !Utils.equals(this.getWarnings(prevProps, prevState), this.getWarnings())
+    ) {
+      this.updateWarnings();
     }
-
-    componentWillReceiveProps(newProps) {
-        if (!isSameSeries(newProps.rawSeries, this.props.rawSeries) || !Utils.equals(newProps.settings, this.props.settings)) {
-            this.transform(newProps);
-        }
+  }
+
+  // $FlowFixMe
+  getWarnings(props = this.props, state = this.state) {
+    let warnings = state.warnings || [];
+    // don't warn about truncated data for table since we show a warning in the row count
+    if (state.series[0].card.display !== "table") {
+      warnings = warnings.concat(
+        props.rawSeries
+          .filter(s => s.data && s.data.rows_truncated != null)
+          .map(
+            s =>
+              t`Data truncated to ${formatNumber(s.data.rows_truncated)} rows.`,
+          ),
+      );
     }
+    return warnings;
+  }
 
-    componentDidMount() {
-        this.updateWarnings();
+  updateWarnings() {
+    if (this.props.onUpdateWarnings) {
+      this.props.onUpdateWarnings(this.getWarnings() || []);
     }
-
-    componentDidUpdate(prevProps, prevState) {
-        if (!Utils.equals(this.getWarnings(prevProps, prevState), this.getWarnings())) {
-            this.updateWarnings();
-        }
+  }
+
+  transform(newProps) {
+    this.setState({
+      hovered: null,
+      clicked: null,
+      error: null,
+      warnings: [],
+      yAxisSplit: null,
+      ...getVisualizationTransformed(extractRemappings(newProps.rawSeries)),
+    });
+  }
+
+  handleHoverChange = hovered => {
+    if (hovered) {
+      const { yAxisSplit } = this.state;
+      // if we have Y axis split info then find the Y axis index (0 = left, 1 = right)
+      if (yAxisSplit) {
+        const axisIndex = _.findIndex(yAxisSplit, indexes =>
+          _.contains(indexes, hovered.index),
+        );
+        hovered = assoc(hovered, "axisIndex", axisIndex);
+      }
+      this.setState({ hovered });
+      // If we previously set a timeout for clearing the hover clear it now since we received
+      // a new hover.
+      if (this._resetHoverTimer !== null) {
+        clearTimeout(this._resetHoverTimer);
+        this._resetHoverTimer = null;
+      }
+    } else {
+      // When reseting the hover wait in case we're simply transitioning from one
+      // element to another. This allows visualizations to use mouseleave events etc.
+      this._resetHoverTimer = setTimeout(() => {
+        this.setState({ hovered: null });
+        this._resetHoverTimer = null;
+      }, 0);
     }
+  };
 
-    // $FlowFixMe
-    getWarnings(props = this.props, state = this.state) {
-        let warnings = state.warnings || [];
-        // don't warn about truncated data for table since we show a warning in the row count
-        if (state.series[0].card.display !== "table") {
-            warnings = warnings.concat(props.rawSeries
-                .filter(s => s.data && s.data.rows_truncated != null)
-                .map(s => t`Data truncated to ${formatNumber(s.data.rows_truncated)} rows.`));
-        }
-        return warnings;
+  getClickActions(clicked: ?ClickObject) {
+    if (!clicked) {
+      return [];
     }
-
-    updateWarnings() {
-        if (this.props.onUpdateWarnings) {
-            this.props.onUpdateWarnings(this.getWarnings() || []);
-        }
+    // TODO: push this logic into Question?
+    const { rawSeries, metadata } = this.props;
+    const seriesIndex = clicked.seriesIndex || 0;
+    const card = rawSeries[seriesIndex].card;
+    const question = new Question(metadata, card);
+    const mode = question.mode();
+    return mode ? mode.actionsForClick(clicked, {}) : [];
+  }
+
+  visualizationIsClickable = (clicked: ClickObject) => {
+    const { onChangeCardAndRun } = this.props;
+    if (!onChangeCardAndRun) {
+      return false;
     }
-
-    transform(newProps) {
-        this.setState({
-            hovered: null,
-            clicked: null,
-            error: null,
-            warnings: [],
-            yAxisSplit: null,
-            ...getVisualizationTransformed(extractRemappings(newProps.rawSeries))
-        });
+    try {
+      return this.getClickActions(clicked).length > 0;
+    } catch (e) {
+      console.warn(e);
+      return false;
     }
-
-    handleHoverChange = (hovered) => {
-        if (hovered) {
-            const { yAxisSplit } = this.state;
-            // if we have Y axis split info then find the Y axis index (0 = left, 1 = right)
-            if (yAxisSplit) {
-                const axisIndex = _.findIndex(yAxisSplit, (indexes) => _.contains(indexes, hovered.index));
-                hovered = assoc(hovered, "axisIndex", axisIndex);
-            }
-            this.setState({ hovered });
-            // If we previously set a timeout for clearing the hover clear it now since we received
-            // a new hover.
-            if (this._resetHoverTimer !== null) {
-                clearTimeout(this._resetHoverTimer);
-                this._resetHoverTimer = null;
-            }
-        } else {
-            // When reseting the hover wait in case we're simply transitioning from one
-            // element to another. This allows visualizations to use mouseleave events etc.
-            this._resetHoverTimer = setTimeout(() => {
-                this.setState({ hovered: null });
-                this._resetHoverTimer = null;
-            }, 0);
-        }
+  };
+
+  handleVisualizationClick = (clicked: ClickObject) => {
+    if (clicked) {
+      MetabaseAnalytics.trackEvent(
+        "Actions",
+        "Clicked",
+        `${clicked.column ? "column" : ""} ${clicked.value ? "value" : ""} ${
+          clicked.dimensions ? "dimensions=" + clicked.dimensions.length : ""
+        }`,
+      );
     }
 
-    getClickActions(clicked: ?ClickObject) {
-        if (!clicked) {
-            return [];
-        }
-        // TODO: push this logic into Question?
-        const { rawSeries, metadata } = this.props;
-        const seriesIndex = clicked.seriesIndex || 0;
-        const card = rawSeries[seriesIndex].card;
-        const question = new Question(metadata, card);
-        const mode = question.mode();
-        return mode ? mode.actionsForClick(clicked, {}) : [];
+    // needs to be delayed so we don't clear it when switching from one drill through to another
+    setTimeout(() => {
+      this.setState({ clicked });
+    }, 100);
+  };
+
+  // Add the underlying card of current series to onChangeCardAndRun if available
+  handleOnChangeCardAndRun = ({
+    nextCard,
+    seriesIndex,
+  }: {
+    nextCard: CardObject,
+    seriesIndex: number,
+  }) => {
+    const { series, clicked } = this.state;
+
+    const index = seriesIndex || (clicked && clicked.seriesIndex) || 0;
+    const previousCard: ?CardObject =
+      series && series[index] && series[index].card;
+
+    this.props.onChangeCardAndRun({ nextCard, previousCard });
+  };
+
+  onRender = ({ yAxisSplit, warnings = [] } = {}) => {
+    this.setState({ yAxisSplit, warnings });
+  };
+
+  onRenderError = error => {
+    this.setState({ error });
+  };
+
+  hideActions = () => {
+    this.setState({ clicked: null });
+  };
+
+  render() {
+    const {
+      actionButtons,
+      className,
+      showTitle,
+      isDashboard,
+      width,
+      height,
+      errorIcon,
+      isSlow,
+      expectedDuration,
+      replacementContent,
+    } = this.props;
+    const { series, CardVisualization } = this.state;
+    const small = width < 330;
+
+    let { hovered, clicked } = this.state;
+
+    const clickActions = this.getClickActions(clicked);
+    if (clickActions.length > 0) {
+      hovered = null;
     }
 
-    visualizationIsClickable = (clicked: ClickObject) => {
-        const { onChangeCardAndRun } = this.props;
-        if (!onChangeCardAndRun) {
-            return false;
-        }
+    let error = this.props.error || this.state.error;
+    let loading = !(
+      series &&
+      series.length > 0 &&
+      _.every(
+        series,
+        s => s.data || _.isObject(s.card.visualization_settings.virtual_card),
+      )
+    );
+    let noResults = false;
+
+    // don't try to load settings unless data is loaded
+    let settings = this.props.settings || {};
+
+    if (!loading && !error) {
+      settings = this.props.settings || getSettings(series);
+      if (!CardVisualization) {
+        error = t`Could not find visualization`;
+      } else {
         try {
-            return this.getClickActions(clicked).length > 0;
+          if (CardVisualization.checkRenderable) {
+            CardVisualization.checkRenderable(series, settings);
+          }
         } catch (e) {
-            console.warn(e);
-            return false;
-        }
-    }
-
-    handleVisualizationClick = (clicked: ClickObject) => {
-        if (clicked) {
-            MetabaseAnalytics.trackEvent(
-                "Actions",
-                "Clicked",
-                `${clicked.column ? "column" : ""} ${clicked.value ? "value" : ""} ${clicked.dimensions ? "dimensions=" + clicked.dimensions.length : ""}`
+          error = e.message || t`Could not display this chart with this data.`;
+          if (
+            e instanceof ChartSettingsError &&
+            this.props.onOpenChartSettings
+          ) {
+            error = (
+              <div>
+                <div>{error}</div>
+                <div className="mt2">
+                  <button
+                    className="Button Button--primary Button--medium"
+                    onClick={this.props.onOpenChartSettings}
+                  >
+                    {e.buttonText}
+                  </button>
+                </div>
+              </div>
             );
+          } else if (e instanceof MinRowsError) {
+            noResults = true;
+          }
         }
-
-        // needs to be delayed so we don't clear it when switching from one drill through to another
-        setTimeout(() => {
-            this.setState({ clicked });
-        }, 100);
-    };
-
-    // Add the underlying card of current series to onChangeCardAndRun if available
-    handleOnChangeCardAndRun = ({ nextCard, seriesIndex }: { nextCard: CardObject, seriesIndex: number }) => {
-        const { series, clicked } = this.state;
-
-        const index = seriesIndex || (clicked && clicked.seriesIndex) || 0;
-        const previousCard: ?CardObject = series && series[index] && series[index].card;
-
-        this.props.onChangeCardAndRun({ nextCard, previousCard });
+      }
     }
 
-    onRender = ({ yAxisSplit, warnings = [] } = {}) => {
-        this.setState({ yAxisSplit, warnings });
+    if (!error) {
+      noResults = _.every(
+        // $FlowFixMe
+        series,
+        s => s && s.data && datasetContainsNoResults(s.data),
+      );
     }
 
-    onRenderError = (error) => {
-        this.setState({ error })
+    let extra = (
+      <span className="flex align-center">
+        {isSlow &&
+          !loading && (
+            <LoadingSpinner
+              size={18}
+              className={cx(
+                "Visualization-slow-spinner",
+                isSlow === "usually-slow" ? "text-gold" : "text-slate",
+              )}
+            />
+          )}
+        {actionButtons}
+      </span>
+    );
+
+    let { gridSize, gridUnit } = this.props;
+    if (!gridSize && gridUnit) {
+      gridSize = {
+        width: Math.round(width / (gridUnit * 4)),
+        height: Math.round(height / (gridUnit * 3)),
+      };
     }
 
-    hideActions = () => {
-        this.setState({ clicked: null })
-    }
-
-    render() {
-        const { actionButtons, className, showTitle, isDashboard, width, height, errorIcon, isSlow, expectedDuration, replacementContent } = this.props;
-        const { series, CardVisualization } = this.state;
-        const small = width < 330;
-
-        let { hovered, clicked } = this.state;
-
-        const clickActions = this.getClickActions(clicked);
-        if (clickActions.length > 0) {
-            hovered = null;
-        }
-
-        let error = this.props.error || this.state.error;
-        let loading = !(series && series.length > 0 && _.every(series, (s) => s.data || _.isObject(s.card.visualization_settings.virtual_card)));
-        let noResults = false;
-
-        // don't try to load settings unless data is loaded
-        let settings = this.props.settings || {};
-
-        if (!loading && !error) {
-            settings = this.props.settings || getSettings(series);
-            if (!CardVisualization) {
-                error = t`Could not find visualization`;
-            } else {
-                try {
-                    if (CardVisualization.checkRenderable) {
-                        CardVisualization.checkRenderable(series, settings);
-                    }
-                } catch (e) {
-                    error = e.message || t`Could not display this chart with this data.`;
-                    if (e instanceof ChartSettingsError && this.props.onOpenChartSettings) {
-                        error = (
-                            <div>
-                                <div>{error}</div>
-                                <div className="mt2">
-                                    <button className="Button Button--primary Button--medium" onClick={this.props.onOpenChartSettings}>
-                                        {e.buttonText}
-                                    </button>
-                                </div>
-                            </div>
-                        );
-                    } else if (e instanceof MinRowsError) {
-                        noResults = true;
-                    }
-                }
+    return (
+      <div className={cx(className, "flex flex-column")}>
+        {(showTitle &&
+          (settings["card.title"] || extra) &&
+          (loading ||
+            error ||
+            noResults ||
+            !(CardVisualization && CardVisualization.noHeader))) ||
+        replacementContent ? (
+          <div className="p1 flex-no-shrink">
+            <LegendHeader
+              series={
+                settings["card.title"]
+                  ? // if we have a card title set, use it
+                    // $FlowFixMe
+                    setIn(series, [0, "card", "name"], settings["card.title"])
+                  : // otherwise use the original series
+                    series
+              }
+              actionButtons={extra}
+              description={settings["card.description"]}
+              settings={settings}
+              onChangeCardAndRun={
+                this.props.onChangeCardAndRun
+                  ? this.handleOnChangeCardAndRun
+                  : null
+              }
+            />
+          </div>
+        ) : null}
+        {replacementContent ? (
+          replacementContent
+        ) : // on dashboards we should show the "No results!" warning if there are no rows or there's a MinRowsError and actualRows === 0
+        isDashboard && noResults ? (
+          <div
+            className={
+              "flex-full px1 pb1 text-centered flex flex-column layout-centered " +
+              (isDashboard ? "text-slate-light" : "text-slate")
             }
-        }
-
-        if (!error) {
+          >
+            <Tooltip tooltip={t`No results!`} isEnabled={small}>
+              <img src="../app/assets/img/no_results.svg" />
+            </Tooltip>
+            {!small && <span className="h4 text-bold">No results!</span>}
+          </div>
+        ) : error ? (
+          <div
+            className={
+              "flex-full px1 pb1 text-centered flex flex-column layout-centered " +
+              (isDashboard ? "text-slate-light" : "text-slate")
+            }
+          >
+            <Tooltip tooltip={error} isEnabled={small}>
+              <Icon className="mb2" name={errorIcon || "warning"} size={50} />
+            </Tooltip>
+            {!small && <span className="h4 text-bold">{error}</span>}
+          </div>
+        ) : loading ? (
+          <div className="flex-full p1 text-centered text-brand flex flex-column layout-centered">
+            {isSlow ? (
+              <div className="text-slate">
+                <div className="h4 text-bold mb1">{t`Still Waiting...`}</div>
+                {isSlow === "usually-slow" ? (
+                  <div>
+                    {jt`This usually takes an average of ${(
+                      <span style={{ whiteSpace: "nowrap" }}>
+                        {duration(expectedDuration)}
+                      </span>
+                    )}.`}
+                    <br />
+                    {t`(This is a bit long for a dashboard)`}
+                  </div>
+                ) : (
+                  <div>
+                    {t`This is usually pretty fast, but seems to be taking awhile right now.`}
+                  </div>
+                )}
+              </div>
+            ) : (
+              <LoadingSpinner className="text-slate" />
+            )}
+          </div>
+        ) : (
+          // $FlowFixMe
+          <CardVisualization
+            {...this.props}
+            className="flex-full"
+            series={series}
+            settings={settings}
             // $FlowFixMe
-            noResults = _.every(series, s => s && s.data && datasetContainsNoResults(s.data));
-        }
-
-        let extra = (
-            <span className="flex align-center">
-                {isSlow && !loading &&
-                    <LoadingSpinner size={18} className={cx("Visualization-slow-spinner", isSlow === "usually-slow" ? "text-gold" : "text-slate")}/>
-                }
-                {actionButtons}
-            </span>
-        );
-
-        let { gridSize, gridUnit } = this.props;
-        if (!gridSize && gridUnit) {
-            gridSize = {
-                width: Math.round(width / (gridUnit * 4)),
-                height: Math.round(height / (gridUnit * 3)),
-            };
-        }
-
-        return (
-            <div className={cx(className, "flex flex-column")}>
-                { showTitle && (settings["card.title"] || extra) && (loading || error || noResults || !(CardVisualization && CardVisualization.noHeader)) || replacementContent ?
-                    <div className="p1 flex-no-shrink">
-                        <LegendHeader
-                            series={
-                                settings["card.title"] ?
-                                    // if we have a card title set, use it
-                                    // $FlowFixMe
-                                    setIn(series, [0, "card", "name"], settings["card.title"]) :
-                                    // otherwise use the original series
-                                    series
-                            }
-                            actionButtons={extra}
-                            description={settings["card.description"]}
-                            settings={settings}
-                            onChangeCardAndRun={this.props.onChangeCardAndRun ? this.handleOnChangeCardAndRun : null}
-                        />
-                    </div>
+            card={series[0].card} // convenience for single-series visualizations
+            // $FlowFixMe
+            data={series[0].data} // convenience for single-series visualizations
+            hovered={hovered}
+            onHoverChange={this.handleHoverChange}
+            onVisualizationClick={this.handleVisualizationClick}
+            visualizationIsClickable={this.visualizationIsClickable}
+            onRenderError={this.onRenderError}
+            onRender={this.onRender}
+            onActionDismissal={this.hideActions}
+            gridSize={gridSize}
+            onChangeCardAndRun={
+              this.props.onChangeCardAndRun
+                ? this.handleOnChangeCardAndRun
                 : null
-                }
-                { replacementContent ?
-                    replacementContent
-                // on dashboards we should show the "No results!" warning if there are no rows or there's a MinRowsError and actualRows === 0
-                : isDashboard && noResults ?
-                    <div className={"flex-full px1 pb1 text-centered flex flex-column layout-centered " + (isDashboard ? "text-slate-light" : "text-slate")}>
-                        <Tooltip tooltip={t`No results!`} isEnabled={small}>
-                            <img src="../app/assets/img/no_results.svg" />
-                        </Tooltip>
-                        { !small &&
-                            <span className="h4 text-bold">
-                                No results!
-                            </span>
-                        }
-                    </div>
-                : error ?
-                    <div className={"flex-full px1 pb1 text-centered flex flex-column layout-centered " + (isDashboard ? "text-slate-light" : "text-slate")}>
-                        <Tooltip tooltip={error} isEnabled={small}>
-                            <Icon className="mb2" name={errorIcon || "warning"} size={50} />
-                        </Tooltip>
-                        { !small &&
-                            <span className="h4 text-bold">
-                                {error}
-                            </span>
-                        }
-                    </div>
-                : loading ?
-                    <div className="flex-full p1 text-centered text-brand flex flex-column layout-centered">
-                        { isSlow ?
-                            <div className="text-slate">
-                                <div className="h4 text-bold mb1">{t`Still Waiting...`}</div>
-                                { isSlow === "usually-slow" ?
-                                    <div>
-                                        {jt`This usually takes an average of ${<span style={{whiteSpace: "nowrap"}}>{duration(expectedDuration)}</span>}.`}
-                                        <br />
-                                        {t`(This is a bit long for a dashboard)`}
-                                    </div>
-                                :
-                                    <div>
-                                        {t`This is usually pretty fast, but seems to be taking awhile right now.`}
-                                    </div>
-                                }
-                            </div>
-                        :
-                            <LoadingSpinner className="text-slate" />
-                        }
-                    </div>
-                :
-                    // $FlowFixMe
-                    <CardVisualization
-                        {...this.props}
-                        className="flex-full"
-                        series={series}
-                        settings={settings}
-                        // $FlowFixMe
-                        card={series[0].card} // convenience for single-series visualizations
-                        // $FlowFixMe
-                        data={series[0].data} // convenience for single-series visualizations
-                        hovered={hovered}
-                        onHoverChange={this.handleHoverChange}
-                        onVisualizationClick={this.handleVisualizationClick}
-                        visualizationIsClickable={this.visualizationIsClickable}
-                        onRenderError={this.onRenderError}
-                        onRender={this.onRender}
-                        onActionDismissal={this.hideActions}
-                        gridSize={gridSize}
-                        onChangeCardAndRun={this.props.onChangeCardAndRun ? this.handleOnChangeCardAndRun : null}
-                    />
-                }
-                <ChartTooltip
-                    series={series}
-                    hovered={hovered}
-                />
-                { this.props.onChangeCardAndRun &&
-                    <ChartClickActions
-                        clicked={clicked}
-                        clickActions={clickActions}
-                        onChangeCardAndRun={this.handleOnChangeCardAndRun}
-                        onClose={this.hideActions}
-                    />
-                }
-            </div>
-        );
-    }
+            }
+          />
+        )}
+        <ChartTooltip series={series} hovered={hovered} />
+        {this.props.onChangeCardAndRun && (
+          <ChartClickActions
+            clicked={clicked}
+            clickActions={clickActions}
+            onChangeCardAndRun={this.handleOnChangeCardAndRun}
+            onClose={this.hideActions}
+          />
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx
index 1cbd1d953792156272f9fecf783c70e6be3bba76..901f91ec780561618515fdfede8f298dac64d516 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorPicker.jsx
@@ -3,12 +3,12 @@ import React, { Component } from "react";
 import ColorPicker from "metabase/components/ColorPicker";
 
 export default class ChartSettingColorPicker extends Component {
-    render() {
-        return (
-            <div className="flex align-center mb1">
-                <ColorPicker {...this.props} triggerSize={12} />
-                {this.props.title && <h4 className="ml1">{this.props.title}</h4>}
-            </div>
-        );
-    }
+  render() {
+    return (
+      <div className="flex align-center mb1">
+        <ColorPicker {...this.props} triggerSize={12} />
+        {this.props.title && <h4 className="ml1">{this.props.title}</h4>}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx
index ff7d45d05ea2087d6ec0cc17df17deaf4f04e347..3a458bf2a0924d7af560ccbb341cb9e43fccde7e 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx
@@ -3,25 +3,25 @@ import React, { Component } from "react";
 import ChartSettingColorPicker from "./ChartSettingColorPicker.jsx";
 
 export default class ChartSettingColorsPicker extends Component {
-    render() {
-        const { value, onChange, seriesTitles } = this.props;
-        return (
-            <div>
-                { seriesTitles.map((title, index) =>
-                    <ChartSettingColorPicker
-                        key={index}
-                        onChange={color =>
-                            onChange([
-                                ...value.slice(0, index),
-                                color,
-                                ...value.slice(index + 1)
-                            ])
-                        }
-                        title={title}
-                        value={value[index]}
-                    />
-                )}
-            </div>
-        );
-    }
+  render() {
+    const { value, onChange, seriesTitles } = this.props;
+    return (
+      <div>
+        {seriesTitles.map((title, index) => (
+          <ChartSettingColorPicker
+            key={index}
+            onChange={color =>
+              onChange([
+                ...value.slice(0, index),
+                color,
+                ...value.slice(index + 1),
+              ])
+            }
+            title={title}
+            value={value[index]}
+          />
+        ))}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
index 7f69d72cfb877b89febacfd555b7d7873fae7712..f503df8cd92fe450f2854d85c5c55ffe7cc4aabe 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldPicker.jsx
@@ -1,29 +1,30 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon";
 import cx from "classnames";
 
 import ChartSettingSelect from "./ChartSettingSelect.jsx";
 
-const ChartSettingFieldPicker = ({ value, options, onChange, onRemove }) =>
-    <div className="flex align-center">
-        <ChartSettingSelect
-            value={value}
-            options={options}
-            onChange={onChange}
-            placeholder={t`Select a field`}
-            placeholderNoOptions={t`No valid fields`}
-            isInitiallyOpen={value === undefined}
-        />
-        <Icon
-            name="close"
-            className={cx("ml1 text-grey-4 text-brand-hover cursor-pointer", {
-                "disabled hidden": !onRemove
-            })}
-            width={12} height={12}
-            onClick={onRemove}
-        />
-    </div>
-
+const ChartSettingFieldPicker = ({ value, options, onChange, onRemove }) => (
+  <div className="flex align-center">
+    <ChartSettingSelect
+      value={value}
+      options={options}
+      onChange={onChange}
+      placeholder={t`Select a field`}
+      placeholderNoOptions={t`No valid fields`}
+      isInitiallyOpen={value === undefined}
+    />
+    <Icon
+      name="close"
+      className={cx("ml1 text-grey-4 text-brand-hover cursor-pointer", {
+        "disabled hidden": !onRemove,
+      })}
+      width={12}
+      height={12}
+      onClick={onRemove}
+    />
+  </div>
+);
 
 export default ChartSettingFieldPicker;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
index 8c5d0a4aa638aa9b70dc7c0a4e50de0fd26c1612..9234a3f31e35cf89928ba1b9b9130faf77c8cf68 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingFieldsPicker.jsx
@@ -1,47 +1,65 @@
 import React from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ChartSettingFieldPicker from "./ChartSettingFieldPicker.jsx";
 
-const ChartSettingFieldsPicker = ({ value = [], options, onChange, addAnother }) =>
-    <div>
-        { Array.isArray(value) ? value.map((v, index) =>
-            <ChartSettingFieldPicker
-                key={index}
-                value={v}
-                options={options}
-                onChange={(v) => {
-                    let newValue = [...value];
-                    // this swaps the position of the existing value
-                    let existingIndex = value.indexOf(v);
-                    if (existingIndex >= 0) {
-                        newValue.splice(existingIndex, 1, value[index]);
-                    }
-                    // replace with the new value
-                    newValue.splice(index, 1, v);
-                    onChange(newValue);
-                }}
-                onRemove={value.filter(v => v != null).length > 1 || (value.length > 1 && v == null) ?
-                    () => onChange([...value.slice(0, index), ...value.slice(index + 1)]) :
-                    null
-                }
-            />
-        ) : <span className="text-error">{t`error`}</span>}
-        { addAnother &&
-            <div className="mt1">
-                <a onClick={() => {
-                    const remaining = options.filter(o => value.indexOf(o.value) < 0);
-                    if (remaining.length === 1) {
-                        // if there's only one unused option, use it
-                        onChange(value.concat([remaining[0].value]));
-                    } else {
-                        // otherwise leave it blank
-                        onChange(value.concat([undefined]));
-                    }
-                }}>
-                    {addAnother}
-                </a>
-            </div>
-        }
-    </div>
+const ChartSettingFieldsPicker = ({
+  value = [],
+  options,
+  onChange,
+  addAnother,
+}) => (
+  <div>
+    {Array.isArray(value) ? (
+      value.map((v, index) => (
+        <ChartSettingFieldPicker
+          key={index}
+          value={v}
+          options={options}
+          onChange={v => {
+            let newValue = [...value];
+            // this swaps the position of the existing value
+            let existingIndex = value.indexOf(v);
+            if (existingIndex >= 0) {
+              newValue.splice(existingIndex, 1, value[index]);
+            }
+            // replace with the new value
+            newValue.splice(index, 1, v);
+            onChange(newValue);
+          }}
+          onRemove={
+            value.filter(v => v != null).length > 1 ||
+            (value.length > 1 && v == null)
+              ? () =>
+                  onChange([
+                    ...value.slice(0, index),
+                    ...value.slice(index + 1),
+                  ])
+              : null
+          }
+        />
+      ))
+    ) : (
+      <span className="text-error">{t`error`}</span>
+    )}
+    {addAnother && (
+      <div className="mt1">
+        <a
+          onClick={() => {
+            const remaining = options.filter(o => value.indexOf(o.value) < 0);
+            if (remaining.length === 1) {
+              // if there's only one unused option, use it
+              onChange(value.concat([remaining[0].value]));
+            } else {
+              // otherwise leave it blank
+              onChange(value.concat([undefined]));
+            }
+          }}
+        >
+          {addAnother}
+        </a>
+      </div>
+    )}
+  </div>
+);
 
 export default ChartSettingFieldsPicker;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx
index 1f1fcfae14e1ea092a185c3a732f58334a948cfa..c5bdbe249f26fae3a0c7afecdfe260369715918e 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInput.jsx
@@ -1,10 +1,11 @@
 import React from "react";
 
-const ChartSettingInput = ({ value, onChange }) =>
-    <input
-        className="input block full"
-        value={value}
-        onChange={(e) => onChange(e.target.value)}
-    />
+const ChartSettingInput = ({ value, onChange }) => (
+  <input
+    className="input block full"
+    value={value}
+    onChange={e => onChange(e.target.value)}
+  />
+);
 
 export default ChartSettingInput;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputGroup.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputGroup.jsx
index 8688e800371a62e0b31ab88214ae1a860c19f957..0b36099d15a204790568be0ce6dbae729a730749 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputGroup.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputGroup.jsx
@@ -4,25 +4,21 @@ import Input from "metabase/components/Input.jsx";
 
 // value is an array of strings. This component provides one input box per string
 export default function ChartSettingInputGroup({ value: values, onChange }) {
-    const inputs = values.map((str, i) => (
-        <Input
-            key={i}
-            className="input block full mb1"
-            value={str}
-            onBlurChange={(e) => {
-                const newStr = e.target.value.trim();
-                if (!newStr || !newStr.length) return;
-                // clone the original values array. It's read-only so we can't just replace the one value we want
-                const newValues = values.slice();
-                newValues[i] = newStr;
-                onChange(newValues);
-            }}
-        />
-    ));
+  const inputs = values.map((str, i) => (
+    <Input
+      key={i}
+      className="input block full mb1"
+      value={str}
+      onBlurChange={e => {
+        const newStr = e.target.value.trim();
+        if (!newStr || !newStr.length) return;
+        // clone the original values array. It's read-only so we can't just replace the one value we want
+        const newValues = values.slice();
+        newValues[i] = newStr;
+        onChange(newValues);
+      }}
+    />
+  ));
 
-    return (
-        <div>
-            {inputs}
-        </div>
-    );
+  return <div>{inputs}</div>;
 }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx
index 9341f85c8423a90722840fda1523a7d2485b815f..e99bb42099b2740b5d0a9f1758a1509bfe47c8fb 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingInputNumeric.jsx
@@ -3,39 +3,44 @@ import React, { Component } from "react";
 import cx from "classnames";
 
 export default class ChartSettingInputNumeric extends Component {
-    constructor(props, context) {
-        super(props, context);
-        this.state = {
-            value: String(props.value == null ? "" : props.value)
-        };
-    }
+  constructor(props, context) {
+    super(props, context);
+    this.state = {
+      value: String(props.value == null ? "" : props.value),
+    };
+  }
 
-    componentWillReceiveProps(nextProps) {
-        this.setState({ value: String(nextProps.value == null ? "" : nextProps.value) });
-    }
+  componentWillReceiveProps(nextProps) {
+    this.setState({
+      value: String(nextProps.value == null ? "" : nextProps.value),
+    });
+  }
 
-    render() {
-        const { onChange } = this.props;
-        return (
-            <input
-                className={cx("input block full", { "border-error": this.state.value !== "" && isNaN(parseFloat(this.state.value)) })}
-                value={this.state.value}
-                onChange={(e) => {
-                    let num = parseFloat(e.target.value);
-                    if (!isNaN(num) && num !== this.props.value) {
-                        onChange(num);
-                    }
-                    this.setState({ value: e.target.value });
-                }}
-                onBlur={(e) => {
-                    let num = parseFloat(e.target.value);
-                    if (isNaN(num)) {
-                        onChange(undefined);
-                    } else {
-                        onChange(num);
-                    }
-                }}
-            />
-        );
-    }
+  render() {
+    const { onChange } = this.props;
+    return (
+      <input
+        className={cx("input block full", {
+          "border-error":
+            this.state.value !== "" && isNaN(parseFloat(this.state.value)),
+        })}
+        value={this.state.value}
+        onChange={e => {
+          let num = parseFloat(e.target.value);
+          if (!isNaN(num) && num !== this.props.value) {
+            onChange(num);
+          }
+          this.setState({ value: e.target.value });
+        }}
+        onBlur={e => {
+          let num = parseFloat(e.target.value);
+          if (isNaN(num)) {
+            onChange(undefined);
+          } else {
+            onChange(num);
+          }
+        }}
+      />
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
index 0b195c3bb95b6b5febb10589f1828e52c93d3971..a55ed1b539da338136c84399a9f8df349f20008a 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx
@@ -1,7 +1,7 @@
 import React, { Component } from "react";
 
-import CheckBox     from "metabase/components/CheckBox.jsx";
-import Icon         from "metabase/components/Icon.jsx";
+import CheckBox from "metabase/components/CheckBox.jsx";
+import Icon from "metabase/components/Icon.jsx";
 import { sortable } from "react-sortable";
 
 import cx from "classnames";
@@ -10,86 +10,111 @@ import cx from "classnames";
 class OrderedFieldListItem extends Component {
   render() {
     return (
-      <div {...this.props} className="list-item">{this.props.children}</div>
-    )
+      <div {...this.props} className="list-item">
+        {this.props.children}
+      </div>
+    );
   }
 }
 
 export default class ChartSettingOrderedFields extends Component {
-    constructor(props) {
-        super(props);
-        this.state = {
-            draggingIndex: null,
-            data: { items: [...this.props.value] }
-        };
-    }
+  constructor(props) {
+    super(props);
+    this.state = {
+      draggingIndex: null,
+      data: { items: [...this.props.value] },
+    };
+  }
 
-    componentWillReceiveProps(nextProps) {
-        this.setState({ data: { items: [...nextProps.value] } })
-    }
+  componentWillReceiveProps(nextProps) {
+    this.setState({ data: { items: [...nextProps.value] } });
+  }
 
-    updateState = (obj) => {
-        this.setState(obj);
-        if (obj.draggingIndex == null) {
-            this.props.onChange([...this.state.data.items]);
-        }
+  updateState = obj => {
+    this.setState(obj);
+    if (obj.draggingIndex == null) {
+      this.props.onChange([...this.state.data.items]);
     }
+  };
 
-    setEnabled = (index, checked) => {
-        const items = [...this.state.data.items];
-        items[index] = { ...items[index], enabled: checked };
-        this.setState({ data: { items } });
-        this.props.onChange([...items]);
-    }
+  setEnabled = (index, checked) => {
+    const items = [...this.state.data.items];
+    items[index] = { ...items[index], enabled: checked };
+    this.setState({ data: { items } });
+    this.props.onChange([...items]);
+  };
 
-    isAnySelected = () => {
-        let selected = false;
-        for ( const item of [...this.state.data.items]) {
-            if ( item.enabled ) {
-                selected = true;
-                break;
-            }
-        }
-        return selected;
+  isAnySelected = () => {
+    let selected = false;
+    for (const item of [...this.state.data.items]) {
+      if (item.enabled) {
+        selected = true;
+        break;
+      }
     }
+    return selected;
+  };
 
-    toggleAll = (anySelected) => {
-        const items = [...this.state.data.items].map((item) => ({ ...item, enabled: !anySelected }));
-        this.setState({ data: { items } });
-        this.props.onChange([...items]);
-    }
+  toggleAll = anySelected => {
+    const items = [...this.state.data.items].map(item => ({
+      ...item,
+      enabled: !anySelected,
+    }));
+    this.setState({ data: { items } });
+    this.props.onChange([...items]);
+  };
 
-    render() {
-        const { columnNames } = this.props;
-        const anySelected = this.isAnySelected();
-        return (
-            <div className="list">
-                <div className="toggle-all">
-                    <div className={cx("flex align-center p1", { "text-grey-2": !anySelected })} >
-                        <CheckBox checked={anySelected} className={cx("text-brand", { "text-grey-2": !anySelected })} onChange={(e) => this.toggleAll(anySelected)} invertChecked />
-                        <span className="ml1 h4">{ anySelected ? 'Unselect all' : 'Select all'}</span>
-                    </div>
-                </div>
-                {this.state.data.items.map((item, i) =>
-                    <OrderedFieldListItem
-                        key={i}
-                        updateState={this.updateState}
-                        items={this.state.data.items}
-                        draggingIndex={this.state.draggingIndex}
-                        sortId={i}
-                        outline="list"
-                    >
-                        <div className={cx("flex align-center p1", { "text-grey-2": !item.enabled })} >
-                            <CheckBox
-                                checked={item.enabled}
-                                onChange={e => this.setEnabled(i, e.target.checked)}
-                            />
-                            <span className="ml1 h4">{columnNames[item.name]}</span>
-                            <Icon className="flex-align-right text-grey-2 mr1 cursor-pointer" name="grabber" width={14} height={14}/>
-                        </div>
-                    </OrderedFieldListItem>
-                )}
+  render() {
+    const { columnNames } = this.props;
+    const anySelected = this.isAnySelected();
+    return (
+      <div className="list">
+        <div className="toggle-all">
+          <div
+            className={cx("flex align-center p1", {
+              "text-grey-2": !anySelected,
+            })}
+          >
+            <CheckBox
+              checked={anySelected}
+              className={cx("text-brand", { "text-grey-2": !anySelected })}
+              onChange={e => this.toggleAll(anySelected)}
+              invertChecked
+            />
+            <span className="ml1 h4">
+              {anySelected ? "Unselect all" : "Select all"}
+            </span>
+          </div>
+        </div>
+        {this.state.data.items.map((item, i) => (
+          <OrderedFieldListItem
+            key={i}
+            updateState={this.updateState}
+            items={this.state.data.items}
+            draggingIndex={this.state.draggingIndex}
+            sortId={i}
+            outline="list"
+          >
+            <div
+              className={cx("flex align-center p1", {
+                "text-grey-2": !item.enabled,
+              })}
+            >
+              <CheckBox
+                checked={item.enabled}
+                onChange={e => this.setEnabled(i, e.target.checked)}
+              />
+              <span className="ml1 h4">{columnNames[item.name]}</span>
+              <Icon
+                className="flex-align-right text-grey-2 mr1 cursor-pointer"
+                name="grabber"
+                width={14}
+                height={14}
+              />
             </div>
-        )
+          </OrderedFieldListItem>
+        ))}
+      </div>
+    );
   }
 }
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx
index f935c2c623e5f95537d5270efe4c99209e342d79..57e6a351449705bf5953d354985abaede2b3dfd1 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingRadio.jsx
@@ -2,13 +2,14 @@ import React from "react";
 
 import Radio from "metabase/components/Radio.jsx";
 
-const ChartSettingRadio = ({ value, onChange, options = [], className }) =>
-    <Radio
-        className={className}
-        value={value}
-        onChange={onChange}
-        options={options}
-        isVertical
-    />
+const ChartSettingRadio = ({ value, onChange, options = [], className }) => (
+  <Radio
+    className={className}
+    value={value}
+    onChange={onChange}
+    options={options}
+    isVertical
+  />
+);
 
 export default ChartSettingRadio;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx
index cabe7746d865de7154e347e15eaa6efd6c173063..88510b0ee98acb1d5bd72741fd777657c5e837d3 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingSelect.jsx
@@ -5,16 +5,29 @@ import Select from "metabase/components/Select.jsx";
 import _ from "underscore";
 import cx from "classnames";
 
-const ChartSettingSelect = ({ value, onChange, options = [], isInitiallyOpen, className, placeholder, placeholderNoOptions }) =>
-    <Select
-        className={cx(className, "block flex-full", { disabled: options.length === 0 || (options.length === 1 && options[0].value === value) })}
-        value={_.findWhere(options, { value })}
-        options={options}
-        optionNameFn={(o) => o.name}
-        optionValueFn={(o) => o.value}
-        onChange={onChange}
-        placeholder={options.length === 0 ? placeholderNoOptions : placeholder}
-        isInitiallyOpen={isInitiallyOpen}
-    />
+const ChartSettingSelect = ({
+  value,
+  onChange,
+  options = [],
+  isInitiallyOpen,
+  className,
+  placeholder,
+  placeholderNoOptions,
+}) => (
+  <Select
+    className={cx(className, "block flex-full", {
+      disabled:
+        options.length === 0 ||
+        (options.length === 1 && options[0].value === value),
+    })}
+    value={_.findWhere(options, { value })}
+    options={options}
+    optionNameFn={o => o.name}
+    optionValueFn={o => o.value}
+    onChange={onChange}
+    placeholder={options.length === 0 ? placeholderNoOptions : placeholder}
+    isInitiallyOpen={isInitiallyOpen}
+  />
+);
 
 export default ChartSettingSelect;
diff --git a/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx b/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx
index d0d7f6680d6a26c08a511bb162dfd75e8c229d69..5a69458de752f1062949f892489c7c5658e8e7e6 100644
--- a/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx
+++ b/frontend/src/metabase/visualizations/components/settings/ChartSettingToggle.jsx
@@ -2,10 +2,8 @@ import React from "react";
 
 import Toggle from "metabase/components/Toggle.jsx";
 
-const ChartSettingToggle = ({ value, onChange }) =>
-    <Toggle
-        value={value}
-        onChange={onChange}
-    />
+const ChartSettingToggle = ({ value, onChange }) => (
+  <Toggle value={value} onChange={onChange} />
+);
 
 export default ChartSettingToggle;
diff --git a/frontend/src/metabase/visualizations/index.js b/frontend/src/metabase/visualizations/index.js
index cee58d595937804ebd0dc975266945817ad864df..492ae0a896654ce35e37d5b5b36f23146dd5cf4f 100644
--- a/frontend/src/metabase/visualizations/index.js
+++ b/frontend/src/metabase/visualizations/index.js
@@ -1,19 +1,19 @@
 /* @flow weak */
 
-import Scalar      from "./visualizations/Scalar.jsx";
-import Progress    from "./visualizations/Progress.jsx";
-import Table       from "./visualizations/Table.jsx";
-import Text        from "./visualizations/Text.jsx";
-import LineChart   from "./visualizations/LineChart.jsx";
-import BarChart    from "./visualizations/BarChart.jsx";
-import RowChart    from "./visualizations/RowChart.jsx";
-import PieChart    from "./visualizations/PieChart.jsx";
-import AreaChart   from "./visualizations/AreaChart.jsx";
-import MapViz      from "./visualizations/Map.jsx";
+import Scalar from "./visualizations/Scalar.jsx";
+import Progress from "./visualizations/Progress.jsx";
+import Table from "./visualizations/Table.jsx";
+import Text from "./visualizations/Text.jsx";
+import LineChart from "./visualizations/LineChart.jsx";
+import BarChart from "./visualizations/BarChart.jsx";
+import RowChart from "./visualizations/RowChart.jsx";
+import PieChart from "./visualizations/PieChart.jsx";
+import AreaChart from "./visualizations/AreaChart.jsx";
+import MapViz from "./visualizations/Map.jsx";
 import ScatterPlot from "./visualizations/ScatterPlot.jsx";
-import Funnel      from "./visualizations/Funnel.jsx";
+import Funnel from "./visualizations/Funnel.jsx";
 import ObjectDetail from "./visualizations/ObjectDetail.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import _ from "underscore";
 
 import type { Series } from "metabase/meta/types/Visualization";
@@ -22,95 +22,103 @@ const visualizations = new Map();
 const aliases = new Map();
 // $FlowFixMe
 visualizations.get = function(key) {
-    return Map.prototype.get.call(this, key) || aliases.get(key) || Table;
-}
+  return Map.prototype.get.call(this, key) || aliases.get(key) || Table;
+};
 
 export function registerVisualization(visualization) {
-    let identifier = visualization.identifier;
-    if (identifier == null) {
-        throw new Error(t`Visualization must define an 'identifier' static variable: ` + visualization.name);
-    }
-    if (visualizations.has(identifier)) {
-        throw new Error(t`Visualization with that identifier is already registered: ` + visualization.name);
-    }
-    visualizations.set(identifier, visualization);
-    for (let alias of visualization.aliases || []) {
-        aliases.set(alias, visualization);
-    }
+  let identifier = visualization.identifier;
+  if (identifier == null) {
+    throw new Error(
+      t`Visualization must define an 'identifier' static variable: ` +
+        visualization.name,
+    );
+  }
+  if (visualizations.has(identifier)) {
+    throw new Error(
+      t`Visualization with that identifier is already registered: ` +
+        visualization.name,
+    );
+  }
+  visualizations.set(identifier, visualization);
+  for (let alias of visualization.aliases || []) {
+    aliases.set(alias, visualization);
+  }
 }
 
 export function getVisualizationRaw(series: Series) {
-    return {
-        series: series,
-        CardVisualization: visualizations.get(series[0].card.display)
-    };
+  return {
+    series: series,
+    CardVisualization: visualizations.get(series[0].card.display),
+  };
 }
 
 export function getVisualizationTransformed(series: Series) {
-    // don't transform if we don't have the data
-    if (_.any(series, s => s.data == null)) {
-        return getVisualizationRaw(series);
-    }
+  // don't transform if we don't have the data
+  if (_.any(series, s => s.data == null)) {
+    return getVisualizationRaw(series);
+  }
 
-    // if a visualization has a transformSeries function, do the transformation until it returns the same visualization / series
-    let CardVisualization, lastSeries;
-    do {
-        CardVisualization = visualizations.get(series[0].card.display);
-        if (!CardVisualization) {
-            throw new Error(t`No visualization for ${series[0].card.display}`);
-        }
-        lastSeries = series;
-        if (typeof CardVisualization.transformSeries === "function") {
-            series = CardVisualization.transformSeries(series);
-        }
-        if (series !== lastSeries) {
-            // $FlowFixMe
-            series = [...series];
-            // $FlowFixMe
-            series._raw = lastSeries;
-        }
-    } while (series !== lastSeries);
+  // if a visualization has a transformSeries function, do the transformation until it returns the same visualization / series
+  let CardVisualization, lastSeries;
+  do {
+    CardVisualization = visualizations.get(series[0].card.display);
+    if (!CardVisualization) {
+      throw new Error(t`No visualization for ${series[0].card.display}`);
+    }
+    lastSeries = series;
+    if (typeof CardVisualization.transformSeries === "function") {
+      series = CardVisualization.transformSeries(series);
+    }
+    if (series !== lastSeries) {
+      // $FlowFixMe
+      series = [...series];
+      // $FlowFixMe
+      series._raw = lastSeries;
+    }
+  } while (series !== lastSeries);
 
-    return { series, CardVisualization };
+  return { series, CardVisualization };
 }
 
-export const extractRemappings = (series) => {
-    const se =  series.map(s => ({
-        ...s,
-        data: s.data && extractRemappedColumns(s.data)
-    }));
-    return se;
-}
+export const extractRemappings = series => {
+  const se = series.map(s => ({
+    ...s,
+    data: s.data && extractRemappedColumns(s.data),
+  }));
+  return se;
+};
 
 // removes columns with `remapped_from` property and adds a `remapping` to the appropriate column
-const extractRemappedColumns = (data) => {
-    const cols = data.cols.map(col => ({
-        ...col,
-        remapped_from_index: col.remapped_from && _.findIndex(data.cols, c => c.name === col.remapped_from),
-        remapping: col.remapped_to && new Map()
-    }));
+const extractRemappedColumns = data => {
+  const cols = data.cols.map(col => ({
+    ...col,
+    remapped_from_index:
+      col.remapped_from &&
+      _.findIndex(data.cols, c => c.name === col.remapped_from),
+    remapping: col.remapped_to && new Map(),
+  }));
 
-    const rows = data.rows.map((row, rowIndex) =>
-        row.filter((value, colIndex) => {
-            const col = cols[colIndex];
-            if (col.remapped_from != null) {
-                cols[col.remapped_from_index].remapped_to_column = col;
-                cols[col.remapped_from_index].remapping.set(
-                    row[col.remapped_from_index],
-                    row[colIndex]
-                );
-                return false;
-            } else {
-                return true;
-            }
-        })
-    )
-    return {
-        ...data,
-        rows,
-        cols: cols.filter(col => col.remapped_from == null)
-    }
-}
+  const rows = data.rows.map((row, rowIndex) =>
+    row.filter((value, colIndex) => {
+      const col = cols[colIndex];
+      if (col.remapped_from != null) {
+        cols[col.remapped_from_index].remapped_to_column = col;
+        cols[col.remapped_from_index].remapping.set(
+          row[col.remapped_from_index],
+          row[colIndex],
+        );
+        return false;
+      } else {
+        return true;
+      }
+    }),
+  );
+  return {
+    ...data,
+    rows,
+    cols: cols.filter(col => col.remapped_from == null),
+  };
+};
 
 registerVisualization(Scalar);
 registerVisualization(Progress);
diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarPostRender.js b/frontend/src/metabase/visualizations/lib/LineAreaBarPostRender.js
index 02d87dba09902e04abc1c83503a643a29aa752b7..ded75cfd38cb51acfe9fb55673c15ba06a9a9eb2 100644
--- a/frontend/src/metabase/visualizations/lib/LineAreaBarPostRender.js
+++ b/frontend/src/metabase/visualizations/lib/LineAreaBarPostRender.js
@@ -9,308 +9,370 @@ import { clipPathReference } from "metabase/lib/dom";
 // The following functions are applied once the chart is rendered.
 
 function onRenderRemoveClipPath(chart) {
-    for (let elem of chart.selectAll(".sub, .chart-body")[0]) {
-        // prevents dots from being clipped:
-        elem.removeAttribute("clip-path");
-    }
+  for (let elem of chart.selectAll(".sub, .chart-body")[0]) {
+    // prevents dots from being clipped:
+    elem.removeAttribute("clip-path");
+  }
 }
 
-
 function onRenderMoveContentToTop(chart) {
-    for (let elem of chart.selectAll(".sub, .chart-body")[0]) {
-        // move chart content on top of axis (z-index doesn't work on SVG):
-        elem.parentNode.appendChild(elem);
-    }
+  for (let elem of chart.selectAll(".sub, .chart-body")[0]) {
+    // move chart content on top of axis (z-index doesn't work on SVG):
+    elem.parentNode.appendChild(elem);
+  }
 }
 
-
 function onRenderSetDotStyle(chart) {
-    for (let elem of chart.svg().selectAll('.dc-tooltip circle.dot')[0]) {
-        // set the color of the dots to the fill color so we can use currentColor in CSS rules:
-        elem.style.color = elem.getAttribute("fill");
-    }
+  for (let elem of chart.svg().selectAll(".dc-tooltip circle.dot")[0]) {
+    // set the color of the dots to the fill color so we can use currentColor in CSS rules:
+    elem.style.color = elem.getAttribute("fill");
+  }
 }
 
-
 const DOT_OVERLAP_COUNT_LIMIT = 8;
-const DOT_OVERLAP_RATIO       = 0.10;
-const DOT_OVERLAP_DISTANCE    = 8;
+const DOT_OVERLAP_RATIO = 0.1;
+const DOT_OVERLAP_DISTANCE = 8;
 
 function onRenderEnableDots(chart, settings) {
-    let enableDots;
-    const dots = chart.svg().selectAll(".dc-tooltip .dot")[0];
-    if (settings["line.marker_enabled"] != null) {
-        enableDots = !!settings["line.marker_enabled"];
-    } else if (dots.length > 500) {
-        // more than 500 dots is almost certainly too dense, don't waste time computing the voronoi map
-        enableDots = false;
-    } else {
-        const vertices = dots.map((e, index) => {
-            let rect = e.getBoundingClientRect();
-            return [rect.left, rect.top, index];
-        });
-        const overlappedIndex = {};
-        // essentially pairs of vertices closest to each other
-        for (let { source, target } of d3.geom.voronoi().links(vertices)) {
-            if (Math.sqrt(Math.pow(source[0] - target[0], 2) + Math.pow(source[1] - target[1], 2)) < DOT_OVERLAP_DISTANCE) {
-                // if they overlap, mark both as overlapped
-                overlappedIndex[source[2]] = overlappedIndex[target[2]] = true;
-            }
-        }
-        const total = vertices.length;
-        const overlapping = Object.keys(overlappedIndex).length;
-        enableDots = overlapping < DOT_OVERLAP_COUNT_LIMIT || (overlapping / total) < DOT_OVERLAP_RATIO;
+  let enableDots;
+  const dots = chart.svg().selectAll(".dc-tooltip .dot")[0];
+  if (settings["line.marker_enabled"] != null) {
+    enableDots = !!settings["line.marker_enabled"];
+  } else if (dots.length > 500) {
+    // more than 500 dots is almost certainly too dense, don't waste time computing the voronoi map
+    enableDots = false;
+  } else {
+    const vertices = dots.map((e, index) => {
+      let rect = e.getBoundingClientRect();
+      return [rect.left, rect.top, index];
+    });
+    const overlappedIndex = {};
+    // essentially pairs of vertices closest to each other
+    for (let { source, target } of d3.geom.voronoi().links(vertices)) {
+      if (
+        Math.sqrt(
+          Math.pow(source[0] - target[0], 2) +
+            Math.pow(source[1] - target[1], 2),
+        ) < DOT_OVERLAP_DISTANCE
+      ) {
+        // if they overlap, mark both as overlapped
+        overlappedIndex[source[2]] = overlappedIndex[target[2]] = true;
+      }
     }
-    chart.svg()
-         .classed("enable-dots", enableDots)
-         .classed("enable-dots-onhover", !enableDots);
+    const total = vertices.length;
+    const overlapping = Object.keys(overlappedIndex).length;
+    enableDots =
+      overlapping < DOT_OVERLAP_COUNT_LIMIT ||
+      overlapping / total < DOT_OVERLAP_RATIO;
+  }
+  chart
+    .svg()
+    .classed("enable-dots", enableDots)
+    .classed("enable-dots-onhover", !enableDots);
 }
 
-
 const VORONOI_TARGET_RADIUS = 25;
-const VORONOI_MAX_POINTS    = 300;
+const VORONOI_MAX_POINTS = 300;
 
 /// dispatchUIEvent used below in the "Voroni Hover" stuff
 function dispatchUIEvent(element, eventName) {
-    let e = document.createEvent("UIEvents");
-    // $FlowFixMe
-    e.initUIEvent(eventName, true, true, window, 1);
-    element.dispatchEvent(e);
+  let e = document.createEvent("UIEvents");
+  // $FlowFixMe
+  e.initUIEvent(eventName, true, true, window, 1);
+  element.dispatchEvent(e);
 }
 
 // logic for determining the bounding shapes for showing tooltips for a given point.
 // Wikipedia has a good explanation here: https://en.wikipedia.org/wiki/Voronoi_diagram
 function onRenderVoronoiHover(chart) {
-    const parent = chart.svg().select("svg > g");
-    const dots = chart.svg().selectAll(".sub .dc-tooltip .dot")[0];
-
-    if (dots.length === 0 || dots.length > VORONOI_MAX_POINTS) {
-        return;
-    }
-
-    const originRect = chart.svg().node().getBoundingClientRect();
-    const vertices = dots.map(e => {
-        const { top, left, width, height } = e.getBoundingClientRect();
-        const px = (left + width / 2) - originRect.left;
-        const py = (top + height / 2) - originRect.top;
-        return [px, py, e];
-    });
-
-    // HACK Atte Keinänen 8/8/17: For some reason the parent node is not present in Jest/Enzyme tests
-    // so simply return empty width and height for preventing the need to do bigger hacks in test code
-    const { width, height } = parent.node() ? parent.node().getBBox() : { width: 0, height: 0 };
-
-    const voronoi = d3.geom.voronoi().clipExtent([[0,0], [width, height]]);
-
-    // circular clip paths to limit distance from actual point
-    parent.append("svg:g")
-          .selectAll("clipPath")
-          .data(vertices)
-          .enter().append("svg:clipPath")
-          .attr("id", (d, i) => "clip-" + i)
-          .append("svg:circle")
-          .attr('cx', (d) => d[0])
-          .attr('cy', (d) => d[1])
-          .attr('r', VORONOI_TARGET_RADIUS);
-
-    // voronoi layout with clip paths applied
-    parent.append("svg:g")
-          .classed("voronoi", true)
-          .selectAll("path")
-          .data(voronoi(vertices), (d) => d&&d.join(","))
-          .enter().append("svg:path")
-          .filter((d) => d != undefined)
-          .attr("d", (d) => "M" + d.join("L") + "Z")
-          .attr("clip-path", (d,i) => clipPathReference("clip-" + i))
-          // in the functions below e is not an event but the circle element being hovered/clicked
-          .on("mousemove", ({ point }) => {
-              let e = point[2];
-              dispatchUIEvent(e, "mousemove");
-              d3.select(e).classed("hover", true);
-          })
-          .on("mouseleave", ({ point }) => {
-              let e = point[2];
-              dispatchUIEvent(e, "mouseleave");
-              d3.select(e).classed("hover", false);
-          })
-          .on("click", ({ point }) => {
-              let e = point[2];
-              dispatchUIEvent(e, "click");
-          })
-          .order();
+  const parent = chart.svg().select("svg > g");
+  const dots = chart.svg().selectAll(".sub .dc-tooltip .dot")[0];
+
+  if (dots.length === 0 || dots.length > VORONOI_MAX_POINTS) {
+    return;
+  }
+
+  const originRect = chart
+    .svg()
+    .node()
+    .getBoundingClientRect();
+  const vertices = dots.map(e => {
+    const { top, left, width, height } = e.getBoundingClientRect();
+    const px = left + width / 2 - originRect.left;
+    const py = top + height / 2 - originRect.top;
+    return [px, py, e];
+  });
+
+  // HACK Atte Keinänen 8/8/17: For some reason the parent node is not present in Jest/Enzyme tests
+  // so simply return empty width and height for preventing the need to do bigger hacks in test code
+  const { width, height } = parent.node()
+    ? parent.node().getBBox()
+    : { width: 0, height: 0 };
+
+  const voronoi = d3.geom.voronoi().clipExtent([[0, 0], [width, height]]);
+
+  // circular clip paths to limit distance from actual point
+  parent
+    .append("svg:g")
+    .selectAll("clipPath")
+    .data(vertices)
+    .enter()
+    .append("svg:clipPath")
+    .attr("id", (d, i) => "clip-" + i)
+    .append("svg:circle")
+    .attr("cx", d => d[0])
+    .attr("cy", d => d[1])
+    .attr("r", VORONOI_TARGET_RADIUS);
+
+  // voronoi layout with clip paths applied
+  parent
+    .append("svg:g")
+    .classed("voronoi", true)
+    .selectAll("path")
+    .data(voronoi(vertices), d => d && d.join(","))
+    .enter()
+    .append("svg:path")
+    .filter(d => d != undefined)
+    .attr("d", d => "M" + d.join("L") + "Z")
+    .attr("clip-path", (d, i) => clipPathReference("clip-" + i))
+    // in the functions below e is not an event but the circle element being hovered/clicked
+    .on("mousemove", ({ point }) => {
+      let e = point[2];
+      dispatchUIEvent(e, "mousemove");
+      d3.select(e).classed("hover", true);
+    })
+    .on("mouseleave", ({ point }) => {
+      let e = point[2];
+      dispatchUIEvent(e, "mouseleave");
+      d3.select(e).classed("hover", false);
+    })
+    .on("click", ({ point }) => {
+      let e = point[2];
+      dispatchUIEvent(e, "click");
+    })
+    .order();
 }
 
-
 function onRenderCleanupGoal(chart, onGoalHover, isSplitAxis) {
-    // remove dots
-    chart.selectAll(".goal .dot").remove();
-
-    // move to end of the parent node so it's on top
-    chart.selectAll(".goal").each(function() { this.parentNode.appendChild(this); });
-    chart.selectAll(".goal .line").attr({
-        "stroke": "rgba(157,160,164, 0.7)",
-        "stroke-dasharray": "5,5"
-    });
-
-    // add the label
-    let goalLine = chart.selectAll(".goal .line")[0][0];
-    if (goalLine) {
-
-        // stretch the goal line all the way across, use x axis as reference
-        let xAxisLine = chart.selectAll(".axis.x .domain")[0][0];
-
-        // HACK Atte Keinänen 8/8/17: For some reason getBBox method is not present in Jest/Enzyme tests
-        if (xAxisLine && goalLine.getBBox) {
-            goalLine.setAttribute("d", `M0,${goalLine.getBBox().y}L${xAxisLine.getBBox().width},${goalLine.getBBox().y}`)
-        }
-
-        const { x, y, width } = goalLine.getBBox ? goalLine.getBBox() : { x: 0, y: 0, width: 0 };
-
-        const labelOnRight = !isSplitAxis;
-        chart.selectAll(".goal .stack._0")
-             .append("text")
-             .text("Goal")
-             .attr({
-                 x: labelOnRight ? x + width : x,
-                 y: y - 5,
-                 "text-anchor": labelOnRight ? "end" : "start",
-                 "font-weight": "bold",
-                 fill: "rgb(157,160,164)",
-             })
-             .on("mouseenter", function() { onGoalHover(this); })
-             .on("mouseleave", function() { onGoalHover(null); });
+  // remove dots
+  chart.selectAll(".goal .dot").remove();
+
+  // move to end of the parent node so it's on top
+  chart.selectAll(".goal").each(function() {
+    this.parentNode.appendChild(this);
+  });
+  chart.selectAll(".goal .line").attr({
+    stroke: "rgba(157,160,164, 0.7)",
+    "stroke-dasharray": "5,5",
+  });
+
+  // add the label
+  let goalLine = chart.selectAll(".goal .line")[0][0];
+  if (goalLine) {
+    // stretch the goal line all the way across, use x axis as reference
+    let xAxisLine = chart.selectAll(".axis.x .domain")[0][0];
+
+    // HACK Atte Keinänen 8/8/17: For some reason getBBox method is not present in Jest/Enzyme tests
+    if (xAxisLine && goalLine.getBBox) {
+      goalLine.setAttribute(
+        "d",
+        `M0,${goalLine.getBBox().y}L${xAxisLine.getBBox().width},${
+          goalLine.getBBox().y
+        }`,
+      );
     }
-}
 
+    const { x, y, width } = goalLine.getBBox
+      ? goalLine.getBBox()
+      : { x: 0, y: 0, width: 0 };
+
+    const labelOnRight = !isSplitAxis;
+    chart
+      .selectAll(".goal .stack._0")
+      .append("text")
+      .text("Goal")
+      .attr({
+        x: labelOnRight ? x + width : x,
+        y: y - 5,
+        "text-anchor": labelOnRight ? "end" : "start",
+        "font-weight": "bold",
+        fill: "rgb(157,160,164)",
+      })
+      .on("mouseenter", function() {
+        onGoalHover(this);
+      })
+      .on("mouseleave", function() {
+        onGoalHover(null);
+      });
+  }
+}
 
 function onRenderHideDisabledLabels(chart, settings) {
-    if (!settings["graph.x_axis.labels_enabled"]) {
-        chart.selectAll(".x-axis-label").remove();
-    }
-    if (!settings["graph.y_axis.labels_enabled"]) {
-        chart.selectAll(".y-axis-label").remove();
-    }
+  if (!settings["graph.x_axis.labels_enabled"]) {
+    chart.selectAll(".x-axis-label").remove();
+  }
+  if (!settings["graph.y_axis.labels_enabled"]) {
+    chart.selectAll(".y-axis-label").remove();
+  }
 }
 
-
 function onRenderHideDisabledAxis(chart, settings) {
-    if (!settings["graph.x_axis.axis_enabled"]) {
-        chart.selectAll(".axis.x").remove();
-    }
-    if (!settings["graph.y_axis.axis_enabled"]) {
-        chart.selectAll(".axis.y, .axis.yr").remove();
-    }
+  if (!settings["graph.x_axis.axis_enabled"]) {
+    chart.selectAll(".axis.x").remove();
+  }
+  if (!settings["graph.y_axis.axis_enabled"]) {
+    chart.selectAll(".axis.y, .axis.yr").remove();
+  }
 }
 
-
 function onRenderHideBadAxis(chart) {
-    if (chart.selectAll(".axis.x .tick")[0].length === 1) {
-        chart.selectAll(".axis.x").remove();
-    }
+  if (chart.selectAll(".axis.x .tick")[0].length === 1) {
+    chart.selectAll(".axis.x").remove();
+  }
 }
 
-
 function onRenderDisableClickFiltering(chart) {
-    chart.selectAll("rect.bar")
-         .on("click", (d) => {
-             chart.filter(null);
-             chart.filter(d.key);
-         });
+  chart.selectAll("rect.bar").on("click", d => {
+    chart.filter(null);
+    chart.filter(d.key);
+  });
 }
 
-
 function onRenderFixStackZIndex(chart) {
-    // reverse the order of .stack-list and .dc-tooltip-list children so 0 points in stacked
-    // charts don't appear on top of non-zero points
-    for (const list of chart.selectAll(".stack-list, .dc-tooltip-list")[0]) {
-        for (const child of list.childNodes) {
-            list.insertBefore(list.firstChild, child);
-        }
+  // reverse the order of .stack-list and .dc-tooltip-list children so 0 points in stacked
+  // charts don't appear on top of non-zero points
+  for (const list of chart.selectAll(".stack-list, .dc-tooltip-list")[0]) {
+    for (const child of list.childNodes) {
+      list.insertBefore(list.firstChild, child);
     }
+  }
 }
 
-
 function onRenderSetClassName(chart, isStacked) {
-    chart.svg().classed("stacked", isStacked);
+  chart.svg().classed("stacked", isStacked);
 }
 
 // the various steps that get called
 function onRender(chart, settings, onGoalHover, isSplitAxis, isStacked) {
-    onRenderRemoveClipPath(chart);
-    onRenderMoveContentToTop(chart);
-    onRenderSetDotStyle(chart);
-    onRenderEnableDots(chart, settings);
-    onRenderVoronoiHover(chart);
-    onRenderCleanupGoal(chart, onGoalHover, isSplitAxis); // do this before hiding x-axis
-    onRenderHideDisabledLabels(chart, settings);
-    onRenderHideDisabledAxis(chart, settings);
-    onRenderHideBadAxis(chart);
-    onRenderDisableClickFiltering(chart);
-    onRenderFixStackZIndex(chart);
-    onRenderSetClassName(chart, isStacked);
+  onRenderRemoveClipPath(chart);
+  onRenderMoveContentToTop(chart);
+  onRenderSetDotStyle(chart);
+  onRenderEnableDots(chart, settings);
+  onRenderVoronoiHover(chart);
+  onRenderCleanupGoal(chart, onGoalHover, isSplitAxis); // do this before hiding x-axis
+  onRenderHideDisabledLabels(chart, settings);
+  onRenderHideDisabledAxis(chart, settings);
+  onRenderHideBadAxis(chart);
+  onRenderDisableClickFiltering(chart);
+  onRenderFixStackZIndex(chart);
+  onRenderSetClassName(chart, isStacked);
 }
 
-
 // +-------------------------------------------------------------------------------------------------------------------+
 // |                                                   BEFORE RENDER                                                   |
 // +-------------------------------------------------------------------------------------------------------------------+
 
 // run these first so the rest of the margin computations take it into account
 function beforeRenderHideDisabledAxesAndLabels(chart, settings) {
-    onRenderHideDisabledLabels(chart, settings);
-    onRenderHideDisabledAxis(chart, settings);
-    onRenderHideBadAxis(chart);
+  onRenderHideDisabledLabels(chart, settings);
+  onRenderHideDisabledAxis(chart, settings);
+  onRenderHideBadAxis(chart);
 }
 
-
 // min margin
-const MARGIN_TOP_MIN        = 20; // needs to be large enough for goal line text
-const MARGIN_BOTTOM_MIN     = 10;
+const MARGIN_TOP_MIN = 20; // needs to be large enough for goal line text
+const MARGIN_BOTTOM_MIN = 10;
 const MARGIN_HORIZONTAL_MIN = 20;
 
 // extra padding for axis
 const X_AXIS_PADDING = 0;
 const Y_AXIS_PADDING = 8;
 
-function adjustMargin(chart, margin, direction, padding, axisSelector, labelSelector) {
-    const axis      = chart.select(axisSelector).node();
-    const label     = chart.select(labelSelector).node();
-    const axisSize  = axis  ? axis.getBoundingClientRect()[direction] + 10 : 0;
-    const labelSize = label ? label.getBoundingClientRect()[direction] + 5 : 0;
-    chart.margins()[margin] = axisSize + labelSize + padding;
+function adjustMargin(
+  chart,
+  margin,
+  direction,
+  padding,
+  axisSelector,
+  labelSelector,
+) {
+  const axis = chart.select(axisSelector).node();
+  const label = chart.select(labelSelector).node();
+  const axisSize = axis ? axis.getBoundingClientRect()[direction] + 10 : 0;
+  const labelSize = label ? label.getBoundingClientRect()[direction] + 5 : 0;
+  chart.margins()[margin] = axisSize + labelSize + padding;
 }
 
 function computeMinHorizontalMargins(chart) {
-    let min = { left: 0, right: 0 };
-    const ticks = chart.selectAll(".axis.x .tick text")[0];
-    if (ticks.length > 0) {
-        const chartRect = chart.select("svg").node().getBoundingClientRect();
-        min.left = chart.margins().left - (ticks[0].getBoundingClientRect().left - chartRect.left);
-        min.right = chart.margins().right - (chartRect.right - ticks[ticks.length - 1].getBoundingClientRect().right);
-    }
-    return min;
+  let min = { left: 0, right: 0 };
+  const ticks = chart.selectAll(".axis.x .tick text")[0];
+  if (ticks.length > 0) {
+    const chartRect = chart
+      .select("svg")
+      .node()
+      .getBoundingClientRect();
+    min.left =
+      chart.margins().left -
+      (ticks[0].getBoundingClientRect().left - chartRect.left);
+    min.right =
+      chart.margins().right -
+      (chartRect.right - ticks[ticks.length - 1].getBoundingClientRect().right);
+  }
+  return min;
 }
 
 function beforeRenderFixMargins(chart, settings) {
-    // run before adjusting margins
-    const mins = computeMinHorizontalMargins(chart);
-
-    // adjust the margins to fit the X and Y axis tick and label sizes, if enabled
-    adjustMargin(chart, "bottom", "height", X_AXIS_PADDING, ".axis.x",  ".x-axis-label",          settings["graph.x_axis.labels_enabled"]);
-    adjustMargin(chart, "left",   "width",  Y_AXIS_PADDING, ".axis.y",  ".y-axis-label.y-label",  settings["graph.y_axis.labels_enabled"]);
-    adjustMargin(chart, "right",  "width",  Y_AXIS_PADDING, ".axis.yr", ".y-axis-label.yr-label", settings["graph.y_axis.labels_enabled"]);
-
-    // set margins to the max of the various mins
-    chart.margins().top    = Math.max(MARGIN_TOP_MIN,        chart.margins().top);
-    chart.margins().left   = Math.max(MARGIN_HORIZONTAL_MIN, chart.margins().left,  mins.left);
-    chart.margins().right  = Math.max(MARGIN_HORIZONTAL_MIN, chart.margins().right, mins.right);
-    chart.margins().bottom = Math.max(MARGIN_BOTTOM_MIN,     chart.margins().bottom);
+  // run before adjusting margins
+  const mins = computeMinHorizontalMargins(chart);
+
+  // adjust the margins to fit the X and Y axis tick and label sizes, if enabled
+  adjustMargin(
+    chart,
+    "bottom",
+    "height",
+    X_AXIS_PADDING,
+    ".axis.x",
+    ".x-axis-label",
+    settings["graph.x_axis.labels_enabled"],
+  );
+  adjustMargin(
+    chart,
+    "left",
+    "width",
+    Y_AXIS_PADDING,
+    ".axis.y",
+    ".y-axis-label.y-label",
+    settings["graph.y_axis.labels_enabled"],
+  );
+  adjustMargin(
+    chart,
+    "right",
+    "width",
+    Y_AXIS_PADDING,
+    ".axis.yr",
+    ".y-axis-label.yr-label",
+    settings["graph.y_axis.labels_enabled"],
+  );
+
+  // set margins to the max of the various mins
+  chart.margins().top = Math.max(MARGIN_TOP_MIN, chart.margins().top);
+  chart.margins().left = Math.max(
+    MARGIN_HORIZONTAL_MIN,
+    chart.margins().left,
+    mins.left,
+  );
+  chart.margins().right = Math.max(
+    MARGIN_HORIZONTAL_MIN,
+    chart.margins().right,
+    mins.right,
+  );
+  chart.margins().bottom = Math.max(MARGIN_BOTTOM_MIN, chart.margins().bottom);
 }
 
 // collection of function calls that get made *before* we tell the Chart to render
 function beforeRender(chart, settings) {
-    beforeRenderHideDisabledAxesAndLabels(chart, settings);
-    beforeRenderFixMargins(chart, settings);
+  beforeRenderHideDisabledAxesAndLabels(chart, settings);
+  beforeRenderFixMargins(chart, settings);
 }
 
 // +-------------------------------------------------------------------------------------------------------------------+
@@ -318,8 +380,16 @@ function beforeRender(chart, settings) {
 // +-------------------------------------------------------------------------------------------------------------------+
 
 /// once chart has rendered and we can access the SVG, do customizations to axis labels / etc that you can't do through dc.js
-export default function lineAndBarOnRender(chart, settings, onGoalHover, isSplitAxis, isStacked) {
-    beforeRender(chart, settings);
-    chart.on("renderlet.on-render", () => onRender(chart, settings, onGoalHover, isSplitAxis, isStacked));
-    chart.render();
+export default function lineAndBarOnRender(
+  chart,
+  settings,
+  onGoalHover,
+  isSplitAxis,
+  isStacked,
+) {
+  beforeRender(chart, settings);
+  chart.on("renderlet.on-render", () =>
+    onRender(chart, settings, onGoalHover, isSplitAxis, isStacked),
+  );
+  chart.render();
 }
diff --git a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
index e8c42a0c22cb8023aa3fc4113737de03b4eaf5fe..e3ce80790d7afacf7e54a35cbeecbadb6e0dc735 100644
--- a/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
+++ b/frontend/src/metabase/visualizations/lib/LineAreaBarRenderer.js
@@ -5,44 +5,49 @@ import d3 from "d3";
 import dc from "dc";
 import _ from "underscore";
 import { updateIn } from "icepick";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 import {
-    computeSplit,
-    getFriendlyName,
-    getXValues,
-    colorShades
+  computeSplit,
+  getFriendlyName,
+  getXValues,
+  colorShades,
 } from "./utils";
 
 import { minTimeseriesUnit, computeTimeseriesDataInverval } from "./timeseries";
 
 import { computeNumericDataInverval } from "./numeric";
 
-import { applyChartTimeseriesXAxis, applyChartQuantitativeXAxis, applyChartOrdinalXAxis, applyChartYAxis } from "./apply_axis";
+import {
+  applyChartTimeseriesXAxis,
+  applyChartQuantitativeXAxis,
+  applyChartOrdinalXAxis,
+  applyChartYAxis,
+} from "./apply_axis";
 
 import { setupTooltips } from "./apply_tooltips";
 
 import fillMissingValuesInDatas from "./fill_data";
 
 import {
-    HACK_parseTimestamp,
-    NULL_DIMENSION_WARNING,
-    forceSortedGroupsOfGroups,
-    initChart, // TODO - probably better named something like `initChartParent`
-    makeIndexMap,
-    reduceGroup,
-    isTimeseries,
-    isQuantitative,
-    isHistogram,
-    isOrdinal,
-    isHistogramBar,
-    isStacked,
-    isNormalized,
-    getFirstNonEmptySeries,
-    isDimensionTimeseries,
-    isDimensionNumeric,
-    isRemappedToString,
-    isMultiCardSeries
+  HACK_parseTimestamp,
+  NULL_DIMENSION_WARNING,
+  forceSortedGroupsOfGroups,
+  initChart, // TODO - probably better named something like `initChartParent`
+  makeIndexMap,
+  reduceGroup,
+  isTimeseries,
+  isQuantitative,
+  isHistogram,
+  isOrdinal,
+  isHistogramBar,
+  isStacked,
+  isNormalized,
+  getFirstNonEmptySeries,
+  isDimensionTimeseries,
+  isDimensionNumeric,
+  isRemappedToString,
+  isMultiCardSeries,
 } from "./renderer_utils";
 
 import lineAndBarOnRender from "./LineAreaBarPostRender";
@@ -50,521 +55,642 @@ import lineAndBarOnRender from "./LineAreaBarPostRender";
 import { formatNumber } from "metabase/lib/formatting";
 import { isStructured } from "metabase/meta/Card";
 
-import { updateDateTimeFilter, updateNumericFilter } from "metabase/qb/lib/actions";
+import {
+  updateDateTimeFilter,
+  updateNumericFilter,
+} from "metabase/qb/lib/actions";
 
-import { lineAddons } from "./graph/addons"
+import { lineAddons } from "./graph/addons";
 import { initBrush } from "./graph/brush";
 
-import type { VisualizationProps } from "metabase/meta/types/Visualization"
-
+import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 const BAR_PADDING_RATIO = 0.2;
 const DEFAULT_INTERPOLATION = "linear";
 
-const UNAGGREGATED_DATA_WARNING = (col) => t`"${getFriendlyName(col)}" is an unaggregated field: if it has more than one value at a point on the x-axis, the values will be summed.`
+const UNAGGREGATED_DATA_WARNING = col =>
+  t`"${getFriendlyName(
+    col,
+  )}" is an unaggregated field: if it has more than one value at a point on the x-axis, the values will be summed.`;
 
-const enableBrush = (series, onChangeCardAndRun) => !!(
+const enableBrush = (series, onChangeCardAndRun) =>
+  !!(
     onChangeCardAndRun &&
     !isMultiCardSeries(series) &&
     isStructured(series[0].card) &&
     !isRemappedToString(series)
-);
+  );
 
 /************************************************************ SETUP ************************************************************/
 
 function checkSeriesIsValid({ series, maxSeries }) {
-    if (getFirstNonEmptySeries(series).data.cols.length < 2) {
-        throw new Error(t`This chart type requires at least 2 columns.`);
-    }
+  if (getFirstNonEmptySeries(series).data.cols.length < 2) {
+    throw new Error(t`This chart type requires at least 2 columns.`);
+  }
 
-    if (series.length > maxSeries) {
-        throw new Error(t`This chart type doesn't support more than ${maxSeries} series of data.`);
-    }
+  if (series.length > maxSeries) {
+    throw new Error(
+      t`This chart type doesn't support more than ${maxSeries} series of data.`,
+    );
+  }
 }
 
 function getDatas({ settings, series }, warn) {
-    return series.map((s) =>
-        s.data.rows.map(row => {
-            const newRow = [
-                // don't parse as timestamp if we're going to display as a quantitative scale, e.x. years and Unix timestamps
-                (isDimensionTimeseries(series) && !isQuantitative(settings)) ? HACK_parseTimestamp(row[0], s.data.cols[0].unit, warn) :
-                isDimensionNumeric(series) ? row[0] :
-                String(row[0])
-                , ...row.slice(1)
-            ]
-            // $FlowFixMe: _origin not typed
-            newRow._origin = row._origin;
-            return newRow;
-        })
-    );
+  return series.map(s =>
+    s.data.rows.map(row => {
+      const newRow = [
+        // don't parse as timestamp if we're going to display as a quantitative scale, e.x. years and Unix timestamps
+        isDimensionTimeseries(series) && !isQuantitative(settings)
+          ? HACK_parseTimestamp(row[0], s.data.cols[0].unit, warn)
+          : isDimensionNumeric(series) ? row[0] : String(row[0]),
+        ...row.slice(1),
+      ];
+      // $FlowFixMe: _origin not typed
+      newRow._origin = row._origin;
+      return newRow;
+    }),
+  );
 }
 
 function getXInterval({ settings, series }, xValues) {
-    if (isTimeseries(settings)) {
-        // compute the interval
-        const unit = minTimeseriesUnit(series.map(s => s.data.cols[0].unit));
-        return computeTimeseriesDataInverval(xValues, unit);
-    } else if (isQuantitative(settings) || isHistogram(settings)) {
-        // Get the bin width from binning_info, if available
-        // TODO: multiseries?
-        const binningInfo = getFirstNonEmptySeries(series).data.cols[0].binning_info;
-        if (binningInfo) return binningInfo.bin_width;
-
-        // Otherwise try to infer from the X values
-        return computeNumericDataInverval(xValues);
-    }
+  if (isTimeseries(settings)) {
+    // compute the interval
+    const unit = minTimeseriesUnit(series.map(s => s.data.cols[0].unit));
+    return computeTimeseriesDataInverval(xValues, unit);
+  } else if (isQuantitative(settings) || isHistogram(settings)) {
+    // Get the bin width from binning_info, if available
+    // TODO: multiseries?
+    const binningInfo = getFirstNonEmptySeries(series).data.cols[0]
+      .binning_info;
+    if (binningInfo) return binningInfo.bin_width;
+
+    // Otherwise try to infer from the X values
+    return computeNumericDataInverval(xValues);
+  }
 }
 
 function getXAxisProps(props, datas) {
-    const xValues = getXValues(datas, props.chartType);
+  const xValues = getXValues(datas, props.chartType);
 
-    return {
-        xValues,
-        xDomain: d3.extent(xValues),
-        xInterval: getXInterval(props, xValues)
-    };
+  return {
+    xValues,
+    xDomain: d3.extent(xValues),
+    xInterval: getXInterval(props, xValues),
+  };
 }
 
-
 ///------------------------------------------------------------ DIMENSIONS & GROUPS ------------------------------------------------------------///
 
 function getDimensionsAndGroupsForScatterChart(datas) {
-    const dataset = crossfilter();
-    datas.map(data => dataset.add(data));
-
-    const dimension = dataset.dimension(row => row);
-    const groups = datas.map(data => {
-        const dim = crossfilter(data).dimension(row => row);
-        return [
-            dim.group().reduceSum((d) => d[2] || 1)
-        ]
-    });
+  const dataset = crossfilter();
+  datas.map(data => dataset.add(data));
 
-    return { dimension, groups };
-}
+  const dimension = dataset.dimension(row => row);
+  const groups = datas.map(data => {
+    const dim = crossfilter(data).dimension(row => row);
+    return [dim.group().reduceSum(d => d[2] || 1)];
+  });
 
+  return { dimension, groups };
+}
 
 /// Add '% ' in from of the names of the appropriate series. E.g. 'Sum' becomes '% Sum'
 function addPercentSignsToDisplayNames(series) {
-    return series.map(s => updateIn(s, ["data", "cols", 1], (col) => ({
-        ...col,
-        display_name: "% " + getFriendlyName(col)
-    })));
+  return series.map(s =>
+    updateIn(s, ["data", "cols", 1], col => ({
+      ...col,
+      display_name: "% " + getFriendlyName(col),
+    })),
+  );
 }
 
-function getDimensionsAndGroupsAndUpdateSeriesDisplayNamesForStackedChart(props, datas, warn) {
-    const dataset = crossfilter();
-
-    const normalized = isNormalized(props.settings, datas);
-    // get the sum of the metric for each dimension value in order to scale
-    const scaleFactors = {};
-    if (normalized) {
-        for (const data of datas) {
-            for (const [d, m] of data) {
-                scaleFactors[d] = (scaleFactors[d] || 0) + m;
-            }
-        }
-
-        props.series = addPercentSignsToDisplayNames(props.series);
+function getDimensionsAndGroupsAndUpdateSeriesDisplayNamesForStackedChart(
+  props,
+  datas,
+  warn,
+) {
+  const dataset = crossfilter();
+
+  const normalized = isNormalized(props.settings, datas);
+  // get the sum of the metric for each dimension value in order to scale
+  const scaleFactors = {};
+  if (normalized) {
+    for (const data of datas) {
+      for (const [d, m] of data) {
+        scaleFactors[d] = (scaleFactors[d] || 0) + m;
+      }
     }
 
-    datas.map((data, i) =>
-        dataset.add(data.map(d => ({
-            [0]: d[0],
-            [i + 1]: normalized ? (d[1] / scaleFactors[d[0]]) : d[1]
-        })))
-    );
-
-    const dimension = dataset.dimension(d => d[0]);
-    const groups = [
-        datas.map((data, seriesIndex) =>
-            reduceGroup(dimension.group(), seriesIndex + 1, () => warn(UNAGGREGATED_DATA_WARNING(props.series[seriesIndex].data.cols[0])))
-        )
-    ];
-
-    return { dimension, groups };
+    props.series = addPercentSignsToDisplayNames(props.series);
+  }
+
+  datas.map((data, i) =>
+    dataset.add(
+      data.map(d => ({
+        [0]: d[0],
+        [i + 1]: normalized ? d[1] / scaleFactors[d[0]] : d[1],
+      })),
+    ),
+  );
+
+  const dimension = dataset.dimension(d => d[0]);
+  const groups = [
+    datas.map((data, seriesIndex) =>
+      reduceGroup(dimension.group(), seriesIndex + 1, () =>
+        warn(UNAGGREGATED_DATA_WARNING(props.series[seriesIndex].data.cols[0])),
+      ),
+    ),
+  ];
+
+  return { dimension, groups };
 }
 
 function getDimensionsAndGroupsForOther({ series }, datas, warn) {
-    const dataset = crossfilter();
-    datas.map(data => dataset.add(data));
-
-    const dimension = dataset.dimension(d => d[0]);
-    const groups = datas.map((data, seriesIndex) => {
-        // If the value is empty, pass a dummy array to crossfilter
-        data = data.length > 0 ? data : [[null, null]];
-
-        const 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])))
-        );
-    });
-
-    return { dimension, groups };
+  const dataset = crossfilter();
+  datas.map(data => dataset.add(data));
+
+  const dimension = dataset.dimension(d => d[0]);
+  const groups = datas.map((data, seriesIndex) => {
+    // If the value is empty, pass a dummy array to crossfilter
+    data = data.length > 0 ? data : [[null, null]];
+
+    const 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])),
+        ),
+      );
+  });
+
+  return { dimension, groups };
 }
 
 /// Return an object containing the `dimension` and `groups` for the chart(s).
 /// For normalized stacked charts, this also updates the dispaly names to add a percent in front of the name (e.g. 'Sum' becomes '% Sum')
 function getDimensionsAndGroupsAndUpdateSeriesDisplayNames(props, datas, warn) {
-    const { settings, chartType } = props;
-
-    return chartType === "scatter"    ? getDimensionsAndGroupsForScatterChart(datas) :
-           isStacked(settings, datas) ? getDimensionsAndGroupsAndUpdateSeriesDisplayNamesForStackedChart(props, datas, warn) :
-           getDimensionsAndGroupsForOther(props, datas, warn);
+  const { settings, chartType } = props;
+
+  return chartType === "scatter"
+    ? getDimensionsAndGroupsForScatterChart(datas)
+    : isStacked(settings, datas)
+      ? getDimensionsAndGroupsAndUpdateSeriesDisplayNamesForStackedChart(
+          props,
+          datas,
+          warn,
+        )
+      : getDimensionsAndGroupsForOther(props, datas, warn);
 }
 
-
 ///------------------------------------------------------------ Y AXIS PROPS ------------------------------------------------------------///
 
-function getYAxisSplit({ settings, chartType, isScalarSeries, series }, datas, yExtents) {
-    // don't auto-split if the metric columns are all identical, i.e. it's a breakout multiseries
-    const hasDifferentYAxisColumns = _.uniq(series.map(s => s.data.cols[1])).length > 1;
-    if (!isScalarSeries && chartType !== "scatter" && !isStacked(settings, datas) && hasDifferentYAxisColumns && settings["graph.y_axis.auto_split"] !== false) {
-        return computeSplit(yExtents);
-    }
-    return [series.map((s,i) => i)];
+function getYAxisSplit(
+  { settings, chartType, isScalarSeries, series },
+  datas,
+  yExtents,
+) {
+  // don't auto-split if the metric columns are all identical, i.e. it's a breakout multiseries
+  const hasDifferentYAxisColumns =
+    _.uniq(series.map(s => s.data.cols[1])).length > 1;
+  if (
+    !isScalarSeries &&
+    chartType !== "scatter" &&
+    !isStacked(settings, datas) &&
+    hasDifferentYAxisColumns &&
+    settings["graph.y_axis.auto_split"] !== false
+  ) {
+    return computeSplit(yExtents);
+  }
+  return [series.map((s, i) => i)];
 }
 
 function getYAxisSplitLeftAndRight(series, yAxisSplit, yExtents) {
-    return yAxisSplit.map(indexes => ({
-        series: indexes.map(index => series[index]),
-        extent: d3.extent([].concat(...indexes.map(index => yExtents[index])))
-    }));
+  return yAxisSplit.map(indexes => ({
+    series: indexes.map(index => series[index]),
+    extent: d3.extent([].concat(...indexes.map(index => yExtents[index]))),
+  }));
 }
 
-
 function getIsSplitYAxis(left, right) {
-    return (right && right.series.length) && (left && left.series.length > 0);
+  return right && right.series.length && (left && left.series.length > 0);
 }
 
 function getYAxisProps(props, groups, datas) {
-    const yExtents = groups.map(group => d3.extent(group[0].all(), d => d.value));
-    const yAxisSplit = getYAxisSplit(props, datas, yExtents);
-
-    const [ yLeftSplit, yRightSplit ] = getYAxisSplitLeftAndRight(props.series, yAxisSplit, yExtents);
-
-    return {
-        yExtents,
-        yAxisSplit,
-        yExtent: d3.extent([].concat(...yExtents)),
-        yLeftSplit,
-        yRightSplit,
-        isSplit: getIsSplitYAxis(yLeftSplit, yRightSplit)
-    };
+  const yExtents = groups.map(group => d3.extent(group[0].all(), d => d.value));
+  const yAxisSplit = getYAxisSplit(props, datas, yExtents);
+
+  const [yLeftSplit, yRightSplit] = getYAxisSplitLeftAndRight(
+    props.series,
+    yAxisSplit,
+    yExtents,
+  );
+
+  return {
+    yExtents,
+    yAxisSplit,
+    yExtent: d3.extent([].concat(...yExtents)),
+    yLeftSplit,
+    yRightSplit,
+    isSplit: getIsSplitYAxis(yLeftSplit, yRightSplit),
+  };
 }
 
 /// make the `onBrushChange()` and `onBrushEnd()` functions we'll use later, as well as an `isBrushing()` function to check
 /// current status.
 function makeBrushChangeFunctions({ series, onChangeCardAndRun }) {
-    let _isBrushing = false;
-
-    const isBrushing = () => _isBrushing;
-
-    function onBrushChange() {
-        _isBrushing = true;
-    }
-
-    function onBrushEnd(range) {
-        _isBrushing = false;
-        if (range) {
-            const column = series[0].data.cols[0];
-            const card = series[0].card;
-            const [start, end] = range;
-            if (isDimensionTimeseries(series)) {
-                onChangeCardAndRun({ nextCard: updateDateTimeFilter(card, column, start, end), previousCard: card });
-            } else {
-                onChangeCardAndRun({ nextCard: updateNumericFilter(card, column, start, end), previousCard: card });
-            }
-        }
+  let _isBrushing = false;
+
+  const isBrushing = () => _isBrushing;
+
+  function onBrushChange() {
+    _isBrushing = true;
+  }
+
+  function onBrushEnd(range) {
+    _isBrushing = false;
+    if (range) {
+      const column = series[0].data.cols[0];
+      const card = series[0].card;
+      const [start, end] = range;
+      if (isDimensionTimeseries(series)) {
+        onChangeCardAndRun({
+          nextCard: updateDateTimeFilter(card, column, start, end),
+          previousCard: card,
+        });
+      } else {
+        onChangeCardAndRun({
+          nextCard: updateNumericFilter(card, column, start, end),
+          previousCard: card,
+        });
+      }
     }
+  }
 
-    return { isBrushing, onBrushChange, onBrushEnd };
+  return { isBrushing, onBrushChange, onBrushEnd };
 }
 
-
 /************************************************************ INDIVIDUAL CHART SETUP ************************************************************/
 
 function getDcjsChart(cardType, parent) {
-    switch (cardType) {
-        case "line":    return lineAddons(dc.lineChart(parent));
-        case "area":    return lineAddons(dc.lineChart(parent));
-        case "bar":     return dc.barChart(parent);
-        case "scatter": return dc.bubbleChart(parent);
-        default:        return dc.barChart(parent);
-    }
+  switch (cardType) {
+    case "line":
+      return lineAddons(dc.lineChart(parent));
+    case "area":
+      return lineAddons(dc.lineChart(parent));
+    case "bar":
+      return dc.barChart(parent);
+    case "scatter":
+      return dc.bubbleChart(parent);
+    default:
+      return dc.barChart(parent);
+  }
 }
 
 function applyChartLineBarSettings(chart, settings, chartType) {
-    // LINE/AREA:
-    // for chart types that have an 'interpolate' option (line/area charts), enable based on settings
-    if (chart.interpolate) chart.interpolate(settings["line.interpolate"] || DEFAULT_INTERPOLATION);
-
-    // AREA:
-    if (chart.renderArea) chart.renderArea(chartType === "area");
-
-    // BAR:
-    if (chart.barPadding) chart.barPadding(BAR_PADDING_RATIO)
-                               .centerBar(settings["graph.x_axis.scale"] !== "ordinal");
+  // LINE/AREA:
+  // for chart types that have an 'interpolate' option (line/area charts), enable based on settings
+  if (chart.interpolate)
+    chart.interpolate(settings["line.interpolate"] || DEFAULT_INTERPOLATION);
+
+  // AREA:
+  if (chart.renderArea) chart.renderArea(chartType === "area");
+
+  // BAR:
+  if (chart.barPadding)
+    chart
+      .barPadding(BAR_PADDING_RATIO)
+      .centerBar(settings["graph.x_axis.scale"] !== "ordinal");
 }
 
-
 // TODO - give this a good name when I figure out what it does
 function doScatterChartStuff(chart, datas, index, { yExtent, yExtents }) {
-    chart.keyAccessor((d) => d.key[0])
-         .valueAccessor((d) => d.key[1])
-
-    if (chart.radiusValueAccessor) {
-        const isBubble = datas[index][0].length > 2;
-        if (isBubble) {
-            const BUBBLE_SCALE_FACTOR_MAX = 64;
-            chart
-                .radiusValueAccessor((d) => d.value)
-                .r(d3.scale.sqrt()
-                     .domain([0, yExtent[1] * BUBBLE_SCALE_FACTOR_MAX])
-                     .range([0, 1])
-                );
-        } else {
-            chart.radiusValueAccessor((d) => 1)
-            chart.MIN_RADIUS = 3
-        }
-        chart.minRadiusWithLabel(Infinity);
+  chart.keyAccessor(d => d.key[0]).valueAccessor(d => d.key[1]);
+
+  if (chart.radiusValueAccessor) {
+    const isBubble = datas[index][0].length > 2;
+    if (isBubble) {
+      const BUBBLE_SCALE_FACTOR_MAX = 64;
+      chart.radiusValueAccessor(d => d.value).r(
+        d3.scale
+          .sqrt()
+          .domain([0, yExtent[1] * BUBBLE_SCALE_FACTOR_MAX])
+          .range([0, 1]),
+      );
+    } else {
+      chart.radiusValueAccessor(d => 1);
+      chart.MIN_RADIUS = 3;
     }
+    chart.minRadiusWithLabel(Infinity);
+  }
 }
 
 /// set the colors for a CHART based on the number of series and type of chart
 /// see http://dc-js.github.io/dc.js/docs/html/dc.colorMixin.html
 function setChartColor({ settings, chartType }, chart, groups, index) {
-    const group  = groups[index];
-    const colors = settings["graph.colors"];
-
-    // multiple series
-    if (groups.length > 1 || chartType === "scatter") {
-        // multiple stacks
-        if (group.length > 1) {
-            // compute shades of the assigned color
-            chart.ordinalColors(colorShades(colors[index % colors.length], group.length));
-        } else {
-            chart.colors(colors[index % colors.length]);
-        }
+  const group = groups[index];
+  const colors = settings["graph.colors"];
+
+  // multiple series
+  if (groups.length > 1 || chartType === "scatter") {
+    // multiple stacks
+    if (group.length > 1) {
+      // compute shades of the assigned color
+      chart.ordinalColors(
+        colorShades(colors[index % colors.length], group.length),
+      );
     } else {
-        chart.ordinalColors(colors);
+      chart.colors(colors[index % colors.length]);
     }
+  } else {
+    chart.ordinalColors(colors);
+  }
 }
 
 /// Return a sequence of little charts for each of the groups.
-function getCharts(props, yAxisProps, parent, datas, groups, dimension, { onBrushChange, onBrushEnd }) {
-    const { settings, chartType, series, onChangeCardAndRun } = props;
-    const { yAxisSplit } = yAxisProps;
-
-    return groups.map((group, index) => {
-        const chart = getDcjsChart(chartType, parent);
-
-        if (enableBrush(series, onChangeCardAndRun)) initBrush(parent, chart, onBrushChange, onBrushEnd);
-
-        // disable clicks
-        chart.onClick = () => {};
-
-        chart.dimension(dimension)
-             .group(group[0])
-             .transitionDuration(0)
-             .useRightYAxis(yAxisSplit.length > 1 && yAxisSplit[1].includes(index));
-
-        if (chartType === "scatter") doScatterChartStuff(chart, datas, index, yAxisProps);
-
-        if (chart.defined) {
-            chart.defined(
-                settings["line.missing"] === "none" ?
-                (d) => d.y != null :
-                (d) => true
-            );
-        }
+function getCharts(
+  props,
+  yAxisProps,
+  parent,
+  datas,
+  groups,
+  dimension,
+  { onBrushChange, onBrushEnd },
+) {
+  const { settings, chartType, series, onChangeCardAndRun } = props;
+  const { yAxisSplit } = yAxisProps;
+
+  return groups.map((group, index) => {
+    const chart = getDcjsChart(chartType, parent);
+
+    if (enableBrush(series, onChangeCardAndRun))
+      initBrush(parent, chart, onBrushChange, onBrushEnd);
+
+    // disable clicks
+    chart.onClick = () => {};
+
+    chart
+      .dimension(dimension)
+      .group(group[0])
+      .transitionDuration(0)
+      .useRightYAxis(yAxisSplit.length > 1 && yAxisSplit[1].includes(index));
+
+    if (chartType === "scatter")
+      doScatterChartStuff(chart, datas, index, yAxisProps);
+
+    if (chart.defined) {
+      chart.defined(
+        settings["line.missing"] === "none" ? d => d.y != null : d => true,
+      );
+    }
 
-        setChartColor(props, chart, groups, index);
+    setChartColor(props, chart, groups, index);
 
-        for (let i = 1; i < group.length; i++) {
-            chart.stack(group[i]);
-        }
+    for (let i = 1; i < group.length; i++) {
+      chart.stack(group[i]);
+    }
 
-        applyChartLineBarSettings(chart, settings, chartType);
+    applyChartLineBarSettings(chart, settings, chartType);
 
-        return chart;
-    });
+    return chart;
+  });
 }
 
-
 /************************************************************ OTHER SETUP ************************************************************/
 
 /// Add a `goalChart` to the end of `charts`, and return an appropriate `onGoalHover` function as needed.
-function addGoalChartAndGetOnGoalHover({ settings, onHoverChange }, xDomain, parent, charts) {
-    if (!settings["graph.show_goal"]) return () => {};
-
-    const goalValue     = settings["graph.goal_value"];
-    const goalData      = [[xDomain[0], goalValue], [xDomain[1], goalValue]];
-    const goalDimension = crossfilter(goalData).dimension(d => d[0]);
-
-    // Take the last point rather than summing in case xDomain[0] === xDomain[1], e.x. when the chart
-    // has just a single row / datapoint
-    const goalGroup = goalDimension.group().reduce((p,d) => d[1], (p,d) => p, () => 0);
-    const goalIndex = charts.length;
-
-    const goalChart = dc.lineChart(parent)
-                        .dimension(goalDimension)
-                        .group(goalGroup)
-                        .on('renderlet', function (chart) {
-                            // remove "sub" class so the goal is not used in voronoi computation
-                            chart.select(".sub._"+goalIndex)
-                                 .classed("sub", false)
-                                 .classed("goal", true);
-                        });
-    charts.push(goalChart);
-
-    return (element) => {
-        onHoverChange(element && {
-            element,
-            data: [{ key: t`Goal`, value: goalValue }]
-        });
-    };
+function addGoalChartAndGetOnGoalHover(
+  { settings, onHoverChange },
+  xDomain,
+  parent,
+  charts,
+) {
+  if (!settings["graph.show_goal"]) return () => {};
+
+  const goalValue = settings["graph.goal_value"];
+  const goalData = [[xDomain[0], goalValue], [xDomain[1], goalValue]];
+  const goalDimension = crossfilter(goalData).dimension(d => d[0]);
+
+  // Take the last point rather than summing in case xDomain[0] === xDomain[1], e.x. when the chart
+  // has just a single row / datapoint
+  const goalGroup = goalDimension
+    .group()
+    .reduce((p, d) => d[1], (p, d) => p, () => 0);
+  const goalIndex = charts.length;
+
+  const goalChart = dc
+    .lineChart(parent)
+    .dimension(goalDimension)
+    .group(goalGroup)
+    .on("renderlet", function(chart) {
+      // remove "sub" class so the goal is not used in voronoi computation
+      chart
+        .select(".sub._" + goalIndex)
+        .classed("sub", false)
+        .classed("goal", true);
+    });
+  charts.push(goalChart);
+
+  return element => {
+    onHoverChange(
+      element && {
+        element,
+        data: [{ key: t`Goal`, value: goalValue }],
+      },
+    );
+  };
 }
 
 function applyXAxisSettings({ settings, series }, xAxisProps, parent) {
-    if      (isTimeseries(settings))     applyChartTimeseriesXAxis(parent, settings, series, xAxisProps);
-    else if (isQuantitative(settings)) applyChartQuantitativeXAxis(parent, settings, series, xAxisProps);
-    else                                    applyChartOrdinalXAxis(parent, settings, series, xAxisProps);
+  if (isTimeseries(settings))
+    applyChartTimeseriesXAxis(parent, settings, series, xAxisProps);
+  else if (isQuantitative(settings))
+    applyChartQuantitativeXAxis(parent, settings, series, xAxisProps);
+  else applyChartOrdinalXAxis(parent, settings, series, xAxisProps);
 }
 
 function applyYAxisSettings({ settings }, { yLeftSplit, yRightSplit }, parent) {
-    if (yLeftSplit  &&  yLeftSplit.series.length > 0) applyChartYAxis(parent, settings, yLeftSplit.series,  yLeftSplit.extent,  "left");
-    if (yRightSplit && yRightSplit.series.length > 0) applyChartYAxis(parent, settings, yRightSplit.series, yRightSplit.extent, "right");
+  if (yLeftSplit && yLeftSplit.series.length > 0)
+    applyChartYAxis(
+      parent,
+      settings,
+      yLeftSplit.series,
+      yLeftSplit.extent,
+      "left",
+    );
+  if (yRightSplit && yRightSplit.series.length > 0)
+    applyChartYAxis(
+      parent,
+      settings,
+      yRightSplit.series,
+      yRightSplit.extent,
+      "right",
+    );
 }
 
-
 // TODO - better name
 function doGroupedBarStuff(parent) {
-    parent.on("renderlet.grouped-bar", function (chart) {
-        // HACK: dc.js doesn't support grouped bar charts so we need to manually resize/reposition them
-        // https://github.com/dc-js/dc.js/issues/558
-        const barCharts = chart.selectAll(".sub rect:first-child")[0].map(node => node.parentNode.parentNode.parentNode);
-        if (barCharts.length > 0) {
-            const oldBarWidth      = parseFloat(barCharts[0].querySelector("rect").getAttribute("width"));
-            const newBarWidthTotal = oldBarWidth / barCharts.length;
-            const seriesPadding    = newBarWidthTotal < 4 ? 0 :
-                                     newBarWidthTotal < 8 ? 1 :
-                                     2;
-            const newBarWidth      = Math.max(1, newBarWidthTotal - seriesPadding);
-
-            chart.selectAll("g.sub rect").attr("width", newBarWidth);
-            barCharts.forEach((barChart, index) => {
-                barChart.setAttribute("transform", "translate(" + ((newBarWidth + seriesPadding) * index) + ", 0)");
-            });
-        }
-    });
+  parent.on("renderlet.grouped-bar", function(chart) {
+    // HACK: dc.js doesn't support grouped bar charts so we need to manually resize/reposition them
+    // https://github.com/dc-js/dc.js/issues/558
+    const barCharts = chart
+      .selectAll(".sub rect:first-child")[0]
+      .map(node => node.parentNode.parentNode.parentNode);
+    if (barCharts.length > 0) {
+      const oldBarWidth = parseFloat(
+        barCharts[0].querySelector("rect").getAttribute("width"),
+      );
+      const newBarWidthTotal = oldBarWidth / barCharts.length;
+      const seriesPadding =
+        newBarWidthTotal < 4 ? 0 : newBarWidthTotal < 8 ? 1 : 2;
+      const newBarWidth = Math.max(1, newBarWidthTotal - seriesPadding);
+
+      chart.selectAll("g.sub rect").attr("width", newBarWidth);
+      barCharts.forEach((barChart, index) => {
+        barChart.setAttribute(
+          "transform",
+          "translate(" + (newBarWidth + seriesPadding) * index + ", 0)",
+        );
+      });
+    }
+  });
 }
 
 // TODO - better name
 function doHistogramBarStuff(parent) {
-    parent.on("renderlet.histogram-bar", function (chart) {
-        const barCharts = chart.selectAll(".sub rect:first-child")[0].map(node => node.parentNode.parentNode.parentNode);
-        if (!barCharts.length) return;
-
-        // manually size bars to fill space, minus 1 pixel padding
-        const bars        = barCharts[0].querySelectorAll("rect");
-        const barWidth    = parseFloat(bars[0].getAttribute("width"));
-        const newBarWidth = parseFloat(bars[1].getAttribute("x")) - parseFloat(bars[0].getAttribute("x")) - 1;
-        if (newBarWidth > barWidth) {
-            chart.selectAll("g.sub .bar").attr("width", newBarWidth);
-        }
-
-        // shift half of bar width so ticks line up with start of each bar
-        for (const barChart of barCharts) {
-            barChart.setAttribute("transform", `translate(${barWidth / 2}, 0)`);
-        }
-    });
-}
-
-
+  parent.on("renderlet.histogram-bar", function(chart) {
+    const barCharts = chart
+      .selectAll(".sub rect:first-child")[0]
+      .map(node => node.parentNode.parentNode.parentNode);
+    if (!barCharts.length) return;
+
+    // manually size bars to fill space, minus 1 pixel padding
+    const bars = barCharts[0].querySelectorAll("rect");
+    const barWidth = parseFloat(bars[0].getAttribute("width"));
+    const newBarWidth =
+      parseFloat(bars[1].getAttribute("x")) -
+      parseFloat(bars[0].getAttribute("x")) -
+      1;
+    if (newBarWidth > barWidth) {
+      chart.selectAll("g.sub .bar").attr("width", newBarWidth);
+    }
 
+    // shift half of bar width so ticks line up with start of each bar
+    for (const barChart of barCharts) {
+      barChart.setAttribute("transform", `translate(${barWidth / 2}, 0)`);
+    }
+  });
+}
 
 /************************************************************ PUTTING IT ALL TOGETHER ************************************************************/
 
 type LineAreaBarProps = VisualizationProps & {
-    chartType: "line" | "area" | "bar" | "scatter",
-    isScalarSeries: boolean,
-    maxSeries: number
-}
+  chartType: "line" | "area" | "bar" | "scatter",
+  isScalarSeries: boolean,
+  maxSeries: number,
+};
 
 export default function lineAreaBar(element: Element, props: LineAreaBarProps) {
-    const { onRender, chartType, isScalarSeries, settings} = props;
+  const { onRender, chartType, isScalarSeries, settings } = props;
 
-    const warnings = {};
-    const warn = (id) => {
-        warnings[id] = (warnings[id] || 0) + 1;
-    }
+  const warnings = {};
+  const warn = id => {
+    warnings[id] = (warnings[id] || 0) + 1;
+  };
 
-    checkSeriesIsValid(props);
+  checkSeriesIsValid(props);
 
-    // force histogram to be ordinal axis with zero-filled missing points
-    if (isHistogram(settings)) {
-        settings["line.missing"]       = "zero";
-        settings["graph.x_axis.scale"] = "ordinal"
-    }
+  // force histogram to be ordinal axis with zero-filled missing points
+  if (isHistogram(settings)) {
+    settings["line.missing"] = "zero";
+    settings["graph.x_axis.scale"] = "ordinal";
+  }
 
-    const datas      = getDatas(props, warn);
-    const xAxisProps = getXAxisProps(props, datas);
+  const datas = getDatas(props, warn);
+  const xAxisProps = getXAxisProps(props, datas);
 
-    fillMissingValuesInDatas(props, xAxisProps, datas);
+  fillMissingValuesInDatas(props, xAxisProps, datas);
 
-    if (isScalarSeries) xAxisProps.xValues = datas.map(data => data[0][0]); // TODO - what is this for?
+  if (isScalarSeries) xAxisProps.xValues = datas.map(data => data[0][0]); // TODO - what is this for?
 
-    const { dimension, groups } = getDimensionsAndGroupsAndUpdateSeriesDisplayNames(props, datas, warn);
+  const {
+    dimension,
+    groups,
+  } = getDimensionsAndGroupsAndUpdateSeriesDisplayNames(props, datas, warn);
 
-    const yAxisProps = getYAxisProps(props, groups, datas);
+  const yAxisProps = getYAxisProps(props, groups, datas);
 
-    // Don't apply to linear or timeseries X-axis since the points are always plotted in order
-    if (!isTimeseries(settings) && !isQuantitative(settings)) forceSortedGroupsOfGroups(groups, makeIndexMap(xAxisProps.xValues));
+  // Don't apply to linear or timeseries X-axis since the points are always plotted in order
+  if (!isTimeseries(settings) && !isQuantitative(settings))
+    forceSortedGroupsOfGroups(groups, makeIndexMap(xAxisProps.xValues));
 
-    const parent = dc.compositeChart(element);
-    initChart(parent, element);
+  const parent = dc.compositeChart(element);
+  initChart(parent, element);
 
-    const brushChangeFunctions = makeBrushChangeFunctions(props);
+  const brushChangeFunctions = makeBrushChangeFunctions(props);
 
-    const charts      = getCharts(props, yAxisProps, parent, datas, groups, dimension, brushChangeFunctions);
-    const onGoalHover = addGoalChartAndGetOnGoalHover(props, xAxisProps.xDomain, parent, charts);
+  const charts = getCharts(
+    props,
+    yAxisProps,
+    parent,
+    datas,
+    groups,
+    dimension,
+    brushChangeFunctions,
+  );
+  const onGoalHover = addGoalChartAndGetOnGoalHover(
+    props,
+    xAxisProps.xDomain,
+    parent,
+    charts,
+  );
 
-    parent.compose(charts);
+  parent.compose(charts);
 
-    if      (groups.length > 1 && !props.isScalarSeries) doGroupedBarStuff(parent);
-    else if (isHistogramBar(props))                      doHistogramBarStuff(parent);
+  if (groups.length > 1 && !props.isScalarSeries) doGroupedBarStuff(parent);
+  else if (isHistogramBar(props)) doHistogramBarStuff(parent);
 
-    // HACK: compositeChart + ordinal X axis shenanigans. See https://github.com/dc-js/dc.js/issues/678 and https://github.com/dc-js/dc.js/issues/662
-    parent._rangeBandPadding(chartType === "bar" ? BAR_PADDING_RATIO : 1) //
+  // HACK: compositeChart + ordinal X axis shenanigans. See https://github.com/dc-js/dc.js/issues/678 and https://github.com/dc-js/dc.js/issues/662
+  parent._rangeBandPadding(chartType === "bar" ? BAR_PADDING_RATIO : 1); //
 
-    applyXAxisSettings(props, xAxisProps, parent);
+  applyXAxisSettings(props, xAxisProps, parent);
 
-    // override tick format for bars. ticks are aligned with beginning of bar, so just show the start value
-    if (isHistogramBar(props)) parent.xAxis().tickFormat(d => formatNumber(d));
+  // override tick format for bars. ticks are aligned with beginning of bar, so just show the start value
+  if (isHistogramBar(props)) parent.xAxis().tickFormat(d => formatNumber(d));
 
-    applyYAxisSettings(props, yAxisProps, parent);
+  applyYAxisSettings(props, yAxisProps, parent);
 
-    setupTooltips(props, datas, parent, brushChangeFunctions);
+  setupTooltips(props, datas, parent, brushChangeFunctions);
 
-    parent.render();
+  parent.render();
 
-    // apply any on-rendering functions (this code lives in `LineAreaBarPostRenderer`)
-    lineAndBarOnRender(parent, settings, onGoalHover, yAxisProps.isSplit, isStacked(settings, datas));
+  // apply any on-rendering functions (this code lives in `LineAreaBarPostRenderer`)
+  lineAndBarOnRender(
+    parent,
+    settings,
+    onGoalHover,
+    yAxisProps.isSplit,
+    isStacked(settings, datas),
+  );
 
-    // only ordinal axis can display "null" values
-    if (isOrdinal(settings)) delete warnings[NULL_DIMENSION_WARNING];
+  // only ordinal axis can display "null" values
+  if (isOrdinal(settings)) delete warnings[NULL_DIMENSION_WARNING];
 
-    if (onRender) onRender({
-        yAxisSplit: yAxisProps.yAxisSplit,
-        warnings: Object.keys(warnings)
+  if (onRender)
+    onRender({
+      yAxisSplit: yAxisProps.yAxisSplit,
+      warnings: Object.keys(warnings),
     });
 
-    return parent;
+  return parent;
 }
 
-export const lineRenderer    = (element, props) => lineAreaBar(element, { ...props, chartType: "line" });
-export const areaRenderer    = (element, props) => lineAreaBar(element, { ...props, chartType: "area" });
-export const barRenderer     = (element, props) => lineAreaBar(element, { ...props, chartType: "bar" });
-export const scatterRenderer = (element, props) => lineAreaBar(element, { ...props, chartType: "scatter" });
+export const lineRenderer = (element, props) =>
+  lineAreaBar(element, { ...props, chartType: "line" });
+export const areaRenderer = (element, props) =>
+  lineAreaBar(element, { ...props, chartType: "area" });
+export const barRenderer = (element, props) =>
+  lineAreaBar(element, { ...props, chartType: "bar" });
+export const scatterRenderer = (element, props) =>
+  lineAreaBar(element, { ...props, chartType: "scatter" });
diff --git a/frontend/src/metabase/visualizations/lib/RowRenderer.js b/frontend/src/metabase/visualizations/lib/RowRenderer.js
index c53f1399e9792007c4a54aa0720a99d2fefb5976..716f8c5cf42e7874d21059c9347cd65e976d88a8 100644
--- a/frontend/src/metabase/visualizations/lib/RowRenderer.js
+++ b/frontend/src/metabase/visualizations/lib/RowRenderer.js
@@ -9,154 +9,163 @@ import { formatValue } from "metabase/lib/formatting";
 import { initChart, forceSortedGroup, makeIndexMap } from "./renderer_utils";
 import { getFriendlyName } from "./utils";
 
-
 export default function rowRenderer(
-    element,
-    { settings, series, onHoverChange, onVisualizationClick, height }
+  element,
+  { settings, series, onHoverChange, onVisualizationClick, height },
 ) {
-    const { cols } = series[0].data;
-
-    if (series.length > 1) {
-        throw new Error("Row chart does not support multiple series");
-    }
-
-    const chart = dc.rowChart(element);
-
-    // disable clicks
-    chart.onClick = () => {};
-
-    const colors = settings["graph.colors"];
-
-    const formatDimension = (row) =>
-        formatValue(row[0], { column: cols[0], type: "axis" })
-
-    // dc.js doesn't give us a way to format the row labels from unformatted data, so we have to
-    // do it here then construct a mapping to get the original dimension for tooltipsd/clicks
-    const rows = series[0].data.rows.map(row => [
-        formatDimension(row),
-        row[1]
-    ]);
-    const formattedDimensionMap = new Map(rows.map(([formattedDimension], index) => [
-        formattedDimension,
-        series[0].data.rows[index][0]
-    ]))
-
-    const dataset = crossfilter(rows);
-    const dimension = dataset.dimension(d => d[0]);
-    const group = dimension.group().reduceSum(d => d[1]);
-    const xDomain = d3.extent(rows, d => d[1]);
-    const yValues = rows.map(d => d[0]);
-
-    forceSortedGroup(group, makeIndexMap(yValues));
-
-    initChart(chart, element);
-
-    chart.on("renderlet.tooltips", chart => {
-        if (onHoverChange) {
-            chart.selectAll(".row rect").on("mousemove", (d, i) => {
-                onHoverChange && onHoverChange({
-                    // for single series bar charts, fade the series and highlght the hovered element with CSS
-                    index: -1,
-                    event: d3.event,
-                    data: [
-                        { key: getFriendlyName(cols[0]), value: formattedDimensionMap.get(d.key), col: cols[0] },
-                        { key: getFriendlyName(cols[1]), value: d.value, col: cols[1] }
-                    ]
-                });
-            }).on("mouseleave", () => {
-                onHoverChange && onHoverChange(null);
-            });
-        }
-
-        if (onVisualizationClick) {
-            chart.selectAll(".row rect").on("click", function(d) {
-                onVisualizationClick({
-                    value: d.value,
-                    column: cols[1],
-                    dimensions: [{
-                        value: formattedDimensionMap.get(d.key),
-                        column: cols[0]
-                    }],
-                    element: this
-                })
+  const { cols } = series[0].data;
+
+  if (series.length > 1) {
+    throw new Error("Row chart does not support multiple series");
+  }
+
+  const chart = dc.rowChart(element);
+
+  // disable clicks
+  chart.onClick = () => {};
+
+  const colors = settings["graph.colors"];
+
+  const formatDimension = row =>
+    formatValue(row[0], { column: cols[0], type: "axis" });
+
+  // dc.js doesn't give us a way to format the row labels from unformatted data, so we have to
+  // do it here then construct a mapping to get the original dimension for tooltipsd/clicks
+  const rows = series[0].data.rows.map(row => [formatDimension(row), row[1]]);
+  const formattedDimensionMap = new Map(
+    rows.map(([formattedDimension], index) => [
+      formattedDimension,
+      series[0].data.rows[index][0],
+    ]),
+  );
+
+  const dataset = crossfilter(rows);
+  const dimension = dataset.dimension(d => d[0]);
+  const group = dimension.group().reduceSum(d => d[1]);
+  const xDomain = d3.extent(rows, d => d[1]);
+  const yValues = rows.map(d => d[0]);
+
+  forceSortedGroup(group, makeIndexMap(yValues));
+
+  initChart(chart, element);
+
+  chart.on("renderlet.tooltips", chart => {
+    if (onHoverChange) {
+      chart
+        .selectAll(".row rect")
+        .on("mousemove", (d, i) => {
+          onHoverChange &&
+            onHoverChange({
+              // for single series bar charts, fade the series and highlght the hovered element with CSS
+              index: -1,
+              event: d3.event,
+              data: [
+                {
+                  key: getFriendlyName(cols[0]),
+                  value: formattedDimensionMap.get(d.key),
+                  col: cols[0],
+                },
+                { key: getFriendlyName(cols[1]), value: d.value, col: cols[1] },
+              ],
             });
-        }
-    });
-
-    chart
-        .ordinalColors([ colors[0] ])
-        .x(d3.scale.linear().domain(xDomain))
-        .elasticX(true)
-        .dimension(dimension)
-        .group(group)
-        .ordering(d => d.index);
-
-    let labelPadHorizontal = 5;
-    let labelPadVertical = 1;
-    let labelsOutside = false;
-
-    chart.on("renderlet.bar-labels", chart => {
-        chart
-            .selectAll("g.row text")
-            .attr("text-anchor", labelsOutside ? "end" : "start")
-            .attr("x", labelsOutside ? -labelPadHorizontal : labelPadHorizontal)
-            .classed(labelsOutside ? "outside" : "inside", true);
-    });
-
-    if (settings["graph.y_axis.labels_enabled"]) {
-        chart.on("renderlet.axis-labels", chart => {
-            chart
-                .svg()
-                .append("text")
-                .attr("class", "x-axis-label")
-                .attr("text-anchor", "middle")
-                .attr("x", chart.width() / 2)
-                .attr("y", chart.height() - 10)
-                .text(settings["graph.y_axis.title_text"]);
+        })
+        .on("mouseleave", () => {
+          onHoverChange && onHoverChange(null);
         });
     }
 
-    // inital render
-    chart.render();
-
-    // bottom label height
-    let axisLabelHeight = 0;
-    if (settings["graph.y_axis.labels_enabled"]) {
-        axisLabelHeight = chart
-            .select(".x-axis-label")
-            .node()
-            .getBoundingClientRect().height;
-        chart.margins().bottom += axisLabelHeight;
+    if (onVisualizationClick) {
+      chart.selectAll(".row rect").on("click", function(d) {
+        onVisualizationClick({
+          value: d.value,
+          column: cols[1],
+          dimensions: [
+            {
+              value: formattedDimensionMap.get(d.key),
+              column: cols[0],
+            },
+          ],
+          element: this,
+        });
+      });
     }
+  });
 
-    // cap number of rows to fit
-    let rects = chart.selectAll(".row rect")[0];
-    let containerHeight = rects[rects.length - 1].getBoundingClientRect().bottom -
-                          rects[0].getBoundingClientRect().top;
-    let maxTextHeight = Math.max(
-        ...chart.selectAll("g.row text")[0].map(
-            e => e.getBoundingClientRect().height
-        )
-    );
-    let rowHeight = maxTextHeight + chart.gap() + labelPadVertical * 2;
-    let cap = Math.max(1, Math.floor(containerHeight / rowHeight));
-    chart.cap(cap);
+  chart
+    .ordinalColors([colors[0]])
+    .x(d3.scale.linear().domain(xDomain))
+    .elasticX(true)
+    .dimension(dimension)
+    .group(group)
+    .ordering(d => d.index);
 
-    chart.render();
+  let labelPadHorizontal = 5;
+  let labelPadVertical = 1;
+  let labelsOutside = false;
 
-    // check if labels overflow after rendering correct number of rows
-    let maxTextWidth = 0;
-    for (const elem of chart.selectAll("g.row")[0]) {
-        let rect = elem.querySelector("rect").getBoundingClientRect();
-        let text = elem.querySelector("text").getBoundingClientRect();
-        maxTextWidth = Math.max(maxTextWidth, text.width);
-        if (rect.width < text.width + labelPadHorizontal * 2) {
-            labelsOutside = true;
-        }
+  chart.on("renderlet.bar-labels", chart => {
+    chart
+      .selectAll("g.row text")
+      .attr("text-anchor", labelsOutside ? "end" : "start")
+      .attr("x", labelsOutside ? -labelPadHorizontal : labelPadHorizontal)
+      .classed(labelsOutside ? "outside" : "inside", true);
+  });
+
+  if (settings["graph.y_axis.labels_enabled"]) {
+    chart.on("renderlet.axis-labels", chart => {
+      chart
+        .svg()
+        .append("text")
+        .attr("class", "x-axis-label")
+        .attr("text-anchor", "middle")
+        .attr("x", chart.width() / 2)
+        .attr("y", chart.height() - 10)
+        .text(settings["graph.y_axis.title_text"]);
+    });
+  }
+
+  // inital render
+  chart.render();
+
+  // bottom label height
+  let axisLabelHeight = 0;
+  if (settings["graph.y_axis.labels_enabled"]) {
+    axisLabelHeight = chart
+      .select(".x-axis-label")
+      .node()
+      .getBoundingClientRect().height;
+    chart.margins().bottom += axisLabelHeight;
+  }
+
+  // cap number of rows to fit
+  let rects = chart.selectAll(".row rect")[0];
+  let containerHeight =
+    rects[rects.length - 1].getBoundingClientRect().bottom -
+    rects[0].getBoundingClientRect().top;
+  let maxTextHeight = Math.max(
+    ...chart
+      .selectAll("g.row text")[0]
+      .map(e => e.getBoundingClientRect().height),
+  );
+  let rowHeight = maxTextHeight + chart.gap() + labelPadVertical * 2;
+  let cap = Math.max(1, Math.floor(containerHeight / rowHeight));
+  chart.cap(cap);
+
+  chart.render();
+
+  // check if labels overflow after rendering correct number of rows
+  let maxTextWidth = 0;
+  for (const elem of chart.selectAll("g.row")[0]) {
+    let rect = elem.querySelector("rect").getBoundingClientRect();
+    let text = elem.querySelector("text").getBoundingClientRect();
+    maxTextWidth = Math.max(maxTextWidth, text.width);
+    if (rect.width < text.width + labelPadHorizontal * 2) {
+      labelsOutside = true;
     }
+  }
 
-    if (labelsOutside) {
-        chart.margins().left += maxTextWidth;
-        chart.render();
-    }
+  if (labelsOutside) {
+    chart.margins().left += maxTextWidth;
+    chart.render();
+  }
 }
diff --git a/frontend/src/metabase/visualizations/lib/apply_axis.js b/frontend/src/metabase/visualizations/lib/apply_axis.js
index e7c41484190f18247e14fd88da660fc077e088d4..39678c8ff2e6455614b047267f070a80edda4b45 100644
--- a/frontend/src/metabase/visualizations/lib/apply_axis.js
+++ b/frontend/src/metabase/visualizations/lib/apply_axis.js
@@ -19,20 +19,20 @@ const Y_LABEL_PADDING = 22;
 /// d3.js is dumb and sometimes numTicks is a number like 10 and other times it is an Array like [10]
 /// if it's an array then convert to a num. Use this function so you're guaranteed to get a number;
 function getNumTicks(axis) {
-    const ticks = axis.ticks();
-    return Array.isArray(ticks) ? ticks[0] : ticks;
+  const ticks = axis.ticks();
+  return Array.isArray(ticks) ? ticks[0] : ticks;
 }
 
 /// adjust the number of ticks to display on the y Axis based on its height in pixels. Since y axis ticks
 /// are all the same height there's no need to do fancy measurement like we do below for the x axis.
 function adjustYAxisTicksIfNeeded(axis, axisHeightPixels) {
-    const MIN_PIXELS_PER_TICK = 32;
+  const MIN_PIXELS_PER_TICK = 32;
 
-    const numTicks = getNumTicks(axis);
+  const numTicks = getNumTicks(axis);
 
-    if ((axisHeightPixels / numTicks) < MIN_PIXELS_PER_TICK) {
-        axis.ticks(Math.floor(axisHeightPixels / MIN_PIXELS_PER_TICK));
-    }
+  if (axisHeightPixels / numTicks < MIN_PIXELS_PER_TICK) {
+    axis.ticks(Math.floor(axisHeightPixels / MIN_PIXELS_PER_TICK));
+  }
 }
 
 /// Calculate the average length of values as strings.
@@ -43,245 +43,308 @@ function adjustYAxisTicksIfNeeded(axis, axisHeightPixels) {
 /// labels. To avoid wasting everyone's time measuring too many strings we only measure the first 100 which seems to
 /// work well enough.
 function averageStringLengthOfValues(values) {
-    const MAX_VALUES_TO_MEASURE = 100;
-    values = values.slice(0, MAX_VALUES_TO_MEASURE);
+  const MAX_VALUES_TO_MEASURE = 100;
+  values = values.slice(0, MAX_VALUES_TO_MEASURE);
 
-    let totalLength = 0;
-    for (let value of values) totalLength += String(value).length;
+  let totalLength = 0;
+  for (let value of values) totalLength += String(value).length;
 
-    return Math.round(totalLength / values.length);
+  return Math.round(totalLength / values.length);
 }
 
 /// adjust the number of ticks displayed on the x axis based on the average width of each xValue. We measure the
 /// xValues to determine an average length and then figure out how many will be able to fit based on the width of the
 /// chart.
 function adjustXAxisTicksIfNeeded(axis, chartWidthPixels, xValues) {
-    // The const below is the number of pixels we should devote to each character for x-axis ticks. It can be thought
-    // of as an average pixel width of a single character; this number is an approximation; adjust it to taste.
-    // Higher values will reduce the number of ticks show on the x axis, increasing space between them; decreasing it
-    // will increase tick density.
-    const APPROXIMATE_AVERAGE_CHAR_WIDTH_PIXELS = 8;
-
-    // calculate the average length of each tick, then convert that to pixels
-    const tickAverageStringLength = averageStringLengthOfValues(xValues);
-    const tickAverageWidthPixels  = tickAverageStringLength * APPROXIMATE_AVERAGE_CHAR_WIDTH_PIXELS;
-
-    // now figure out the approximate number of ticks we'll be able to show based on the width of the chart. Round
-    // down so we error on the side of more space rather than less.
-    const maxTicks = Math.floor(chartWidthPixels / tickAverageWidthPixels);
-
-    // finally, if the chart is currently showing more ticks than we think it can show, adjust it down
-    if (getNumTicks(axis) > maxTicks) axis.ticks(maxTicks);
+  // The const below is the number of pixels we should devote to each character for x-axis ticks. It can be thought
+  // of as an average pixel width of a single character; this number is an approximation; adjust it to taste.
+  // Higher values will reduce the number of ticks show on the x axis, increasing space between them; decreasing it
+  // will increase tick density.
+  const APPROXIMATE_AVERAGE_CHAR_WIDTH_PIXELS = 8;
+
+  // calculate the average length of each tick, then convert that to pixels
+  const tickAverageStringLength = averageStringLengthOfValues(xValues);
+  const tickAverageWidthPixels =
+    tickAverageStringLength * APPROXIMATE_AVERAGE_CHAR_WIDTH_PIXELS;
+
+  // now figure out the approximate number of ticks we'll be able to show based on the width of the chart. Round
+  // down so we error on the side of more space rather than less.
+  const maxTicks = Math.floor(chartWidthPixels / tickAverageWidthPixels);
+
+  // finally, if the chart is currently showing more ticks than we think it can show, adjust it down
+  if (getNumTicks(axis) > maxTicks) axis.ticks(maxTicks);
 }
 
-export function applyChartTimeseriesXAxis(chart, settings, series, { xValues, xDomain, xInterval }) {
-    // find the first nonempty single series
-    // $FlowFixMe
-    const firstSeries: SingleSeries = _.find(series, (s) => !datasetContainsNoResults(s.data));
-
-    // setup an x-axis where the dimension is a timeseries
-    let dimensionColumn = firstSeries.data.cols[0];
-
-    // get the data's timezone offset from the first row
-    let dataOffset = parseTimestamp(firstSeries.data.rows[0][0]).utcOffset() / 60;
-
-    // compute the data interval
-    let dataInterval = xInterval;
-    let tickInterval = dataInterval;
-
-    if (settings["graph.x_axis.labels_enabled"]) {
-        chart.xAxisLabel(settings["graph.x_axis.title_text"] || getFriendlyName(dimensionColumn), X_LABEL_PADDING);
-    }
-    if (settings["graph.x_axis.axis_enabled"]) {
-        chart.renderVerticalGridLines(settings["graph.x_axis.gridLine_enabled"]);
-
-        if (dimensionColumn.unit == null) {
-            dimensionColumn = { ...dimensionColumn, unit: dataInterval.interval };
-        }
-
-        // special handling for weeks
-        // TODO: are there any other cases where we should do this?
-        if (dataInterval.interval === "week") {
-            // if tick interval is compressed then show months instead of weeks because they're nicer formatted
-            const newTickInterval = computeTimeseriesTicksInterval(xDomain, tickInterval, chart.width());
-            if (newTickInterval.interval !== tickInterval.interval || newTickInterval.count !== tickInterval.count) {
-                dimensionColumn = { ...dimensionColumn, unit: "month" },
-                tickInterval = { interval: "month", count: 1 };
-            }
-        }
-
-        chart.xAxis().tickFormat(timestamp => {
-            // timestamp is a plain Date object which discards the timezone,
-            // so add it back in so it's formatted correctly
-            const timestampFixed = moment(timestamp).utcOffset(dataOffset).format();
-            return formatValue(timestampFixed, { column: dimensionColumn, type: "axis" })
-        });
-
-        // Compute a sane interval to display based on the data granularity, domain, and chart width
-        tickInterval = computeTimeseriesTicksInterval(xDomain, tickInterval, chart.width());
-        chart.xAxis().ticks(tickInterval.rangeFn, tickInterval.count);
-    } else {
-        chart.xAxis().ticks(0);
+export function applyChartTimeseriesXAxis(
+  chart,
+  settings,
+  series,
+  { xValues, xDomain, xInterval },
+) {
+  // find the first nonempty single series
+  // $FlowFixMe
+  const firstSeries: SingleSeries = _.find(
+    series,
+    s => !datasetContainsNoResults(s.data),
+  );
+
+  // setup an x-axis where the dimension is a timeseries
+  let dimensionColumn = firstSeries.data.cols[0];
+
+  // get the data's timezone offset from the first row
+  let dataOffset = parseTimestamp(firstSeries.data.rows[0][0]).utcOffset() / 60;
+
+  // compute the data interval
+  let dataInterval = xInterval;
+  let tickInterval = dataInterval;
+
+  if (settings["graph.x_axis.labels_enabled"]) {
+    chart.xAxisLabel(
+      settings["graph.x_axis.title_text"] || getFriendlyName(dimensionColumn),
+      X_LABEL_PADDING,
+    );
+  }
+  if (settings["graph.x_axis.axis_enabled"]) {
+    chart.renderVerticalGridLines(settings["graph.x_axis.gridLine_enabled"]);
+
+    if (dimensionColumn.unit == null) {
+      dimensionColumn = { ...dimensionColumn, unit: dataInterval.interval };
     }
 
-    // pad the domain slightly to prevent clipping
-    xDomain[0] = moment(xDomain[0]).subtract(dataInterval.count * 0.75, dataInterval.interval);
-    xDomain[1] = moment(xDomain[1]).add(dataInterval.count * 0.75, dataInterval.interval);
-
-    // set the x scale
-    chart.x(d3.time.scale.utc().domain(xDomain));//.nice(d3.time[dataInterval.interval]));
-
-    // set the x units (used to compute bar size)
-    chart.xUnits((start, stop) => Math.ceil(1 + moment(stop).diff(start, dataInterval.interval) / dataInterval.count));
-}
-
-export function applyChartQuantitativeXAxis(chart, settings, series, { xValues, xDomain, xInterval }) {
-    // find the first nonempty single series
-    // $FlowFixMe
-    const firstSeries: SingleSeries = _.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);
+    // special handling for weeks
+    // TODO: are there any other cases where we should do this?
+    if (dataInterval.interval === "week") {
+      // if tick interval is compressed then show months instead of weeks because they're nicer formatted
+      const newTickInterval = computeTimeseriesTicksInterval(
+        xDomain,
+        tickInterval,
+        chart.width(),
+      );
+      if (
+        newTickInterval.interval !== tickInterval.interval ||
+        newTickInterval.count !== tickInterval.count
+      ) {
+        (dimensionColumn = { ...dimensionColumn, unit: "month" }),
+          (tickInterval = { interval: "month", count: 1 });
+      }
     }
-    if (settings["graph.x_axis.axis_enabled"]) {
-        chart.renderVerticalGridLines(settings["graph.x_axis.gridLine_enabled"]);
-        adjustXAxisTicksIfNeeded(chart.xAxis(), chart.width(), xValues);
 
-        chart.xAxis().tickFormat(d => formatValue(d, { column: dimensionColumn }));
-    } else {
-        chart.xAxis().ticks(0);
-        chart.xAxis().tickFormat('');
-    }
+    chart.xAxis().tickFormat(timestamp => {
+      // timestamp is a plain Date object which discards the timezone,
+      // so add it back in so it's formatted correctly
+      const timestampFixed = moment(timestamp)
+        .utcOffset(dataOffset)
+        .format();
+      return formatValue(timestampFixed, {
+        column: dimensionColumn,
+        type: "axis",
+      });
+    });
+
+    // Compute a sane interval to display based on the data granularity, domain, and chart width
+    tickInterval = computeTimeseriesTicksInterval(
+      xDomain,
+      tickInterval,
+      chart.width(),
+    );
+    chart.xAxis().ticks(tickInterval.rangeFn, tickInterval.count);
+  } else {
+    chart.xAxis().ticks(0);
+  }
+
+  // pad the domain slightly to prevent clipping
+  xDomain[0] = moment(xDomain[0]).subtract(
+    dataInterval.count * 0.75,
+    dataInterval.interval,
+  );
+  xDomain[1] = moment(xDomain[1]).add(
+    dataInterval.count * 0.75,
+    dataInterval.interval,
+  );
+
+  // set the x scale
+  chart.x(d3.time.scale.utc().domain(xDomain)); //.nice(d3.time[dataInterval.interval]));
+
+  // set the x units (used to compute bar size)
+  chart.xUnits((start, stop) =>
+    Math.ceil(
+      1 + moment(stop).diff(start, dataInterval.interval) / dataInterval.count,
+    ),
+  );
+}
 
-    let scale;
-    if (settings["graph.x_axis.scale"] === "pow") {
-        scale = d3.scale.pow().exponent(0.5);
-    } else if (settings["graph.x_axis.scale"] === "log") {
-        scale = d3.scale.log().base(Math.E);
-        if (!((xDomain[0] < 0 && xDomain[1] < 0) || (xDomain[0] > 0 && xDomain[1] > 0))) {
-            throw "X-axis must not cross 0 when using log scale.";
-        }
-    } else {
-        scale = d3.scale.linear();
+export function applyChartQuantitativeXAxis(
+  chart,
+  settings,
+  series,
+  { xValues, xDomain, xInterval },
+) {
+  // find the first nonempty single series
+  // $FlowFixMe
+  const firstSeries: SingleSeries = _.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,
+    );
+  }
+  if (settings["graph.x_axis.axis_enabled"]) {
+    chart.renderVerticalGridLines(settings["graph.x_axis.gridLine_enabled"]);
+    adjustXAxisTicksIfNeeded(chart.xAxis(), chart.width(), xValues);
+
+    chart.xAxis().tickFormat(d => formatValue(d, { column: dimensionColumn }));
+  } else {
+    chart.xAxis().ticks(0);
+    chart.xAxis().tickFormat("");
+  }
+
+  let scale;
+  if (settings["graph.x_axis.scale"] === "pow") {
+    scale = d3.scale.pow().exponent(0.5);
+  } else if (settings["graph.x_axis.scale"] === "log") {
+    scale = d3.scale.log().base(Math.E);
+    if (
+      !(
+        (xDomain[0] < 0 && xDomain[1] < 0) ||
+        (xDomain[0] > 0 && xDomain[1] > 0)
+      )
+    ) {
+      throw "X-axis must not cross 0 when using log scale.";
     }
+  } else {
+    scale = d3.scale.linear();
+  }
 
-    // pad the domain slightly to prevent clipping
-    xDomain = [
-        xDomain[0] - xInterval * 0.75,
-        xDomain[1] + xInterval * 0.75
-    ];
+  // pad the domain slightly to prevent clipping
+  xDomain = [xDomain[0] - xInterval * 0.75, xDomain[1] + xInterval * 0.75];
 
-    chart.x(scale.domain(xDomain))
-         .xUnits(dc.units.fp.precision(xInterval));
+  chart.x(scale.domain(xDomain)).xUnits(dc.units.fp.precision(xInterval));
 }
 
 export function applyChartOrdinalXAxis(chart, settings, series, { xValues }) {
-    // find the first nonempty single series
-    // $FlowFixMe
-    const firstSeries: SingleSeries = _.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);
-    }
-    if (settings["graph.x_axis.axis_enabled"]) {
-        chart.renderVerticalGridLines(settings["graph.x_axis.gridLine_enabled"]);
-        chart.xAxis().ticks(xValues.length);
-        adjustXAxisTicksIfNeeded(chart.xAxis(), chart.width(), xValues);
-
-        // unfortunately with ordinal axis you can't rely on xAxis.ticks(num) to control the display of labels
-        // so instead if we want to display fewer ticks than our full set we need to calculate visibleTicks()
-        const numTicks = getNumTicks(chart.xAxis());
-        if (numTicks < xValues.length) {
-            let keyInterval = Math.round(xValues.length / numTicks);
-            let visibleKeys = xValues.filter((v, i) => i % keyInterval === 0);
-            chart.xAxis().tickValues(visibleKeys);
-        }
-        chart.xAxis().tickFormat(d => formatValue(d, { column: dimensionColumn }));
-    } else {
-        chart.xAxis().ticks(0);
-        chart.xAxis().tickFormat('');
+  // find the first nonempty single series
+  // $FlowFixMe
+  const firstSeries: SingleSeries = _.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,
+    );
+  }
+  if (settings["graph.x_axis.axis_enabled"]) {
+    chart.renderVerticalGridLines(settings["graph.x_axis.gridLine_enabled"]);
+    chart.xAxis().ticks(xValues.length);
+    adjustXAxisTicksIfNeeded(chart.xAxis(), chart.width(), xValues);
+
+    // unfortunately with ordinal axis you can't rely on xAxis.ticks(num) to control the display of labels
+    // so instead if we want to display fewer ticks than our full set we need to calculate visibleTicks()
+    const numTicks = getNumTicks(chart.xAxis());
+    if (numTicks < xValues.length) {
+      let keyInterval = Math.round(xValues.length / numTicks);
+      let visibleKeys = xValues.filter((v, i) => i % keyInterval === 0);
+      chart.xAxis().tickValues(visibleKeys);
     }
+    chart.xAxis().tickFormat(d => formatValue(d, { column: dimensionColumn }));
+  } else {
+    chart.xAxis().ticks(0);
+    chart.xAxis().tickFormat("");
+  }
 
-    chart.x(d3.scale.ordinal().domain(xValues))
-         .xUnits(dc.units.ordinal);
+  chart.x(d3.scale.ordinal().domain(xValues)).xUnits(dc.units.ordinal);
 }
 
 export function applyChartYAxis(chart, settings, series, yExtent, axisName) {
-    let axis;
-    if (axisName !== "right") {
-        axis = {
-            scale:   (...args) => chart.y(...args),
-            axis:    (...args) => chart.yAxis(...args),
-            label:   (...args) => chart.yAxisLabel(...args),
-            setting: (name) => settings["graph.y_axis." + name]
-        };
+  let axis;
+  if (axisName !== "right") {
+    axis = {
+      scale: (...args) => chart.y(...args),
+      axis: (...args) => chart.yAxis(...args),
+      label: (...args) => chart.yAxisLabel(...args),
+      setting: name => settings["graph.y_axis." + name],
+    };
+  } else {
+    axis = {
+      scale: (...args) => chart.rightY(...args),
+      axis: (...args) => chart.rightYAxis(...args),
+      label: (...args) => chart.rightYAxisLabel(...args),
+      setting: name => settings["graph.y_axis." + name], // TODO: right axis settings
+    };
+  }
+
+  if (axis.setting("labels_enabled")) {
+    // left
+    if (axis.setting("title_text")) {
+      axis.label(axis.setting("title_text"), Y_LABEL_PADDING);
     } else {
-        axis = {
-            scale:   (...args) => chart.rightY(...args),
-            axis:    (...args) => chart.rightYAxis(...args),
-            label:   (...args) => chart.rightYAxisLabel(...args),
-            setting: (name) => settings["graph.y_axis." + name] // TODO: right axis settings
-        };
+      // only use the column name if all in the series are the same
+      const labels = _.uniq(series.map(s => getFriendlyName(s.data.cols[1])));
+      if (labels.length === 1) {
+        axis.label(labels[0], Y_LABEL_PADDING);
+      }
     }
-
-    if (axis.setting("labels_enabled")) {
-        // left
-        if (axis.setting("title_text")) {
-            axis.label(axis.setting("title_text"), Y_LABEL_PADDING);
-        } else {
-            // only use the column name if all in the series are the same
-            const labels = _.uniq(series.map(s => getFriendlyName(s.data.cols[1])));
-            if (labels.length === 1) {
-                axis.label(labels[0], Y_LABEL_PADDING);
-            }
-        }
+  }
+
+  if (axis.setting("axis_enabled")) {
+    // special case for normalized stacked charts
+    // for normalized stacked charts the y-axis is a percentage number. In Javascript, 0.07 * 100.0 = 7.000000000000001 (try it) so we
+    // round that number to get something nice like "7". Then we append "%" to get a nice tick like "7%"
+    if (settings["stackable.stack_type"] === "normalized") {
+      axis.axis().tickFormat(value => Math.round(value * 100) + "%");
     }
-
-    if (axis.setting("axis_enabled")) {
-        // special case for normalized stacked charts
-        // for normalized stacked charts the y-axis is a percentage number. In Javascript, 0.07 * 100.0 = 7.000000000000001 (try it) so we
-        // round that number to get something nice like "7". Then we append "%" to get a nice tick like "7%"
-        if (settings["stackable.stack_type"] === "normalized") {
-            axis.axis().tickFormat(value => Math.round(value * 100) + "%");
-        }
-        chart.renderHorizontalGridLines(true);
-        adjustYAxisTicksIfNeeded(axis.axis(), chart.height());
-    } else {
-        axis.axis().ticks(0);
-    }
-
-    let scale;
-    if (axis.setting("scale") === "pow") {
-        scale = d3.scale.pow().exponent(0.5);
-    } else if (axis.setting("scale") === "log") {
-        scale = d3.scale.log().base(Math.E);
-        // axis.axis().tickFormat((d) => scale.tickFormat(4,d3.format(",d"))(d));
+    chart.renderHorizontalGridLines(true);
+    adjustYAxisTicksIfNeeded(axis.axis(), chart.height());
+  } else {
+    axis.axis().ticks(0);
+  }
+
+  let scale;
+  if (axis.setting("scale") === "pow") {
+    scale = d3.scale.pow().exponent(0.5);
+  } else if (axis.setting("scale") === "log") {
+    scale = d3.scale.log().base(Math.E);
+    // axis.axis().tickFormat((d) => scale.tickFormat(4,d3.format(",d"))(d));
+  } else {
+    scale = d3.scale.linear();
+  }
+
+  if (axis.setting("auto_range")) {
+    // elasticY not compatible with log scale
+    if (axis.setting("scale") !== "log") {
+      // TODO: right axis?
+      chart.elasticY(true);
     } else {
-        scale = d3.scale.linear();
+      if (
+        !(
+          (yExtent[0] < 0 && yExtent[1] < 0) ||
+          (yExtent[0] > 0 && yExtent[1] > 0)
+        )
+      ) {
+        throw "Y-axis must not cross 0 when using log scale.";
+      }
+      scale.domain(yExtent);
     }
-
-    if (axis.setting("auto_range")) {
-        // elasticY not compatible with log scale
-        if (axis.setting("scale") !== "log") {
-            // TODO: right axis?
-            chart.elasticY(true);
-        } else {
-            if (!((yExtent[0] < 0 && yExtent[1] < 0) || (yExtent[0] > 0 && yExtent[1] > 0))) {
-                throw "Y-axis must not cross 0 when using log scale.";
-            }
-            scale.domain(yExtent);
-        }
-        axis.scale(scale);
-    } else {
-        if (axis.setting("scale") === "log" && !(
-            (axis.setting("min") < 0 && axis.setting("max") < 0) ||
-            (axis.setting("min") > 0 && axis.setting("max") > 0)
-        )) {
-            throw "Y-axis must not cross 0 when using log scale.";
-        }
-        axis.scale(scale.domain([axis.setting("min"), axis.setting("max")]))
+    axis.scale(scale);
+  } else {
+    if (
+      axis.setting("scale") === "log" &&
+      !(
+        (axis.setting("min") < 0 && axis.setting("max") < 0) ||
+        (axis.setting("min") > 0 && axis.setting("max") > 0)
+      )
+    ) {
+      throw "Y-axis must not cross 0 when using log scale.";
     }
+    axis.scale(scale.domain([axis.setting("min"), axis.setting("max")]));
+  }
 }
diff --git a/frontend/src/metabase/visualizations/lib/apply_tooltips.js b/frontend/src/metabase/visualizations/lib/apply_tooltips.js
index 2de7ea8509f444c87b875e0b578f513523594104..ae2e22c71a5029861127cc97d454a968da561843 100644
--- a/frontend/src/metabase/visualizations/lib/apply_tooltips.js
+++ b/frontend/src/metabase/visualizations/lib/apply_tooltips.js
@@ -4,226 +4,260 @@ import _ from "underscore";
 import d3 from "d3";
 
 import { formatValue } from "metabase/lib/formatting";
-import type { ClickObject } from "metabase/meta/types/Visualization"
+import type { ClickObject } from "metabase/meta/types/Visualization";
 
 import { isNormalized, isStacked } from "./renderer_utils";
 import { determineSeriesIndexFromElement } from "./tooltip";
 import { getFriendlyName } from "./utils";
 
 // series = an array of serieses (?) in the chart. There's only one thing in here unless we're dealing with a multiseries chart
-function applyChartTooltips(chart, series, isStacked, isNormalized, isScalarSeries, onHoverChange, onVisualizationClick) {
-    let [{ data: { cols } }] = series;
-    chart.on("renderlet.tooltips", function(chart) {
-        chart.selectAll("title").remove();
-
-        if (onHoverChange) {
-            chart.selectAll(".bar, .dot, .area, .line, .bubble")
-                 .on("mousemove", function(d, i) {
-                     const seriesIndex       = determineSeriesIndexFromElement(this, isStacked);
-                     const card              = series[seriesIndex].card;
-                     const isSingleSeriesBar = this.classList.contains("bar") && series.length === 1;
-                     const isArea            = this.classList.contains("area");
-
-                     let data = [];
-                     if (Array.isArray(d.key)) { // scatter
-                         if (d.key._origin) {
-                             data = d.key._origin.row.map((value, index) => {
-                                 const col = d.key._origin.cols[index];
-                                 return { key: getFriendlyName(col), value: value, col };
-                             });
-                         } else {
-                             data = d.key.map((value, index) => (
-                                 { key: getFriendlyName(cols[index]), value: value, col: cols[index] }
-                             ));
-                         }
-                     } else if (d.data) { // line, area, bar
-                         if (!isSingleSeriesBar) {
-                             cols = series[seriesIndex].data.cols;
-                         }
-
-                         data = [
-                             {
-                                 key: getFriendlyName(cols[0]),
-                                 value: d.data.key,
-                                 col: cols[0]
-                             },
-                             {
-                                 key: getFriendlyName(cols[1]),
-                                 value: isNormalized
-                                      ? `${formatValue(d.data.value) * 100}%`
-                                      : d.data.value,
-                                 col: cols[1]
-                             }
-                         ];
-
-                         // now add entries to the tooltip for columns that aren't the X or Y axis. These aren't in
-                         // the normal `cols` array, which is just the cols used in the graph axes; look in `_rawCols`
-                         // for any other columns. If we find them, add them at the end of the `data` array.
-                         //
-                         // To find the actual row where data is coming from is somewhat overcomplicated because i
-                         // seems to follow a strange pattern that doesn't directly correspond to the rows in our
-                         // data. Not sure why but it appears values of i follow this pattern:
-                         //
-                         //  [Series 1]  i = 7   i = 8   i = 9  i = 10   i = 11
-                         //  [Series 0]  i = 1   i = 2   i = 3  i = 4    i = 5
-                         //             [Row 0] [Row 1] [Row 2] [Row 3] [Row 4]
-                         //
-                         // Deriving the rowIndex from i can be done as follows:
-                         // rowIndex = (i % (numRows + 1)) - 1;
-                         //
-                         // example: for series 1, i = 10
-                         // rowIndex = (10 % 6) - 1 = 4 - 1 = 3
-                         //
-                         // for series 0, i = 3
-                         // rowIndex = (3 % 6) - 1 = 3 - 1 = 2
-                         const seriesData = series[seriesIndex].data || {};
-                         const rawCols    = seriesData._rawCols;
-                         const rows       = seriesData && seriesData.rows;
-                         const rowIndex   = rows && ((i % (rows.length + 1)) - 1);
-                         const row        = (rowIndex != null) && seriesData.rows[rowIndex];
-                         const rawRow     = row && row._origin && row._origin.row; // get the raw query result row
-                         // make sure the row index we've determined with our formula above is correct. Check the
-                         // x/y axis values ("key" & "value") and make sure they match up with the row before setting
-                         // the data for the tooltip
-                         if (rawRow && row[0] === d.data.key && row[1] === d.data.value) {
-                             // rather than just append the additional values we'll just create a new `data` array.
-                             // simply appending the additional values would result in tooltips whose order switches
-                             // between different series.
-                             // Loop over *all* of the columns and create the new array
-                             data = rawCols.map((col, i) => {
-                                 // if this was one of the original x/y columns keep the original object because it
-                                 // may have the `isNormalized` tweak above.
-                                 if (col === data[0].col) return data[0];
-                                 if (col === data[1].col) return data[1];
-                                 // otherwise just create a new object for any other columns.
-                                 return {
-                                     key: getFriendlyName(col),
-                                     value: rawRow[i],
-                                     col: col
-                                 };
-                             });
-                         }
-                     }
-
-                     if (data && series.length > 1) {
-                         if (card._breakoutColumn) {
-                             data.unshift({
-                                 key: getFriendlyName(card._breakoutColumn),
-                                 value: card._breakoutValue,
-                                 col: card._breakoutColumn
-                             });
-                         }
-                     }
-
-                     data = _.uniq(data, (d) => d.col);
-
-                     onHoverChange({
-                         // for single series bar charts, fade the series and highlght the hovered element with CSS
-                         index: isSingleSeriesBar ? -1 : seriesIndex,
-                         // for area charts, use the mouse location rather than the DOM element
-                         element: isArea ? null : this,
-                         event: isArea ? d3.event : null,
-                         data: data.length > 0 ? data : null,
-                     });
-                 })
-                 .on("mouseleave", function() {
-                     if (!onHoverChange) {
-                         return;
-                     }
-                     onHoverChange(null);
-                 })
-        }
+function applyChartTooltips(
+  chart,
+  series,
+  isStacked,
+  isNormalized,
+  isScalarSeries,
+  onHoverChange,
+  onVisualizationClick,
+) {
+  let [{ data: { cols } }] = series;
+  chart.on("renderlet.tooltips", function(chart) {
+    chart.selectAll("title").remove();
+
+    if (onHoverChange) {
+      chart
+        .selectAll(".bar, .dot, .area, .line, .bubble")
+        .on("mousemove", function(d, i) {
+          const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
+          const card = series[seriesIndex].card;
+          const isSingleSeriesBar =
+            this.classList.contains("bar") && series.length === 1;
+          const isArea = this.classList.contains("area");
 
-        if (onVisualizationClick) {
-            const onClick = function(d) {
-                const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
-                const card = series[seriesIndex].card;
-                const isSingleSeriesBar = this.classList.contains("bar") && series.length === 1;
-
-                let clicked: ?ClickObject;
-                if (Array.isArray(d.key)) { // scatter
-                    clicked = {
-                        value: d.key[2],
-                        column: cols[2],
-                        dimensions: [
-                            { value: d.key[0], column: cols[0] },
-                            { value: d.key[1], column: cols[1] }
-                        ].filter(({ column }) =>
-                            // don't include aggregations since we can't filter on them
-                            column.source !== "aggregation"
-                        ),
-                        origin: d.key._origin
-                    }
-                } else if (isScalarSeries) {
-                    // special case for multi-series scalar series, which should be treated as scalars
-                    clicked = {
-                        value: d.data.value,
-                        column: series[seriesIndex].data.cols[1]
-                    };
-                } else if (d.data) { // line, area, bar
-                    if (!isSingleSeriesBar) {
-                        cols = series[seriesIndex].data.cols;
-                    }
-                    clicked = {
-                        value: d.data.value,
-                        column: cols[1],
-                        dimensions: [
-                            { value: d.data.key, column: cols[0] }
-                        ]
-                    }
-                } else {
-                    clicked = {
-                        dimensions: []
-                    };
-                }
-
-                // handle multiseries
-                if (clicked && series.length > 1) {
-                    if (card._breakoutColumn) {
-                        // $FlowFixMe
-                        clicked.dimensions.push({
-                            value: card._breakoutValue,
-                            column: card._breakoutColumn
-                        });
-                    }
-                }
-
-                if (card._seriesIndex != null) {
-                    // $FlowFixMe
-                    clicked.seriesIndex = card._seriesIndex;
-                }
-
-                if (clicked) {
-                    const isLine = this.classList.contains("dot");
-                    onVisualizationClick({
-                        ...clicked,
-                        element: isLine ? this : null,
-                        event: isLine ? null : d3.event,
-                    });
-                }
+          let data = [];
+          if (Array.isArray(d.key)) {
+            // scatter
+            if (d.key._origin) {
+              data = d.key._origin.row.map((value, index) => {
+                const col = d.key._origin.cols[index];
+                return { key: getFriendlyName(col), value: value, col };
+              });
+            } else {
+              data = d.key.map((value, index) => ({
+                key: getFriendlyName(cols[index]),
+                value: value,
+                col: cols[index],
+              }));
+            }
+          } else if (d.data) {
+            // line, area, bar
+            if (!isSingleSeriesBar) {
+              cols = series[seriesIndex].data.cols;
             }
 
-            // for some reason interaction with brush requires we use click for .dot and .bubble but mousedown for bar
-            chart.selectAll(".dot, .bubble")
-                 .style({ "cursor": "pointer" })
-                 .on("click", onClick);
-            chart.selectAll(".bar")
-                 .style({ "cursor": "pointer" })
-                 .on("mousedown", onClick);
-        }
-    });
-}
+            data = [
+              {
+                key: getFriendlyName(cols[0]),
+                value: d.data.key,
+                col: cols[0],
+              },
+              {
+                key: getFriendlyName(cols[1]),
+                value: isNormalized
+                  ? `${formatValue(d.data.value) * 100}%`
+                  : d.data.value,
+                col: cols[1],
+              },
+            ];
 
+            // now add entries to the tooltip for columns that aren't the X or Y axis. These aren't in
+            // the normal `cols` array, which is just the cols used in the graph axes; look in `_rawCols`
+            // for any other columns. If we find them, add them at the end of the `data` array.
+            //
+            // To find the actual row where data is coming from is somewhat overcomplicated because i
+            // seems to follow a strange pattern that doesn't directly correspond to the rows in our
+            // data. Not sure why but it appears values of i follow this pattern:
+            //
+            //  [Series 1]  i = 7   i = 8   i = 9  i = 10   i = 11
+            //  [Series 0]  i = 1   i = 2   i = 3  i = 4    i = 5
+            //             [Row 0] [Row 1] [Row 2] [Row 3] [Row 4]
+            //
+            // Deriving the rowIndex from i can be done as follows:
+            // rowIndex = (i % (numRows + 1)) - 1;
+            //
+            // example: for series 1, i = 10
+            // rowIndex = (10 % 6) - 1 = 4 - 1 = 3
+            //
+            // for series 0, i = 3
+            // rowIndex = (3 % 6) - 1 = 3 - 1 = 2
+            const seriesData = series[seriesIndex].data || {};
+            const rawCols = seriesData._rawCols;
+            const rows = seriesData && seriesData.rows;
+            const rowIndex = rows && i % (rows.length + 1) - 1;
+            const row = rowIndex != null && seriesData.rows[rowIndex];
+            const rawRow = row && row._origin && row._origin.row; // get the raw query result row
+            // make sure the row index we've determined with our formula above is correct. Check the
+            // x/y axis values ("key" & "value") and make sure they match up with the row before setting
+            // the data for the tooltip
+            if (rawRow && row[0] === d.data.key && row[1] === d.data.value) {
+              // rather than just append the additional values we'll just create a new `data` array.
+              // simply appending the additional values would result in tooltips whose order switches
+              // between different series.
+              // Loop over *all* of the columns and create the new array
+              data = rawCols.map((col, i) => {
+                // if this was one of the original x/y columns keep the original object because it
+                // may have the `isNormalized` tweak above.
+                if (col === data[0].col) return data[0];
+                if (col === data[1].col) return data[1];
+                // otherwise just create a new object for any other columns.
+                return {
+                  key: getFriendlyName(col),
+                  value: rawRow[i],
+                  col: col,
+                };
+              });
+            }
+          }
 
-export function setupTooltips({ settings, series, isScalarSeries, onHoverChange, onVisualizationClick }, datas, parent, { isBrushing }) {
-    applyChartTooltips(parent, series, isStacked(settings, datas), isNormalized(settings, datas), isScalarSeries, (hovered) => {
-        // disable tooltips while brushing
-        if (onHoverChange && !isBrushing()) {
-            // disable tooltips on lines
-            if (hovered && hovered.element && hovered.element.classList.contains("line")) {
-                delete hovered.element;
+          if (data && series.length > 1) {
+            if (card._breakoutColumn) {
+              data.unshift({
+                key: getFriendlyName(card._breakoutColumn),
+                value: card._breakoutValue,
+                col: card._breakoutColumn,
+              });
             }
-            onHoverChange(hovered);
+          }
+
+          data = _.uniq(data, d => d.col);
+
+          onHoverChange({
+            // for single series bar charts, fade the series and highlght the hovered element with CSS
+            index: isSingleSeriesBar ? -1 : seriesIndex,
+            // for area charts, use the mouse location rather than the DOM element
+            element: isArea ? null : this,
+            event: isArea ? d3.event : null,
+            data: data.length > 0 ? data : null,
+          });
+        })
+        .on("mouseleave", function() {
+          if (!onHoverChange) {
+            return;
+          }
+          onHoverChange(null);
+        });
+    }
+
+    if (onVisualizationClick) {
+      const onClick = function(d) {
+        const seriesIndex = determineSeriesIndexFromElement(this, isStacked);
+        const card = series[seriesIndex].card;
+        const isSingleSeriesBar =
+          this.classList.contains("bar") && series.length === 1;
+
+        let clicked: ?ClickObject;
+        if (Array.isArray(d.key)) {
+          // scatter
+          clicked = {
+            value: d.key[2],
+            column: cols[2],
+            dimensions: [
+              { value: d.key[0], column: cols[0] },
+              { value: d.key[1], column: cols[1] },
+            ].filter(
+              ({ column }) =>
+                // don't include aggregations since we can't filter on them
+                column.source !== "aggregation",
+            ),
+            origin: d.key._origin,
+          };
+        } else if (isScalarSeries) {
+          // special case for multi-series scalar series, which should be treated as scalars
+          clicked = {
+            value: d.data.value,
+            column: series[seriesIndex].data.cols[1],
+          };
+        } else if (d.data) {
+          // line, area, bar
+          if (!isSingleSeriesBar) {
+            cols = series[seriesIndex].data.cols;
+          }
+          clicked = {
+            value: d.data.value,
+            column: cols[1],
+            dimensions: [{ value: d.data.key, column: cols[0] }],
+          };
+        } else {
+          clicked = {
+            dimensions: [],
+          };
+        }
+
+        // handle multiseries
+        if (clicked && series.length > 1) {
+          if (card._breakoutColumn) {
+            // $FlowFixMe
+            clicked.dimensions.push({
+              value: card._breakoutValue,
+              column: card._breakoutColumn,
+            });
+          }
+        }
+
+        if (card._seriesIndex != null) {
+          // $FlowFixMe
+          clicked.seriesIndex = card._seriesIndex;
+        }
+
+        if (clicked) {
+          const isLine = this.classList.contains("dot");
+          onVisualizationClick({
+            ...clicked,
+            element: isLine ? this : null,
+            event: isLine ? null : d3.event,
+          });
+        }
+      };
+
+      // for some reason interaction with brush requires we use click for .dot and .bubble but mousedown for bar
+      chart
+        .selectAll(".dot, .bubble")
+        .style({ cursor: "pointer" })
+        .on("click", onClick);
+      chart
+        .selectAll(".bar")
+        .style({ cursor: "pointer" })
+        .on("mousedown", onClick);
+    }
+  });
+}
+
+export function setupTooltips(
+  { settings, series, isScalarSeries, onHoverChange, onVisualizationClick },
+  datas,
+  parent,
+  { isBrushing },
+) {
+  applyChartTooltips(
+    parent,
+    series,
+    isStacked(settings, datas),
+    isNormalized(settings, datas),
+    isScalarSeries,
+    hovered => {
+      // disable tooltips while brushing
+      if (onHoverChange && !isBrushing()) {
+        // disable tooltips on lines
+        if (
+          hovered &&
+          hovered.element &&
+          hovered.element.classList.contains("line")
+        ) {
+          delete hovered.element;
         }
-    }, onVisualizationClick);
+        onHoverChange(hovered);
+      }
+    },
+    onVisualizationClick,
+  );
 }
diff --git a/frontend/src/metabase/visualizations/lib/errors.js b/frontend/src/metabase/visualizations/lib/errors.js
index 6b96ff3a8ba62d8ae61189c6b6cd3b4e611bd01a..1a9064134e070f793c001ddf16012ff43a2a1583 100644
--- a/frontend/src/metabase/visualizations/lib/errors.js
+++ b/frontend/src/metabase/visualizations/lib/errors.js
@@ -1,33 +1,45 @@
 /* @flow */
 
 import { inflect } from "metabase/lib/formatting";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 // NOTE: extending Error with Babel requires babel-plugin-transform-builtin-extend
 
 export class MinColumnsError extends Error {
-    constructor(minColumns: number, actualColumns: number) {
-        super(t`Doh! The data from your query doesn't fit the chosen display choice. This visualization requires at least ${actualColumns} ${inflect("column", actualColumns)} of data.`);
-    }
+  constructor(minColumns: number, actualColumns: number) {
+    super(
+      t`Doh! The data from your query doesn't fit the chosen display choice. This visualization requires at least ${actualColumns} ${inflect(
+        "column",
+        actualColumns,
+      )} of data.`,
+    );
+  }
 }
 
 export class MinRowsError extends Error {
-    constructor(minRows: number, actualRows: number) {
-        super(t`No dice. We have ${actualRows} data ${inflect("point", actualRows)} to show and that's not enough for this visualization.`);
-    }
+  constructor(minRows: number, actualRows: number) {
+    super(
+      t`No dice. We have ${actualRows} data ${inflect(
+        "point",
+        actualRows,
+      )} to show and that's not enough for this visualization.`,
+    );
+  }
 }
 
 export class LatitudeLongitudeError extends Error {
-    constructor() {
-        super(t`Bummer. We can't actually do a pin map for this data because we require both a latitude and longitude column.`);
-    }
+  constructor() {
+    super(
+      t`Bummer. We can't actually do a pin map for this data because we require both a latitude and longitude column.`,
+    );
+  }
 }
 
 export class ChartSettingsError extends Error {
-    section: ?string;
-    buttonText: ?string;
-    constructor(message: string, section?: string, buttonText?: string) {
-        super(message || t`Please configure this chart in the chart settings`);
-        this.section = section;
-        this.buttonText = buttonText || t`Edit Settings`;
-    }
+  section: ?string;
+  buttonText: ?string;
+  constructor(message: string, section?: string, buttonText?: string) {
+    super(message || t`Please configure this chart in the chart settings`);
+    this.section = section;
+    this.buttonText = buttonText || t`Edit Settings`;
+  }
 }
diff --git a/frontend/src/metabase/visualizations/lib/fill_data.js b/frontend/src/metabase/visualizations/lib/fill_data.js
index 37f9ce63265321ee8c9754ad143f0032ff9500d9..b95d52a5007ee24dc7b91819b9ccadf763310928 100644
--- a/frontend/src/metabase/visualizations/lib/fill_data.js
+++ b/frontend/src/metabase/visualizations/lib/fill_data.js
@@ -1,94 +1,102 @@
 // code for filling in the missing values in a set of "datas"
 
+import { t } from "c-3po";
 import d3 from "d3";
 import moment from "moment";
 
-import { isTimeseries, isQuantitative, isHistogram, isHistogramBar } from "./renderer_utils";
+import {
+  isTimeseries,
+  isQuantitative,
+  isHistogram,
+  isHistogramBar,
+} from "./renderer_utils";
 
 // max number of points to "fill"
 // TODO: base on pixel width of chart?
 const MAX_FILL_COUNT = 10000;
 
-function fillMissingValues(datas, xValues, fillValue, getKey = (v) => v) {
-    try {
-        return datas.map(rows => {
-            const fillValues = rows[0].slice(1).map(d => fillValue);
+function fillMissingValues(datas, xValues, fillValue, getKey = v => v) {
+  try {
+    return datas.map(rows => {
+      const fillValues = rows[0].slice(1).map(d => fillValue);
 
-            let map = new Map();
-            for (const row of rows) {
-                map.set(getKey(row[0]), row);
-            }
-            let newRows = xValues.map(value => {
-                const key = getKey(value);
-                const row = map.get(key);
-                if (row) {
-                    map.delete(key);
-                    return [value, ...row.slice(1)];
-                } else {
-                    return [value, ...fillValues];
-                }
-            });
-            if (map.size > 0) {
-                console.warn(t`"xValues missing!`, map, newRows)
-            }
-            return newRows;
-        });
-    } catch (e) {
-        console.warn(e);
-        return datas;
-    }
+      let map = new Map();
+      for (const row of rows) {
+        map.set(getKey(row[0]), row);
+      }
+      let newRows = xValues.map(value => {
+        const key = getKey(value);
+        const row = map.get(key);
+        if (row) {
+          map.delete(key);
+          return [value, ...row.slice(1)];
+        } else {
+          return [value, ...fillValues];
+        }
+      });
+      if (map.size > 0) {
+        console.warn(t`"xValues missing!`, map, newRows);
+      }
+      return newRows;
+    });
+  } catch (e) {
+    console.warn(e);
+    return datas;
+  }
 }
 
-
-export default function fillMissingValuesInDatas(props, { xValues, xDomain, xInterval }, datas) {
-    const { settings } = props;
-    if (settings["line.missing"] === "zero" || settings["line.missing"] === "none") {
-        const fillValue = settings["line.missing"] === "zero" ? 0 : null;
-        if (isTimeseries(settings)) {
-            // $FlowFixMe
-            const { interval, count } = xInterval;
-            if (count <= MAX_FILL_COUNT) {
-                // replace xValues with
-                xValues = d3.time[interval]
-                            .range(xDomain[0], moment(xDomain[1]).add(1, "ms"), count)
-                            .map(d => moment(d));
-                datas = fillMissingValues(
-                    datas,
-                    xValues,
-                    fillValue,
-                    (m) => d3.round(m.toDate().getTime(), -1) // sometimes rounds up 1ms?
-                );
-            }
-        }
-        if (isQuantitative(settings) || isHistogram(settings)) {
-            // $FlowFixMe
-            const count = Math.abs((xDomain[1] - xDomain[0]) / xInterval);
-            if (count <= MAX_FILL_COUNT) {
-                let [start, end] = xDomain;
-                if (isHistogramBar(props)) {
-                    // NOTE: intentionally add an end point for bar histograms
-                    // $FlowFixMe
-                    end += xInterval * 1.5
-                } else {
-                    // NOTE: avoid including endpoint due to floating point error
-                    // $FlowFixMe
-                    end += xInterval * 0.5
-                }
-                xValues = d3.range(start, end, xInterval);
-                datas = fillMissingValues(
-                    datas,
-                    xValues,
-                    fillValue,
-                    // NOTE: normalize to xInterval to avoid floating point issues
-                    (v) => Math.round(v / xInterval)
-                );
-            }
+export default function fillMissingValuesInDatas(
+  props,
+  { xValues, xDomain, xInterval },
+  datas,
+) {
+  const { settings } = props;
+  if (
+    settings["line.missing"] === "zero" ||
+    settings["line.missing"] === "none"
+  ) {
+    const fillValue = settings["line.missing"] === "zero" ? 0 : null;
+    if (isTimeseries(settings)) {
+      // $FlowFixMe
+      const { interval, count } = xInterval;
+      if (count <= MAX_FILL_COUNT) {
+        // replace xValues with
+        xValues = d3.time[interval]
+          .range(xDomain[0], moment(xDomain[1]).add(1, "ms"), count)
+          .map(d => moment(d));
+        datas = fillMissingValues(
+          datas,
+          xValues,
+          fillValue,
+          m => d3.round(m.toDate().getTime(), -1), // sometimes rounds up 1ms?
+        );
+      }
+    }
+    if (isQuantitative(settings) || isHistogram(settings)) {
+      // $FlowFixMe
+      const count = Math.abs((xDomain[1] - xDomain[0]) / xInterval);
+      if (count <= MAX_FILL_COUNT) {
+        let [start, end] = xDomain;
+        if (isHistogramBar(props)) {
+          // NOTE: intentionally add an end point for bar histograms
+          // $FlowFixMe
+          end += xInterval * 1.5;
         } else {
-            datas = fillMissingValues(
-                datas,
-                xValues,
-                fillValue
-            );
+          // NOTE: avoid including endpoint due to floating point error
+          // $FlowFixMe
+          end += xInterval * 0.5;
         }
+        xValues = d3.range(start, end, xInterval);
+        datas = fillMissingValues(
+          datas,
+          xValues,
+          fillValue,
+          // NOTE: normalize to xInterval to avoid floating point issues
+          v => Math.round(v / xInterval),
+        );
+      }
+    } else {
+      datas = fillMissingValues(datas, xValues, fillValue);
     }
+  }
 }
diff --git a/frontend/src/metabase/visualizations/lib/graph/addons.js b/frontend/src/metabase/visualizations/lib/graph/addons.js
index 93a149bc9a510a530c7018ab2d6e53233b6ed754..27a9c7f84e486ca4d30b46c634afbe8b959172a1 100644
--- a/frontend/src/metabase/visualizations/lib/graph/addons.js
+++ b/frontend/src/metabase/visualizations/lib/graph/addons.js
@@ -4,45 +4,41 @@ import dc from "dc";
 import moment from "moment";
 
 export const lineAddons = _chart => {
-    _chart.fadeDeselectedArea = function() {
-        var dots = _chart.chartBodyG().selectAll(".dot");
-        var extent = _chart.brush().extent();
+  _chart.fadeDeselectedArea = function() {
+    var dots = _chart.chartBodyG().selectAll(".dot");
+    var extent = _chart.brush().extent();
 
-        if (_chart.isOrdinal()) {
-            if (_chart.hasFilter()) {
-                dots.classed(dc.constants.SELECTED_CLASS, function(d) {
-                    return _chart.hasFilter(d.x);
-                });
-                dots.classed(dc.constants.DESELECTED_CLASS, function(d) {
-                    return !_chart.hasFilter(d.x);
-                });
-            } else {
-                dots.classed(dc.constants.SELECTED_CLASS, false);
-                dots.classed(dc.constants.DESELECTED_CLASS, false);
-            }
-        } else {
-            if (!_chart.brushIsEmpty(extent)) {
-                var start = extent[0];
-                var end = extent[1];
-                const isSelected = d => {
-                    if (moment.isDate(start)) {
-                        return !(moment(d.x).isBefore(start) ||
-                            moment(d.x).isAfter(end));
-                    } else {
-                        return !(d.x < start || d.x >= end);
-                    }
-                };
-                dots.classed(
-                    dc.constants.DESELECTED_CLASS,
-                    d => !isSelected(d)
-                );
-                dots.classed(dc.constants.SELECTED_CLASS, d => isSelected(d));
-            } else {
-                dots.classed(dc.constants.DESELECTED_CLASS, false);
-                dots.classed(dc.constants.SELECTED_CLASS, false);
-            }
-        }
-    };
+    if (_chart.isOrdinal()) {
+      if (_chart.hasFilter()) {
+        dots.classed(dc.constants.SELECTED_CLASS, function(d) {
+          return _chart.hasFilter(d.x);
+        });
+        dots.classed(dc.constants.DESELECTED_CLASS, function(d) {
+          return !_chart.hasFilter(d.x);
+        });
+      } else {
+        dots.classed(dc.constants.SELECTED_CLASS, false);
+        dots.classed(dc.constants.DESELECTED_CLASS, false);
+      }
+    } else {
+      if (!_chart.brushIsEmpty(extent)) {
+        var start = extent[0];
+        var end = extent[1];
+        const isSelected = d => {
+          if (moment.isDate(start)) {
+            return !(moment(d.x).isBefore(start) || moment(d.x).isAfter(end));
+          } else {
+            return !(d.x < start || d.x >= end);
+          }
+        };
+        dots.classed(dc.constants.DESELECTED_CLASS, d => !isSelected(d));
+        dots.classed(dc.constants.SELECTED_CLASS, d => isSelected(d));
+      } else {
+        dots.classed(dc.constants.DESELECTED_CLASS, false);
+        dots.classed(dc.constants.SELECTED_CLASS, false);
+      }
+    }
+  };
 
-    return _chart;
+  return _chart;
 };
diff --git a/frontend/src/metabase/visualizations/lib/graph/brush.js b/frontend/src/metabase/visualizations/lib/graph/brush.js
index 14a039fb86857c44b2049ecc35a7eb3e787d14dc..518201bcf76ff6b3e93d75a1fa6c3f830c0e4136 100644
--- a/frontend/src/metabase/visualizations/lib/graph/brush.js
+++ b/frontend/src/metabase/visualizations/lib/graph/brush.js
@@ -2,86 +2,86 @@ import { KEYCODE_ESCAPE } from "metabase/lib/keyboard";
 import { moveToBack, moveToFront } from "metabase/lib/dom";
 
 export function initBrush(parent, child, onBrushChange, onBrushEnd) {
-    if (!parent.brushOn || !child.brushOn) {
-        return;
-    }
+  if (!parent.brushOn || !child.brushOn) {
+    return;
+  }
 
-    // enable brush
-    parent.brushOn(true);
-    child.brushOn(true);
+  // enable brush
+  parent.brushOn(true);
+  child.brushOn(true);
 
-    // normally dots are disabled if brush is on but we want them anyway
-    if (child.xyTipsOn) {
-        child.xyTipsOn("always");
-    }
+  // normally dots are disabled if brush is on but we want them anyway
+  if (child.xyTipsOn) {
+    child.xyTipsOn("always");
+  }
 
-    // the brush has been cancelled by pressing escape
-    let cancelled = false;
-    // the last updated range when brushing
-    let range = null;
+  // the brush has been cancelled by pressing escape
+  let cancelled = false;
+  // the last updated range when brushing
+  let range = null;
 
-    // start
-    parent.brush().on("brushstart.custom", () => {
-        // reset "range"
-        range = null;
-        // reset "cancelled" flag
-        cancelled = false;
-        // add "dragging" class to chart
-        parent.svg().classed("dragging", true);
-        // move the brush element to the front
-        moveToFront(parent.select(".brush").node());
-        // add an escape keydown listener
-        window.addEventListener("keydown", onKeyDown, true);
-    });
+  // start
+  parent.brush().on("brushstart.custom", () => {
+    // reset "range"
+    range = null;
+    // reset "cancelled" flag
+    cancelled = false;
+    // add "dragging" class to chart
+    parent.svg().classed("dragging", true);
+    // move the brush element to the front
+    moveToFront(parent.select(".brush").node());
+    // add an escape keydown listener
+    window.addEventListener("keydown", onKeyDown, true);
+  });
 
-    // change
-    child.addFilterHandler((filters, r) => {
-        // update "range" with new filter range
-        range = r;
+  // change
+  child.addFilterHandler((filters, r) => {
+    // update "range" with new filter range
+    range = r;
 
-        // emit "onBrushChange" event
-        onBrushChange(range);
+    // emit "onBrushChange" event
+    onBrushChange(range);
 
-        // fade deselected bars
-        parent.fadeDeselectedArea();
+    // fade deselected bars
+    parent.fadeDeselectedArea();
 
-        // return filters unmodified
-        return filters;
-    });
+    // return filters unmodified
+    return filters;
+  });
 
-    // end
-    parent.brush().on("brushend.custom", () => {
-        // remove the "dragging" classed
-        parent.svg().classed("dragging", false)
-        // reset brush opacity (if the brush was cancelled)
-        parent.select(".brush").style("opacity", 1);
-        // move the brush to the back
-        moveToBack(parent.select(".brush").node());
-        // remove the escape keydown listener
-        window.removeEventListener("keydown", onKeyDown, true);
-        // reset the fitler and redraw
-        child.filterAll();
-        parent.redraw();
+  // end
+  parent.brush().on("brushend.custom", () => {
+    // remove the "dragging" classed
+    parent.svg().classed("dragging", false);
+    // reset brush opacity (if the brush was cancelled)
+    parent.select(".brush").style("opacity", 1);
+    // move the brush to the back
+    moveToBack(parent.select(".brush").node());
+    // remove the escape keydown listener
+    window.removeEventListener("keydown", onKeyDown, true);
+    // reset the fitler and redraw
+    child.filterAll();
+    parent.redraw();
 
-        // if not cancelled, emit the onBrushEnd event with the last filter range
-        onBrushEnd(cancelled ? null : range);
-        range = null;
-    });
+    // if not cancelled, emit the onBrushEnd event with the last filter range
+    onBrushEnd(cancelled ? null : range);
+    range = null;
+  });
 
-    // cancel
-    const onKeyDown = e => {
-        if (e.keyCode === KEYCODE_ESCAPE) {
-            // set the "cancelled" flag
-            cancelled = true;
-            // dispatch a mouseup to end brushing early
-            window.dispatchEvent(new MouseEvent("mouseup"));
-        }
-    };
+  // cancel
+  const onKeyDown = e => {
+    if (e.keyCode === KEYCODE_ESCAPE) {
+      // set the "cancelled" flag
+      cancelled = true;
+      // dispatch a mouseup to end brushing early
+      window.dispatchEvent(new MouseEvent("mouseup"));
+    }
+  };
 
-    parent.on("pretransition.custom", function(chart) {
-        // move brush to the back so tootips/clicks still work
-        moveToBack(chart.select(".brush").node());
-        // remove the handles since we can't adjust them anyway
-        chart.selectAll(".brush .resize").remove();
-    });
+  parent.on("pretransition.custom", function(chart) {
+    // move brush to the back so tootips/clicks still work
+    moveToBack(chart.select(".brush").node());
+    // remove the handles since we can't adjust them anyway
+    chart.selectAll(".brush .resize").remove();
+  });
 }
diff --git a/frontend/src/metabase/visualizations/lib/mapping.js b/frontend/src/metabase/visualizations/lib/mapping.js
index ed2b7ae07666c7e4009ca11a9eb6ae2fce5e321b..5bf162e4a816f39849620740b4149ec4c3842d12 100644
--- a/frontend/src/metabase/visualizations/lib/mapping.js
+++ b/frontend/src/metabase/visualizations/lib/mapping.js
@@ -4,138 +4,146 @@ import L from "leaflet/dist/leaflet-src.js";
 import d3 from "d3";
 
 export function computeMinimalBounds(features) {
-    const points = getAllFeaturesPoints(features);
-    const gap = computeLargestGap(points, (d) => d[0]);
-    const [west, east] = d3.extent(points, (d) => d[0]);
-    const [north, south] = d3.extent(points, (d) => d[1]);
+  const points = getAllFeaturesPoints(features);
+  const gap = computeLargestGap(points, d => d[0]);
+  const [west, east] = d3.extent(points, d => d[0]);
+  const [north, south] = d3.extent(points, d => d[1]);
 
-    const normalGapSize = gap[1] - gap[0];
-    const antemeridianGapSize = (180 + west) + (180 - east);
+  const normalGapSize = gap[1] - gap[0];
+  const antemeridianGapSize = 180 + west + (180 - east);
 
-    if (antemeridianGapSize > normalGapSize) {
-        return L.latLngBounds(
-            L.latLng(south, west), // SW
-            L.latLng(north, east)  // NE
-        )
-    } else {
-        return L.latLngBounds(
-            L.latLng(south, -360 + gap[1]), // SW
-            L.latLng(north, gap[0])  // NE
-        )
-    }
+  if (antemeridianGapSize > normalGapSize) {
+    return L.latLngBounds(
+      L.latLng(south, west), // SW
+      L.latLng(north, east), // NE
+    );
+  } else {
+    return L.latLngBounds(
+      L.latLng(south, -360 + gap[1]), // SW
+      L.latLng(north, gap[0]), // NE
+    );
+  }
 }
 
-export function computeLargestGap(items, valueAccessor = (d) => d) {
-    const [xMin, xMax] = d3.extent(items, valueAccessor);
-    if (xMin === xMax) {
-        return [xMin, xMax];
-    }
+export function computeLargestGap(items, valueAccessor = d => d) {
+  const [xMin, xMax] = d3.extent(items, valueAccessor);
+  if (xMin === xMax) {
+    return [xMin, xMax];
+  }
 
-    const buckets = [];
-    const bucketSize = (xMax - xMin) / items.length;
-    for (const item of items) {
-        const x = valueAccessor(item);
-        const k = Math.floor((x - xMin) / bucketSize);
-        if (buckets[k] === undefined) {
-            buckets[k] = [x, x];
-        } else {
-            buckets[k] = [Math.min(x, buckets[k][0]), Math.max(x, buckets[k][1])];
-        }
+  const buckets = [];
+  const bucketSize = (xMax - xMin) / items.length;
+  for (const item of items) {
+    const x = valueAccessor(item);
+    const k = Math.floor((x - xMin) / bucketSize);
+    if (buckets[k] === undefined) {
+      buckets[k] = [x, x];
+    } else {
+      buckets[k] = [Math.min(x, buckets[k][0]), Math.max(x, buckets[k][1])];
     }
-    let largestGap = [0, 0];
-    for (let i = 0; i < items.length; i++) {
-        if (buckets[i + 1] === undefined) {
-            buckets[i + 1] = buckets[i];
-        } else if (buckets[i + 1][0] - buckets[i][1] > largestGap[1] - largestGap[0]) {
-            largestGap = [buckets[i][1], buckets[i + 1][0]];
-        }
+  }
+  let largestGap = [0, 0];
+  for (let i = 0; i < items.length; i++) {
+    if (buckets[i + 1] === undefined) {
+      buckets[i + 1] = buckets[i];
+    } else if (
+      buckets[i + 1][0] - buckets[i][1] >
+      largestGap[1] - largestGap[0]
+    ) {
+      largestGap = [buckets[i][1], buckets[i + 1][0]];
     }
-    return largestGap;
+  }
+  return largestGap;
 }
 
 export function getAllFeaturesPoints(features) {
-    let points = [];
-    for (let feature of features) {
-        if (feature.geometry.type === "Polygon") {
-            for (let coordinates of feature.geometry.coordinates) {
-                points = points.concat(coordinates);
-            }
-        } else if (feature.geometry.type === "MultiPolygon") {
-            for (let coordinatesList of feature.geometry.coordinates) {
-                for (let coordinates of coordinatesList) {
-                    points = points.concat(coordinates);
-                }
-            }
-        } else {
-            console.warn("Unimplemented feature.geometry.type", feature.geometry.type)
+  let points = [];
+  for (let feature of features) {
+    if (feature.geometry.type === "Polygon") {
+      for (let coordinates of feature.geometry.coordinates) {
+        points = points.concat(coordinates);
+      }
+    } else if (feature.geometry.type === "MultiPolygon") {
+      for (let coordinatesList of feature.geometry.coordinates) {
+        for (let coordinates of coordinatesList) {
+          points = points.concat(coordinates);
         }
+      }
+    } else {
+      console.warn(
+        "Unimplemented feature.geometry.type",
+        feature.geometry.type,
+      );
     }
-    return points;
+  }
+  return points;
 }
 
 const STATE_CODES = [
-    ["AL", "Alabama"],
-    ["AK", "Alaska"],
-    ["AS", "American Samoa"],
-    ["AZ", "Arizona"],
-    ["AR", "Arkansas"],
-    ["CA", "California"],
-    ["CO", "Colorado"],
-    ["CT", "Connecticut"],
-    ["DE", "Delaware"],
-    ["DC", "District Of Columbia"],
-    ["FM", "Federated States Of Micronesia"],
-    ["FL", "Florida"],
-    ["GA", "Georgia"],
-    ["GU", "Guam"],
-    ["HI", "Hawaii"],
-    ["ID", "Idaho"],
-    ["IL", "Illinois"],
-    ["IN", "Indiana"],
-    ["IA", "Iowa"],
-    ["KS", "Kansas"],
-    ["KY", "Kentucky"],
-    ["LA", "Louisiana"],
-    ["ME", "Maine"],
-    ["MH", "Marshall Islands"],
-    ["MD", "Maryland"],
-    ["MA", "Massachusetts"],
-    ["MI", "Michigan"],
-    ["MN", "Minnesota"],
-    ["MS", "Mississippi"],
-    ["MO", "Missouri"],
-    ["MT", "Montana"],
-    ["NE", "Nebraska"],
-    ["NV", "Nevada"],
-    ["NH", "New Hampshire"],
-    ["NJ", "New Jersey"],
-    ["NM", "New Mexico"],
-    ["NY", "New York"],
-    ["NC", "North Carolina"],
-    ["ND", "North Dakota"],
-    ["MP", "Northern Mariana Islands"],
-    ["OH", "Ohio"],
-    ["OK", "Oklahoma"],
-    ["OR", "Oregon"],
-    ["PW", "Palau"],
-    ["PA", "Pennsylvania"],
-    ["PR", "Puerto Rico"],
-    ["RI", "Rhode Island"],
-    ["SC", "South Carolina"],
-    ["SD", "South Dakota"],
-    ["TN", "Tennessee"],
-    ["TX", "Texas"],
-    ["UT", "Utah"],
-    ["VT", "Vermont"],
-    ["VI", "Virgin Islands"],
-    ["VA", "Virginia"],
-    ["WA", "Washington"],
-    ["WV", "West Virginia"],
-    ["WI", "Wisconsin"],
-    ["WY", "Wyoming"],
+  ["AL", "Alabama"],
+  ["AK", "Alaska"],
+  ["AS", "American Samoa"],
+  ["AZ", "Arizona"],
+  ["AR", "Arkansas"],
+  ["CA", "California"],
+  ["CO", "Colorado"],
+  ["CT", "Connecticut"],
+  ["DE", "Delaware"],
+  ["DC", "District Of Columbia"],
+  ["FM", "Federated States Of Micronesia"],
+  ["FL", "Florida"],
+  ["GA", "Georgia"],
+  ["GU", "Guam"],
+  ["HI", "Hawaii"],
+  ["ID", "Idaho"],
+  ["IL", "Illinois"],
+  ["IN", "Indiana"],
+  ["IA", "Iowa"],
+  ["KS", "Kansas"],
+  ["KY", "Kentucky"],
+  ["LA", "Louisiana"],
+  ["ME", "Maine"],
+  ["MH", "Marshall Islands"],
+  ["MD", "Maryland"],
+  ["MA", "Massachusetts"],
+  ["MI", "Michigan"],
+  ["MN", "Minnesota"],
+  ["MS", "Mississippi"],
+  ["MO", "Missouri"],
+  ["MT", "Montana"],
+  ["NE", "Nebraska"],
+  ["NV", "Nevada"],
+  ["NH", "New Hampshire"],
+  ["NJ", "New Jersey"],
+  ["NM", "New Mexico"],
+  ["NY", "New York"],
+  ["NC", "North Carolina"],
+  ["ND", "North Dakota"],
+  ["MP", "Northern Mariana Islands"],
+  ["OH", "Ohio"],
+  ["OK", "Oklahoma"],
+  ["OR", "Oregon"],
+  ["PW", "Palau"],
+  ["PA", "Pennsylvania"],
+  ["PR", "Puerto Rico"],
+  ["RI", "Rhode Island"],
+  ["SC", "South Carolina"],
+  ["SD", "South Dakota"],
+  ["TN", "Tennessee"],
+  ["TX", "Texas"],
+  ["UT", "Utah"],
+  ["VT", "Vermont"],
+  ["VI", "Virgin Islands"],
+  ["VA", "Virginia"],
+  ["WA", "Washington"],
+  ["WV", "West Virginia"],
+  ["WI", "Wisconsin"],
+  ["WY", "Wyoming"],
 ];
 
-const stateNamesMap = new Map(STATE_CODES.map(([key, name]) => [name.toLowerCase(), key.toLowerCase()]))
+const stateNamesMap = new Map(
+  STATE_CODES.map(([key, name]) => [name.toLowerCase(), key.toLowerCase()]),
+);
 
 /**
  * Canonicalizes row values to match those in the GeoJSONs.
@@ -143,12 +151,12 @@ const stateNamesMap = new Map(STATE_CODES.map(([key, name]) => [name.toLowerCase
  * Currently transforms US state names to state codes for the "us_states" region map, and just lowercases all others.
  */
 export function getCanonicalRowKey(key, region) {
-    key = String(key).toLowerCase();
-    // Special case for supporting both US state names and state codes
-    // This should be ok because we know there's no overlap between state names and codes, and we know the "us_states" region map expects codes
-    if (region === "us_states" && stateNamesMap.has(key)) {
-        return stateNamesMap.get(key);
-    } else {
-        return key;
-    }
+  key = String(key).toLowerCase();
+  // Special case for supporting both US state names and state codes
+  // This should be ok because we know there's no overlap between state names and codes, and we know the "us_states" region map expects codes
+  if (region === "us_states" && stateNamesMap.has(key)) {
+    return stateNamesMap.get(key);
+  } else {
+    return key;
+  }
 }
diff --git a/frontend/src/metabase/visualizations/lib/numeric.js b/frontend/src/metabase/visualizations/lib/numeric.js
index 8d0e296af07a27a9d1cd26d0382c6bd9224ab6ff..f2b7047b0c21f31e34cbb2e523e3cddd9050367a 100644
--- a/frontend/src/metabase/visualizations/lib/numeric.js
+++ b/frontend/src/metabase/visualizations/lib/numeric.js
@@ -3,48 +3,56 @@
 import { isNumeric } from "metabase/lib/schema_metadata";
 
 export function dimensionIsNumeric({ cols, rows }, i = 0) {
-    return isNumeric(cols[i]) || typeof (rows[0] && rows[0][i]) === "number";
+  return isNumeric(cols[i]) || typeof (rows[0] && rows[0][i]) === "number";
 }
 
 export function precision(a) {
-    if (!isFinite(a)) {
-        return 0;
-    }
-    if (!a) {
-        return 0;
-    }
-    var e = 1;
-    while (Math.round(a / e) !== (a / e)) {
-        e /= 10;
-    }
-    while (Math.round(a / Math.pow(10, e)) === (a / Math.pow(10, e))) {
-        e *= 10;
-    }
-    return e;
+  if (!isFinite(a)) {
+    return 0;
+  }
+  if (!a) {
+    return 0;
+  }
+  var e = 1;
+  while (Math.round(a / e) !== a / e) {
+    e /= 10;
+  }
+  while (Math.round(a / Math.pow(10, e)) === a / Math.pow(10, e)) {
+    e *= 10;
+  }
+  return e;
 }
 
 export function decimalCount(a) {
-    if (!isFinite(a)) return 0;
-    var e = 1, p = 0;
-    while (Math.round(a * e) / e !== a) { e *= 10; p++; }
-    return p;
+  if (!isFinite(a)) return 0;
+  var e = 1,
+    p = 0;
+  while (Math.round(a * e) / e !== a) {
+    e *= 10;
+    p++;
+  }
+  return p;
 }
 
 export function computeNumericDataInverval(xValues) {
-    let bestPrecision = Infinity;
-    for (const value of xValues) {
-        let p = precision(value) || 1;
-        if (p < bestPrecision) {
-            bestPrecision = p;
-        }
+  let bestPrecision = Infinity;
+  for (const value of xValues) {
+    let p = precision(value) || 1;
+    if (p < bestPrecision) {
+      bestPrecision = p;
     }
-    return bestPrecision;
+  }
+  return bestPrecision;
 }
 
 // logTickFormat(chart.xAxis())
 export function logTickFormat(axis) {
-    let superscript = "⁰¹²³⁴⁵⁶⁷⁸⁹";
-    let formatPower = (d) => (d + "").split("").map((c) => superscript[c]).join("");
-    let formatTick = (d) =>  10 + formatPower(Math.round(Math.log(d) / Math.LN10));
-    axis.tickFormat(formatTick);
+  let superscript = "⁰¹²³⁴⁵⁶⁷⁸⁹";
+  let formatPower = d =>
+    (d + "")
+      .split("")
+      .map(c => superscript[c])
+      .join("");
+  let formatTick = d => 10 + formatPower(Math.round(Math.log(d) / Math.LN10));
+  axis.tickFormat(formatTick);
 }
diff --git a/frontend/src/metabase/visualizations/lib/renderer_utils.js b/frontend/src/metabase/visualizations/lib/renderer_utils.js
index 7345a0220d8fb871a64be2fd58823855a0519fd2..ffa77b40ad51dfb42fe19b2d08b84c0767766b34 100644
--- a/frontend/src/metabase/visualizations/lib/renderer_utils.js
+++ b/frontend/src/metabase/visualizations/lib/renderer_utils.js
@@ -13,48 +13,56 @@ import { getAvailableCanvasWidth, getAvailableCanvasHeight } from "./utils";
 export const NULL_DIMENSION_WARNING = "Data includes missing dimension values.";
 
 export function initChart(chart, element) {
-    // set the bounds
-    chart.width(getAvailableCanvasWidth(element));
-    chart.height(getAvailableCanvasHeight(element));
-    // disable animations
-    chart.transitionDuration(0);
-    // disable brush
-    if (chart.brushOn) {
-        chart.brushOn(false);
-    }
+  // set the bounds
+  chart.width(getAvailableCanvasWidth(element));
+  chart.height(getAvailableCanvasHeight(element));
+  // disable animations
+  chart.transitionDuration(0);
+  // disable brush
+  if (chart.brushOn) {
+    chart.brushOn(false);
+  }
 }
 
 export function makeIndexMap(values: Array<Value>): Map<Value, number> {
-    let indexMap = new Map()
-    for (const [index, key] of values.entries()) {
-        indexMap.set(key, index);
-    }
-    return indexMap;
+  let indexMap = new Map();
+  for (const [index, key] of values.entries()) {
+    indexMap.set(key, index);
+  }
+  return indexMap;
 }
 
 type CrossfilterGroup = {
-    top: (n: number) => { key: any, value: any },
-    all: () => { key: any, value: any },
-}
+  top: (n: number) => { key: any, value: any },
+  all: () => { key: any, value: any },
+};
 
 // HACK: This ensures each group is sorted by the same order as xValues,
 // otherwise we can end up with line charts with x-axis labels in the correct order
 // but the points in the wrong order. There may be a more efficient way to do this.
-export function forceSortedGroup(group: CrossfilterGroup, indexMap: Map<Value, number>): void {
-    // $FlowFixMe
-    const sorted = group.top(Infinity).sort((a, b) => indexMap.get(a.key) - indexMap.get(b.key));
-    for (let i = 0; i < sorted.length; i++) {
-        sorted[i].index = i;
-    }
-    group.all = () => sorted;
+export function forceSortedGroup(
+  group: CrossfilterGroup,
+  indexMap: Map<Value, number>,
+): void {
+  // $FlowFixMe
+  const sorted = group
+    .top(Infinity)
+    .sort((a, b) => indexMap.get(a.key) - indexMap.get(b.key));
+  for (let i = 0; i < sorted.length; i++) {
+    sorted[i].index = i;
+  }
+  group.all = () => sorted;
 }
 
-export function forceSortedGroupsOfGroups(groupsOfGroups: CrossfilterGroup[][], indexMap: Map<Value, number>): void {
-    for (const groups of groupsOfGroups) {
-        for (const group of groups) {
-            forceSortedGroup(group, indexMap)
-        }
+export function forceSortedGroupsOfGroups(
+  groupsOfGroups: CrossfilterGroup[][],
+  indexMap: Map<Value, number>,
+): void {
+  for (const groups of groupsOfGroups) {
+    for (const group of groups) {
+      forceSortedGroup(group, indexMap);
     }
+  }
 }
 
 /*
@@ -62,90 +70,102 @@ export function forceSortedGroupsOfGroups(groupsOfGroups: CrossfilterGroup[][],
  */
 
 export function reduceGroup(group, key, warnUnaggregated) {
-    return group.reduce(
-        (acc, d) => {
-            if (acc == null && d[key] == null) {
-                return null;
-            } else {
-                if (acc != null) {
-                    warnUnaggregated();
-                    return acc + (d[key] || 0);
-                } else {
-                    return (d[key] || 0);
-                }
-            }
-        },
-        (acc, d) => {
-            if (acc == null && d[key] == null) {
-                return null;
-            } else {
-                if (acc != null) {
-                    warnUnaggregated();
-                    return acc - (d[key] || 0);
-                } else {
-                    return - (d[key] || 0);
-                }
-            }
-        },
-        () => null
-    );
+  return group.reduce(
+    (acc, d) => {
+      if (acc == null && d[key] == null) {
+        return null;
+      } else {
+        if (acc != null) {
+          warnUnaggregated();
+          return acc + (d[key] || 0);
+        } else {
+          return d[key] || 0;
+        }
+      }
+    },
+    (acc, d) => {
+      if (acc == null && d[key] == null) {
+        return null;
+      } else {
+        if (acc != null) {
+          warnUnaggregated();
+          return acc - (d[key] || 0);
+        } else {
+          return -(d[key] || 0);
+        }
+      }
+    },
+    () => null,
+  );
 }
 
 // Crossfilter calls toString on each moment object, which calls format(), which is very slow.
 // Replace toString with a function that just returns the unparsed ISO input date, since that works
 // just as well and is much faster
 function moment_fast_toString() {
-    return this._i;
+  return this._i;
 }
 
 export function HACK_parseTimestamp(value, unit, warn) {
-    if (value == null) {
-        warn(NULL_DIMENSION_WARNING);
-        return null;
-    } else {
-        let m = parseTimestamp(value, unit);
-        m.toString = moment_fast_toString
-        return m;
-    }
+  if (value == null) {
+    warn(NULL_DIMENSION_WARNING);
+    return null;
+  } else {
+    let m = parseTimestamp(value, unit);
+    m.toString = moment_fast_toString;
+    return m;
+  }
 }
 
 /************************************************************ PROPERTIES ************************************************************/
 
-export const isTimeseries   = (settings) => settings["graph.x_axis.scale"] === "timeseries";
-export const isQuantitative = (settings) => ["linear", "log", "pow"].indexOf(settings["graph.x_axis.scale"]) >= 0;
-export const isHistogram    = (settings) => settings["graph.x_axis.scale"] === "histogram";
-export const isOrdinal      = (settings) => !isTimeseries(settings) && !isHistogram(settings);
+export const isTimeseries = settings =>
+  settings["graph.x_axis.scale"] === "timeseries";
+export const isQuantitative = settings =>
+  ["linear", "log", "pow"].indexOf(settings["graph.x_axis.scale"]) >= 0;
+export const isHistogram = settings =>
+  settings["graph.x_axis.scale"] === "histogram";
+export const isOrdinal = settings =>
+  !isTimeseries(settings) && !isHistogram(settings);
 
 // bar histograms have special tick formatting:
 // * aligned with beginning of bar to show bin boundaries
 // * label only shows beginning value of bin
 // * includes an extra tick at the end for the end of the last bin
-export const isHistogramBar = ({ settings, chartType }) => isHistogram(settings) && chartType === "bar";
+export const isHistogramBar = ({ settings, chartType }) =>
+  isHistogram(settings) && chartType === "bar";
 
-export const isStacked    = (settings, datas) => settings["stackable.stack_type"] && datas.length > 1;
-export const isNormalized = (settings, datas) => isStacked(settings, datas) && settings["stackable.stack_type"] === "normalized";
+export const isStacked = (settings, datas) =>
+  settings["stackable.stack_type"] && datas.length > 1;
+export const isNormalized = (settings, datas) =>
+  isStacked(settings, datas) &&
+  settings["stackable.stack_type"] === "normalized";
 
 // find the first nonempty single series
-export const getFirstNonEmptySeries = (series) => _.find(series, (s) => !datasetContainsNoResults(s.data));
-export const isDimensionTimeseries  = (series) => dimensionIsTimeseries(getFirstNonEmptySeries(series).data);
-export const isDimensionNumeric     = (series) => dimensionIsNumeric(getFirstNonEmptySeries(series).data);
+export const getFirstNonEmptySeries = series =>
+  _.find(series, s => !datasetContainsNoResults(s.data));
+export const isDimensionTimeseries = series =>
+  dimensionIsTimeseries(getFirstNonEmptySeries(series).data);
+export const isDimensionNumeric = series =>
+  dimensionIsNumeric(getFirstNonEmptySeries(series).data);
 
 function hasRemappingAndValuesAreStrings({ cols }, i = 0) {
-    const column = cols[i];
-
-    if (column.remapping && column.remapping.size > 0) {
-        // We have remapped values, so check their type for determining whether the dimension is numeric
-        // ES6 Map makes the lookup of first value a little verbose
-        return typeof column.remapping.values().next().value === "string";
-    } else {
-        return false
-    }
+  const column = cols[i];
+
+  if (column.remapping && column.remapping.size > 0) {
+    // We have remapped values, so check their type for determining whether the dimension is numeric
+    // ES6 Map makes the lookup of first value a little verbose
+    return typeof column.remapping.values().next().value === "string";
+  } else {
+    return false;
+  }
 }
 
-export const isRemappedToString = (series) => hasRemappingAndValuesAreStrings(getFirstNonEmptySeries(series).data);
+export const isRemappedToString = series =>
+  hasRemappingAndValuesAreStrings(getFirstNonEmptySeries(series).data);
 
 // is this a dashboard multiseries?
 // TODO: better way to detect this?
-export const isMultiCardSeries = (series) => (
-    series.length > 1 && getIn(series, [0, "card", "id"]) !== getIn(series, [1, "card", "id"])
-);
+export const isMultiCardSeries = series =>
+  series.length > 1 &&
+  getIn(series, [0, "card", "id"]) !== getIn(series, [1, "card", "id"]);
diff --git a/frontend/src/metabase/visualizations/lib/settings.js b/frontend/src/metabase/visualizations/lib/settings.js
index 93a10b8d7fab05eee6429d8ee095917bc5be3c9e..145b8ff75fae30d3b892165cb934cb8886fd4c02 100644
--- a/frontend/src/metabase/visualizations/lib/settings.js
+++ b/frontend/src/metabase/visualizations/lib/settings.js
@@ -1,13 +1,13 @@
 import { getVisualizationRaw } from "metabase/visualizations";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import {
-    columnsAreValid,
-    getChartTypeFromData,
-    DIMENSION_DIMENSION_METRIC,
-    DIMENSION_METRIC,
-    DIMENSION_METRIC_METRIC,
-    getColumnCardinality,
-    getFriendlyName
+  columnsAreValid,
+  getChartTypeFromData,
+  DIMENSION_DIMENSION_METRIC,
+  DIMENSION_METRIC,
+  DIMENSION_METRIC_METRIC,
+  getColumnCardinality,
+  getFriendlyName,
 } from "./utils";
 
 import { isDate, isMetric, isDimension } from "metabase/lib/schema_metadata";
@@ -24,264 +24,296 @@ import ChartSettingColorPicker from "metabase/visualizations/components/settings
 import ChartSettingColorsPicker from "metabase/visualizations/components/settings/ChartSettingColorsPicker.jsx";
 
 const WIDGETS = {
-    input: ChartSettingInput,
-    inputGroup: ChartSettingInputGroup,
-    number: ChartSettingInputNumeric,
-    radio: ChartSettingRadio,
-    select: ChartSettingSelect,
-    toggle: ChartSettingToggle,
-    field: ChartSettingFieldPicker,
-    fields: ChartSettingFieldsPicker,
-    color: ChartSettingColorPicker,
-    colors: ChartSettingColorsPicker,
-}
+  input: ChartSettingInput,
+  inputGroup: ChartSettingInputGroup,
+  number: ChartSettingInputNumeric,
+  radio: ChartSettingRadio,
+  select: ChartSettingSelect,
+  toggle: ChartSettingToggle,
+  field: ChartSettingFieldPicker,
+  fields: ChartSettingFieldsPicker,
+  color: ChartSettingColorPicker,
+  colors: ChartSettingColorsPicker,
+};
 
 export function getDefaultColumns(series) {
-    if (series[0].card.display === "scatter") {
-        return getDefaultScatterColumns(series);
-    } else {
-        return getDefaultLineAreaBarColumns(series);
-    }
+  if (series[0].card.display === "scatter") {
+    return getDefaultScatterColumns(series);
+  } else {
+    return getDefaultLineAreaBarColumns(series);
+  }
 }
 
 function getDefaultScatterColumns([{ data: { cols, rows } }]) {
-    let dimensions = cols.filter(isDimension);
-    let metrics = cols.filter(isMetric);
-    if (dimensions.length === 2 && metrics.length < 2) {
-        return {
-            dimensions: [dimensions[0].name],
-            metrics: [dimensions[1].name],
-            bubble: metrics.length === 1 ? metrics[0].name : null
-        }
-    } else {
-        return {
-            dimensions: [null],
-            metrics: [null],
-            bubble: null
-        };
-    }
+  let dimensions = cols.filter(isDimension);
+  let metrics = cols.filter(isMetric);
+  if (dimensions.length === 2 && metrics.length < 2) {
+    return {
+      dimensions: [dimensions[0].name],
+      metrics: [dimensions[1].name],
+      bubble: metrics.length === 1 ? metrics[0].name : null,
+    };
+  } else {
+    return {
+      dimensions: [null],
+      metrics: [null],
+      bubble: null,
+    };
+  }
 }
 
 function getDefaultLineAreaBarColumns([{ data: { cols, rows } }]) {
-    let type = getChartTypeFromData(cols, rows, false);
-    switch (type) {
-        case DIMENSION_DIMENSION_METRIC:
-            let dimensions = [cols[0], cols[1]];
-            if (isDate(dimensions[1]) && !isDate(dimensions[0])) {
-                // if the series dimension is a date but the axis dimension is not then swap them
-                dimensions.reverse();
-            } else if (getColumnCardinality(cols, rows, 1) > getColumnCardinality(cols, rows, 0)) {
-                // if the series dimension is higher cardinality than the axis dimension then swap them
-                dimensions.reverse();
-            }
-            return {
-                dimensions: dimensions.map(col => col.name),
-                metrics: [cols[2].name]
-            };
-        case DIMENSION_METRIC:
-            return {
-                dimensions: [cols[0].name],
-                metrics: [cols[1].name]
-            };
-        case DIMENSION_METRIC_METRIC:
-            return {
-                dimensions: [cols[0].name],
-                metrics: cols.slice(1).map(col => col.name)
-            };
-        default:
-            return {
-                dimensions: [null],
-                metrics: [null]
-            };
-    }
+  let type = getChartTypeFromData(cols, rows, false);
+  switch (type) {
+    case DIMENSION_DIMENSION_METRIC:
+      let dimensions = [cols[0], cols[1]];
+      if (isDate(dimensions[1]) && !isDate(dimensions[0])) {
+        // if the series dimension is a date but the axis dimension is not then swap them
+        dimensions.reverse();
+      } else if (
+        getColumnCardinality(cols, rows, 1) >
+        getColumnCardinality(cols, rows, 0)
+      ) {
+        // if the series dimension is higher cardinality than the axis dimension then swap them
+        dimensions.reverse();
+      }
+      return {
+        dimensions: dimensions.map(col => col.name),
+        metrics: [cols[2].name],
+      };
+    case DIMENSION_METRIC:
+      return {
+        dimensions: [cols[0].name],
+        metrics: [cols[1].name],
+      };
+    case DIMENSION_METRIC_METRIC:
+      return {
+        dimensions: [cols[0].name],
+        metrics: cols.slice(1).map(col => col.name),
+      };
+    default:
+      return {
+        dimensions: [null],
+        metrics: [null],
+      };
+  }
 }
 
 export function getDefaultDimensionAndMetric([{ data: { cols, rows } }]) {
-    const type = getChartTypeFromData(cols, rows, false);
-    if (type === DIMENSION_METRIC) {
-        return {
-            dimension: cols[0].name,
-            metric: cols[1].name
-        };
-    } else if (type === DIMENSION_DIMENSION_METRIC) {
-        return {
-            dimension: null,
-            metric: cols[2].name
-        };
-    } else {
-        return {
-            dimension: null,
-            metric: null
-        };
-    }
+  const type = getChartTypeFromData(cols, rows, false);
+  if (type === DIMENSION_METRIC) {
+    return {
+      dimension: cols[0].name,
+      metric: cols[1].name,
+    };
+  } else if (type === DIMENSION_DIMENSION_METRIC) {
+    return {
+      dimension: null,
+      metric: cols[2].name,
+    };
+  } else {
+    return {
+      dimension: null,
+      metric: null,
+    };
+  }
 }
 
 export function getOptionFromColumn(col) {
-    return {
-        name: getFriendlyName(col),
-        value: col.name
-    };
+  return {
+    name: getFriendlyName(col),
+    value: col.name,
+  };
 }
 
 export function metricSetting(id) {
-    return fieldSetting(id, isMetric, (series) => getDefaultDimensionAndMetric(series).metric)
+  return fieldSetting(
+    id,
+    isMetric,
+    series => getDefaultDimensionAndMetric(series).metric,
+  );
 }
 
 export function dimensionSetting(id) {
-    return fieldSetting(id, isDimension, (series) => getDefaultDimensionAndMetric(series).dimension)
+  return fieldSetting(
+    id,
+    isDimension,
+    series => getDefaultDimensionAndMetric(series).dimension,
+  );
 }
 
 export function fieldSetting(id, filter, getDefault) {
-    return {
-        widget: "select",
-        isValid: ([{ card, data }], vizSettings) =>
-            columnsAreValid(card.visualization_settings[id], data, filter),
-        getDefault: getDefault,
-        getProps: ([{ card, data: { cols }}]) => ({
-            options: cols.filter(filter).map(getOptionFromColumn)
-        }),
-    };
+  return {
+    widget: "select",
+    isValid: ([{ card, data }], vizSettings) =>
+      columnsAreValid(card.visualization_settings[id], data, filter),
+    getDefault: getDefault,
+    getProps: ([{ card, data: { cols } }]) => ({
+      options: cols.filter(filter).map(getOptionFromColumn),
+    }),
+  };
 }
 
 const COMMON_SETTINGS = {
-    "card.title": {
-        title: t`Title`,
-        widget: "input",
-        getDefault: (series) => series.length === 1 ? series[0].card.name : null,
-        dashboard: true,
-        useRawSeries: true
-    },
-    "card.description": {
-        title: t`Description`,
-        widget: "input",
-        getDefault: (series) => series.length === 1 ? series[0].card.description : null,
-        dashboard: true,
-        useRawSeries: true
-    },
+  "card.title": {
+    title: t`Title`,
+    widget: "input",
+    getDefault: series => (series.length === 1 ? series[0].card.name : null),
+    dashboard: true,
+    useRawSeries: true,
+  },
+  "card.description": {
+    title: t`Description`,
+    widget: "input",
+    getDefault: series =>
+      series.length === 1 ? series[0].card.description : null,
+    dashboard: true,
+    useRawSeries: true,
+  },
 };
 
 function getSetting(settingDefs, id, vizSettings, series) {
-    if (id in vizSettings) {
-        return;
-    }
+  if (id in vizSettings) {
+    return;
+  }
 
-    const settingDef = settingDefs[id] || {};
-    const [{ card }] = series;
-    const visualization_settings = card.visualization_settings || {};
+  const settingDef = settingDefs[id] || {};
+  const [{ card }] = series;
+  const visualization_settings = card.visualization_settings || {};
 
-    for (let dependentId of settingDef.readDependencies || []) {
-        getSetting(settingDefs, dependentId, vizSettings, series);
-    }
+  for (let dependentId of settingDef.readDependencies || []) {
+    getSetting(settingDefs, dependentId, vizSettings, series);
+  }
 
-    if (settingDef.useRawSeries && series._raw) {
-        series = series._raw;
-    }
+  if (settingDef.useRawSeries && series._raw) {
+    series = series._raw;
+  }
 
-    try {
-        if (settingDef.getValue) {
-            return vizSettings[id] = settingDef.getValue(series, vizSettings);
-        }
+  try {
+    if (settingDef.getValue) {
+      return (vizSettings[id] = settingDef.getValue(series, vizSettings));
+    }
 
-        if (visualization_settings[id] !== undefined) {
-            if (!settingDef.isValid || settingDef.isValid(series, vizSettings)) {
-                return vizSettings[id] = visualization_settings[id];
-            }
-        }
+    if (visualization_settings[id] !== undefined) {
+      if (!settingDef.isValid || settingDef.isValid(series, vizSettings)) {
+        return (vizSettings[id] = visualization_settings[id]);
+      }
+    }
 
-        if (settingDef.getDefault) {
-            const defaultValue = settingDef.getDefault(series, vizSettings)
+    if (settingDef.getDefault) {
+      const defaultValue = settingDef.getDefault(series, vizSettings);
 
-            return vizSettings[id] = defaultValue;
-        }
+      return (vizSettings[id] = defaultValue);
+    }
 
-        if ("default" in settingDef) {
-            return vizSettings[id] = settingDef.default;
-        }
-    } catch (e) {
-        console.error("Error getting setting", id, e);
+    if ("default" in settingDef) {
+      return (vizSettings[id] = settingDef.default);
     }
-    return vizSettings[id] = undefined;
+  } catch (e) {
+    console.error("Error getting setting", id, e);
+  }
+  return (vizSettings[id] = undefined);
 }
 
-
 function getSettingDefintionsForSeries(series) {
-    const { CardVisualization } = getVisualizationRaw(series);
-    const definitions = {
-        ...COMMON_SETTINGS,
-        ...(CardVisualization.settings || {})
-    }
-    for (const id in definitions) {
-        definitions[id].id = id
-    }
-    return definitions;
+  const { CardVisualization } = getVisualizationRaw(series);
+  const definitions = {
+    ...COMMON_SETTINGS,
+    ...(CardVisualization.settings || {}),
+  };
+  for (const id in definitions) {
+    definitions[id].id = id;
+  }
+  return definitions;
 }
 
 export function getPersistableDefaultSettings(series) {
-    // A complete set of settings (not only defaults) is loaded because
-    // some persistable default settings need other settings as dependency for calculating the default value
-    const completeSettings = getSettings(series)
+  // A complete set of settings (not only defaults) is loaded because
+  // some persistable default settings need other settings as dependency for calculating the default value
+  const completeSettings = getSettings(series);
 
-    let persistableDefaultSettings = {};
-    let settingsDefs = getSettingDefintionsForSeries(series);
+  let persistableDefaultSettings = {};
+  let settingsDefs = getSettingDefintionsForSeries(series);
 
-    for (let id in settingsDefs) {
-        const settingDef = settingsDefs[id]
-        const seriesForSettingsDef = settingDef.useRawSeries && series._raw ? series._raw : series
+  for (let id in settingsDefs) {
+    const settingDef = settingsDefs[id];
+    const seriesForSettingsDef =
+      settingDef.useRawSeries && series._raw ? series._raw : series;
 
-        if (settingDef.persistDefault) {
-            persistableDefaultSettings[id] = settingDef.getDefault(seriesForSettingsDef, completeSettings)
-        }
+    if (settingDef.persistDefault) {
+      persistableDefaultSettings[id] = settingDef.getDefault(
+        seriesForSettingsDef,
+        completeSettings,
+      );
     }
+  }
 
-    return persistableDefaultSettings;
+  return persistableDefaultSettings;
 }
 
 export function getSettings(series) {
-    let vizSettings = {};
-    let settingsDefs = getSettingDefintionsForSeries(series);
-    for (let id in settingsDefs) {
-        getSetting(settingsDefs, id, vizSettings, series);
-    }
-    return vizSettings;
+  let vizSettings = {};
+  let settingsDefs = getSettingDefintionsForSeries(series);
+  for (let id in settingsDefs) {
+    getSetting(settingsDefs, id, vizSettings, series);
+  }
+  return vizSettings;
 }
 
 function getSettingWidget(settingDef, vizSettings, series, onChangeSettings) {
-    const id = settingDef.id;
-    const value = vizSettings[id];
-    const onChange = (value) => {
-        const newSettings = { [id]: value };
-        for (const id of (settingDef.writeDependencies || [])) {
-            newSettings[id] = vizSettings[id];
-        }
-        onChangeSettings(newSettings)
+  const id = settingDef.id;
+  const value = vizSettings[id];
+  const onChange = value => {
+    const newSettings = { [id]: value };
+    for (const id of settingDef.writeDependencies || []) {
+      newSettings[id] = vizSettings[id];
     }
-    if (settingDef.useRawSeries && series._raw) {
-        series = series._raw;
-    }
-    return {
-        ...settingDef,
-        id: id,
-        value: value,
-        title: settingDef.getTitle ? settingDef.getTitle(series, vizSettings) : settingDef.title,
-        hidden: settingDef.getHidden ? settingDef.getHidden(series, vizSettings) : false,
-        disabled: settingDef.getDisabled ? settingDef.getDisabled(series, vizSettings) : false,
-        props: {
-            ...(settingDef.props ? settingDef.props : {}),
-            ...(settingDef.getProps ? settingDef.getProps(series, vizSettings, onChange) : {})
-        },
-        widget: typeof settingDef.widget === "string" ?
-            WIDGETS[settingDef.widget] :
-            settingDef.widget,
-        onChange
-    };
+    onChangeSettings(newSettings);
+  };
+  if (settingDef.useRawSeries && series._raw) {
+    series = series._raw;
+  }
+  return {
+    ...settingDef,
+    id: id,
+    value: value,
+    title: settingDef.getTitle
+      ? settingDef.getTitle(series, vizSettings)
+      : settingDef.title,
+    hidden: settingDef.getHidden
+      ? settingDef.getHidden(series, vizSettings)
+      : false,
+    disabled: settingDef.getDisabled
+      ? settingDef.getDisabled(series, vizSettings)
+      : false,
+    props: {
+      ...(settingDef.props ? settingDef.props : {}),
+      ...(settingDef.getProps
+        ? settingDef.getProps(series, vizSettings, onChange)
+        : {}),
+    },
+    widget:
+      typeof settingDef.widget === "string"
+        ? WIDGETS[settingDef.widget]
+        : settingDef.widget,
+    onChange,
+  };
 }
 
-export function getSettingsWidgets(series, onChangeSettings, isDashboard = false) {
-    const vizSettings = getSettings(series);
-    return Object.values(getSettingDefintionsForSeries(series)).map(settingDef =>
-        getSettingWidget(settingDef, vizSettings, series, onChangeSettings)
-    ).filter(widget =>
-        widget.widget && !widget.hidden &&
-        (widget.dashboard === undefined || widget.dashboard === isDashboard)
+export function getSettingsWidgets(
+  series,
+  onChangeSettings,
+  isDashboard = false,
+) {
+  const vizSettings = getSettings(series);
+  return Object.values(getSettingDefintionsForSeries(series))
+    .map(settingDef =>
+      getSettingWidget(settingDef, vizSettings, series, onChangeSettings),
+    )
+    .filter(
+      widget =>
+        widget.widget &&
+        !widget.hidden &&
+        (widget.dashboard === undefined || widget.dashboard === isDashboard),
     );
 }
diff --git a/frontend/src/metabase/visualizations/lib/settings/graph.js b/frontend/src/metabase/visualizations/lib/settings/graph.js
index 2d5c73172c0b4e751a68cb87f9caf9b625af5807..3e76166b5d836647d8700fa2dd7a3cb4b5ade143 100644
--- a/frontend/src/metabase/visualizations/lib/settings/graph.js
+++ b/frontend/src/metabase/visualizations/lib/settings/graph.js
@@ -1,287 +1,352 @@
 import { capitalize } from "metabase/lib/formatting";
-import { isDimension, isMetric, isNumeric, isAny } from "metabase/lib/schema_metadata";
-import { t } from 'c-3po';
-import { getDefaultColumns, getOptionFromColumn } from "metabase/visualizations/lib/settings";
-import { columnsAreValid, getCardColors, getFriendlyName } from "metabase/visualizations/lib/utils";
+import {
+  isDimension,
+  isMetric,
+  isNumeric,
+  isAny,
+} from "metabase/lib/schema_metadata";
+import { t } from "c-3po";
+import {
+  getDefaultColumns,
+  getOptionFromColumn,
+} from "metabase/visualizations/lib/settings";
+import {
+  columnsAreValid,
+  getCardColors,
+  getFriendlyName,
+} from "metabase/visualizations/lib/utils";
 import { dimensionIsNumeric } from "metabase/visualizations/lib/numeric";
 import { dimensionIsTimeseries } from "metabase/visualizations/lib/timeseries";
 
 import _ from "underscore";
 
 function getSeriesDefaultTitles(series, vizSettings) {
-    return series.map(s => s.card.name);
+  return series.map(s => s.card.name);
 }
 
 function getSeriesTitles(series, vizSettings) {
-    return vizSettings["graph.series_labels"] || getSeriesDefaultTitles(series, vizSettings);
+  return (
+    vizSettings["graph.series_labels"] ||
+    getSeriesDefaultTitles(series, vizSettings)
+  );
 }
 
 export const GRAPH_DATA_SETTINGS = {
   "graph._dimension_filter": {
-      getDefault: ([{ card }]) => card.display === "scatter" ? isAny : isDimension,
-      useRawSeries: true
+    getDefault: ([{ card }]) =>
+      card.display === "scatter" ? isAny : isDimension,
+    useRawSeries: true,
   },
   "graph._metric_filter": {
-      getDefault: ([{ card }]) => card.display === "scatter" ? isNumeric : isMetric,
-      useRawSeries: true
+    getDefault: ([{ card }]) =>
+      card.display === "scatter" ? isNumeric : isMetric,
+    useRawSeries: true,
   },
   "graph.dimensions": {
-      section: "Data",
-      title: t`X-axis`,
-      widget: "fields",
-      isValid: ([{ card, data }], vizSettings) =>
-          columnsAreValid(card.visualization_settings["graph.dimensions"], data, vizSettings["graph._dimension_filter"]) &&
-          columnsAreValid(card.visualization_settings["graph.metrics"], data, vizSettings["graph._metric_filter"]),
-      getDefault: (series, vizSettings) =>
-          getDefaultColumns(series).dimensions,
-      persistDefault: true,
-      getProps: ([{ card, data }], vizSettings) => {
-          const value = vizSettings["graph.dimensions"];
-          const options = data.cols.filter(vizSettings["graph._dimension_filter"]).map(getOptionFromColumn);
-          return {
-              options,
-              addAnother: (options.length > value.length && value.length < 2 && vizSettings["graph.metrics"].length < 2) ?
-                  t`Add a series breakout...` : null
-          };
-      },
-      readDependencies: ["graph._dimension_filter", "graph._metric_filter"],
-      writeDependencies: ["graph.metrics"],
-      dashboard: false,
-      useRawSeries: true
+    section: "Data",
+    title: t`X-axis`,
+    widget: "fields",
+    isValid: ([{ card, data }], vizSettings) =>
+      columnsAreValid(
+        card.visualization_settings["graph.dimensions"],
+        data,
+        vizSettings["graph._dimension_filter"],
+      ) &&
+      columnsAreValid(
+        card.visualization_settings["graph.metrics"],
+        data,
+        vizSettings["graph._metric_filter"],
+      ),
+    getDefault: (series, vizSettings) => getDefaultColumns(series).dimensions,
+    persistDefault: true,
+    getProps: ([{ card, data }], vizSettings) => {
+      const value = vizSettings["graph.dimensions"];
+      const options = data.cols
+        .filter(vizSettings["graph._dimension_filter"])
+        .map(getOptionFromColumn);
+      return {
+        options,
+        addAnother:
+          options.length > value.length &&
+          value.length < 2 &&
+          vizSettings["graph.metrics"].length < 2
+            ? t`Add a series breakout...`
+            : null,
+      };
+    },
+    readDependencies: ["graph._dimension_filter", "graph._metric_filter"],
+    writeDependencies: ["graph.metrics"],
+    dashboard: false,
+    useRawSeries: true,
   },
   "graph.metrics": {
-      section: "Data",
-      title: t`Y-axis`,
-      widget: "fields",
-      isValid: ([{ card, data }], vizSettings) =>
-          columnsAreValid(card.visualization_settings["graph.dimensions"], data, vizSettings["graph._dimension_filter"]) &&
-          columnsAreValid(card.visualization_settings["graph.metrics"], data, vizSettings["graph._metric_filter"]),
-      getDefault: (series, vizSettings) =>
-          getDefaultColumns(series).metrics,
-      persistDefault: true,
-      getProps: ([{ card, data }], vizSettings) => {
-          const value = vizSettings["graph.dimensions"];
-          const options = data.cols.filter(vizSettings["graph._metric_filter"]).map(getOptionFromColumn);
-          return {
-              options,
-              addAnother: options.length > value.length && vizSettings["graph.dimensions"].length < 2 ?
-                  t`Add another series...` : null
-          };
-      },
-      readDependencies: ["graph._dimension_filter", "graph._metric_filter"],
-      writeDependencies: ["graph.dimensions"],
-      dashboard: false,
-      useRawSeries: true
+    section: "Data",
+    title: t`Y-axis`,
+    widget: "fields",
+    isValid: ([{ card, data }], vizSettings) =>
+      columnsAreValid(
+        card.visualization_settings["graph.dimensions"],
+        data,
+        vizSettings["graph._dimension_filter"],
+      ) &&
+      columnsAreValid(
+        card.visualization_settings["graph.metrics"],
+        data,
+        vizSettings["graph._metric_filter"],
+      ),
+    getDefault: (series, vizSettings) => getDefaultColumns(series).metrics,
+    persistDefault: true,
+    getProps: ([{ card, data }], vizSettings) => {
+      const value = vizSettings["graph.dimensions"];
+      const options = data.cols
+        .filter(vizSettings["graph._metric_filter"])
+        .map(getOptionFromColumn);
+      return {
+        options,
+        addAnother:
+          options.length > value.length &&
+          vizSettings["graph.dimensions"].length < 2
+            ? t`Add another series...`
+            : null,
+      };
+    },
+    readDependencies: ["graph._dimension_filter", "graph._metric_filter"],
+    writeDependencies: ["graph.dimensions"],
+    dashboard: false,
+    useRawSeries: true,
   },
 };
 
 export const GRAPH_BUBBLE_SETTINGS = {
-    "scatter.bubble": {
-        section: "Data",
-        title: t`Bubble size`,
-        widget: "field",
-        isValid: ([{ card, data }], vizSettings) =>
-            columnsAreValid([card.visualization_settings["scatter.bubble"]], data, isNumeric),
-        getDefault: (series) =>
-            getDefaultColumns(series).bubble,
-        getProps: ([{ card, data }], vizSettings, onChange) => {
-            const options = data.cols.filter(isNumeric).map(getOptionFromColumn);
-            return {
-                options,
-                onRemove: vizSettings["scatter.bubble"] ? () => onChange(null) : null
-            };
-        },
-        writeDependencies: ["graph.dimensions"],
-        dashboard: false,
-        useRawSeries: true
+  "scatter.bubble": {
+    section: "Data",
+    title: t`Bubble size`,
+    widget: "field",
+    isValid: ([{ card, data }], vizSettings) =>
+      columnsAreValid(
+        [card.visualization_settings["scatter.bubble"]],
+        data,
+        isNumeric,
+      ),
+    getDefault: series => getDefaultColumns(series).bubble,
+    getProps: ([{ card, data }], vizSettings, onChange) => {
+      const options = data.cols.filter(isNumeric).map(getOptionFromColumn);
+      return {
+        options,
+        onRemove: vizSettings["scatter.bubble"] ? () => onChange(null) : null,
+      };
     },
-}
+    writeDependencies: ["graph.dimensions"],
+    dashboard: false,
+    useRawSeries: true,
+  },
+};
 
 export const LINE_SETTINGS = {
   "line.interpolate": {
-      section: "Display",
-      title: t`Style`,
-      widget: "select",
-      props: {
-          options: [
-              { name: t`Line`, value: "linear" },
-              { name: t`Curve`, value: "cardinal" },
-              { name: t`Step`, value: "step-after" },
-          ]
-      },
-      getDefault: () => "linear"
+    section: "Display",
+    title: t`Style`,
+    widget: "select",
+    props: {
+      options: [
+        { name: t`Line`, value: "linear" },
+        { name: t`Curve`, value: "cardinal" },
+        { name: t`Step`, value: "step-after" },
+      ],
+    },
+    getDefault: () => "linear",
   },
   "line.marker_enabled": {
-      section: "Display",
-      title: t`Show point markers on lines`,
-      widget: "toggle"
+    section: "Display",
+    title: t`Show point markers on lines`,
+    widget: "toggle",
   },
-}
+};
 
 export const STACKABLE_SETTINGS = {
   "stackable.stack_type": {
-      section: "Display",
-      title: t`Stacking`,
-      widget: "radio",
-      getProps: (series, vizSettings) => ({
-          options: [
-              { name: t`Don't stack`, value: null },
-              { name: t`Stack`, value: "stacked" },
-              { name: t`Stack - 100%`, value: "normalized" }
-          ]
-      }),
-      getDefault: ([{ card, data }], vizSettings) =>
-          // legacy setting and default for D-M-M+ charts
-          vizSettings["stackable.stacked"] || (card.display === "area" && vizSettings["graph.metrics"].length > 1) ?
-              "stacked" : null,
-      getHidden: (series) =>
-          series.length < 2,
-      readDependencies: ["graph.metrics"]
-  }
-}
+    section: "Display",
+    title: t`Stacking`,
+    widget: "radio",
+    getProps: (series, vizSettings) => ({
+      options: [
+        { name: t`Don't stack`, value: null },
+        { name: t`Stack`, value: "stacked" },
+        { name: t`Stack - 100%`, value: "normalized" },
+      ],
+    }),
+    getDefault: ([{ card, data }], vizSettings) =>
+      // legacy setting and default for D-M-M+ charts
+      vizSettings["stackable.stacked"] ||
+      (card.display === "area" && vizSettings["graph.metrics"].length > 1)
+        ? "stacked"
+        : null,
+    getHidden: series => series.length < 2,
+    readDependencies: ["graph.metrics"],
+  },
+};
 
 export const GRAPH_GOAL_SETTINGS = {
   "graph.show_goal": {
-      section: "Display",
-      title: t`Show goal`,
-      widget: "toggle",
-      default: false
+    section: "Display",
+    title: t`Show goal`,
+    widget: "toggle",
+    default: false,
   },
   "graph.goal_value": {
-      section: "Display",
-      title: t`Goal value`,
-      widget: "number",
-      default: 0,
-      getHidden: (series, vizSettings) => vizSettings["graph.show_goal"] !== true,
-      readDependencies: ["graph.show_goal"]
+    section: "Display",
+    title: t`Goal value`,
+    widget: "number",
+    default: 0,
+    getHidden: (series, vizSettings) => vizSettings["graph.show_goal"] !== true,
+    readDependencies: ["graph.show_goal"],
   },
-}
+};
 
 export const LINE_SETTINGS_2 = {
   "line.missing": {
-      section: "Display",
-      title: t`Replace missing values with`,
-      widget: "select",
-      default: "interpolate",
-      getProps: (series, vizSettings) => ({
-          options: [
-              { name: t`Zero`, value: "zero" },
-              { name: t`Nothing`, value: "none" },
-              { name: t`Linear Interpolated`, value: "interpolate" },
-          ]
-      })
+    section: "Display",
+    title: t`Replace missing values with`,
+    widget: "select",
+    default: "interpolate",
+    getProps: (series, vizSettings) => ({
+      options: [
+        { name: t`Zero`, value: "zero" },
+        { name: t`Nothing`, value: "none" },
+        { name: t`Linear Interpolated`, value: "interpolate" },
+      ],
+    }),
   },
-}
+};
 
 export const GRAPH_COLORS_SETTINGS = {
   "graph.colors": {
-      section: "Display",
-      getTitle: ([{ card: { display } }]) =>
-          capitalize(display === "scatter" ? "bubble" : display) + " colors",
-      widget: "colors",
-      readDependencies: ["graph.dimensions", "graph.metrics", "graph.series_labels"],
-      getDefault: ([{ card }], vizSettings) => {
-          return getCardColors(card);
-      },
-      getProps: (series, vizSettings) => {
-          return { seriesTitles: getSeriesTitles(series, vizSettings) };
-      }
-  }
-}
+    section: "Display",
+    getTitle: ([{ card: { display } }]) =>
+      capitalize(display === "scatter" ? "bubble" : display) + " colors",
+    widget: "colors",
+    readDependencies: [
+      "graph.dimensions",
+      "graph.metrics",
+      "graph.series_labels",
+    ],
+    getDefault: ([{ card }], vizSettings) => {
+      return getCardColors(card);
+    },
+    getProps: (series, vizSettings) => {
+      return { seriesTitles: getSeriesTitles(series, vizSettings) };
+    },
+  },
+};
 
 export const GRAPH_AXIS_SETTINGS = {
   "graph.x_axis._is_timeseries": {
-      readDependencies: ["graph.dimensions"],
-      getDefault: ([{ data }], vizSettings) =>
-          dimensionIsTimeseries(data, _.findIndex(data.cols, (c) => c.name === vizSettings["graph.dimensions"].filter(d => d)[0]))
+    readDependencies: ["graph.dimensions"],
+    getDefault: ([{ data }], vizSettings) =>
+      dimensionIsTimeseries(
+        data,
+        _.findIndex(
+          data.cols,
+          c => c.name === vizSettings["graph.dimensions"].filter(d => d)[0],
+        ),
+      ),
   },
   "graph.x_axis._is_numeric": {
-      readDependencies: ["graph.dimensions"],
-      getDefault: ([{ data }], vizSettings) =>
-          dimensionIsNumeric(data, _.findIndex(data.cols, (c) => c.name === vizSettings["graph.dimensions"].filter(d => d)[0]))
+    readDependencies: ["graph.dimensions"],
+    getDefault: ([{ data }], vizSettings) =>
+      dimensionIsNumeric(
+        data,
+        _.findIndex(
+          data.cols,
+          c => c.name === vizSettings["graph.dimensions"].filter(d => d)[0],
+        ),
+      ),
   },
   "graph.x_axis._is_histogram": {
-      getDefault: ([{ data: { cols } }], vizSettings) =>
-        cols[0].binning_info != null
+    getDefault: ([{ data: { cols } }], vizSettings) =>
+      cols[0].binning_info != null,
   },
   "graph.x_axis.scale": {
-      section: "Axes",
-      title: t`X-axis scale`,
-      widget: "select",
-      default: "ordinal",
-      readDependencies: [
-          "graph.x_axis._is_timeseries",
-          "graph.x_axis._is_numeric",
-          "graph.x_axis._is_histogram"
-      ],
-      getDefault: (series, vizSettings) =>
-          vizSettings["graph.x_axis._is_histogram"] ? "histogram" :
-          vizSettings["graph.x_axis._is_timeseries"] ? "timeseries" :
-          vizSettings["graph.x_axis._is_numeric"] ? "linear" :
-          "ordinal",
-      getProps: (series, vizSettings) => {
-          const options = [];
-          if (vizSettings["graph.x_axis._is_timeseries"]) {
-              options.push({ name: t`Timeseries`, value: "timeseries" });
-          }
-          if (vizSettings["graph.x_axis._is_numeric"]) {
-              options.push({ name: t`Linear`, value: "linear" });
-              if (!vizSettings["graph.x_axis._is_histogram"]) {
-                  options.push({ name: t`Power`, value: "pow" });
-                  options.push({ name: t`Log`, value: "log" });
-              }
-              options.push({ name: t`Histogram`, value: "histogram" });
-          }
-          options.push({ name: t`Ordinal`, value: "ordinal" });
-          return { options };
+    section: "Axes",
+    title: t`X-axis scale`,
+    widget: "select",
+    default: "ordinal",
+    readDependencies: [
+      "graph.x_axis._is_timeseries",
+      "graph.x_axis._is_numeric",
+      "graph.x_axis._is_histogram",
+    ],
+    getDefault: (series, vizSettings) =>
+      vizSettings["graph.x_axis._is_histogram"]
+        ? "histogram"
+        : vizSettings["graph.x_axis._is_timeseries"]
+          ? "timeseries"
+          : vizSettings["graph.x_axis._is_numeric"] ? "linear" : "ordinal",
+    getProps: (series, vizSettings) => {
+      const options = [];
+      if (vizSettings["graph.x_axis._is_timeseries"]) {
+        options.push({ name: t`Timeseries`, value: "timeseries" });
+      }
+      if (vizSettings["graph.x_axis._is_numeric"]) {
+        options.push({ name: t`Linear`, value: "linear" });
+        if (!vizSettings["graph.x_axis._is_histogram"]) {
+          options.push({ name: t`Power`, value: "pow" });
+          options.push({ name: t`Log`, value: "log" });
+        }
+        options.push({ name: t`Histogram`, value: "histogram" });
       }
+      options.push({ name: t`Ordinal`, value: "ordinal" });
+      return { options };
+    },
   },
   "graph.y_axis.scale": {
-      section: "Axes",
-      title: t`Y-axis scale`,
-      widget: "select",
-      default: "linear",
-      getProps: (series, vizSettings) => ({
-          options: [
-              { name: t`Linear`, value: "linear" },
-              { name: t`Power`, value: "pow" },
-              { name: t`Log`, value: "log" }
-          ]
-      })
+    section: "Axes",
+    title: t`Y-axis scale`,
+    widget: "select",
+    default: "linear",
+    getProps: (series, vizSettings) => ({
+      options: [
+        { name: t`Linear`, value: "linear" },
+        { name: t`Power`, value: "pow" },
+        { name: t`Log`, value: "log" },
+      ],
+    }),
   },
   "graph.x_axis.axis_enabled": {
-      section: "Axes",
-      title: t`Show x-axis line and marks`,
-      widget: "toggle",
-      default: true
+    section: "Axes",
+    title: t`Show x-axis line and marks`,
+    widget: "toggle",
+    default: true,
   },
   "graph.y_axis.axis_enabled": {
-      section: "Axes",
-      title: t`Show y-axis line and marks`,
-      widget: "toggle",
-      default: true
+    section: "Axes",
+    title: t`Show y-axis line and marks`,
+    widget: "toggle",
+    default: true,
   },
   "graph.y_axis.auto_range": {
-      section: "Axes",
-      title: t`Auto y-axis range`,
-      widget: "toggle",
-      default: true
+    section: "Axes",
+    title: t`Auto y-axis range`,
+    widget: "toggle",
+    default: true,
   },
   "graph.y_axis.min": {
-      section: "Axes",
-      title: t`Min`,
-      widget: "number",
-      default: 0,
-      getHidden: (series, vizSettings) => vizSettings["graph.y_axis.auto_range"] !== false
+    section: "Axes",
+    title: t`Min`,
+    widget: "number",
+    default: 0,
+    getHidden: (series, vizSettings) =>
+      vizSettings["graph.y_axis.auto_range"] !== false,
   },
   "graph.y_axis.max": {
-      section: "Axes",
-      title: t`Max`,
-      widget: "number",
-      default: 100,
-      getHidden: (series, vizSettings) => vizSettings["graph.y_axis.auto_range"] !== false
+    section: "Axes",
+    title: t`Max`,
+    widget: "number",
+    default: 100,
+    getHidden: (series, vizSettings) =>
+      vizSettings["graph.y_axis.auto_range"] !== false,
   },
-/*
+  /*
   "graph.y_axis_right.auto_range": {
       section: "Axes",
       title: t`Auto right-hand y-axis range`,
@@ -304,48 +369,49 @@ export const GRAPH_AXIS_SETTINGS = {
   },
 */
   "graph.y_axis.auto_split": {
-      section: "Axes",
-      title: t`Use a split y-axis when necessary`,
-      widget: "toggle",
-      default: true,
-      getHidden: (series) => series.length < 2
+    section: "Axes",
+    title: t`Use a split y-axis when necessary`,
+    widget: "toggle",
+    default: true,
+    getHidden: series => series.length < 2,
   },
   "graph.x_axis.labels_enabled": {
-      section: "Labels",
-      title: t`Show label on x-axis`,
-      widget: "toggle",
-      default: true
+    section: "Labels",
+    title: t`Show label on x-axis`,
+    widget: "toggle",
+    default: true,
   },
   "graph.x_axis.title_text": {
-      section: "Labels",
-      title: t`X-axis label`,
-      widget: "input",
-      getHidden: (series, vizSettings) =>
-          vizSettings["graph.x_axis.labels_enabled"] === false,
-      getDefault: (series, vizSettings) =>
-          series.length === 1 ? getFriendlyName(series[0].data.cols[0]) : null
+    section: "Labels",
+    title: t`X-axis label`,
+    widget: "input",
+    getHidden: (series, vizSettings) =>
+      vizSettings["graph.x_axis.labels_enabled"] === false,
+    getDefault: (series, vizSettings) =>
+      series.length === 1 ? getFriendlyName(series[0].data.cols[0]) : null,
   },
   "graph.y_axis.labels_enabled": {
-      section: "Labels",
-      title: t`Show label on y-axis`,
-      widget: "toggle",
-      default: true
+    section: "Labels",
+    title: t`Show label on y-axis`,
+    widget: "toggle",
+    default: true,
   },
   "graph.y_axis.title_text": {
-      section: "Labels",
-      title: t`Y-axis label`,
-      widget: "input",
-      getHidden: (series, vizSettings) =>
-          vizSettings["graph.y_axis.labels_enabled"] === false,
-      getDefault: (series, vizSettings) =>
-          series.length === 1 ? getFriendlyName(series[0].data.cols[1]) : null
+    section: "Labels",
+    title: t`Y-axis label`,
+    widget: "input",
+    getHidden: (series, vizSettings) =>
+      vizSettings["graph.y_axis.labels_enabled"] === false,
+    getDefault: (series, vizSettings) =>
+      series.length === 1 ? getFriendlyName(series[0].data.cols[1]) : null,
   },
-    "graph.series_labels": {
-        section: "Labels",
-        title: "Series labels",
-        widget: "inputGroup",
-        readDependencies: ["graph.dimensions", "graph.metrics"],
-        getHidden: (series) => series.length < 2,
-        getDefault: (series, vizSettings) => getSeriesDefaultTitles(series, vizSettings)
-    },
-}
+  "graph.series_labels": {
+    section: "Labels",
+    title: "Series labels",
+    widget: "inputGroup",
+    readDependencies: ["graph.dimensions", "graph.metrics"],
+    getHidden: series => series.length < 2,
+    getDefault: (series, vizSettings) =>
+      getSeriesDefaultTitles(series, vizSettings),
+  },
+};
diff --git a/frontend/src/metabase/visualizations/lib/table.js b/frontend/src/metabase/visualizations/lib/table.js
index 8203808bc944ae10cd35f03ef6df5a49e6df4930..de2dcfab6f0c183a48af5061413c96ada672cac7 100644
--- a/frontend/src/metabase/visualizations/lib/table.js
+++ b/frontend/src/metabase/visualizations/lib/table.js
@@ -4,37 +4,42 @@ import type { DatasetData, Column } from "metabase/meta/types/Dataset";
 import type { ClickObject } from "metabase/meta/types/Visualization";
 import { isNumber, isCoordinate } from "metabase/lib/schema_metadata";
 
-export function getTableCellClickedObject(data: DatasetData, rowIndex: number, columnIndex: number, isPivoted: boolean): ClickObject {
-    const { rows, cols } = data;
+export function getTableCellClickedObject(
+  data: DatasetData,
+  rowIndex: number,
+  columnIndex: number,
+  isPivoted: boolean,
+): ClickObject {
+  const { rows, cols } = data;
 
-    const column = cols[columnIndex];
-    const row = rows[rowIndex];
-    const value = row[columnIndex];
+  const column = cols[columnIndex];
+  const row = rows[rowIndex];
+  const value = row[columnIndex];
 
-    if (isPivoted) {
-        // if it's a pivot table, the first column is
-        if (columnIndex === 0) {
-            // $FlowFixMe: _dimension
-            return row._dimension;
-        } else {
-            return {
-                value,
-                column,
-                // $FlowFixMe: _dimension
-                dimensions: [row._dimension, column._dimension]
-            };
-        }
-    } else if (column.source === "aggregation") {
-        return {
-            value,
-            column,
-            dimensions: cols
-                .map((column, index) => ({ value: row[index], column }))
-                .filter(dimension => dimension.column.source === "breakout")
-        };
+  if (isPivoted) {
+    // if it's a pivot table, the first column is
+    if (columnIndex === 0) {
+      // $FlowFixMe: _dimension
+      return row._dimension;
     } else {
-        return { value, column };
+      return {
+        value,
+        column,
+        // $FlowFixMe: _dimension
+        dimensions: [row._dimension, column._dimension],
+      };
     }
+  } else if (column.source === "aggregation") {
+    return {
+      value,
+      column,
+      dimensions: cols
+        .map((column, index) => ({ value: row[index], column }))
+        .filter(dimension => dimension.column.source === "breakout"),
+    };
+  } else {
+    return { value, column };
+  }
 }
 
 /*
@@ -42,5 +47,5 @@ export function getTableCellClickedObject(data: DatasetData, rowIndex: number, c
  * Includes numbers and lat/lon coordinates, but not zip codes, IDs, etc.
  */
 export function isColumnRightAligned(column: Column) {
-    return isNumber(column) || isCoordinate(column);
+  return isNumber(column) || isCoordinate(column);
 }
diff --git a/frontend/src/metabase/visualizations/lib/timeseries.js b/frontend/src/metabase/visualizations/lib/timeseries.js
index 66c200dc507326c9b43d26513da0ec31e2e8c05e..73024a50183f1620d5dd6f8cb4153b075b8d87b0 100644
--- a/frontend/src/metabase/visualizations/lib/timeseries.js
+++ b/frontend/src/metabase/visualizations/lib/timeseries.js
@@ -8,21 +8,22 @@ import { isDate } from "metabase/lib/schema_metadata";
 import { parseTimestamp } from "metabase/lib/time";
 
 const TIMESERIES_UNITS = new Set([
-    "minute",
-    "hour",
-    "day",
-    "week",
-    "month",
-    "quarter",
-    "year" // https://github.com/metabase/metabase/issues/1992
+  "minute",
+  "hour",
+  "day",
+  "week",
+  "month",
+  "quarter",
+  "year", // https://github.com/metabase/metabase/issues/1992
 ]);
 
 // investigate the response from a dataset query and determine if the dimension is a timeseries
 export function dimensionIsTimeseries({ cols, rows }, i = 0) {
-    return (
-        (isDate(cols[i]) && (cols[i].unit == null || TIMESERIES_UNITS.has(cols[i].unit))) ||
-        moment(rows[0] && rows[0][i], moment.ISO_8601).isValid()
-    );
+  return (
+    (isDate(cols[i]) &&
+      (cols[i].unit == null || TIMESERIES_UNITS.has(cols[i].unit))) ||
+    moment(rows[0] && rows[0][i], moment.ISO_8601).isValid()
+  );
 }
 
 // mostly matches
@@ -39,72 +40,185 @@ export function dimensionIsTimeseries({ cols, rows }, i = 0) {
 // TODO - I'm not sure what the appropriate thing to put for rangeFn for milliseconds is. This matches the previous
 // behavior, which may have been wrong in the first place. See https://github.com/d3/d3/issues/1529 for a similar issue
 const TIMESERIES_INTERVALS = [
-    { interval: "ms",     count: 1,   rangeFn: undefined,       testFn: (d) => 0                            }, //  (0) millisecond
-    { interval: "second", count: 1,   rangeFn: d3.time.seconds, testFn: (d) => parseTimestamp(d).milliseconds() }, //  (1) 1 second
-    { interval: "second", count: 5,   rangeFn: d3.time.seconds, testFn: (d) => parseTimestamp(d).seconds() % 5  }, //  (2) 5 seconds
-    { interval: "second", count: 15,  rangeFn: d3.time.seconds, testFn: (d) => parseTimestamp(d).seconds() % 15 }, //  (3) 15 seconds
-    { interval: "second", count: 30,  rangeFn: d3.time.seconds, testFn: (d) => parseTimestamp(d).seconds() % 30 }, //  (4) 30 seconds
-    { interval: "minute", count: 1,   rangeFn: d3.time.minutes, testFn: (d) => parseTimestamp(d).seconds()      }, //  (5) 1 minute
-    { interval: "minute", count: 5,   rangeFn: d3.time.minutes, testFn: (d) => parseTimestamp(d).minutes() % 5  }, //  (6) 5 minutes
-    { interval: "minute", count: 15,  rangeFn: d3.time.minutes, testFn: (d) => parseTimestamp(d).minutes() % 15 }, //  (7) 15 minutes
-    { interval: "minute", count: 30,  rangeFn: d3.time.minutes, testFn: (d) => parseTimestamp(d).minutes() % 30 }, //  (8) 30 minutes
-    { interval: "hour",   count: 1,   rangeFn: d3.time.hours,   testFn: (d) => parseTimestamp(d).minutes()      }, //  (9) 1 hour
-    { interval: "hour",   count: 3,   rangeFn: d3.time.hours,   testFn: (d) => parseTimestamp(d).hours() % 3    }, // (10) 3 hours
-    { interval: "hour",   count: 6,   rangeFn: d3.time.hours,   testFn: (d) => parseTimestamp(d).hours() % 6    }, // (11) 6 hours
-    { interval: "hour",   count: 12,  rangeFn: d3.time.hours,   testFn: (d) => parseTimestamp(d).hours() % 12   }, // (12) 12 hours
-    { interval: "day",    count: 1,   rangeFn: d3.time.days,    testFn: (d) => parseTimestamp(d).hours()        }, // (13) 1 day
-    { interval: "week",   count: 1,   rangeFn: d3.time.weeks,   testFn: (d) => parseTimestamp(d).date() % 7     }, // (14) 7 days / 1 week
-    { interval: "month",  count: 1,   rangeFn: d3.time.months,  testFn: (d) => parseTimestamp(d).date()         }, // (15) 1 months
-    { interval: "month",  count: 3,   rangeFn: d3.time.months,  testFn: (d) => parseTimestamp(d).month() % 3    }, // (16) 3 months / 1 quarter
-    { interval: "year",   count: 1,   rangeFn: d3.time.years,   testFn: (d) => parseTimestamp(d).month()        }, // (17) 1 year
-    { interval: "year",   count: 5,   rangeFn: d3.time.years,   testFn: (d) => parseTimestamp(d).year() % 5     }, // (18) 5 year
-    { interval: "year",   count: 10,  rangeFn: d3.time.years,   testFn: (d) => parseTimestamp(d).year() % 10    }, // (19) 10 year
-    { interval: "year",   count: 50,  rangeFn: d3.time.years,   testFn: (d) => parseTimestamp(d).year() % 50    }, // (20) 50 year
-    { interval: "year",   count: 100, rangeFn: d3.time.years,   testFn: (d) => parseTimestamp(d).year() % 100   }  // (21) 100 year
+  { interval: "ms", count: 1, rangeFn: undefined, testFn: d => 0 }, //  (0) millisecond
+  {
+    interval: "second",
+    count: 1,
+    rangeFn: d3.time.seconds,
+    testFn: d => parseTimestamp(d).milliseconds(),
+  }, //  (1) 1 second
+  {
+    interval: "second",
+    count: 5,
+    rangeFn: d3.time.seconds,
+    testFn: d => parseTimestamp(d).seconds() % 5,
+  }, //  (2) 5 seconds
+  {
+    interval: "second",
+    count: 15,
+    rangeFn: d3.time.seconds,
+    testFn: d => parseTimestamp(d).seconds() % 15,
+  }, //  (3) 15 seconds
+  {
+    interval: "second",
+    count: 30,
+    rangeFn: d3.time.seconds,
+    testFn: d => parseTimestamp(d).seconds() % 30,
+  }, //  (4) 30 seconds
+  {
+    interval: "minute",
+    count: 1,
+    rangeFn: d3.time.minutes,
+    testFn: d => parseTimestamp(d).seconds(),
+  }, //  (5) 1 minute
+  {
+    interval: "minute",
+    count: 5,
+    rangeFn: d3.time.minutes,
+    testFn: d => parseTimestamp(d).minutes() % 5,
+  }, //  (6) 5 minutes
+  {
+    interval: "minute",
+    count: 15,
+    rangeFn: d3.time.minutes,
+    testFn: d => parseTimestamp(d).minutes() % 15,
+  }, //  (7) 15 minutes
+  {
+    interval: "minute",
+    count: 30,
+    rangeFn: d3.time.minutes,
+    testFn: d => parseTimestamp(d).minutes() % 30,
+  }, //  (8) 30 minutes
+  {
+    interval: "hour",
+    count: 1,
+    rangeFn: d3.time.hours,
+    testFn: d => parseTimestamp(d).minutes(),
+  }, //  (9) 1 hour
+  {
+    interval: "hour",
+    count: 3,
+    rangeFn: d3.time.hours,
+    testFn: d => parseTimestamp(d).hours() % 3,
+  }, // (10) 3 hours
+  {
+    interval: "hour",
+    count: 6,
+    rangeFn: d3.time.hours,
+    testFn: d => parseTimestamp(d).hours() % 6,
+  }, // (11) 6 hours
+  {
+    interval: "hour",
+    count: 12,
+    rangeFn: d3.time.hours,
+    testFn: d => parseTimestamp(d).hours() % 12,
+  }, // (12) 12 hours
+  {
+    interval: "day",
+    count: 1,
+    rangeFn: d3.time.days,
+    testFn: d => parseTimestamp(d).hours(),
+  }, // (13) 1 day
+  {
+    interval: "week",
+    count: 1,
+    rangeFn: d3.time.weeks,
+    testFn: d => parseTimestamp(d).date() % 7,
+  }, // (14) 7 days / 1 week
+  {
+    interval: "month",
+    count: 1,
+    rangeFn: d3.time.months,
+    testFn: d => parseTimestamp(d).date(),
+  }, // (15) 1 months
+  {
+    interval: "month",
+    count: 3,
+    rangeFn: d3.time.months,
+    testFn: d => parseTimestamp(d).month() % 3,
+  }, // (16) 3 months / 1 quarter
+  {
+    interval: "year",
+    count: 1,
+    rangeFn: d3.time.years,
+    testFn: d => parseTimestamp(d).month(),
+  }, // (17) 1 year
+  {
+    interval: "year",
+    count: 5,
+    rangeFn: d3.time.years,
+    testFn: d => parseTimestamp(d).year() % 5,
+  }, // (18) 5 year
+  {
+    interval: "year",
+    count: 10,
+    rangeFn: d3.time.years,
+    testFn: d => parseTimestamp(d).year() % 10,
+  }, // (19) 10 year
+  {
+    interval: "year",
+    count: 50,
+    rangeFn: d3.time.years,
+    testFn: d => parseTimestamp(d).year() % 50,
+  }, // (20) 50 year
+  {
+    interval: "year",
+    count: 100,
+    rangeFn: d3.time.years,
+    testFn: d => parseTimestamp(d).year() % 100,
+  }, // (21) 100 year
 ];
 
 // mapping from Metabase "unit" to d3 intervals above
 const INTERVAL_INDEX_BY_UNIT = {
-    "minute": 1,
-    "hour": 9,
-    "day": 13,
-    "week": 14,
-    "month": 15,
-    "quarter": 16,
-    "year": 17
+  minute: 1,
+  hour: 9,
+  day: 13,
+  week: 14,
+  month: 15,
+  quarter: 16,
+  year: 17,
 };
 
 export function minTimeseriesUnit(units) {
-    return units.reduce((minUnit, unit) =>
-        unit != null && (minUnit == null || INTERVAL_INDEX_BY_UNIT[unit] < INTERVAL_INDEX_BY_UNIT[minUnit]) ? unit : minUnit
-    , null);
+  return units.reduce(
+    (minUnit, unit) =>
+      unit != null &&
+      (minUnit == null ||
+        INTERVAL_INDEX_BY_UNIT[unit] < INTERVAL_INDEX_BY_UNIT[minUnit])
+        ? unit
+        : minUnit,
+    null,
+  );
 }
 
 function computeTimeseriesDataInvervalIndex(xValues, unit) {
-    if (unit && INTERVAL_INDEX_BY_UNIT[unit] != undefined) {
-        return INTERVAL_INDEX_BY_UNIT[unit];
+  if (unit && INTERVAL_INDEX_BY_UNIT[unit] != undefined) {
+    return INTERVAL_INDEX_BY_UNIT[unit];
+  }
+  // Keep track of the value seen for each level of granularity,
+  // if any don't match then we know the data is *at least* that granular.
+  let values = [];
+  let index = TIMESERIES_INTERVALS.length;
+  for (let xValue of xValues) {
+    // Only need to check more granular than the current interval
+    for (let i = 0; i < TIMESERIES_INTERVALS.length && i < index; i++) {
+      const interval = TIMESERIES_INTERVALS[i];
+      const value = interval.testFn(xValue);
+      if (values[i] === undefined) {
+        values[i] = value;
+      } else if (values[i] !== value) {
+        index = i;
+      }
     }
-    // Keep track of the value seen for each level of granularity,
-    // if any don't match then we know the data is *at least* that granular.
-    let values = [];
-    let index = TIMESERIES_INTERVALS.length;
-    for (let xValue of xValues) {
-        // Only need to check more granular than the current interval
-        for (let i = 0; i < TIMESERIES_INTERVALS.length && i < index; i++) {
-            const interval = TIMESERIES_INTERVALS[i];
-            const value    = interval.testFn(xValue);
-            if (values[i] === undefined) {
-                values[i] = value;
-            } else if (values[i] !== value) {
-                index = i;
-            }
-        }
-    }
-    return index - 1;
+  }
+  return index - 1;
 }
 
 export function computeTimeseriesDataInverval(xValues, unit) {
-    return TIMESERIES_INTERVALS[computeTimeseriesDataInvervalIndex(xValues, unit)];
+  return TIMESERIES_INTERVALS[
+    computeTimeseriesDataInvervalIndex(xValues, unit)
+  ];
 }
 
 // ------------------------- Computing the TIMESERIES_INTERVALS entry to use for a chart ------------------------- //
@@ -112,40 +226,52 @@ export function computeTimeseriesDataInverval(xValues, unit) {
 /// The number of milliseconds between each tick for an entry in TIMESERIES_INTERVALS.
 /// For example a "5 seconds" interval would have a tick "distance" of 5000 milliseconds.
 function intervalTickDistanceMilliseconds(interval) {
-    // add COUNT nuumber of INTERVALS to the UNIX timestamp 0. e.g. add '5 hours' to 0. Then get the new timestamp
-    // (in milliseconds). Since we added to 0 this will be the interval between each tick
-    return moment(0).add(interval.count, interval.interval).valueOf();
+  // add COUNT nuumber of INTERVALS to the UNIX timestamp 0. e.g. add '5 hours' to 0. Then get the new timestamp
+  // (in milliseconds). Since we added to 0 this will be the interval between each tick
+  return moment(0)
+    .add(interval.count, interval.interval)
+    .valueOf();
 }
 
 /// Return the number of ticks we can expect to see over a time range using the TIMESERIES_INTERVALS entry interval.
 /// for example a "5 seconds" interval over a time range of a minute should have an expected tick count of 20.
 function expectedTickCount(interval, timeRangeMilliseconds) {
-    return Math.ceil(timeRangeMilliseconds / intervalTickDistanceMilliseconds(interval));
+  return Math.ceil(
+    timeRangeMilliseconds / intervalTickDistanceMilliseconds(interval),
+  );
 }
 
 /// Get the appropriate tick interval option option from the TIMESERIES_INTERVALS above based on the xAxis bucketing
 /// and the max number of ticks we want to show (itself calculated from chart width).
-function timeseriesTicksInterval(xInterval, timeRangeMilliseconds, maxTickCount) {
-    // first we want to find out where in TIMESERIES_INTERVALS we should start looking for a good match. Find the
-    // interval with a matching interval and count (e.g. `hour` and `1`) and we'll start there.
-    let initialIndex = _.findIndex(TIMESERIES_INTERVALS, ({ interval, count }) => {
-        return interval === xInterval.interval && count === xInterval.count;
-    });
-    // if we weren't able to find soemthing matching then we'll start from the beginning and try everything
-    if (initialIndex === -1) initialIndex = 0;
-
-    // now starting at the TIMESERIES_INTERVALS entry in question, calculate the expected tick count for that interval
-    // based on the time range we are displaying. If the expected tick count is less than or equal to the target
-    // maxTickCount, we can go ahead and use this interval. Otherwise continue on to the next larger interval, for
-    // example every 3 hours instead of every one hour. Continue until we find something with an interval large enough
-    // to keep the total tick count under the max tick count
-    for (const interval of _.rest(TIMESERIES_INTERVALS, initialIndex)) {
-        if (expectedTickCount(interval, timeRangeMilliseconds) <= maxTickCount) return interval;
-    }
+function timeseriesTicksInterval(
+  xInterval,
+  timeRangeMilliseconds,
+  maxTickCount,
+) {
+  // first we want to find out where in TIMESERIES_INTERVALS we should start looking for a good match. Find the
+  // interval with a matching interval and count (e.g. `hour` and `1`) and we'll start there.
+  let initialIndex = _.findIndex(
+    TIMESERIES_INTERVALS,
+    ({ interval, count }) => {
+      return interval === xInterval.interval && count === xInterval.count;
+    },
+  );
+  // if we weren't able to find soemthing matching then we'll start from the beginning and try everything
+  if (initialIndex === -1) initialIndex = 0;
+
+  // now starting at the TIMESERIES_INTERVALS entry in question, calculate the expected tick count for that interval
+  // based on the time range we are displaying. If the expected tick count is less than or equal to the target
+  // maxTickCount, we can go ahead and use this interval. Otherwise continue on to the next larger interval, for
+  // example every 3 hours instead of every one hour. Continue until we find something with an interval large enough
+  // to keep the total tick count under the max tick count
+  for (const interval of _.rest(TIMESERIES_INTERVALS, initialIndex)) {
+    if (expectedTickCount(interval, timeRangeMilliseconds) <= maxTickCount)
+      return interval;
+  }
 
-    // If we still failed to find an interval that will produce less ticks than the max then fall back to the largest
-    // tick interval (every 100 years)
-    return TIMESERIES_INTERVALS[TIMESERIES_INTERVALS.length - 1];
+  // If we still failed to find an interval that will produce less ticks than the max then fall back to the largest
+  // tick interval (every 100 years)
+  return TIMESERIES_INTERVALS[TIMESERIES_INTERVALS.length - 1];
 }
 
 /// return the maximum number of ticks to show for a timeseries chart of a given width. Unlike other chart types, this
@@ -153,20 +279,24 @@ function timeseriesTicksInterval(xInterval, timeRangeMilliseconds, maxTickCount)
 /// hardcoded below.
 /// TODO - it would be nice to rework this a bit so we
 function maxTicksForChartWidth(chartWidth) {
-    const MIN_PIXELS_PER_TICK = 160;
-    return Math.floor(chartWidth / MIN_PIXELS_PER_TICK); // round down so we don't end up with too many ticks
+  const MIN_PIXELS_PER_TICK = 160;
+  return Math.floor(chartWidth / MIN_PIXELS_PER_TICK); // round down so we don't end up with too many ticks
 }
 
 /// return the range, in milliseconds, of the xDomain. ("Range" in this sense refers to the total "width"" of the
 /// chart in millisecodns.)
 function timeRangeMilliseconds(xDomain) {
-    const startTime = xDomain[0]; // these are UNIX timestamps in milliseconds
-    const endTime   = xDomain[1];
-    return endTime - startTime;
+  const startTime = xDomain[0]; // these are UNIX timestamps in milliseconds
+  const endTime = xDomain[1];
+  return endTime - startTime;
 }
 
 /// return the appropriate entry in TIMESERIES_INTERVALS for a given chart with domain, interval, and width.
 /// The entry is used to calculate how often a tick should be displayed for this chart (e.g. one tick every 5 minutes)
 export function computeTimeseriesTicksInterval(xDomain, xInterval, chartWidth) {
-    return timeseriesTicksInterval(xInterval, timeRangeMilliseconds(xDomain), maxTicksForChartWidth(chartWidth));
+  return timeseriesTicksInterval(
+    xInterval,
+    timeRangeMilliseconds(xDomain),
+    maxTicksForChartWidth(chartWidth),
+  );
 }
diff --git a/frontend/src/metabase/visualizations/lib/tooltip.js b/frontend/src/metabase/visualizations/lib/tooltip.js
index dbc72ca44b4722dd264d4364e67b7f73cf07393e..e36abc14f1c8489dada3497a59cc447a93843b16 100644
--- a/frontend/src/metabase/visualizations/lib/tooltip.js
+++ b/frontend/src/metabase/visualizations/lib/tooltip.js
@@ -1,29 +1,35 @@
 /* @flow weak */
 
 function getElementIndex(e) {
-    return e && [...e.classList].map(c => c.match(/^_(\d+)$/)).filter(c => c).map(c => parseInt(c[1], 10))[0];
+  return (
+    e &&
+    [...e.classList]
+      .map(c => c.match(/^_(\d+)$/))
+      .filter(c => c)
+      .map(c => parseInt(c[1], 10))[0]
+  );
 }
 
 function getParentWithClass(element, className) {
-    while (element) {
-        if (element.classList && element.classList.contains(className)) {
-            return element;
-        }
-        element = element.parentNode
+  while (element) {
+    if (element.classList && element.classList.contains(className)) {
+      return element;
     }
-    return null;
+    element = element.parentNode;
+  }
+  return null;
 }
 
 // HACK: This determines the index of the series the provided element belongs to since DC doesn't seem to provide another way
 export function determineSeriesIndexFromElement(element, isStacked): number {
-    if (isStacked) {
-        if (element.classList.contains("dot")) {
-            // .dots are children of dc-tooltip
-            return getElementIndex(getParentWithClass(element, "dc-tooltip"))
-        } else {
-            return getElementIndex(getParentWithClass(element, "stack"))
-        }
+  if (isStacked) {
+    if (element.classList.contains("dot")) {
+      // .dots are children of dc-tooltip
+      return getElementIndex(getParentWithClass(element, "dc-tooltip"));
     } else {
-        return getElementIndex(getParentWithClass(element, "sub"))
+      return getElementIndex(getParentWithClass(element, "stack"));
     }
+  } else {
+    return getElementIndex(getParentWithClass(element, "sub"));
+  }
 }
diff --git a/frontend/src/metabase/visualizations/lib/utils.js b/frontend/src/metabase/visualizations/lib/utils.js
index 73d501edac2cc370b6ef6cd28ad8d63d2865cb87..31cc7e71796697cce2e079a884e69c82aca35fee 100644
--- a/frontend/src/metabase/visualizations/lib/utils.js
+++ b/frontend/src/metabase/visualizations/lib/utils.js
@@ -3,7 +3,7 @@
 import React from "react";
 import _ from "underscore";
 import d3 from "d3";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import crossfilter from "crossfilter";
 
 import * as colors from "metabase/lib/colors";
@@ -14,175 +14,208 @@ const SPLIT_AXIS_COST_FACTOR = 2;
 // NOTE Atte Keinänen 8/3/17: Moved from settings.js because this way we
 // are able to avoid circular dependency errors in integrated tests
 export function columnsAreValid(colNames, data, filter = () => true) {
-    if (typeof colNames === "string") {
-        colNames = [colNames]
-    }
-    if (!data || !Array.isArray(colNames)) {
-        return false;
-    }
-    const colsByName = {};
-    for (const col of data.cols) {
-        colsByName[col.name] = col;
-    }
-    return colNames.reduce((acc, name) =>
-        acc && (name == undefined || (colsByName[name] && filter(colsByName[name])))
-        , true);
+  if (typeof colNames === "string") {
+    colNames = [colNames];
+  }
+  if (!data || !Array.isArray(colNames)) {
+    return false;
+  }
+  const colsByName = {};
+  for (const col of data.cols) {
+    colsByName[col.name] = col;
+  }
+  return colNames.reduce(
+    (acc, name) =>
+      acc &&
+      (name == undefined || (colsByName[name] && filter(colsByName[name]))),
+    true,
+  );
 }
 
 // computed size properties (drop 'px' and convert string -> Number)
 function getComputedSizeProperty(prop, element) {
-    const val = document.defaultView.getComputedStyle(element, null).getPropertyValue(prop);
-    return val ? parseFloat(val.replace("px", "")) : 0;
+  const val = document.defaultView
+    .getComputedStyle(element, null)
+    .getPropertyValue(prop);
+  return val ? parseFloat(val.replace("px", "")) : 0;
 }
 
 /// height available for rendering the card
 export function getAvailableCanvasHeight(element) {
-    const parent              = element.parentElement;
-    const parentHeight        = getComputedSizeProperty("height", parent);
-    const parentPaddingTop    = getComputedSizeProperty("padding-top", parent);
-    const parentPaddingBottom = getComputedSizeProperty("padding-bottom", parent);
+  const parent = element.parentElement;
+  const parentHeight = getComputedSizeProperty("height", parent);
+  const parentPaddingTop = getComputedSizeProperty("padding-top", parent);
+  const parentPaddingBottom = getComputedSizeProperty("padding-bottom", parent);
 
-    // NOTE: if this magic number is not 3 we can get into infinite re-render loops
-    return parentHeight - parentPaddingTop - parentPaddingBottom - 3; // why the magic number :/
+  // NOTE: if this magic number is not 3 we can get into infinite re-render loops
+  return parentHeight - parentPaddingTop - parentPaddingBottom - 3; // why the magic number :/
 }
 
 /// width available for rendering the card
 export function getAvailableCanvasWidth(element) {
-    const parent             = element.parentElement;
-    const parentWidth        = getComputedSizeProperty("width", parent);
-    const parentPaddingLeft  = getComputedSizeProperty("padding-left", parent);
-    const parentPaddingRight = getComputedSizeProperty("padding-right", parent);
+  const parent = element.parentElement;
+  const parentWidth = getComputedSizeProperty("width", parent);
+  const parentPaddingLeft = getComputedSizeProperty("padding-left", parent);
+  const parentPaddingRight = getComputedSizeProperty("padding-right", parent);
 
-    return parentWidth - parentPaddingLeft - parentPaddingRight;
+  return parentWidth - parentPaddingLeft - parentPaddingRight;
 }
 
 function generateSplits(list, left = [], right = []) {
-    // NOTE: currently generates all permutations, some of which are equivalent
-    if (list.length === 0) {
-        return [[left, right]];
-    } else {
-        return [
-            ...generateSplits(list.slice(1), left.concat([list[0]]), right),
-            ...generateSplits(list.slice(1), left, right.concat([list[0]]))
-        ];
-    }
+  // NOTE: currently generates all permutations, some of which are equivalent
+  if (list.length === 0) {
+    return [[left, right]];
+  } else {
+    return [
+      ...generateSplits(list.slice(1), left.concat([list[0]]), right),
+      ...generateSplits(list.slice(1), left, right.concat([list[0]])),
+    ];
+  }
 }
 
 function cost(seriesExtents) {
-    let axisExtent = d3.extent([].concat(...seriesExtents)); // concat to flatten the array
-    let axisRange = axisExtent[1] - axisExtent[0];
-    if (seriesExtents.length === 0) {
-        return SPLIT_AXIS_UNSPLIT_COST;
-    } else if (axisRange === 0) {
-        return 0;
-    } else {
-        return seriesExtents.reduce((sum, seriesExtent) =>
-            sum + Math.pow(axisRange / (seriesExtent[1] - seriesExtent[0]), SPLIT_AXIS_COST_FACTOR)
-        , 0);
-    }
+  let axisExtent = d3.extent([].concat(...seriesExtents)); // concat to flatten the array
+  let axisRange = axisExtent[1] - axisExtent[0];
+  if (seriesExtents.length === 0) {
+    return SPLIT_AXIS_UNSPLIT_COST;
+  } else if (axisRange === 0) {
+    return 0;
+  } else {
+    return seriesExtents.reduce(
+      (sum, seriesExtent) =>
+        sum +
+        Math.pow(
+          axisRange / (seriesExtent[1] - seriesExtent[0]),
+          SPLIT_AXIS_COST_FACTOR,
+        ),
+      0,
+    );
+  }
 }
 
 export function computeSplit(extents) {
-    let best, bestCost;
-    let splits = generateSplits(extents.map((e,i) => i)).map(split =>
-        [split, cost(split[0].map(i => extents[i])) + cost(split[1].map(i => extents[i]))]
-    );
-    for (let [split, splitCost] of splits) {
-        if (!best || splitCost < bestCost) {
-            best = split;
-            bestCost = splitCost;
-        }
+  let best, bestCost;
+  let splits = generateSplits(extents.map((e, i) => i)).map(split => [
+    split,
+    cost(split[0].map(i => extents[i])) + cost(split[1].map(i => extents[i])),
+  ]);
+  for (let [split, splitCost] of splits) {
+    if (!best || splitCost < bestCost) {
+      best = split;
+      bestCost = splitCost;
     }
-    return best && best.sort((a,b) => a[0] - b[0]);
+  }
+  return best && best.sort((a, b) => a[0] - b[0]);
 }
 
 const FRIENDLY_NAME_MAP = {
-    "avg": t`Average`,
-    "count": t`Count`,
-    "sum": t`Sum`,
-    "distinct": t`Distinct`,
-    "stddev": t`Standard Deviation`
+  avg: t`Average`,
+  count: t`Count`,
+  sum: t`Sum`,
+  distinct: t`Distinct`,
+  stddev: t`Standard Deviation`,
 };
 
 export function getXValues(datas, chartType) {
-    let xValues = _.chain(datas)
-        .map((data) => _.pluck(data, "0"))
-        .flatten(true)
-        .uniq()
-        .value();
-
-    // detect if every series' dimension is strictly ascending or descending and use that to sort xValues
-    let isAscending = true;
-    let isDescending = true;
-    outer: for (const rows of datas) {
-        for (let i = 1; i < rows.length; i++) {
-            isAscending = isAscending && rows[i - 1][0] <= rows[i][0];
-            isDescending = isDescending && rows[i - 1][0] >= rows[i][0];
-            if (!isAscending && !isDescending) {
-                break outer;
-            }
-        }
+  let xValues = _.chain(datas)
+    .map(data => _.pluck(data, "0"))
+    .flatten(true)
+    .uniq()
+    .value();
+
+  // detect if every series' dimension is strictly ascending or descending and use that to sort xValues
+  let isAscending = true;
+  let isDescending = true;
+  outer: for (const rows of datas) {
+    for (let i = 1; i < rows.length; i++) {
+      isAscending = isAscending && rows[i - 1][0] <= rows[i][0];
+      isDescending = isDescending && rows[i - 1][0] >= rows[i][0];
+      if (!isAscending && !isDescending) {
+        break outer;
+      }
     }
-    if (isDescending) {
-        // JavaScript's .sort() sorts lexicographically by default (e.x. 1, 10, 2)
-        // We could implement a comparator but _.sortBy handles strings, numbers, and dates correctly
-        xValues = _.sortBy(xValues, x => x).reverse();
-    } else if (isAscending) {
-        // default line/area charts to ascending since otherwise lines could be wonky
-        xValues = _.sortBy(xValues, x => x);
-    }
-    return xValues;
+  }
+  if (isDescending) {
+    // JavaScript's .sort() sorts lexicographically by default (e.x. 1, 10, 2)
+    // We could implement a comparator but _.sortBy handles strings, numbers, and dates correctly
+    xValues = _.sortBy(xValues, x => x).reverse();
+  } else if (isAscending) {
+    // default line/area charts to ascending since otherwise lines could be wonky
+    xValues = _.sortBy(xValues, x => x);
+  }
+  return xValues;
 }
 
 export function getFriendlyName(column) {
-    if (column.display_name && column.display_name !== column.name) {
-        return column.display_name
-    } else {
-        // NOTE Atte Keinänen 8/7/17:
-        // Values `display_name` and `name` are same for breakout columns so check FRIENDLY_NAME_MAP
-        // before returning either `display_name` or `name`
-        return FRIENDLY_NAME_MAP[column.name.toLowerCase().trim()] || column.display_name || column.name;
-    }
+  if (column.display_name && column.display_name !== column.name) {
+    return column.display_name;
+  } else {
+    // NOTE Atte Keinänen 8/7/17:
+    // Values `display_name` and `name` are same for breakout columns so check FRIENDLY_NAME_MAP
+    // before returning either `display_name` or `name`
+    return (
+      FRIENDLY_NAME_MAP[column.name.toLowerCase().trim()] ||
+      column.display_name ||
+      column.name
+    );
+  }
 }
 
 export function getCardColors(card) {
-    let settings = card.visualization_settings;
-    let chartColor, chartColorList;
-    if (card.display === "bar" && settings.bar) {
-        chartColor = settings.bar.color;
-        chartColorList = settings.bar.colors;
-    } else if (card.display !== "bar" && settings.line) {
-        chartColor = settings.line.lineColor;
-        chartColorList = settings.line.colors;
-    }
-    return _.uniq([chartColor || Object.values(colors.harmony)[0]].concat(chartColorList || Object.values(colors.harmony)));
+  let settings = card.visualization_settings;
+  let chartColor, chartColorList;
+  if (card.display === "bar" && settings.bar) {
+    chartColor = settings.bar.color;
+    chartColorList = settings.bar.colors;
+  } else if (card.display !== "bar" && settings.line) {
+    chartColor = settings.line.lineColor;
+    chartColorList = settings.line.colors;
+  }
+  return _.uniq(
+    [chartColor || Object.values(colors.harmony)[0]].concat(
+      chartColorList || Object.values(colors.harmony),
+    ),
+  );
 }
 
 export function isSameSeries(seriesA, seriesB) {
-    return (seriesA && seriesA.length) === (seriesB && seriesB.length) &&
-        _.zip(seriesA, seriesB).reduce((acc, [a, b]) => {
-            let sameData = a.data === b.data;
-            let sameDisplay = (a.card && a.card.display) === (b.card && b.card.display);
-            let sameVizSettings = (a.card && JSON.stringify(a.card.visualization_settings)) === (b.card && JSON.stringify(b.card.visualization_settings));
-            return acc && (sameData && sameDisplay && sameVizSettings);
-        }, true);
+  return (
+    (seriesA && seriesA.length) === (seriesB && seriesB.length) &&
+    _.zip(seriesA, seriesB).reduce((acc, [a, b]) => {
+      let sameData = a.data === b.data;
+      let sameDisplay =
+        (a.card && a.card.display) === (b.card && b.card.display);
+      let sameVizSettings =
+        (a.card && JSON.stringify(a.card.visualization_settings)) ===
+        (b.card && JSON.stringify(b.card.visualization_settings));
+      return acc && (sameData && sameDisplay && sameVizSettings);
+    }, true)
+  );
 }
 
 export function colorShades(color, count) {
-    return _.range(count).map(i => colorShade(color, 1 - Math.min(0.25, 1 / count) * i))
+  return _.range(count).map(i =>
+    colorShade(color, 1 - Math.min(0.25, 1 / count) * i),
+  );
 }
 
 export function colorShade(hex, shade = 0) {
-    let match = hex.match(/#(?:(..)(..)(..)|(.)(.)(.))/);
-    if (!match) {
-        return hex;
-    }
-    let components = (match[1] != null ? match.slice(1,4) : match.slice(4,7)).map((d) => parseInt(d, 16))
-    let min = Math.min(...components);
-    let max = Math.max(...components);
-    return "#" + components.map(c =>
-        Math.round(min + (max - min) * shade * (c / 255)).toString(16)
-    ).join("");
+  let match = hex.match(/#(?:(..)(..)(..)|(.)(.)(.))/);
+  if (!match) {
+    return hex;
+  }
+  let components = (match[1] != null
+    ? match.slice(1, 4)
+    : match.slice(4, 7)
+  ).map(d => parseInt(d, 16));
+  let min = Math.min(...components);
+  let max = Math.max(...components);
+  return (
+    "#" +
+    components
+      .map(c => Math.round(min + (max - min) * shade * (c / 255)).toString(16))
+      .join("")
+  );
 }
 
 import { isDimension, isMetric } from "metabase/lib/schema_metadata";
@@ -195,109 +228,131 @@ export const DIMENSION_DIMENSION_METRIC = "DIMENSION_DIMENSION_METRIC";
 // const MAX_SERIES = 10;
 
 export const isDimensionMetric = (cols, strict = true) =>
-    (!strict || cols.length === 2) &&
-    isDimension(cols[0]) &&
-    isMetric(cols[1])
+  (!strict || cols.length === 2) && isDimension(cols[0]) && isMetric(cols[1]);
 
 export const isDimensionDimensionMetric = (cols, strict = true) =>
-    (!strict || cols.length === 3) &&
-    isDimension(cols[0]) &&
-    isDimension(cols[1]) &&
-    isMetric(cols[2])
+  (!strict || cols.length === 3) &&
+  isDimension(cols[0]) &&
+  isDimension(cols[1]) &&
+  isMetric(cols[2]);
 
 export const isDimensionMetricMetric = (cols, strict = true) =>
-    cols.length >= 3 &&
-    isDimension(cols[0]) &&
-    cols.slice(1).reduce((acc, col) => acc && isMetric(col), true)
+  cols.length >= 3 &&
+  isDimension(cols[0]) &&
+  cols.slice(1).reduce((acc, col) => acc && isMetric(col), true);
 
 // cache computed cardinalities in a weak map since they are computationally expensive
 const cardinalityCache = new WeakMap();
 
 export function getColumnCardinality(cols, rows, index) {
-    const col = cols[index];
-    if (!cardinalityCache.has(col)) {
-        let dataset = crossfilter(rows);
-        cardinalityCache.set(col, dataset.dimension(d => d[index]).group().size())
-    }
-    return cardinalityCache.get(col);
+  const col = cols[index];
+  if (!cardinalityCache.has(col)) {
+    let dataset = crossfilter(rows);
+    cardinalityCache.set(
+      col,
+      dataset
+        .dimension(d => d[index])
+        .group()
+        .size(),
+    );
+  }
+  return cardinalityCache.get(col);
 }
 
 export function getChartTypeFromData(cols, rows, strict = true) {
-    // this should take precendence for backwards compatibilty
-    if (isDimensionMetricMetric(cols, strict)) {
-        return DIMENSION_METRIC_METRIC;
-    } else if (isDimensionDimensionMetric(cols, strict)) {
-        // if (getColumnCardinality(cols, rows, 0) < MAX_SERIES || getColumnCardinality(cols, rows, 1) < MAX_SERIES) {
-            return DIMENSION_DIMENSION_METRIC;
-        // }
-    } else if (isDimensionMetric(cols, strict)) {
-        return DIMENSION_METRIC;
-    }
-    return null;
+  // this should take precendence for backwards compatibilty
+  if (isDimensionMetricMetric(cols, strict)) {
+    return DIMENSION_METRIC_METRIC;
+  } else if (isDimensionDimensionMetric(cols, strict)) {
+    // if (getColumnCardinality(cols, rows, 0) < MAX_SERIES || getColumnCardinality(cols, rows, 1) < MAX_SERIES) {
+    return DIMENSION_DIMENSION_METRIC;
+    // }
+  } else if (isDimensionMetric(cols, strict)) {
+    return DIMENSION_METRIC;
+  }
+  return null;
 }
 
-export function enableVisualizationEasterEgg(code, OriginalVisualization, EasterEggVisualization) {
-    if (!code) {
-        code = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
-    } else if (typeof code === "string") {
-        code = code.split("").map(c => c.charCodeAt(0));
-    }
-    wrapMethod(OriginalVisualization.prototype, "componentWillMount", function easterEgg() {
-        let keypresses = [];
-        let enabled = false;
-        let render_original = this.render;
-        let render_egg = function() {
-            return <EasterEggVisualization {...this.props} />;
-        };
-        this._keyListener = (e) => {
-            keypresses = keypresses.concat(e.keyCode).slice(-code.length);
-            if (code.reduce((ok, value, index) => ok && value === keypresses[index], true)) {
-                enabled = !enabled;
-                this.render = enabled ? render_egg : render_original;
-                this.forceUpdate();
-            }
+export function enableVisualizationEasterEgg(
+  code,
+  OriginalVisualization,
+  EasterEggVisualization,
+) {
+  if (!code) {
+    code = [38, 38, 40, 40, 37, 39, 37, 39, 66, 65];
+  } else if (typeof code === "string") {
+    code = code.split("").map(c => c.charCodeAt(0));
+  }
+  wrapMethod(
+    OriginalVisualization.prototype,
+    "componentWillMount",
+    function easterEgg() {
+      let keypresses = [];
+      let enabled = false;
+      let render_original = this.render;
+      let render_egg = function() {
+        return <EasterEggVisualization {...this.props} />;
+      };
+      this._keyListener = e => {
+        keypresses = keypresses.concat(e.keyCode).slice(-code.length);
+        if (
+          code.reduce(
+            (ok, value, index) => ok && value === keypresses[index],
+            true,
+          )
+        ) {
+          enabled = !enabled;
+          this.render = enabled ? render_egg : render_original;
+          this.forceUpdate();
         }
-        window.addEventListener("keyup", this._keyListener, false);
-    });
-    wrapMethod(OriginalVisualization.prototype, "componentWillUnmount", function cleanupEasterEgg() {
-        window.removeEventListener("keyup", this._keyListener, false);
-    });
+      };
+      window.addEventListener("keyup", this._keyListener, false);
+    },
+  );
+  wrapMethod(
+    OriginalVisualization.prototype,
+    "componentWillUnmount",
+    function cleanupEasterEgg() {
+      window.removeEventListener("keyup", this._keyListener, false);
+    },
+  );
 }
 
 function wrapMethod(object, name, method) {
-    let method_original = object[name];
-    object[name] = function() {
-        method.apply(this, arguments);
-        if (typeof method_original === "function") {
-            return method_original.apply(this, arguments);
-        }
+  let method_original = object[name];
+  object[name] = function() {
+    method.apply(this, arguments);
+    if (typeof method_original === "function") {
+      return method_original.apply(this, arguments);
     }
+  };
 }
 // TODO Atte Keinänen 5/30/17 Extract to metabase-lib card/question logic
 export const cardHasBecomeDirty = (nextCard, previousCard) =>
-    !_.isEqual(previousCard.dataset_query, nextCard.dataset_query) || previousCard.display !== nextCard.display;
+  !_.isEqual(previousCard.dataset_query, nextCard.dataset_query) ||
+  previousCard.display !== nextCard.display;
 
 export function getCardAfterVisualizationClick(nextCard, previousCard) {
-    if (cardHasBecomeDirty(nextCard, previousCard)) {
-        const isMultiseriesQuestion = !nextCard.id;
-        const alreadyHadLineage = !!previousCard.original_card_id;
-
-        return {
-            ...nextCard,
-            // Original card id is needed for showing the "started from" lineage in dirty cards.
-            original_card_id: alreadyHadLineage
-                // Just recycle the original card id of previous card if there was one
-                ? previousCard.original_card_id
-                // A multi-aggregation or multi-breakout series legend / drill-through action
-                // should always use the id of underlying/previous card
-                : (isMultiseriesQuestion ? previousCard.id : nextCard.id)
-        }
-    } else {
-        // Even though the card is currently clean, we might still apply dashboard parameters to it,
-        // so add the original_card_id to ensure a correct behavior in that context
-        return {
-            ...nextCard,
-            original_card_id: nextCard.id
-        };
-    }
+  if (cardHasBecomeDirty(nextCard, previousCard)) {
+    const isMultiseriesQuestion = !nextCard.id;
+    const alreadyHadLineage = !!previousCard.original_card_id;
+
+    return {
+      ...nextCard,
+      // Original card id is needed for showing the "started from" lineage in dirty cards.
+      original_card_id: alreadyHadLineage
+        ? // Just recycle the original card id of previous card if there was one
+          previousCard.original_card_id
+        : // A multi-aggregation or multi-breakout series legend / drill-through action
+          // should always use the id of underlying/previous card
+          isMultiseriesQuestion ? previousCard.id : nextCard.id,
+    };
+  } else {
+    // Even though the card is currently clean, we might still apply dashboard parameters to it,
+    // so add the original_card_id to ensure a correct behavior in that context
+    return {
+      ...nextCard,
+      original_card_id: nextCard.id,
+    };
+  }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
index 495810557dcd94724c52e26edb8411e2aebb19fc..9c4f76413d6537bc445dade4ab8e455c90a36e3b 100644
--- a/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/AreaChart.jsx
@@ -1,34 +1,34 @@
 /* @flow */
 
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { areaRenderer } from "../lib/LineAreaBarRenderer";
 
 import {
-    GRAPH_DATA_SETTINGS,
-    LINE_SETTINGS,
-    STACKABLE_SETTINGS,
-    GRAPH_GOAL_SETTINGS,
-    LINE_SETTINGS_2,
-    GRAPH_COLORS_SETTINGS,
-    GRAPH_AXIS_SETTINGS
+  GRAPH_DATA_SETTINGS,
+  LINE_SETTINGS,
+  STACKABLE_SETTINGS,
+  GRAPH_GOAL_SETTINGS,
+  LINE_SETTINGS_2,
+  GRAPH_COLORS_SETTINGS,
+  GRAPH_AXIS_SETTINGS,
 } from "../lib/settings/graph";
 
 export default class AreaChart extends LineAreaBarChart {
-    static uiName = t`Area`;
-    static identifier = "area";
-    static iconName = "area";
-    static noun = t`area chart`;
+  static uiName = t`Area`;
+  static identifier = "area";
+  static iconName = "area";
+  static noun = t`area chart`;
 
-    static settings = {
-        ...GRAPH_DATA_SETTINGS,
-        ...LINE_SETTINGS,
-        ...STACKABLE_SETTINGS,
-        ...GRAPH_GOAL_SETTINGS,
-        ...LINE_SETTINGS_2,
-        ...GRAPH_COLORS_SETTINGS,
-        ...GRAPH_AXIS_SETTINGS
-    };
+  static settings = {
+    ...GRAPH_DATA_SETTINGS,
+    ...LINE_SETTINGS,
+    ...STACKABLE_SETTINGS,
+    ...GRAPH_GOAL_SETTINGS,
+    ...LINE_SETTINGS_2,
+    ...GRAPH_COLORS_SETTINGS,
+    ...GRAPH_AXIS_SETTINGS,
+  };
 
-    static renderer = areaRenderer;
+  static renderer = areaRenderer;
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
index 084143bd3413e4cfaa0bc9beb2de8e97ea514e28..349f082b6531cd833abe657de8097a78a45d0f38 100644
--- a/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/BarChart.jsx
@@ -1,30 +1,30 @@
 /* @flow */
 
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { barRenderer } from "../lib/LineAreaBarRenderer";
 
 import {
-    GRAPH_DATA_SETTINGS,
-    STACKABLE_SETTINGS,
-    GRAPH_GOAL_SETTINGS,
-    GRAPH_COLORS_SETTINGS,
-    GRAPH_AXIS_SETTINGS
+  GRAPH_DATA_SETTINGS,
+  STACKABLE_SETTINGS,
+  GRAPH_GOAL_SETTINGS,
+  GRAPH_COLORS_SETTINGS,
+  GRAPH_AXIS_SETTINGS,
 } from "../lib/settings/graph";
 
 export default class BarChart extends LineAreaBarChart {
-    static uiName = t`Bar`;
-    static identifier = "bar";
-    static iconName = "bar";
-    static noun = t`bar chart`;
+  static uiName = t`Bar`;
+  static identifier = "bar";
+  static iconName = "bar";
+  static noun = t`bar chart`;
 
-    static settings = {
-        ...GRAPH_DATA_SETTINGS,
-        ...STACKABLE_SETTINGS,
-        ...GRAPH_GOAL_SETTINGS,
-        ...GRAPH_COLORS_SETTINGS,
-        ...GRAPH_AXIS_SETTINGS
-    };
+  static settings = {
+    ...GRAPH_DATA_SETTINGS,
+    ...STACKABLE_SETTINGS,
+    ...GRAPH_GOAL_SETTINGS,
+    ...GRAPH_COLORS_SETTINGS,
+    ...GRAPH_AXIS_SETTINGS,
+  };
 
-    static renderer = barRenderer;
+  static renderer = barRenderer;
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
index 8c77fa47e8b15640b801b9d13450ccaf3e017ad6..bfa0090c6b9cce075402d13e77dd1ee48373f397 100644
--- a/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Funnel.jsx
@@ -1,12 +1,19 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
-import { MinRowsError, ChartSettingsError } from "metabase/visualizations/lib/errors";
+import { t } from "c-3po";
+import {
+  MinRowsError,
+  ChartSettingsError,
+} from "metabase/visualizations/lib/errors";
 
 import { formatValue } from "metabase/lib/formatting";
 
-import { getSettings, metricSetting, dimensionSetting } from "metabase/visualizations/lib/settings";
+import {
+  getSettings,
+  metricSetting,
+  dimensionSetting,
+} from "metabase/visualizations/lib/settings";
 
 import FunnelNormal from "../components/FunnelNormal";
 import FunnelBar from "../components/FunnelBar";
@@ -19,122 +26,145 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization";
 import { TitleLegendHeader } from "metabase/visualizations/components/TitleLegendHeader";
 
 export default class Funnel extends Component {
-    props: VisualizationProps;
+  props: VisualizationProps;
 
-    static uiName = t`Funnel`;
-    static identifier = "funnel";
-    static iconName = "funnel";
+  static uiName = t`Funnel`;
+  static identifier = "funnel";
+  static iconName = "funnel";
 
-    static noHeader = true;
+  static noHeader = true;
 
-    static minSize = {
-        width: 5,
-        height: 4
-    };
+  static minSize = {
+    width: 5,
+    height: 4,
+  };
 
-    static isSensible(cols, rows) {
-        return cols.length === 2;
-    }
+  static isSensible(cols, rows) {
+    return cols.length === 2;
+  }
 
-    static checkRenderable(series, settings) {
-        const [{ data: { rows} }] = series;
-        if (series.length > 1) {
-            return;
-        }
-
-        if (rows.length < 1) {
-            throw new MinRowsError(1, rows.length);
-        }
-        if (!settings["funnel.dimension"] || !settings["funnel.metric"]) {
-            throw new ChartSettingsError(t`Which fields do you want to use?`, t`Data`, t`Choose fields`);
-        }
+  static checkRenderable(series, settings) {
+    const [{ data: { rows } }] = series;
+    if (series.length > 1) {
+      return;
     }
 
-    static settings = {
-        "funnel.dimension": {
-            section: "Data",
-            title: t`Step`,
-            ...dimensionSetting("funnel.dimension"),
-            dashboard: false,
-            useRawSeries: true,
-        },
-        "funnel.metric": {
-            section: "Data",
-            title: t`Measure`,
-            ...metricSetting("funnel.metric"),
-            dashboard: false,
-            useRawSeries: true,
+    if (rows.length < 1) {
+      throw new MinRowsError(1, rows.length);
+    }
+    if (!settings["funnel.dimension"] || !settings["funnel.metric"]) {
+      throw new ChartSettingsError(
+        t`Which fields do you want to use?`,
+        t`Data`,
+        t`Choose fields`,
+      );
+    }
+  }
+
+  static settings = {
+    "funnel.dimension": {
+      section: "Data",
+      title: t`Step`,
+      ...dimensionSetting("funnel.dimension"),
+      dashboard: false,
+      useRawSeries: true,
+    },
+    "funnel.metric": {
+      section: "Data",
+      title: t`Measure`,
+      ...metricSetting("funnel.metric"),
+      dashboard: false,
+      useRawSeries: true,
+    },
+    "funnel.type": {
+      title: t`Funnel type`,
+      section: "Display",
+      widget: "select",
+      props: {
+        options: [
+          { name: t`Funnel`, value: "funnel" },
+          { name: t`Bar chart`, value: "bar" },
+        ],
+      },
+      // legacy "bar" funnel was only previously available via multiseries
+      getDefault: series => (series.length > 1 ? "bar" : "funnel"),
+      useRawSeries: true,
+    },
+  };
+
+  static transformSeries(series) {
+    let [{ card, data: { rows, cols } }] = series;
+
+    const settings = getSettings(series);
+
+    const dimensionIndex = _.findIndex(
+      cols,
+      col => col.name === settings["funnel.dimension"],
+    );
+    const metricIndex = _.findIndex(
+      cols,
+      col => col.name === settings["funnel.metric"],
+    );
+
+    if (
+      !card._transformed &&
+      series.length === 1 &&
+      rows.length > 1 &&
+      dimensionIndex >= 0 &&
+      metricIndex >= 0
+    ) {
+      return rows.map(row => ({
+        card: {
+          ...card,
+          name: formatValue(row[dimensionIndex], {
+            column: cols[dimensionIndex],
+          }),
+          _transformed: true,
         },
-        "funnel.type": {
-            title: t`Funnel type`,
-            section: "Display",
-            widget: "select",
-            props: {
-                options: [{ name: t`Funnel`, value: "funnel"}, { name: t`Bar chart`, value: "bar"}]
-            },
-            // legacy "bar" funnel was only previously available via multiseries
-            getDefault: (series) => series.length > 1 ? "bar" : "funnel",
-            useRawSeries: true
+        data: {
+          rows: [[row[dimensionIndex], row[metricIndex]]],
+          cols: [cols[dimensionIndex], cols[metricIndex]],
         },
+      }));
+    } else {
+      return series;
     }
-
-    static transformSeries(series) {
-        let [{ card, data: { rows, cols }}] = series;
-
-        const settings = getSettings(series);
-
-        const dimensionIndex = _.findIndex(cols, (col) => col.name === settings["funnel.dimension"]);
-        const metricIndex = _.findIndex(cols, (col) => col.name === settings["funnel.metric"]);
-
-        if (!card._transformed &&
-            series.length === 1 && rows.length > 1 &&
-            dimensionIndex >= 0 && metricIndex >= 0
-        ) {
-            return rows.map(row => ({
-                card: {
-                    ...card,
-                    name: formatValue(row[dimensionIndex], { column: cols[dimensionIndex] }),
-                    _transformed: true
-                },
-                data: {
-                    rows: [[row[dimensionIndex], row[metricIndex]]],
-                    cols: [cols[dimensionIndex], cols[metricIndex]]
-                }
-            }));
-        } else {
-            return series;
-        }
-    }
-
-    render() {
-        const { settings } = this.props;
-
-        const hasTitle = settings["card.title"];
-
-        if (settings["funnel.type"] === "bar") {
-            return <FunnelBar {...this.props} />
-        } else {
-            const { actionButtons, className, onChangeCardAndRun, series } = this.props;
-            return (
-                <div className={cx(className, "flex flex-column p1")}>
-                    { hasTitle &&
-                        <TitleLegendHeader
-                            series={series}
-                            settings={settings}
-                            onChangeCardAndRun={onChangeCardAndRun}
-                            actionButtons={actionButtons}
-                        />
-                    }
-                    <LegendHeader
-                        className="flex-no-shrink"
-                        // $FlowFixMe
-                        series={series._raw || series}
-                        actionButtons={!hasTitle && actionButtons}
-                        onChangeCardAndRun={onChangeCardAndRun}
-                    />
-                    <FunnelNormal {...this.props} className="flex-full" />
-                </div>
-            )
-        }
+  }
+
+  render() {
+    const { settings } = this.props;
+
+    const hasTitle = settings["card.title"];
+
+    if (settings["funnel.type"] === "bar") {
+      return <FunnelBar {...this.props} />;
+    } else {
+      const {
+        actionButtons,
+        className,
+        onChangeCardAndRun,
+        series,
+      } = this.props;
+      return (
+        <div className={cx(className, "flex flex-column p1")}>
+          {hasTitle && (
+            <TitleLegendHeader
+              series={series}
+              settings={settings}
+              onChangeCardAndRun={onChangeCardAndRun}
+              actionButtons={actionButtons}
+            />
+          )}
+          <LegendHeader
+            className="flex-no-shrink"
+            // $FlowFixMe
+            series={series._raw || series}
+            actionButtons={!hasTitle && actionButtons}
+            onChangeCardAndRun={onChangeCardAndRun}
+          />
+          <FunnelNormal {...this.props} className="flex-full" />
+        </div>
+      );
     }
+  }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
index 64a81a533a6a2e826b3c466fb359921bd338b2e7..187e1584582da4bc2c4648065c3f250d4d852701 100644
--- a/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/LineChart.jsx
@@ -1,32 +1,32 @@
 /* @flow */
 
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { lineRenderer } from "../lib/LineAreaBarRenderer";
 
 import {
-    GRAPH_DATA_SETTINGS,
-    LINE_SETTINGS,
-    GRAPH_GOAL_SETTINGS,
-    LINE_SETTINGS_2,
-    GRAPH_COLORS_SETTINGS,
-    GRAPH_AXIS_SETTINGS
+  GRAPH_DATA_SETTINGS,
+  LINE_SETTINGS,
+  GRAPH_GOAL_SETTINGS,
+  LINE_SETTINGS_2,
+  GRAPH_COLORS_SETTINGS,
+  GRAPH_AXIS_SETTINGS,
 } from "../lib/settings/graph";
 
 export default class LineChart extends LineAreaBarChart {
-    static uiName = t`Line`;
-    static identifier = "line";
-    static iconName = "line";
-    static noun = t`line chart`;
+  static uiName = t`Line`;
+  static identifier = "line";
+  static iconName = "line";
+  static noun = t`line chart`;
 
-    static settings = {
-        ...GRAPH_DATA_SETTINGS,
-        ...LINE_SETTINGS,
-        ...GRAPH_GOAL_SETTINGS,
-        ...LINE_SETTINGS_2,
-        ...GRAPH_COLORS_SETTINGS,
-        ...GRAPH_AXIS_SETTINGS
-    };
+  static settings = {
+    ...GRAPH_DATA_SETTINGS,
+    ...LINE_SETTINGS,
+    ...GRAPH_GOAL_SETTINGS,
+    ...LINE_SETTINGS_2,
+    ...GRAPH_COLORS_SETTINGS,
+    ...GRAPH_AXIS_SETTINGS,
+  };
 
-    static renderer = lineRenderer;
+  static renderer = lineRenderer;
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/Map.jsx b/frontend/src/metabase/visualizations/visualizations/Map.jsx
index 0eccd185a46a43e982bf0e685557ede767acaa44..3b0099cab25a4b1c7ce941216513a7f92668bf8c 100644
--- a/frontend/src/metabase/visualizations/visualizations/Map.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Map.jsx
@@ -1,13 +1,24 @@
 /* @flow */
 
 import React, { Component } from "react";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ChoroplethMap from "../components/ChoroplethMap.jsx";
 import PinMap from "../components/PinMap.jsx";
 
 import { ChartSettingsError } from "metabase/visualizations/lib/errors";
-import { isNumeric, isLatitude, isLongitude, hasLatitudeAndLongitudeColumns, isState, isCountry } from "metabase/lib/schema_metadata";
-import { metricSetting, dimensionSetting, fieldSetting } from "metabase/visualizations/lib/settings";
+import {
+  isNumeric,
+  isLatitude,
+  isLongitude,
+  hasLatitudeAndLongitudeColumns,
+  isState,
+  isCountry,
+} from "metabase/lib/schema_metadata";
+import {
+  metricSetting,
+  dimensionSetting,
+  fieldSetting,
+} from "metabase/visualizations/lib/settings";
 import MetabaseSettings from "metabase/lib/settings";
 
 import { isSameSeries } from "metabase/visualizations/lib/utils";
@@ -19,191 +30,211 @@ import _ from "underscore";
 const PIN_MAP_TYPES = new Set(["pin"]);
 
 export default class Map extends Component {
-    static uiName = t`Map`;
-    static identifier = "map";
-    static iconName = "pinmap";
+  static uiName = t`Map`;
+  static identifier = "map";
+  static iconName = "pinmap";
 
-    static aliases = ["state", "country", "pin_map"];
+  static aliases = ["state", "country", "pin_map"];
 
-    static minSize = { width: 4, height: 4 };
+  static minSize = { width: 4, height: 4 };
 
-    static isSensible(cols, rows) {
-        return true;
-    }
-
-    static settings = {
-        "map.type": {
-            title: t`Map type`,
-            widget: "select",
-            props: {
-                options: [
-                    { name: t`Region map`, value: "region" },
-                    { name: t`Pin map`, value: "pin" }
-                    // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
-                    // { name: "Heat map", value: "heat" },
-                    // { name: "Grid map", value: "grid" }
-                ]
-            },
-            getDefault: ([{ card, data: { cols } }], settings) => {
-                switch (card.display) {
-                    case "state":
-                    case "country":
-                        return "region";
-                    case "pin_map":
-                        return "pin";
-                    default:
-                        // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
-                        if (hasLatitudeAndLongitudeColumns(cols)) {
-                        //     const latitudeColumn = _.findWhere(cols, { name: settings["map.latitude_column"] });
-                        //     const longitudeColumn = _.findWhere(cols, { name: settings["map.longitude_column"] });
-                        //     if (latitudeColumn && longitudeColumn && latitudeColumn.binning_info && longitudeColumn.binning_info) {
-                        //         // lat/lon columns are binned, use grid by default
-                        //         return "grid";
-                        //     } else if (settings["map.metric_column"]) {
-                        //         //
-                        //         return "heat";
-                        //     } else {
-                            return "pin";
-                        //     }
-                        } else {
-                            return "region";
-                        }
-                }
-            },
-            readDependencies: ["map.latitude_column", "map.longitude_column", "map.metric_column"]
-        },
-        "map.pin_type": {
-            title: t`Pin type`,
-            // Don't expose this in the UI for now
-            // widget: "select",
-            props: {
-                options: [
-                    { name: t`Tiles`, value: "tiles" },
-                    { name: t`Markers`, value: "markers" },
-                    // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
-                    // { name: "Heat", value: "heat" },
-                    // { name: "Grid", value: "grid" }
-                ]
-            },
-            getDefault: (series, vizSettings) =>
-                vizSettings["map.type"] === "heat" ?
-                    "heat"
-                : vizSettings["map.type"] === "grid" ?
-                    "grid"
-                : series[0].data.rows.length >= 1000 ?
-                    "tiles"
-                :
-                    "markers",
-            getHidden: (series, vizSettings) => !PIN_MAP_TYPES.has(vizSettings["map.type"])
-        },
-        "map.latitude_column": {
-            title: t`Latitude field`,
-            ...fieldSetting("map.latitude_column", isNumeric,
-                ([{ data: { cols }}]) => (_.find(cols, isLatitude) || {}).name),
-            getHidden: (series, vizSettings) => !PIN_MAP_TYPES.has(vizSettings["map.type"])
-        },
-        "map.longitude_column": {
-            title: t`Longitude field`,
-            ...fieldSetting("map.longitude_column", isNumeric,
-                ([{ data: { cols }}]) => (_.find(cols, isLongitude) || {}).name),
-            getHidden: (series, vizSettings) => !PIN_MAP_TYPES.has(vizSettings["map.type"])
-        },
-        "map.metric_column": {
-            title: t`Metric field`,
-            ...metricSetting("map.metric_column"),
-            getHidden: (series, vizSettings) =>
-                !PIN_MAP_TYPES.has(vizSettings["map.type"]) || (
-                    (vizSettings["map.pin_type"] !== "heat" && vizSettings["map.pin_type"] !== "grid")
-                ),
-        },
-        "map.region": {
-            title: t`Region map`,
-            widget: "select",
-            getDefault: ([{ card, data: { cols }}]) => {
-                if (card.display === "state" || _.any(cols, isState)) {
-                    return "us_states";
-                } else if (card.display === "country" || _.any(cols, isCountry)) {
-                    return "world_countries";
-                }
-                return null;
-            },
-            getProps: () => ({
-                // $FlowFixMe:
-                options: Object.entries(MetabaseSettings.get("custom_geojson", {})).map(([key, value]) => ({ name: value.name, value: key }))
-            }),
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region"
-        },
-        "map.metric": {
-            title: t`Metric field`,
-            ...metricSetting("map.metric"),
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region"
-        },
-        "map.dimension": {
-            title: t`Region field`,
-            widget: "select",
-            ...dimensionSetting("map.dimension"),
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region"
-        },
-        "map.zoom": {
-        },
-        "map.center_latitude": {
-        },
-        "map.center_longitude": {
-        },
-        "map.heat.radius": {
-            title: t`Radius`,
-            widget: "number",
-            default: 30,
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
-        },
-        "map.heat.blur": {
-            title: t`Blur`,
-            widget: "number",
-            default: 60,
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
-        },
-        "map.heat.min-opacity": {
-            title: t`Min Opacity`,
-            widget: "number",
-            default: 0,
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
-        },
-        "map.heat.max-zoom": {
-            title: t`Max Zoom`,
-            widget: "number",
-            default: 1,
-            getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat"
-        },
-    }
+  static isSensible(cols, rows) {
+    return true;
+  }
 
-    static checkRenderable([{ data: { cols, rows} }], settings) {
-        if (PIN_MAP_TYPES.has(settings["map.type"])) {
-            if (!settings["map.longitude_column"] || !settings["map.latitude_column"]) {
-                throw new ChartSettingsError(t`Please select longitude and latitude columns in the chart settings.`, "Data");
-            }
-        } else if (settings["map.type"] === "region"){
-            if (!settings["map.region"]) {
-                throw new ChartSettingsError(t`Please select a region map.`, "Data");
-            }
-            if (!settings["map.dimension"] || !settings["map.metric"]) {
-                throw new ChartSettingsError(t`Please select region and metric columns in the chart settings.`, "Data");
+  static settings = {
+    "map.type": {
+      title: t`Map type`,
+      widget: "select",
+      props: {
+        options: [
+          { name: t`Region map`, value: "region" },
+          { name: t`Pin map`, value: "pin" },
+          // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
+          // { name: "Heat map", value: "heat" },
+          // { name: "Grid map", value: "grid" }
+        ],
+      },
+      getDefault: ([{ card, data: { cols } }], settings) => {
+        switch (card.display) {
+          case "state":
+          case "country":
+            return "region";
+          case "pin_map":
+            return "pin";
+          default:
+            // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
+            if (hasLatitudeAndLongitudeColumns(cols)) {
+              //     const latitudeColumn = _.findWhere(cols, { name: settings["map.latitude_column"] });
+              //     const longitudeColumn = _.findWhere(cols, { name: settings["map.longitude_column"] });
+              //     if (latitudeColumn && longitudeColumn && latitudeColumn.binning_info && longitudeColumn.binning_info) {
+              //         // lat/lon columns are binned, use grid by default
+              //         return "grid";
+              //     } else if (settings["map.metric_column"]) {
+              //         //
+              //         return "heat";
+              //     } else {
+              return "pin";
+              //     }
+            } else {
+              return "region";
             }
         }
-    }
+      },
+      readDependencies: [
+        "map.latitude_column",
+        "map.longitude_column",
+        "map.metric_column",
+      ],
+    },
+    "map.pin_type": {
+      title: t`Pin type`,
+      // Don't expose this in the UI for now
+      // widget: "select",
+      props: {
+        options: [
+          { name: t`Tiles`, value: "tiles" },
+          { name: t`Markers`, value: "markers" },
+          // NOTE Atte Keinänen 8/2/17: Heat/grid maps disabled in the first merged version of binning
+          // { name: "Heat", value: "heat" },
+          // { name: "Grid", value: "grid" }
+        ],
+      },
+      getDefault: (series, vizSettings) =>
+        vizSettings["map.type"] === "heat"
+          ? "heat"
+          : vizSettings["map.type"] === "grid"
+            ? "grid"
+            : series[0].data.rows.length >= 1000 ? "tiles" : "markers",
+      getHidden: (series, vizSettings) =>
+        !PIN_MAP_TYPES.has(vizSettings["map.type"]),
+    },
+    "map.latitude_column": {
+      title: t`Latitude field`,
+      ...fieldSetting(
+        "map.latitude_column",
+        isNumeric,
+        ([{ data: { cols } }]) => (_.find(cols, isLatitude) || {}).name,
+      ),
+      getHidden: (series, vizSettings) =>
+        !PIN_MAP_TYPES.has(vizSettings["map.type"]),
+    },
+    "map.longitude_column": {
+      title: t`Longitude field`,
+      ...fieldSetting(
+        "map.longitude_column",
+        isNumeric,
+        ([{ data: { cols } }]) => (_.find(cols, isLongitude) || {}).name,
+      ),
+      getHidden: (series, vizSettings) =>
+        !PIN_MAP_TYPES.has(vizSettings["map.type"]),
+    },
+    "map.metric_column": {
+      title: t`Metric field`,
+      ...metricSetting("map.metric_column"),
+      getHidden: (series, vizSettings) =>
+        !PIN_MAP_TYPES.has(vizSettings["map.type"]) ||
+        (vizSettings["map.pin_type"] !== "heat" &&
+          vizSettings["map.pin_type"] !== "grid"),
+    },
+    "map.region": {
+      title: t`Region map`,
+      widget: "select",
+      getDefault: ([{ card, data: { cols } }]) => {
+        if (card.display === "state" || _.any(cols, isState)) {
+          return "us_states";
+        } else if (card.display === "country" || _.any(cols, isCountry)) {
+          return "world_countries";
+        }
+        return null;
+      },
+      getProps: () => ({
+        options: Object.entries(MetabaseSettings.get("custom_geojson", {})).map(
+          // $FlowFixMe:
+          ([key, value]) => ({ name: value.name, value: key }),
+        ),
+      }),
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region",
+    },
+    "map.metric": {
+      title: t`Metric field`,
+      ...metricSetting("map.metric"),
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region",
+    },
+    "map.dimension": {
+      title: t`Region field`,
+      widget: "select",
+      ...dimensionSetting("map.dimension"),
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "region",
+    },
+    "map.zoom": {},
+    "map.center_latitude": {},
+    "map.center_longitude": {},
+    "map.heat.radius": {
+      title: t`Radius`,
+      widget: "number",
+      default: 30,
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat",
+    },
+    "map.heat.blur": {
+      title: t`Blur`,
+      widget: "number",
+      default: 60,
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat",
+    },
+    "map.heat.min-opacity": {
+      title: t`Min Opacity`,
+      widget: "number",
+      default: 0,
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat",
+    },
+    "map.heat.max-zoom": {
+      title: t`Max Zoom`,
+      widget: "number",
+      default: 1,
+      getHidden: (series, vizSettings) => vizSettings["map.type"] !== "heat",
+    },
+  };
 
-    shouldComponentUpdate(nextProps: any, nextState: any) {
-        let sameSize = (this.props.width === nextProps.width && this.props.height === nextProps.height);
-        let sameSeries = isSameSeries(this.props.series, nextProps.series);
-        return !(sameSize && sameSeries);
+  static checkRenderable([{ data: { cols, rows } }], settings) {
+    if (PIN_MAP_TYPES.has(settings["map.type"])) {
+      if (
+        !settings["map.longitude_column"] ||
+        !settings["map.latitude_column"]
+      ) {
+        throw new ChartSettingsError(
+          t`Please select longitude and latitude columns in the chart settings.`,
+          "Data",
+        );
+      }
+    } else if (settings["map.type"] === "region") {
+      if (!settings["map.region"]) {
+        throw new ChartSettingsError(t`Please select a region map.`, "Data");
+      }
+      if (!settings["map.dimension"] || !settings["map.metric"]) {
+        throw new ChartSettingsError(
+          t`Please select region and metric columns in the chart settings.`,
+          "Data",
+        );
+      }
     }
+  }
 
-    render() {
-        const { settings } = this.props;
-        const type = settings["map.type"];
-        if (PIN_MAP_TYPES.has(type)) {
-            return <PinMap {...this.props} />
-        } else if (type === "region") {
-            return <ChoroplethMap {...this.props} />
-        }
+  shouldComponentUpdate(nextProps: any, nextState: any) {
+    let sameSize =
+      this.props.width === nextProps.width &&
+      this.props.height === nextProps.height;
+    let sameSeries = isSameSeries(this.props.series, nextProps.series);
+    return !(sameSize && sameSeries);
+  }
+
+  render() {
+    const { settings } = this.props;
+    const type = settings["map.type"];
+    if (PIN_MAP_TYPES.has(type)) {
+      return <PinMap {...this.props} />;
+    } else if (type === "region") {
+      return <ChoroplethMap {...this.props} />;
     }
+  }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
index 8688e235772f01345c362eef1dc40c16c9536915..4abaeff15294bcca60321eb2e86461ccb15066a1 100644
--- a/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/ObjectDetail.jsx
@@ -1,21 +1,28 @@
 /* @flow weak */
 
 import React, { Component } from "react";
-import { connect } from 'react-redux';
-import { t, jt } from 'c-3po';
-import DirectionalButton from 'metabase/components/DirectionalButton';
-import ExpandableString from 'metabase/query_builder/components/ExpandableString.jsx';
-import Icon from 'metabase/components/Icon.jsx';
-import IconBorder from 'metabase/components/IconBorder.jsx';
-import LoadingSpinner from 'metabase/components/LoadingSpinner.jsx';
-
-import { isID, isPK, foreignKeyCountsByOriginTable } from 'metabase/lib/schema_metadata';
+import { connect } from "react-redux";
+import { t, jt } from "c-3po";
+import DirectionalButton from "metabase/components/DirectionalButton";
+import ExpandableString from "metabase/query_builder/components/ExpandableString.jsx";
+import Icon from "metabase/components/Icon.jsx";
+import IconBorder from "metabase/components/IconBorder.jsx";
+import LoadingSpinner from "metabase/components/LoadingSpinner.jsx";
+
+import {
+  isID,
+  isPK,
+  foreignKeyCountsByOriginTable,
+} from "metabase/lib/schema_metadata";
 import { TYPE, isa } from "metabase/lib/types";
-import { singularize, inflect } from 'inflection';
+import { singularize, inflect } from "inflection";
 import { formatValue, formatColumn } from "metabase/lib/formatting";
 import { isQueryable } from "metabase/lib/table";
 
-import { viewPreviousObjectDetail, viewNextObjectDetail } from 'metabase/query_builder/actions'
+import {
+  viewPreviousObjectDetail,
+  viewNextObjectDetail,
+} from "metabase/query_builder/actions";
 
 import cx from "classnames";
 import _ from "underscore";
@@ -23,263 +30,295 @@ import _ from "underscore";
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 type Props = VisualizationProps & {
-    viewNextObjectDetail: () => void,
-    viewPreviousObjectDetail: () => void
-}
+  viewNextObjectDetail: () => void,
+  viewPreviousObjectDetail: () => void,
+};
 
-const mapStateToProps = () => ({})
+const mapStateToProps = () => ({});
 
 const mapDispatchToProps = {
-    viewPreviousObjectDetail,
-    viewNextObjectDetail
-}
+  viewPreviousObjectDetail,
+  viewNextObjectDetail,
+};
 
 export class ObjectDetail extends Component {
-    props: Props
+  props: Props;
 
-    static uiName = t`Object Detail`;
-    static identifier = "object";
-    static iconName = "document";
-    static noun = t`object`;
+  static uiName = t`Object Detail`;
+  static identifier = "object";
+  static iconName = "document";
+  static noun = t`object`;
 
-    static hidden = true;
+  static hidden = true;
 
-    componentDidMount() {
-        // load up FK references
-        this.props.loadObjectDetailFKReferences();
-        window.addEventListener('keydown', this.onKeyDown, true)
-    }
+  componentDidMount() {
+    // load up FK references
+    this.props.loadObjectDetailFKReferences();
+    window.addEventListener("keydown", this.onKeyDown, true);
+  }
 
-    componentWillUnmount() {
-        window.removeEventListener('keydown', this.onKeyDown, true)
-    }
+  componentWillUnmount() {
+    window.removeEventListener("keydown", this.onKeyDown, true);
+  }
 
-    componentWillReceiveProps(nextProps) {
-        // if the card has changed then reload fk references
-        if (this.props.data != nextProps.data) {
-            this.props.loadObjectDetailFKReferences();
-        }
+  componentWillReceiveProps(nextProps) {
+    // if the card has changed then reload fk references
+    if (this.props.data != nextProps.data) {
+      this.props.loadObjectDetailFKReferences();
     }
-
-    getIdValue() {
-        if (!this.props.data) return null;
-
-        const { data: { cols, rows }} = this.props;
-        const columnIndex = _.findIndex(cols, col => isPK(col));
-        return rows[0][columnIndex];
+  }
+
+  getIdValue() {
+    if (!this.props.data) return null;
+
+    const { data: { cols, rows } } = this.props;
+    const columnIndex = _.findIndex(cols, col => isPK(col));
+    return rows[0][columnIndex];
+  }
+
+  foreignKeyClicked = fk => {
+    this.props.followForeignKey(fk);
+  };
+
+  cellRenderer(column, value, isColumn) {
+    const { onVisualizationClick, visualizationIsClickable } = this.props;
+
+    let cellValue;
+    let clicked;
+    let isLink;
+
+    if (isColumn) {
+      cellValue = column !== null ? formatColumn(column) : null;
+      clicked = {
+        column,
+      };
+      isLink = false;
+    } else {
+      if (value === null || value === undefined || value === "") {
+        cellValue = <span className="text-grey-2">{t`Empty`}</span>;
+      } else if (isa(column.special_type, TYPE.SerializedJSON)) {
+        let formattedJson = JSON.stringify(JSON.parse(value), null, 2);
+        cellValue = <pre className="ObjectJSON">{formattedJson}</pre>;
+      } else if (typeof value === "object") {
+        let formattedJson = JSON.stringify(value, null, 2);
+        cellValue = <pre className="ObjectJSON">{formattedJson}</pre>;
+      } else {
+        cellValue = formatValue(value, { column: column, jsx: true });
+        if (typeof cellValue === "string") {
+          cellValue = <ExpandableString str={cellValue} length={140} />;
+        }
+      }
+      clicked = {
+        column,
+        value,
+      };
+      isLink = isID(column);
     }
 
-    foreignKeyClicked = (fk) => {
-        this.props.followForeignKey(fk);
+    const isClickable =
+      onVisualizationClick && visualizationIsClickable(clicked);
+
+    return (
+      <div>
+        <span
+          className={cx({
+            "cursor-pointer": isClickable,
+            link: isClickable && isLink,
+          })}
+          onClick={
+            isClickable &&
+            (e => {
+              onVisualizationClick({ ...clicked, element: e.currentTarget });
+            })
+          }
+        >
+          {cellValue}
+        </span>
+      </div>
+    );
+  }
+
+  renderDetailsTable() {
+    const { data: { cols, rows } } = this.props;
+    return cols.map((column, columnIndex) => (
+      <div className="Grid Grid--1of2 mb2" key={columnIndex}>
+        <div className="Grid-cell">
+          {this.cellRenderer(column, rows[0][columnIndex], true)}
+        </div>
+        <div
+          style={{ wordWrap: "break-word" }}
+          className="Grid-cell text-bold text-dark"
+        >
+          {this.cellRenderer(column, rows[0][columnIndex], false)}
+        </div>
+      </div>
+    ));
+  }
+
+  renderRelationships() {
+    let { tableForeignKeys, tableForeignKeyReferences } = this.props;
+    if (!tableForeignKeys) {
+      return null;
     }
 
-    cellRenderer(column, value, isColumn) {
-        const { onVisualizationClick, visualizationIsClickable } = this.props;
+    tableForeignKeys = tableForeignKeys.filter(fk =>
+      isQueryable(fk.origin.table),
+    );
 
-        let cellValue;
-        let clicked;
-        let isLink;
+    if (tableForeignKeys.length < 1) {
+      return <p className="my4 text-centered">{t`No relationships found.`}</p>;
+    }
 
-        if (isColumn) {
-            cellValue = (column !== null) ? formatColumn(column) : null;
-            clicked = {
-                column
-            };
-            isLink = false;
-        } else {
-            if (value === null || value === undefined || value === "") {
-                cellValue = (<span className="text-grey-2">{t`Empty`}</span>);
-            } else if (isa(column.special_type, TYPE.SerializedJSON)) {
-                let formattedJson = JSON.stringify(JSON.parse(value), null, 2);
-                cellValue = (<pre className="ObjectJSON">{formattedJson}</pre>);
-            } else if (typeof value === "object") {
-                let formattedJson = JSON.stringify(value, null, 2);
-                cellValue = (<pre className="ObjectJSON">{formattedJson}</pre>);
-            } else {
-                cellValue = formatValue(value, { column: column, jsx: true });
-                if (typeof cellValue === "string") {
-                    cellValue = (<ExpandableString str={cellValue} length={140}></ExpandableString>);
-                }
+    const fkCountsByTable = foreignKeyCountsByOriginTable(tableForeignKeys);
+
+    const relationships = tableForeignKeys
+      .sort((a, b) =>
+        a.origin.table.display_name.localeCompare(b.origin.table.display_name),
+      )
+      .map(fk => {
+        let fkCount = <LoadingSpinner size={25} />;
+        let fkCountValue = 0;
+        let fkClickable = false;
+        if (tableForeignKeyReferences) {
+          const fkCountInfo = tableForeignKeyReferences[fk.origin.id];
+          if (fkCountInfo && fkCountInfo.status === 1) {
+            fkCount = <span>{fkCountInfo.value}</span>;
+
+            if (fkCountInfo.value) {
+              fkCountValue = fkCountInfo.value;
+              fkClickable = true;
             }
-            clicked = {
-                column,
-                value
-            };
-            isLink = isID(column);
+          }
         }
+        const chevron = (
+          <IconBorder className="flex-align-right">
+            <Icon name="chevronright" size={10} />
+          </IconBorder>
+        );
 
-        const isClickable = onVisualizationClick && visualizationIsClickable(clicked);
-
-        return (
-            <div>
-                <span
-                    className={cx({ "cursor-pointer": isClickable, "link": isClickable && isLink })}
-                    onClick={isClickable && ((e) => {
-                        onVisualizationClick({ ...clicked, element: e.currentTarget });
-                    })}
-                >
-                    {cellValue}
-                </span>
-            </div>
+        const relationName = inflect(
+          fk.origin.table.display_name,
+          fkCountValue,
         );
-    }
+        const via =
+          fkCountsByTable[fk.origin.table.id] > 1 ? (
+            <span className="text-grey-3 text-normal">
+              {" "}
+              {t`via ${fk.origin.display_name}`}
+            </span>
+          ) : null;
+
+        const info = (
+          <div>
+            <h2>{fkCount}</h2>
+            <h5 className="block">
+              {relationName}
+              {via}
+            </h5>
+          </div>
+        );
+        let fkReference;
+        const referenceClasses = cx("flex align-center my2 pb2 border-bottom", {
+          "text-brand-hover cursor-pointer text-dark": fkClickable,
+          "text-grey-3": !fkClickable,
+        });
 
-    renderDetailsTable() {
-        const { data: { cols, rows }} = this.props;
-        return cols.map((column, columnIndex) =>
-            <div className="Grid Grid--1of2 mb2" key={columnIndex}>
-                <div className="Grid-cell">
-                    {this.cellRenderer(column, rows[0][columnIndex], true)}
-                </div>
-                <div style={{wordWrap: 'break-word'}} className="Grid-cell text-bold text-dark">
-                    {this.cellRenderer(column, rows[0][columnIndex], false)}
-                </div>
+        if (fkClickable) {
+          fkReference = (
+            <div
+              className={referenceClasses}
+              key={fk.id}
+              onClick={this.foreignKeyClicked.bind(null, fk)}
+            >
+              {info}
+              {chevron}
             </div>
-        )
-    }
-
-    renderRelationships() {
-        let { tableForeignKeys, tableForeignKeyReferences } = this.props;
-        if (!tableForeignKeys) {
-            return null;
+          );
+        } else {
+          fkReference = (
+            <div className={referenceClasses} key={fk.id}>
+              {info}
+            </div>
+          );
         }
 
-        tableForeignKeys = tableForeignKeys.filter(fk => isQueryable(fk.origin.table));
+        return <li>{fkReference}</li>;
+      });
 
-        if (tableForeignKeys.length < 1) {
-            return (<p className="my4 text-centered">{t`No relationships found.`}</p>);
-        }
+    return <ul className="px4">{relationships}</ul>;
+  }
 
-        const fkCountsByTable = foreignKeyCountsByOriginTable(tableForeignKeys);
-
-        const relationships = tableForeignKeys.sort((a, b) =>
-            a.origin.table.display_name.localeCompare(b.origin.table.display_name)
-        ).map((fk) => {
-            let fkCount = (<LoadingSpinner size={25} />)
-            let fkCountValue = 0;
-            let fkClickable = false;
-            if (tableForeignKeyReferences) {
-                const fkCountInfo = tableForeignKeyReferences[fk.origin.id];
-                if (fkCountInfo && fkCountInfo.status === 1) {
-                    fkCount = (<span>{fkCountInfo.value}</span>);
-
-                    if (fkCountInfo.value) {
-                        fkCountValue = fkCountInfo.value;
-                        fkClickable = true;
-                    }
-                }
-            }
-            const chevron = (
-                <IconBorder className="flex-align-right">
-                    <Icon name='chevronright' size={10} />
-                </IconBorder>
-            );
-
-            const relationName = inflect(fk.origin.table.display_name, fkCountValue);
-            const via = (fkCountsByTable[fk.origin.table.id] > 1) ? (<span className="text-grey-3 text-normal"> {t`via ${fk.origin.display_name}`}</span>) : null;
-
-            const info = (
-                <div>
-                    <h2>{fkCount}</h2>
-                    <h5 className="block">{relationName}{via}</h5>
-                 </div>
-            );
-            let fkReference;
-            const referenceClasses = cx('flex align-center my2 pb2 border-bottom', {
-                'text-brand-hover cursor-pointer text-dark': fkClickable,
-                'text-grey-3': !fkClickable
-            });
-
-            if (fkClickable) {
-                fkReference = (
-                    <div className={referenceClasses} key={fk.id} onClick={this.foreignKeyClicked.bind(null, fk)}>
-                        {info}
-                        {chevron}
-                    </div>
-                );
-            } else {
-                fkReference = (
-                    <div className={referenceClasses} key={fk.id}>
-                        {info}
-                    </div>
-                );
-            }
-
-            return (
-                <li>
-                    {fkReference}
-                </li>
-            );
-        });
-
-        return (
-            <ul className="px4">
-                {relationships}
-            </ul>
-        );
+  onKeyDown = event => {
+    if (event.key === "ArrowLeft") {
+      this.props.viewPreviousObjectDetail();
     }
-
-    onKeyDown = (event) => {
-        if(event.key === 'ArrowLeft') {
-            this.props.viewPreviousObjectDetail()
-        }
-        if(event.key === 'ArrowRight') {
-            this.props.viewNextObjectDetail()
-        }
+    if (event.key === "ArrowRight") {
+      this.props.viewNextObjectDetail();
     }
+  };
 
-    render() {
-        if(!this.props.data) {
-            return false;
-        }
+  render() {
+    if (!this.props.data) {
+      return false;
+    }
 
-        const tableName = (this.props.tableMetadata) ? singularize(this.props.tableMetadata.display_name) : t`Unknown`;
-        // TODO: once we nail down the "title" column of each table this should be something other than the id
-        const idValue = this.getIdValue();
-
-        return (
-            <div className="ObjectDetail rounded mt2">
-                <div className="Grid ObjectDetail-headingGroup">
-                    <div className="Grid-cell ObjectDetail-infoMain px4 py3 ml2 arrow-right">
-                        <div className="text-brand text-bold">
-                            <span>{tableName}</span>
-                            <h1>{idValue}</h1>
-                        </div>
-                    </div>
-                    <div className="Grid-cell flex align-center Cell--1of3 bg-alt">
-                        <div className="p4 flex align-center text-bold text-grey-3">
-                            <Icon name="connections" size={17} />
-                            <div className="ml2">
-                                {jt`This ${<span className="text-dark">{tableName}</span>} is connected to:`}
-                            </div>
-                        </div>
-                    </div>
-                </div>
-                <div className="Grid">
-                    <div className="Grid-cell ObjectDetail-infoMain p4">{this.renderDetailsTable()}</div>
-                    <div className="Grid-cell Cell--1of3 bg-alt">{this.renderRelationships()}</div>
-                </div>
-                <div
-                    className={cx("fixed left cursor-pointer text-brand-hover lg-ml2", { "disabled": idValue <= 1 })}
-                    style={{ top: '50%', left: '1em', transform: 'translate(0, -50%)' }}
-                >
-                    <DirectionalButton
-                        direction="back"
-                        onClick={this.props.viewPreviousObjectDetail}
-                    />
-                </div>
-                <div
-                    className="fixed right cursor-pointer text-brand-hover lg-ml2"
-                    style={{ top: '50%', right: '1em', transform: 'translate(0, -50%)' }}
-                >
-                    <DirectionalButton
-                        direction="forward"
-                        onClick={this.props.viewNextObjectDetail}
-                    />
-                </div>
+    const tableName = this.props.tableMetadata
+      ? singularize(this.props.tableMetadata.display_name)
+      : t`Unknown`;
+    // TODO: once we nail down the "title" column of each table this should be something other than the id
+    const idValue = this.getIdValue();
+
+    return (
+      <div className="ObjectDetail rounded mt2">
+        <div className="Grid ObjectDetail-headingGroup">
+          <div className="Grid-cell ObjectDetail-infoMain px4 py3 ml2 arrow-right">
+            <div className="text-brand text-bold">
+              <span>{tableName}</span>
+              <h1>{idValue}</h1>
             </div>
-        );
-    }
+          </div>
+          <div className="Grid-cell flex align-center Cell--1of3 bg-alt">
+            <div className="p4 flex align-center text-bold text-grey-3">
+              <Icon name="connections" size={17} />
+              <div className="ml2">
+                {jt`This ${(
+                  <span className="text-dark">{tableName}</span>
+                )} is connected to:`}
+              </div>
+            </div>
+          </div>
+        </div>
+        <div className="Grid">
+          <div className="Grid-cell ObjectDetail-infoMain p4">
+            {this.renderDetailsTable()}
+          </div>
+          <div className="Grid-cell Cell--1of3 bg-alt">
+            {this.renderRelationships()}
+          </div>
+        </div>
+        <div
+          className={cx("fixed left cursor-pointer text-brand-hover lg-ml2", {
+            disabled: idValue <= 1,
+          })}
+          style={{ top: "50%", left: "1em", transform: "translate(0, -50%)" }}
+        >
+          <DirectionalButton
+            direction="back"
+            onClick={this.props.viewPreviousObjectDetail}
+          />
+        </div>
+        <div
+          className="fixed right cursor-pointer text-brand-hover lg-ml2"
+          style={{ top: "50%", right: "1em", transform: "translate(0, -50%)" }}
+        >
+          <DirectionalButton
+            direction="forward"
+            onClick={this.props.viewNextObjectDetail}
+          />
+        </div>
+      </div>
+    );
+  }
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(ObjectDetail)
+export default connect(mapStateToProps, mapDispatchToProps)(ObjectDetail);
diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.css b/frontend/src/metabase/visualizations/visualizations/PieChart.css
index a726e4116c0eba8f88901426390c42047a960dce..e8416580450c54a991fd517322e88b336950284d 100644
--- a/frontend/src/metabase/visualizations/visualizations/PieChart.css
+++ b/frontend/src/metabase/visualizations/visualizations/PieChart.css
@@ -26,13 +26,13 @@
 }
 
 :local .Value {
-  color: #676C72;
+  color: #676c72;
   font-size: 22px;
   font-weight: bolder;
 }
 
 :local .Title {
-  color: #B8C0C9;
+  color: #b8c0c9;
   font-size: 14px;
   font-weight: bold;
   text-transform: uppercase;
diff --git a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
index c1201592aeed6aa6b012ed7f1e1d6ce651d17fbd..27c21b882adc8466cd65f8f23a69696cf955a413 100644
--- a/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/PieChart.jsx
@@ -3,13 +3,16 @@
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
 import styles from "./PieChart.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import ChartTooltip from "../components/ChartTooltip.jsx";
 import ChartWithLegend from "../components/ChartWithLegend.jsx";
 
 import { ChartSettingsError } from "metabase/visualizations/lib/errors";
 import { getFriendlyName } from "metabase/visualizations/lib/utils";
-import { metricSetting, dimensionSetting } from "metabase/visualizations/lib/settings";
+import {
+  metricSetting,
+  dimensionSetting,
+} from "metabase/visualizations/lib/settings";
 
 import { formatValue } from "metabase/lib/formatting";
 
@@ -23,7 +26,7 @@ import _ from "underscore";
 const OUTER_RADIUS = 50; // within 100px canvas
 const INNER_RADIUS_RATIO = 3 / 5;
 
-const PAD_ANGLE = (Math.PI / 180) * 1; // 1 degree in radians
+const PAD_ANGLE = Math.PI / 180 * 1; // 1 degree in radians
 const SLICE_THRESHOLD = 1 / 360; // 1 degree in percentage
 const OTHER_SLICE_MIN_PERCENTAGE = 0.003;
 
@@ -32,202 +35,285 @@ const PERCENT_REGEX = /percent/i;
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class PieChart extends Component {
-    props: VisualizationProps;
+  props: VisualizationProps;
 
-    static uiName = t`Pie`;
-    static identifier = "pie";
-    static iconName = "pie";
+  static uiName = t`Pie`;
+  static identifier = "pie";
+  static iconName = "pie";
 
-    static minSize = { width: 4, height: 4 };
+  static minSize = { width: 4, height: 4 };
 
-    static isSensible(cols, rows) {
-        return cols.length === 2;
-    }
+  static isSensible(cols, rows) {
+    return cols.length === 2;
+  }
 
-    static checkRenderable([{ data: { cols, rows} }], settings) {
-        if (!settings["pie.dimension"] || !settings["pie.metric"]) {
-            throw new ChartSettingsError(t`Which columns do you want to use?`, t`Data`);
-        }
+  static checkRenderable([{ data: { cols, rows } }], settings) {
+    if (!settings["pie.dimension"] || !settings["pie.metric"]) {
+      throw new ChartSettingsError(
+        t`Which columns do you want to use?`,
+        t`Data`,
+      );
     }
-
-    static settings = {
-        "pie.dimension": {
-            section: "Data",
-            title: t`Dimension`,
-            ...dimensionSetting("pie.dimension")
-        },
-        "pie.metric": {
-            section: "Data",
-            title: t`Measure`,
-            ...metricSetting("pie.metric")
-        },
-        "pie.show_legend": {
-            section: "Display",
-            title: t`Show legend`,
-            widget: "toggle"
-        },
-        "pie.show_legend_perecent": {
-            section: "Display",
-            title: t`Show percentages in legend`,
-            widget: "toggle",
-            default: true
-        },
-        "pie.slice_threshold": {
-            section: "Display",
-            title: t`Minimum slice percentage`,
-            widget: "number"
-        },
+  }
+
+  static settings = {
+    "pie.dimension": {
+      section: "Data",
+      title: t`Dimension`,
+      ...dimensionSetting("pie.dimension"),
+    },
+    "pie.metric": {
+      section: "Data",
+      title: t`Measure`,
+      ...metricSetting("pie.metric"),
+    },
+    "pie.show_legend": {
+      section: "Display",
+      title: t`Show legend`,
+      widget: "toggle",
+    },
+    "pie.show_legend_perecent": {
+      section: "Display",
+      title: t`Show percentages in legend`,
+      widget: "toggle",
+      default: true,
+    },
+    "pie.slice_threshold": {
+      section: "Display",
+      title: t`Minimum slice percentage`,
+      widget: "number",
+    },
+  };
+
+  componentDidUpdate() {
+    let groupElement = ReactDOM.findDOMNode(this.refs.group);
+    let detailElement = ReactDOM.findDOMNode(this.refs.detail);
+    if (groupElement.getBoundingClientRect().width < 100) {
+      detailElement.classList.add("hide");
+    } else {
+      detailElement.classList.remove("hide");
     }
-
-    componentDidUpdate() {
-        let groupElement = ReactDOM.findDOMNode(this.refs.group);
-        let detailElement = ReactDOM.findDOMNode(this.refs.detail);
-        if (groupElement.getBoundingClientRect().width < 100) {
-            detailElement.classList.add("hide");
-        } else {
-            detailElement.classList.remove("hide");
-        }
+  }
+
+  render() {
+    const {
+      series,
+      hovered,
+      onHoverChange,
+      visualizationIsClickable,
+      onVisualizationClick,
+      className,
+      gridSize,
+      settings,
+    } = this.props;
+
+    const [{ data: { cols, rows } }] = series;
+    const dimensionIndex = _.findIndex(
+      cols,
+      col => col.name === settings["pie.dimension"],
+    );
+    const metricIndex = _.findIndex(
+      cols,
+      col => col.name === settings["pie.metric"],
+    );
+
+    const formatDimension = (dimension, jsx = true) =>
+      formatValue(dimension, {
+        column: cols[dimensionIndex],
+        jsx,
+        majorWidth: 0,
+      });
+    const formatMetric = (metric, jsx = true) =>
+      formatValue(metric, { column: cols[metricIndex], jsx, majorWidth: 0 });
+    const formatPercent = percent => (100 * percent).toFixed(2) + "%";
+
+    const showPercentInTooltip =
+      !PERCENT_REGEX.test(cols[metricIndex].name) &&
+      !PERCENT_REGEX.test(cols[metricIndex].display_name);
+
+    // $FlowFixMe
+    let total: number = rows.reduce((sum, row) => sum + row[metricIndex], 0);
+
+    // use standard colors for up to 5 values otherwise use color harmony to help differentiate slices
+    let sliceColors = Object.values(
+      rows.length > 5 ? colors.harmony : colors.normal,
+    );
+    let sliceThreshold =
+      typeof settings["pie.slice_threshold"] === "number"
+        ? settings["pie.slice_threshold"] / 100
+        : SLICE_THRESHOLD;
+
+    let [slices, others] = _.chain(rows)
+      .map((row, index) => ({
+        key: row[dimensionIndex],
+        value: row[metricIndex],
+        percentage: row[metricIndex] / total,
+        color: sliceColors[index % sliceColors.length],
+      }))
+      .partition(d => d.percentage > sliceThreshold)
+      .value();
+
+    let otherSlice;
+    if (others.length > 1) {
+      let otherTotal = others.reduce((acc, o) => acc + o.value, 0);
+      if (otherTotal > 0) {
+        otherSlice = {
+          key: "Other",
+          value: otherTotal,
+          percentage: otherTotal / total,
+          color: "gray",
+        };
+        slices.push(otherSlice);
+      }
+    } else {
+      slices.push(...others);
     }
 
-    render() {
-        const { series, hovered, onHoverChange, visualizationIsClickable, onVisualizationClick, className, gridSize, settings } = this.props;
-
-        const [{ data: { cols, rows }}] = series;
-        const dimensionIndex = _.findIndex(cols, (col) => col.name === settings["pie.dimension"]);
-        const metricIndex = _.findIndex(cols, (col) => col.name === settings["pie.metric"]);
-
-        const formatDimension = (dimension, jsx = true) => formatValue(dimension, { column: cols[dimensionIndex], jsx, majorWidth: 0 })
-        const formatMetric    =    (metric, jsx = true) => formatValue(metric, { column: cols[metricIndex], jsx, majorWidth: 0 })
-        const formatPercent   =               (percent) => (100 * percent).toFixed(2) + "%"
-
-        const showPercentInTooltip = !PERCENT_REGEX.test(cols[metricIndex].name) && !PERCENT_REGEX.test(cols[metricIndex].display_name);
-
-        // $FlowFixMe
-        let total: number = rows.reduce((sum, row) => sum + row[metricIndex], 0);
-
-        // use standard colors for up to 5 values otherwise use color harmony to help differentiate slices
-        let sliceColors = Object.values(rows.length > 5 ? colors.harmony : colors.normal);
-        let sliceThreshold = typeof settings["pie.slice_threshold"] === "number" ? settings["pie.slice_threshold"] / 100 : SLICE_THRESHOLD;
+    // increase "other" slice so it's barely visible
+    if (otherSlice && otherSlice.percentage < OTHER_SLICE_MIN_PERCENTAGE) {
+      otherSlice.value = total * OTHER_SLICE_MIN_PERCENTAGE;
+    }
 
-        let [slices, others] = _.chain(rows)
-            .map((row, index) => ({
-                key: row[dimensionIndex],
-                value: row[metricIndex],
-                percentage: row[metricIndex] / total,
-                color: sliceColors[index % sliceColors.length]
+    let legendTitles = slices.map(slice => [
+      slice.key === "Other" ? slice.key : formatDimension(slice.key, true),
+      settings["pie.show_legend_perecent"]
+        ? formatPercent(slice.percentage)
+        : undefined,
+    ]);
+    let legendColors = slices.map(slice => slice.color);
+
+    const pie = d3.layout
+      .pie()
+      .sort(null)
+      .padAngle(PAD_ANGLE)
+      .value(d => d.value);
+    const arc = d3.svg
+      .arc()
+      .outerRadius(OUTER_RADIUS)
+      .innerRadius(OUTER_RADIUS * INNER_RADIUS_RATIO);
+
+    const hoverForIndex = (index, event) => ({
+      index,
+      event: event && event.nativeEvent,
+      data:
+        slices[index] === otherSlice
+          ? others.map(o => ({
+              key: formatDimension(o.key, false),
+              value: formatMetric(o.value, false),
             }))
-            .partition((d) => d.percentage > sliceThreshold)
-            .value();
-
-        let otherSlice;
-        if (others.length > 1) {
-            let otherTotal = others.reduce((acc, o) => acc + o.value, 0);
-            if (otherTotal > 0) {
-                otherSlice = {
-                    key: "Other",
-                    value: otherTotal,
-                    percentage: otherTotal / total,
-                    color: "gray"
-                };
-                slices.push(otherSlice);
-            }
-        } else {
-            slices.push(...others);
-        }
-
-        // increase "other" slice so it's barely visible
-        if (otherSlice && otherSlice.percentage < OTHER_SLICE_MIN_PERCENTAGE) {
-            otherSlice.value = total * OTHER_SLICE_MIN_PERCENTAGE;
-        }
+          : [
+              {
+                key: getFriendlyName(cols[dimensionIndex]),
+                value: formatDimension(slices[index].key),
+              },
+              {
+                key: getFriendlyName(cols[metricIndex]),
+                value: formatMetric(slices[index].value),
+              },
+            ].concat(
+              showPercentInTooltip
+                ? [
+                    {
+                      key: "Percentage",
+                      value: formatPercent(slices[index].percentage),
+                    },
+                  ]
+                : [],
+            ),
+    });
+
+    let value, title;
+    if (
+      hovered &&
+      hovered.index != null &&
+      slices[hovered.index] !== otherSlice
+    ) {
+      title = formatDimension(slices[hovered.index].key);
+      value = formatMetric(slices[hovered.index].value);
+    } else {
+      title = t`Total`;
+      value = formatMetric(total);
+    }
 
-        let legendTitles = slices.map(slice => [
-            slice.key === "Other" ? slice.key : formatDimension(slice.key, true),
-            settings["pie.show_legend_perecent"] ? formatPercent(slice.percentage) : undefined
-        ]);
-        let legendColors = slices.map(slice => slice.color);
-
-        const pie = d3.layout.pie()
-            .sort(null)
-            .padAngle(PAD_ANGLE)
-            .value(d => d.value);
-        const arc = d3.svg.arc()
-            .outerRadius(OUTER_RADIUS)
-            .innerRadius(OUTER_RADIUS * INNER_RADIUS_RATIO);
-
-        const hoverForIndex = (index, event) => ({
-            index,
-            event: event && event.nativeEvent,
-            data: slices[index] === otherSlice ?
-                others.map(o => ({
-                    key: formatDimension(o.key, false),
-                    value: formatMetric(o.value, false)
-                }))
-            : [
-                { key: getFriendlyName(cols[dimensionIndex]), value: formatDimension(slices[index].key) },
-                { key: getFriendlyName(cols[metricIndex]), value: formatMetric(slices[index].value) },
-            ].concat(showPercentInTooltip ? [{ key: "Percentage", value: formatPercent(slices[index].percentage) }] : [])
-        });
-
-        let value, title;
-        if (hovered && hovered.index != null && slices[hovered.index] !== otherSlice) {
-            title = formatDimension(slices[hovered.index].key);
-            value = formatMetric(slices[hovered.index].value);
-        } else {
-            title = t`Total`;
-            value = formatMetric(total);
+    const getSliceClickObject = index => ({
+      value: slices[index].value,
+      column: cols[metricIndex],
+      dimensions: [
+        {
+          value: slices[index].key,
+          column: cols[dimensionIndex],
+        },
+      ],
+    });
+
+    const isClickable =
+      onVisualizationClick && visualizationIsClickable(getSliceClickObject(0));
+    const getSliceIsClickable = index =>
+      isClickable && slices[index] !== otherSlice;
+
+    return (
+      <ChartWithLegend
+        className={className}
+        legendTitles={legendTitles}
+        legendColors={legendColors}
+        gridSize={gridSize}
+        hovered={hovered}
+        onHoverChange={d =>
+          onHoverChange &&
+          onHoverChange(d && { ...d, ...hoverForIndex(d.index) })
         }
-
-        const getSliceClickObject = (index) => ({
-            value:      slices[index].value,
-            column:     cols[metricIndex],
-            dimensions: [{
-                value: slices[index].key,
-                column: cols[dimensionIndex],
-            }]
-        })
-
-        const isClickable = onVisualizationClick && visualizationIsClickable(getSliceClickObject(0));
-        const getSliceIsClickable = (index) => isClickable && slices[index] !== otherSlice;
-
-        return (
-            <ChartWithLegend
-                className={className}
-                legendTitles={legendTitles} legendColors={legendColors}
-                gridSize={gridSize}
-                hovered={hovered} onHoverChange={(d) => onHoverChange && onHoverChange(d && { ...d, ...hoverForIndex(d.index) })}
-                showLegend={settings["pie.show_legend"]}
+        showLegend={settings["pie.show_legend"]}
+      >
+        <div className={styles.ChartAndDetail}>
+          <div ref="detail" className={styles.Detail}>
+            <div
+              className={cx(
+                styles.Value,
+                "fullscreen-normal-text fullscreen-night-text",
+              )}
             >
-                <div className={styles.ChartAndDetail}>
-                    <div ref="detail" className={styles.Detail}>
-                        <div className={cx(styles.Value, "fullscreen-normal-text fullscreen-night-text")}>{value}</div>
-                        <div className={styles.Title}>{title}</div>
-                    </div>
-                    <div className={styles.Chart}>
-                        <svg className={styles.Donut+ " m1"} viewBox="0 0 100 100">
-                            <g ref="group" transform={`translate(50,50)`}>
-                                {pie(slices).map((slice, index) =>
-                                    <path
-                                        key={index}
-                                        d={arc(slice)}
-                                        fill={slices[index].color}
-                                        opacity={(hovered && hovered.index != null && hovered.index !== index) ? 0.3 : 1}
-                                        onMouseMove={(e) => onHoverChange && onHoverChange(hoverForIndex(index, e))}
-                                        onMouseLeave={() => onHoverChange && onHoverChange(null)}
-                                        className={cx({ "cursor-pointer": getSliceIsClickable(index) })}
-                                        onClick={getSliceIsClickable(index) && ((e) =>
-                                            onVisualizationClick({
-                                                ...getSliceClickObject(index),
-                                                event: e.nativeEvent
-                                            })
-                                        )}
-                                    />
-                                )}
-                            </g>
-                        </svg>
-                    </div>
-                </div>
-                <ChartTooltip series={series} hovered={hovered} />
-            </ChartWithLegend>
-        );
-    }
+              {value}
+            </div>
+            <div className={styles.Title}>{title}</div>
+          </div>
+          <div className={styles.Chart}>
+            <svg className={styles.Donut + " m1"} viewBox="0 0 100 100">
+              <g ref="group" transform={`translate(50,50)`}>
+                {pie(slices).map((slice, index) => (
+                  <path
+                    key={index}
+                    d={arc(slice)}
+                    fill={slices[index].color}
+                    opacity={
+                      hovered &&
+                      hovered.index != null &&
+                      hovered.index !== index
+                        ? 0.3
+                        : 1
+                    }
+                    onMouseMove={e =>
+                      onHoverChange && onHoverChange(hoverForIndex(index, e))
+                    }
+                    onMouseLeave={() => onHoverChange && onHoverChange(null)}
+                    className={cx({
+                      "cursor-pointer": getSliceIsClickable(index),
+                    })}
+                    onClick={
+                      getSliceIsClickable(index) &&
+                      (e =>
+                        onVisualizationClick({
+                          ...getSliceClickObject(index),
+                          event: e.nativeEvent,
+                        }))
+                    }
+                  />
+                ))}
+              </g>
+            </svg>
+          </div>
+        </div>
+        <ChartTooltip series={series} hovered={hovered} />
+      </ChartWithLegend>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/Progress.jsx b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
index 860e8b6acf064a5e63f1fd2177ecd72ed58e35d3..ee312a55d51a06f9009e38db1c4229ffa5e37cb7 100644
--- a/frontend/src/metabase/visualizations/visualizations/Progress.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Progress.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import ReactDOM from "react-dom";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import { formatValue } from "metabase/lib/formatting";
 import { isNumeric } from "metabase/lib/schema_metadata";
 import Icon from "metabase/components/Icon.jsx";
@@ -18,181 +18,198 @@ const MAX_BAR_HEIGHT = 65;
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class Progress extends Component {
-    props: VisualizationProps;
+  props: VisualizationProps;
 
-    static uiName = t`Progress`;
-    static identifier = "progress";
-    static iconName = "progress";
+  static uiName = t`Progress`;
+  static identifier = "progress";
+  static iconName = "progress";
 
-    static minSize = { width: 3, height: 3 };
+  static minSize = { width: 3, height: 3 };
 
-    static isSensible(cols, rows) {
-        return rows.length === 1 && cols.length === 1;
-    }
+  static isSensible(cols, rows) {
+    return rows.length === 1 && cols.length === 1;
+  }
 
-    static checkRenderable([{ data: { cols, rows} }]) {
-        if (!isNumeric(cols[0])) {
-            throw new Error(t`Progress visualization requires a number.`);
-        }
+  static checkRenderable([{ data: { cols, rows } }]) {
+    if (!isNumeric(cols[0])) {
+      throw new Error(t`Progress visualization requires a number.`);
     }
-
-    static settings = {
-        "progress.goal": {
-            section: "Display",
-            title: t`Goal`,
-            widget: "number",
-            default: 0
-        },
-        "progress.color": {
-            section: "Display",
-            title: t`Color`,
-            widget: "color",
-            default: normal.green
-        },
+  }
+
+  static settings = {
+    "progress.goal": {
+      section: "Display",
+      title: t`Goal`,
+      widget: "number",
+      default: 0,
+    },
+    "progress.color": {
+      section: "Display",
+      title: t`Color`,
+      widget: "color",
+      default: normal.green,
+    },
+  };
+
+  componentDidMount() {
+    this.componentDidUpdate();
+  }
+
+  componentDidUpdate() {
+    const component = ReactDOM.findDOMNode(this);
+    const pointer = ReactDOM.findDOMNode(this.refs.pointer);
+    const label = ReactDOM.findDOMNode(this.refs.label);
+    const container = ReactDOM.findDOMNode(this.refs.container);
+    const bar = ReactDOM.findDOMNode(this.refs.bar);
+
+    // Safari not respecting `height: 25%` so just do it here ¯\_(ツ)_/¯
+    bar.style.height = Math.min(MAX_BAR_HEIGHT, component.offsetHeight) + "px";
+
+    if (this.props.gridSize && this.props.gridSize.height < 4) {
+      pointer.parentNode.style.display = "none";
+      label.parentNode.style.display = "none";
+      // no need to do the rest of the repositioning
+      return;
+    } else {
+      pointer.parentNode.style.display = null;
+      label.parentNode.style.display = null;
     }
 
-    componentDidMount() {
-        this.componentDidUpdate();
+    // reset the pointer transform for these computations
+    pointer.style.transform = null;
+
+    // position the label
+    const containerWidth = container.offsetWidth;
+    const labelWidth = label.offsetWidth;
+    const pointerWidth = pointer.offsetWidth;
+    const pointerCenter = pointer.offsetLeft + pointerWidth / 2;
+    const minOffset = -pointerWidth / 2 + BORDER_RADIUS;
+    if (pointerCenter - labelWidth / 2 < minOffset) {
+      label.style.left = minOffset + "px";
+      label.style.right = null;
+    } else if (pointerCenter + labelWidth / 2 > containerWidth - minOffset) {
+      label.style.left = null;
+      label.style.right = minOffset + "px";
+    } else {
+      label.style.left = pointerCenter - labelWidth / 2 + "px";
+      label.style.right = null;
     }
 
-    componentDidUpdate() {
-        const component = ReactDOM.findDOMNode(this);
-        const pointer = ReactDOM.findDOMNode(this.refs.pointer);
-        const label = ReactDOM.findDOMNode(this.refs.label);
-        const container = ReactDOM.findDOMNode(this.refs.container);
-        const bar = ReactDOM.findDOMNode(this.refs.bar);
-
-        // Safari not respecting `height: 25%` so just do it here ¯\_(ツ)_/¯
-        bar.style.height = Math.min(MAX_BAR_HEIGHT, component.offsetHeight) + "px";
-
-        if (this.props.gridSize && this.props.gridSize.height < 4) {
-            pointer.parentNode.style.display = "none";
-            label.parentNode.style.display = "none";
-            // no need to do the rest of the repositioning
-            return;
-        } else {
-            pointer.parentNode.style.display = null;
-            label.parentNode.style.display = null;
-        }
-
-        // reset the pointer transform for these computations
-        pointer.style.transform = null;
-
-        // position the label
-        const containerWidth = container.offsetWidth;
-        const labelWidth = label.offsetWidth;
-        const pointerWidth = pointer.offsetWidth;
-        const pointerCenter = pointer.offsetLeft + pointerWidth / 2;
-        const minOffset = (-pointerWidth / 2) + BORDER_RADIUS
-        if (pointerCenter - labelWidth / 2 < minOffset) {
-            label.style.left = minOffset + "px";
-            label.style.right = null
-        } else if (pointerCenter + labelWidth / 2 > containerWidth - minOffset) {
-            label.style.left = null
-            label.style.right = minOffset + "px";
-        } else {
-            label.style.left = (pointerCenter - labelWidth / 2) + "px";
-            label.style.right = null;
-        }
-
-        // shift pointer at ends inward to line up with border radius
-        if (pointerCenter < BORDER_RADIUS) {
-            pointer.style.transform = "translate(" + BORDER_RADIUS + "px,0)";
-        } else if (pointerCenter > containerWidth - 5) {
-            pointer.style.transform = "translate(-" + BORDER_RADIUS + "px,0)";
-        }
+    // shift pointer at ends inward to line up with border radius
+    if (pointerCenter < BORDER_RADIUS) {
+      pointer.style.transform = "translate(" + BORDER_RADIUS + "px,0)";
+    } else if (pointerCenter > containerWidth - 5) {
+      pointer.style.transform = "translate(-" + BORDER_RADIUS + "px,0)";
+    }
+  }
+
+  render() {
+    const {
+      series: [{ data: { rows, cols } }],
+      settings,
+      onVisualizationClick,
+      visualizationIsClickable,
+    } = this.props;
+    const value: number = rows[0][0];
+    const goal = settings["progress.goal"] || 0;
+
+    const mainColor = settings["progress.color"];
+    const lightColor = Color(mainColor)
+      .lighten(0.25)
+      .rgb()
+      .string();
+    const darkColor = Color(mainColor)
+      .darken(0.3)
+      .rgb()
+      .string();
+
+    const progressColor = mainColor;
+    const restColor = value > goal ? darkColor : lightColor;
+    const arrowColor = value > goal ? darkColor : mainColor;
+
+    const barPercent = Math.max(0, value < goal ? value / goal : goal / value);
+    const arrowPercent = Math.max(0, value < goal ? value / goal : 1);
+
+    let barMessage;
+    if (value === goal) {
+      barMessage = t`Goal met`;
+    } else if (value > goal) {
+      barMessage = t`Goal exceeded`;
     }
 
-    render() {
-        const { series: [{ data: { rows, cols } }], settings, onVisualizationClick, visualizationIsClickable } = this.props;
-        const value: number = rows[0][0];
-        const goal = settings["progress.goal"] || 0;
-
-        const mainColor = settings["progress.color"];
-        const lightColor = Color(mainColor).lighten(0.25).rgb().string();
-        const darkColor = Color(mainColor).darken(0.30).rgb().string();
-
-        const progressColor = mainColor;
-        const restColor = value > goal ? darkColor : lightColor;
-        const arrowColor = value > goal ? darkColor : mainColor;
-
-        const barPercent = Math.max(0, value < goal ? value / goal : goal / value);
-        const arrowPercent = Math.max(0, value < goal ? value / goal : 1);
-
-        let barMessage;
-        if (value === goal) {
-            barMessage = t`Goal met`;
-        } else if (value > goal) {
-            barMessage = t`Goal exceeded`;
-        }
-
-        const clicked = {
-            value: value,
-            column: cols[0]
-        };
-        const isClickable = visualizationIsClickable(clicked);
-
-        return (
-            <div className={cx(this.props.className, "flex layout-centered")}>
-                <div className="flex-full full-height flex flex-column justify-center" style={{ padding: 10, paddingTop: 0 }}>
-                    <div
-                        ref="container"
-                        className="relative text-bold text-grey-4"
-                        style={{ height: 20 }}
-                    >
-                        <div
-                            ref="label"
-                            style={{ position: "absolute" }}
-                        >
-                            {formatValue(value, { comma: true })}
-                        </div>
-                    </div>
-                    <div className="relative" style={{ height: 10, marginBottom: 5 }}>
-                        <div
-                            ref="pointer"
-                            style={{
-                                width: 0,
-                                height: 0,
-                                position: "absolute",
-                                left: (arrowPercent * 100) + "%",
-                                marginLeft: -10,
-                                borderLeft: "10px solid transparent",
-                                borderRight: "10px solid transparent",
-                                borderTop: "10px solid " + arrowColor
-                            }}
-                        />
-                    </div>
-                    <div
-                        ref="bar"
-                        className={cx("relative", { "cursor-pointer": isClickable })}
-                        style={{
-                            backgroundColor: restColor,
-                            borderRadius: BORDER_RADIUS,
-                            overflow: "hidden"
-                        }}
-                        onClick={isClickable && ((e) => onVisualizationClick({ ...clicked, event: e.nativeEvent }))}
-                    >
-                        <div style={{
-                                backgroundColor: progressColor,
-                                width: (barPercent * 100) + "%",
-                                height: "100%"
-                            }}
-                        />
-                        { barMessage &&
-                            <div className="flex align-center absolute spread text-white text-bold px2">
-                                <IconBorder borderWidth={2}>
-                                    <Icon name="check" size={14} />
-                                </IconBorder>
-                                <div className="pl2">{barMessage}</div>
-                            </div>
-                        }
-                    </div>
-                    <div className="mt1">
-                        <span className="float-left">0</span>
-                        <span className="float-right">{t`Goal ${formatValue(goal, { comma: true })}`}</span>
-                    </div>
-                </div>
+    const clicked = {
+      value: value,
+      column: cols[0],
+    };
+    const isClickable = visualizationIsClickable(clicked);
+
+    return (
+      <div className={cx(this.props.className, "flex layout-centered")}>
+        <div
+          className="flex-full full-height flex flex-column justify-center"
+          style={{ padding: 10, paddingTop: 0 }}
+        >
+          <div
+            ref="container"
+            className="relative text-bold text-grey-4"
+            style={{ height: 20 }}
+          >
+            <div ref="label" style={{ position: "absolute" }}>
+              {formatValue(value, { comma: true })}
             </div>
-        );
-    }
+          </div>
+          <div className="relative" style={{ height: 10, marginBottom: 5 }}>
+            <div
+              ref="pointer"
+              style={{
+                width: 0,
+                height: 0,
+                position: "absolute",
+                left: arrowPercent * 100 + "%",
+                marginLeft: -10,
+                borderLeft: "10px solid transparent",
+                borderRight: "10px solid transparent",
+                borderTop: "10px solid " + arrowColor,
+              }}
+            />
+          </div>
+          <div
+            ref="bar"
+            className={cx("relative", { "cursor-pointer": isClickable })}
+            style={{
+              backgroundColor: restColor,
+              borderRadius: BORDER_RADIUS,
+              overflow: "hidden",
+            }}
+            onClick={
+              isClickable &&
+              (e => onVisualizationClick({ ...clicked, event: e.nativeEvent }))
+            }
+          >
+            <div
+              style={{
+                backgroundColor: progressColor,
+                width: barPercent * 100 + "%",
+                height: "100%",
+              }}
+            />
+            {barMessage && (
+              <div className="flex align-center absolute spread text-white text-bold px2">
+                <IconBorder borderWidth={2}>
+                  <Icon name="check" size={14} />
+                </IconBorder>
+                <div className="pl2">{barMessage}</div>
+              </div>
+            )}
+          </div>
+          <div className="mt1">
+            <span className="float-left">0</span>
+            <span className="float-right">{t`Goal ${formatValue(goal, {
+              comma: true,
+            })}`}</span>
+          </div>
+        </div>
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
index 9ab5caaa23102c030082bcd0d5f3321dd6f01b0a..52cdb2375775a24c2ea007e5900ef8d2676959d2 100644
--- a/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/RowChart.jsx
@@ -1,36 +1,36 @@
 /* @flow */
 
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import rowRenderer from "../lib/RowRenderer.js";
 
 import {
-    GRAPH_DATA_SETTINGS,
-    GRAPH_COLORS_SETTINGS
+  GRAPH_DATA_SETTINGS,
+  GRAPH_COLORS_SETTINGS,
 } from "metabase/visualizations/lib/settings/graph";
 
 export default class RowChart extends LineAreaBarChart {
-    static uiName = t`Row Chart`;
-    static identifier = "row";
-    static iconName = "horizontal_bar";
-    static noun = t`row chart`;
+  static uiName = t`Row Chart`;
+  static identifier = "row";
+  static iconName = "horizontal_bar";
+  static noun = t`row chart`;
 
-    static supportsSeries = false;
+  static supportsSeries = false;
 
-    static renderer = rowRenderer;
+  static renderer = rowRenderer;
 
-    static settings = {
-        ...GRAPH_DATA_SETTINGS,
-        ...GRAPH_COLORS_SETTINGS
-    }
+  static settings = {
+    ...GRAPH_DATA_SETTINGS,
+    ...GRAPH_COLORS_SETTINGS,
+  };
 }
 
 // rename these settings
 RowChart.settings["graph.metrics"] = {
-    ...RowChart.settings["graph.metrics"],
-    title: t`X-axis`
-}
+  ...RowChart.settings["graph.metrics"],
+  title: t`X-axis`,
+};
 RowChart.settings["graph.dimensions"] = {
-    ...RowChart.settings["graph.dimensions"],
-    title: t`Y-axis`
-}
+  ...RowChart.settings["graph.dimensions"],
+  title: t`Y-axis`,
+};
diff --git a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
index e4f2b72218071737550a7f06ca2629b927f07159..44a37ce3666faf8268b6a2d2ae279a60444a14ef 100644
--- a/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Scalar.jsx
@@ -2,7 +2,7 @@
 
 import React, { Component } from "react";
 import styles from "./Scalar.css";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import Icon from "metabase/components/Icon.jsx";
 import Tooltip from "metabase/components/Tooltip.jsx";
 import Ellipsified from "metabase/components/Ellipsified.jsx";
@@ -17,208 +17,242 @@ import d3 from "d3";
 import type { VisualizationProps } from "metabase/meta/types/Visualization";
 
 export default class Scalar extends Component {
-    props: VisualizationProps;
+  props: VisualizationProps;
 
-    static uiName = t`Number`;
-    static identifier = "scalar";
-    static iconName = "number";
+  static uiName = t`Number`;
+  static identifier = "scalar";
+  static iconName = "number";
 
-    static noHeader = true;
-    static supportsSeries = true;
+  static noHeader = true;
+  static supportsSeries = true;
 
-    static minSize = { width: 3, height: 3 };
+  static minSize = { width: 3, height: 3 };
 
-    _scalar: ?HTMLElement;
+  _scalar: ?HTMLElement;
 
-    static isSensible(cols, rows) {
-        return rows.length === 1 && cols.length === 1;
-    }
+  static isSensible(cols, rows) {
+    return rows.length === 1 && cols.length === 1;
+  }
 
-    static checkRenderable([{ data: { cols, rows} }]) {
-        // scalar can always be rendered, nothing needed here
-    }
+  static checkRenderable([{ data: { cols, rows } }]) {
+    // scalar can always be rendered, nothing needed here
+  }
 
-    static seriesAreCompatible(initialSeries, newSeries) {
-        if (newSeries.data.cols && newSeries.data.cols.length === 1) {
-            return true;
-        }
-        return false;
+  static seriesAreCompatible(initialSeries, newSeries) {
+    if (newSeries.data.cols && newSeries.data.cols.length === 1) {
+      return true;
     }
+    return false;
+  }
 
-    static transformSeries(series) {
-        if (series.length > 1) {
-            return series.map((s, seriesIndex) => ({
-                card: {
-                    ...s.card,
-                    display: "funnel",
-                    visualization_settings: {
-                        ...s.card.visualization_settings,
-                        "graph.x_axis.labels_enabled": false
-                    },
-                    _seriesIndex: seriesIndex,
-                },
-                data: {
-                    cols: [
-                        { base_type: TYPE.Text, display_name: t`Name`, name: "name" },
-                        { ...s.data.cols[0] }],
-                    rows: [
-                        [s.card.name, s.data.rows[0][0]]
-                    ]
-                }
-            }));
-        } else {
-            return series;
-        }
-    }
-
-    static settings = {
-        "scalar.locale": {
-            title: t`Separator style`,
-            widget: "select",
-            props: {
-                options: [
-                    { name: "100000.00", value: null },
-                    { name: "100,000.00", value: "en" },
-                    { name: "100 000,00", value: "fr" },
-                    { name: "100.000,00", value: "de" }
-                ]
-            },
-            default: "en"
+  static transformSeries(series) {
+    if (series.length > 1) {
+      return series.map((s, seriesIndex) => ({
+        card: {
+          ...s.card,
+          display: "funnel",
+          visualization_settings: {
+            ...s.card.visualization_settings,
+            "graph.x_axis.labels_enabled": false,
+          },
+          _seriesIndex: seriesIndex,
         },
-        "scalar.decimals": {
-            title: t`Number of decimal places`,
-            widget: "number"
+        data: {
+          cols: [
+            { base_type: TYPE.Text, display_name: t`Name`, name: "name" },
+            { ...s.data.cols[0] },
+          ],
+          rows: [[s.card.name, s.data.rows[0][0]]],
         },
-        "scalar.prefix": {
-            title: t`Add a prefix`,
-            widget: "input"
-        },
-        "scalar.suffix": {
-            title: t`Add a suffix`,
-            widget: "input"
-        },
-        "scalar.scale": {
-            title: t`Multiply by a number`,
-            widget: "number"
-        },
-    };
+      }));
+    } else {
+      return series;
+    }
+  }
 
-    render() {
-        let { series: [{ card, data: { cols, rows }}], className, actionButtons, gridSize, settings, onChangeCardAndRun, visualizationIsClickable, onVisualizationClick } = this.props;
-        let description = settings["card.description"];
+  static settings = {
+    "scalar.locale": {
+      title: t`Separator style`,
+      widget: "select",
+      props: {
+        options: [
+          { name: "100000.00", value: null },
+          { name: "100,000.00", value: "en" },
+          { name: "100 000,00", value: "fr" },
+          { name: "100.000,00", value: "de" },
+        ],
+      },
+      default: "en",
+    },
+    "scalar.decimals": {
+      title: t`Number of decimal places`,
+      widget: "number",
+    },
+    "scalar.prefix": {
+      title: t`Add a prefix`,
+      widget: "input",
+    },
+    "scalar.suffix": {
+      title: t`Add a suffix`,
+      widget: "input",
+    },
+    "scalar.scale": {
+      title: t`Multiply by a number`,
+      widget: "number",
+    },
+  };
 
-        let isSmall = gridSize && gridSize.width < 4;
-        const column = cols[0];
+  render() {
+    let {
+      series: [{ card, data: { cols, rows } }],
+      className,
+      actionButtons,
+      gridSize,
+      settings,
+      onChangeCardAndRun,
+      visualizationIsClickable,
+      onVisualizationClick,
+    } = this.props;
+    let description = settings["card.description"];
 
-        let scalarValue = rows[0] && rows[0][0];
-        if (scalarValue == null) {
-            scalarValue = "";
-        }
+    let isSmall = gridSize && gridSize.width < 4;
+    const column = cols[0];
 
-        let compactScalarValue, fullScalarValue;
+    let scalarValue = rows[0] && rows[0][0];
+    if (scalarValue == null) {
+      scalarValue = "";
+    }
 
-        // TODO: some or all of these options should be part of formatValue
-        if (typeof scalarValue === "number" && isNumber(column)) {
-            let number = scalarValue;
+    let compactScalarValue, fullScalarValue;
 
-            // scale
-            const scale =  parseFloat(settings["scalar.scale"]);
-            if (!isNaN(scale)) {
-                number *= scale;
-            }
+    // TODO: some or all of these options should be part of formatValue
+    if (typeof scalarValue === "number" && isNumber(column)) {
+      let number = scalarValue;
 
-            const localeStringOptions = {};
+      // scale
+      const scale = parseFloat(settings["scalar.scale"]);
+      if (!isNaN(scale)) {
+        number *= scale;
+      }
 
-            // decimals
-            let decimals = parseFloat(settings["scalar.decimals"]);
-            if (!isNaN(decimals)) {
-                number = d3.round(number, decimals);
-                localeStringOptions.minimumFractionDigits = decimals;
-            }
+      const localeStringOptions = {};
 
-            // currency
-            if (settings["scalar.currency"] != null) {
-                localeStringOptions.style = "currency";
-                localeStringOptions.currency = settings["scalar.currency"];
-            }
+      // decimals
+      let decimals = parseFloat(settings["scalar.decimals"]);
+      if (!isNaN(decimals)) {
+        number = d3.round(number, decimals);
+        localeStringOptions.minimumFractionDigits = decimals;
+      }
 
-            try {
-                // format with separators and correct number of decimals
-                if (settings["scalar.locale"]) {
-                    number = number.toLocaleString(settings["scalar.locale"], localeStringOptions);
-                } else {
-                    // HACK: no locales that don't thousands separators?
-                    number = number.toLocaleString("en", localeStringOptions).replace(/,/g, "");
-                }
-            } catch (e) {
-                console.warn("error formatting scalar", e);
-            }
-            fullScalarValue = formatValue(number, { column: column });
+      // currency
+      if (settings["scalar.currency"] != null) {
+        localeStringOptions.style = "currency";
+        localeStringOptions.currency = settings["scalar.currency"];
+      }
+
+      try {
+        // format with separators and correct number of decimals
+        if (settings["scalar.locale"]) {
+          number = number.toLocaleString(
+            settings["scalar.locale"],
+            localeStringOptions,
+          );
         } else {
-            fullScalarValue = formatValue(scalarValue, { column: column });
+          // HACK: no locales that don't thousands separators?
+          number = number
+            .toLocaleString("en", localeStringOptions)
+            .replace(/,/g, "");
         }
+      } catch (e) {
+        console.warn("error formatting scalar", e);
+      }
+      fullScalarValue = formatValue(number, { column: column });
+    } else {
+      fullScalarValue = formatValue(scalarValue, { column: column });
+    }
 
-        compactScalarValue = isSmall ? formatValue(scalarValue, { column: column, compact: true }) : fullScalarValue
-
-        if (settings["scalar.prefix"]) {
-            compactScalarValue = settings["scalar.prefix"] + compactScalarValue;
-            fullScalarValue = settings["scalar.prefix"] + fullScalarValue;
-        }
-        if (settings["scalar.suffix"]) {
-            compactScalarValue = compactScalarValue + settings["scalar.suffix"];
-            fullScalarValue = fullScalarValue + settings["scalar.suffix"];
-        }
+    compactScalarValue = isSmall
+      ? formatValue(scalarValue, { column: column, compact: true })
+      : fullScalarValue;
 
-        const clicked = {
-            value: rows[0] && rows[0][0],
-            column: cols[0]
-        };
-        const isClickable = visualizationIsClickable(clicked);
-
-        return (
-            <div className={cx(className, styles.Scalar, styles[isSmall ? "small" : "large"])}>
-                <div className="Card-title absolute top right p1 px2">{actionButtons}</div>
-                <Ellipsified
-                    className={cx(styles.Value, 'ScalarValue text-dark fullscreen-normal-text fullscreen-night-text', {
-                        "text-brand-hover cursor-pointer": isClickable
-                    })}
-                    tooltip={fullScalarValue}
-                    alwaysShowTooltip={fullScalarValue !== compactScalarValue}
-                    style={{maxWidth: '100%'}}
-                >
-                    <span
-                        onClick={isClickable && (() => this._scalar && onVisualizationClick({ ...clicked, element: this._scalar }))}
-                        ref={scalar => this._scalar = scalar}
-                    >
-                        {compactScalarValue}
-                    </span>
-                </Ellipsified>
-                { this.props.isDashboard  && (
-                    <div className={styles.Title + " flex align-center relative"}>
-                        <Ellipsified tooltip={card.name}>
-                            <span
-                                onClick={onChangeCardAndRun && (() => onChangeCardAndRun({ nextCard: card }))}
-                                className={cx("fullscreen-normal-text fullscreen-night-text", {
-                                    "cursor-pointer": !!onChangeCardAndRun
-                                })}
-                            >
-                                <span className="Scalar-title">{settings["card.title"]}</span>
-                            </span>
-
-                        </Ellipsified>
-                        { description &&
-                            <div
-                                className="absolute top bottom hover-child flex align-center justify-center"
-                                style={{ right: -20, top: 2 }}
-                            >
-                              <Tooltip tooltip={description} maxWidth={'22em'}>
-                                  <Icon name='infooutlined' />
-                              </Tooltip>
-                          </div>
-                        }
-                    </div>
-                )}
-            </div>
-        );
+    if (settings["scalar.prefix"]) {
+      compactScalarValue = settings["scalar.prefix"] + compactScalarValue;
+      fullScalarValue = settings["scalar.prefix"] + fullScalarValue;
+    }
+    if (settings["scalar.suffix"]) {
+      compactScalarValue = compactScalarValue + settings["scalar.suffix"];
+      fullScalarValue = fullScalarValue + settings["scalar.suffix"];
     }
+
+    const clicked = {
+      value: rows[0] && rows[0][0],
+      column: cols[0],
+    };
+    const isClickable = visualizationIsClickable(clicked);
+
+    return (
+      <div
+        className={cx(
+          className,
+          styles.Scalar,
+          styles[isSmall ? "small" : "large"],
+        )}
+      >
+        <div className="Card-title absolute top right p1 px2">
+          {actionButtons}
+        </div>
+        <Ellipsified
+          className={cx(
+            styles.Value,
+            "ScalarValue text-dark fullscreen-normal-text fullscreen-night-text",
+            {
+              "text-brand-hover cursor-pointer": isClickable,
+            },
+          )}
+          tooltip={fullScalarValue}
+          alwaysShowTooltip={fullScalarValue !== compactScalarValue}
+          style={{ maxWidth: "100%" }}
+        >
+          <span
+            onClick={
+              isClickable &&
+              (() =>
+                this._scalar &&
+                onVisualizationClick({ ...clicked, element: this._scalar }))
+            }
+            ref={scalar => (this._scalar = scalar)}
+          >
+            {compactScalarValue}
+          </span>
+        </Ellipsified>
+        {this.props.isDashboard && (
+          <div className={styles.Title + " flex align-center relative"}>
+            <Ellipsified tooltip={card.name}>
+              <span
+                onClick={
+                  onChangeCardAndRun &&
+                  (() => onChangeCardAndRun({ nextCard: card }))
+                }
+                className={cx("fullscreen-normal-text fullscreen-night-text", {
+                  "cursor-pointer": !!onChangeCardAndRun,
+                })}
+              >
+                <span className="Scalar-title">{settings["card.title"]}</span>
+              </span>
+            </Ellipsified>
+            {description && (
+              <div
+                className="absolute top bottom hover-child flex align-center justify-center"
+                style={{ right: -20, top: 2 }}
+              >
+                <Tooltip tooltip={description} maxWidth={"22em"}>
+                  <Icon name="infooutlined" />
+                </Tooltip>
+              </div>
+            )}
+          </div>
+        )}
+      </div>
+    );
+  }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
index 484c88b72fe6a8e94dc936586a7b86c8abb71206..043d57e13e6abaadb7f0f0788ddafe34b5a5bb9f 100644
--- a/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/ScatterPlot.jsx
@@ -1,30 +1,30 @@
 /* @flow */
 
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import LineAreaBarChart from "../components/LineAreaBarChart.jsx";
 import { scatterRenderer } from "../lib/LineAreaBarRenderer";
 
 import {
-    GRAPH_DATA_SETTINGS,
-    GRAPH_BUBBLE_SETTINGS,
-    GRAPH_GOAL_SETTINGS,
-    GRAPH_COLORS_SETTINGS,
-    GRAPH_AXIS_SETTINGS
+  GRAPH_DATA_SETTINGS,
+  GRAPH_BUBBLE_SETTINGS,
+  GRAPH_GOAL_SETTINGS,
+  GRAPH_COLORS_SETTINGS,
+  GRAPH_AXIS_SETTINGS,
 } from "../lib/settings/graph";
 
 export default class ScatterPlot extends LineAreaBarChart {
-    static uiName = t`Scatter`;
-    static identifier = "scatter";
-    static iconName = "bubble";
-    static noun = t`scatter plot`;
+  static uiName = t`Scatter`;
+  static identifier = "scatter";
+  static iconName = "bubble";
+  static noun = t`scatter plot`;
 
-    static renderer = scatterRenderer;
+  static renderer = scatterRenderer;
 
-    static settings = {
-        ...GRAPH_DATA_SETTINGS,
-        ...GRAPH_BUBBLE_SETTINGS,
-        ...GRAPH_GOAL_SETTINGS,
-        ...GRAPH_COLORS_SETTINGS,
-        ...GRAPH_AXIS_SETTINGS
-    }
+  static settings = {
+    ...GRAPH_DATA_SETTINGS,
+    ...GRAPH_BUBBLE_SETTINGS,
+    ...GRAPH_GOAL_SETTINGS,
+    ...GRAPH_COLORS_SETTINGS,
+    ...GRAPH_AXIS_SETTINGS,
+  };
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/Table.jsx b/frontend/src/metabase/visualizations/visualizations/Table.jsx
index 12e1eb50c76f2326aa40d243142fd1c586842729..dd5289924cbdb321526fe560f9872c035e079e5d 100644
--- a/frontend/src/metabase/visualizations/visualizations/Table.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Table.jsx
@@ -4,12 +4,15 @@ import React, { Component } from "react";
 
 import TableInteractive from "../components/TableInteractive.jsx";
 import TableSimple from "../components/TableSimple.jsx";
-import { t } from 'c-3po';
+import { t } from "c-3po";
 import * as DataGrid from "metabase/lib/data_grid";
 
 import Query from "metabase/lib/query";
 import { isMetric, isDimension } from "metabase/lib/schema_metadata";
-import { columnsAreValid, getFriendlyName } from "metabase/visualizations/lib/utils";
+import {
+  columnsAreValid,
+  getFriendlyName,
+} from "metabase/visualizations/lib/utils";
 import ChartSettingOrderedFields from "metabase/visualizations/components/settings/ChartSettingOrderedFields.jsx";
 
 import _ from "underscore";
@@ -21,145 +24,162 @@ import type { DatasetData } from "metabase/meta/types/Dataset";
 import type { Card, VisualizationSettings } from "metabase/meta/types/Card";
 
 type Props = {
-    card: Card,
-    data: DatasetData,
-    settings: VisualizationSettings,
-    isDashboard: boolean,
-}
+  card: Card,
+  data: DatasetData,
+  settings: VisualizationSettings,
+  isDashboard: boolean,
+};
 type State = {
-    data: ?DatasetData
-}
+  data: ?DatasetData,
+};
 
 export default class Table extends Component {
-    props: Props;
-    state: State;
-
-    static uiName = t`Table`;
-    static identifier = "table";
-    static iconName = "table";
-
-    static minSize = { width: 4, height: 3 };
-
-    static isSensible(cols, rows) {
-        return true;
-    }
-
-    static checkRenderable([{ data: { cols, rows} }]) {
-        // scalar can always be rendered, nothing needed here
+  props: Props;
+  state: State;
+
+  static uiName = t`Table`;
+  static identifier = "table";
+  static iconName = "table";
+
+  static minSize = { width: 4, height: 3 };
+
+  static isSensible(cols, rows) {
+    return true;
+  }
+
+  static checkRenderable([{ data: { cols, rows } }]) {
+    // scalar can always be rendered, nothing needed here
+  }
+
+  static settings = {
+    "table.pivot": {
+      title: t`Pivot the table`,
+      widget: "toggle",
+      getHidden: ([{ card, data }]) => data && data.cols.length !== 3,
+      getDefault: ([{ card, data }]) =>
+        data &&
+        data.cols.length === 3 &&
+        Query.isStructured(card.dataset_query) &&
+        data.cols.filter(isMetric).length === 1 &&
+        data.cols.filter(isDimension).length === 2,
+    },
+    "table.columns": {
+      title: t`Fields to include`,
+      widget: ChartSettingOrderedFields,
+      getHidden: (series, vizSettings) => vizSettings["table.pivot"],
+      isValid: ([{ card, data }]) =>
+        card.visualization_settings["table.columns"] &&
+        columnsAreValid(
+          card.visualization_settings["table.columns"].map(x => x.name),
+          data,
+        ),
+      getDefault: ([{ data: { cols } }]) =>
+        cols.map(col => ({
+          name: col.name,
+          enabled: col.visibility_type !== "details-only",
+        })),
+      getProps: ([{ data: { cols } }]) => ({
+        columnNames: cols.reduce(
+          (o, col) => ({ ...o, [col.name]: getFriendlyName(col) }),
+          {},
+        ),
+      }),
+    },
+    "table.column_widths": {},
+  };
+
+  constructor(props: Props) {
+    super(props);
+
+    this.state = {
+      data: null,
+    };
+  }
+
+  componentWillMount() {
+    this._updateData(this.props);
+  }
+
+  componentWillReceiveProps(newProps: Props) {
+    // TODO: remove use of deprecated "card" and "data" props
+    if (
+      newProps.data !== this.props.data ||
+      !_.isEqual(newProps.settings, this.props.settings)
+    ) {
+      this._updateData(newProps);
     }
+  }
 
-    static settings = {
-        "table.pivot": {
-            title: t`Pivot the table`,
-            widget: "toggle",
-            getHidden: ([{ card, data }]) => (
-                data && data.cols.length !== 3
-            ),
-            getDefault: ([{ card, data }]) => (
-                (data && data.cols.length === 3) &&
-                Query.isStructured(card.dataset_query) &&
-                data.cols.filter(isMetric).length === 1 &&
-                data.cols.filter(isDimension).length === 2
-            )
-        },
-        "table.columns": {
-            title: t`Fields to include`,
-            widget: ChartSettingOrderedFields,
-            getHidden: (series, vizSettings) => vizSettings["table.pivot"],
-            isValid: ([{ card, data }]) =>
-                card.visualization_settings["table.columns"] &&
-                columnsAreValid(card.visualization_settings["table.columns"].map(x => x.name), data),
-            getDefault: ([{ data: { cols }}]) => cols.map(col => ({
-                name: col.name,
-                enabled: col.visibility_type !== "details-only"
-            })),
-            getProps: ([{ data: { cols }}]) => ({
-                columnNames: cols.reduce((o, col) => ({ ...o, [col.name]: getFriendlyName(col)}), {})
-            })
-        },
-        "table.column_widths": {
+  _updateData({
+    data,
+    settings,
+  }: {
+    data: DatasetData,
+    settings: VisualizationSettings,
+  }) {
+    if (settings["table.pivot"]) {
+      this.setState({
+        data: DataGrid.pivot(data),
+      });
+    } else {
+      const { cols, rows, columns } = data;
+      const columnIndexes = settings["table.columns"]
+        .filter(f => f.enabled)
+        .map(f => _.findIndex(cols, c => c.name === f.name))
+        .filter(i => i >= 0 && i < cols.length);
+
+      this.setState({
+        data: {
+          cols: columnIndexes.map(i => cols[i]),
+          columns: columnIndexes.map(i => columns[i]),
+          rows: rows.map(row => columnIndexes.map(i => row[i])),
         },
+      });
     }
-
-    constructor(props: Props) {
-        super(props);
-
-        this.state = {
-            data: null
-        };
-    }
-
-    componentWillMount() {
-        this._updateData(this.props);
-    }
-
-    componentWillReceiveProps(newProps: Props) {
-        // TODO: remove use of deprecated "card" and "data" props
-        if (newProps.data !== this.props.data || !_.isEqual(newProps.settings, this.props.settings)) {
-            this._updateData(newProps);
-        }
-    }
-
-    _updateData({ data, settings }: { data: DatasetData, settings: VisualizationSettings }) {
-        if (settings["table.pivot"]) {
-            this.setState({
-                data: DataGrid.pivot(data)
-            });
-        } else {
-            const { cols, rows, columns } = data;
-            const columnIndexes = settings["table.columns"]
-                .filter(f => f.enabled)
-                .map(f => _.findIndex(cols, (c) => c.name === f.name))
-                .filter(i => i >= 0 && i < cols.length);
-
-            this.setState({
-                data: {
-                    cols: columnIndexes.map(i => cols[i]),
-                    columns: columnIndexes.map(i => columns[i]),
-                    rows: rows.map(row => columnIndexes.map(i => row[i]))
-                }
-            });
-        }
+  }
+
+  render() {
+    const { card, isDashboard, settings } = this.props;
+    const { data } = this.state;
+    const sort = getIn(card, ["dataset_query", "query", "order_by"]) || null;
+    const isPivoted = settings["table.pivot"];
+    const isColumnsDisabled =
+      (settings["table.columns"] || []).filter(f => f.enabled).length < 1;
+    const TableComponent = isDashboard ? TableSimple : TableInteractive;
+
+    if (!data) {
+      return null;
     }
 
-    render() {
-        const { card, isDashboard, settings } = this.props;
-        const { data } = this.state;
-        const sort = getIn(card, ["dataset_query", "query", "order_by"]) || null;
-        const isPivoted = settings["table.pivot"];
-        const isColumnsDisabled = (settings["table.columns"] || []).filter(f => f.enabled).length < 1;
-        const TableComponent = isDashboard ? TableSimple : TableInteractive;
-
-        if (!data) {
-            return null;
-        }
-
-        if (isColumnsDisabled) {
-            return (
-                <div className={cx("flex-full px1 pb1 text-centered flex flex-column layout-centered", { "text-slate-light": isDashboard, "text-slate": !isDashboard })} >
-                    <RetinaImage
-                        width={99}
-                        src="app/assets/img/hidden-field.png"
-                        forceOriginalDimensions={false}
-                        className="mb2"
-                    />
-                    <span className="h4 text-bold">
-                        Every field is hidden right now
-                    </span>
-                </div>
-            )
-        } else {
-            return (
-                // $FlowFixMe
-                <TableComponent
-                    {...this.props}
-                    data={data}
-                    isPivoted={isPivoted}
-                    sort={sort}
-                />
-            );
-        }
+    if (isColumnsDisabled) {
+      return (
+        <div
+          className={cx(
+            "flex-full px1 pb1 text-centered flex flex-column layout-centered",
+            { "text-slate-light": isDashboard, "text-slate": !isDashboard },
+          )}
+        >
+          <RetinaImage
+            width={99}
+            src="app/assets/img/hidden-field.png"
+            forceOriginalDimensions={false}
+            className="mb2"
+          />
+          <span className="h4 text-bold">Every field is hidden right now</span>
+        </div>
+      );
+    } else {
+      return (
+        // $FlowFixMe
+        <TableComponent
+          {...this.props}
+          data={data}
+          isPivoted={isPivoted}
+          sort={sort}
+        />
+      );
     }
+  }
 }
 
 /**
@@ -167,9 +187,11 @@ export default class Table extends Component {
  * It always uses TableSimple which Enzyme is able to render correctly.
  * TableInteractive uses react-virtualized library which requires a real browser viewport.
  */
-export const TestTable = (props: Props) => <Table {...props} isDashboard={true} />
+export const TestTable = (props: Props) => (
+  <Table {...props} isDashboard={true} />
+);
 TestTable.uiName = Table.uiName;
 TestTable.identifier = Table.identifier;
 TestTable.iconName = Table.iconName;
 TestTable.minSize = Table.minSize;
-TestTable.settings = Table.settings;
\ No newline at end of file
+TestTable.settings = Table.settings;
diff --git a/frontend/src/metabase/visualizations/visualizations/Text.css b/frontend/src/metabase/visualizations/visualizations/Text.css
index cba2218ea83149235cfd6576e227cd241aeb4dee..4baca99c897a67e6add07950719c7f35882ef375 100644
--- a/frontend/src/metabase/visualizations/visualizations/Text.css
+++ b/frontend/src/metabase/visualizations/visualizations/Text.css
@@ -1,5 +1,5 @@
 :root {
-  --text-card-padding: .65em;
+  --text-card-padding: 0.65em;
 }
 
 :local .Text {
@@ -12,7 +12,7 @@
   padding: 2.8em 1.3em var(--text-card-padding) 1.3em;
 }
 :local .Text .text-card-textarea {
-  padding: .5em .75em .5em .75em;
+  padding: 0.5em 0.75em 0.5em 0.75em;
   font-size: 1.143em;
   line-height: 1.602em;
   pointer-events: all;
@@ -23,63 +23,63 @@
 :local .Text .text-card-textarea:focus {
   border-color: var(--brand-color);
   background-color: white;
-  box-shadow: 0 1px 7px rgba(0, 0, 0, .1);
+  box-shadow: 0 1px 7px rgba(0, 0, 0, 0.1);
 }
 :local .Text .text-card-markdown {
   overflow: auto;
-  pointer-events: all
+  pointer-events: all;
 }
 
 :local .Text .text-card-markdown h1 {
   font-weight: 900;
   font-size: 1.831em;
-  margin: .375em 0 .25em 0;
+  margin: 0.375em 0 0.25em 0;
   padding-right: 1.5em;
 }
 :local .Text .text-card-markdown h2 {
   font-size: 1.627em;
-  margin: .375em 0 .25em 0;
+  margin: 0.375em 0 0.25em 0;
   padding-right: 1.5em;
 }
 :local .Text .text-card-markdown h3 {
   font-size: 1.447em;
-  margin: .375em 0 .25em 0;
+  margin: 0.375em 0 0.25em 0;
   padding-right: 1.5em;
 }
 :local .Text .text-card-markdown h4 {
   font-size: 1.286em;
-  margin: .375em 0 .25em 0;
+  margin: 0.375em 0 0.25em 0;
   padding-right: 1.5em;
 }
 :local .Text .text-card-markdown h5 {
   font-size: 1.143em;
-  margin: .375em 0 .25em 0;
+  margin: 0.375em 0 0.25em 0;
   padding-right: 1.5em;
 }
 :local .Text .text-card-markdown p {
   font-size: 1.143em;
   line-height: 1.602em;
   padding: 0;
-  margin: 0 1.5em .5em 0;
+  margin: 0 1.5em 0.5em 0;
   max-width: 620px;
 }
 
 :local .text-card-markdown ul {
   font-size: 16px;
-  margin: .5em 0 .5em 0;
+  margin: 0.5em 0 0.5em 0;
   padding: 0 1.5em 0 1em;
   list-style-type: disc;
 }
 :local .text-card-markdown ol {
   font-size: 16px;
-  margin: .5em 0 .5em 0;
+  margin: 0.5em 0 0.5em 0;
   padding: 0 1.5em 0 1em;
   list-style-type: decimal;
 }
 
 :local .text-card-markdown li {
   list-style-position: inside;
-  padding: .25em 0 0 0;
+  padding: 0.25em 0 0 0;
 }
 :local .text-card-markdown ol li::before {
   content: "";
@@ -101,7 +101,9 @@
   text-decoration: underline;
 }
 
-:local .text-card-markdown th { text-align: left; }
+:local .text-card-markdown th {
+  text-align: left;
+}
 :local .text-card-markdown table {
   /* standard table reset */
   border-collapse: collapse;
@@ -122,7 +124,7 @@
 }
 :local .text-card-markdown th,
 :local .text-card-markdown td {
-  padding: .75em;
+  padding: 0.75em;
   border: 1px solid var(--table-border-color);
 }
 
@@ -130,7 +132,7 @@
   font-family: Monaco, monospace;
   font-size: 12.64px;
   line-height: 20px;
-  padding: 0 .25em;
+  padding: 0 0.25em;
   background-color: var(--base-grey);
   border-radius: var(--default-border-radius);
 }
@@ -145,7 +147,7 @@
   color: var(--grey-4);
   border-left: 5px solid var(--grey-1);
   padding: 0 1.5em 0 17px;
-  margin: .5em 0 .5em 1em;
+  margin: 0.5em 0 0.5em 1em;
 }
 :local .text-card-markdown blockquote p {
   padding: 0;
@@ -159,6 +161,6 @@
 
 @media screen and (--breakpoint-max-lg) {
   :local .Text {
-    padding: .5em 0 .5em 1em;
+    padding: 0.5em 0 0.5em 1em;
   }
 }
diff --git a/frontend/src/metabase/visualizations/visualizations/Text.jsx b/frontend/src/metabase/visualizations/visualizations/Text.jsx
index 2800f5e20e4b863067702e06800d65eba6bcf65f..03f3abf7708382789ae91b62ab891f72e2def16d 100644
--- a/frontend/src/metabase/visualizations/visualizations/Text.jsx
+++ b/frontend/src/metabase/visualizations/visualizations/Text.jsx
@@ -13,140 +13,187 @@ import type { VisualizationProps } from "metabase/meta/types/Visualization";
 const HEADER_ICON_SIZE = 16;
 
 const HEADER_ACTION_STYLE = {
-    padding: 4
+  padding: 4,
 };
 
 type State = {
-    isShowingRenderedOutput: boolean,
-    text: string
-}
+  isShowingRenderedOutput: boolean,
+  text: string,
+};
 
 export default class Text extends Component {
-    props: VisualizationProps;
-    state: State;
-
-    constructor(props: VisualizationProps) {
-        super(props);
-
-        this.state = {
-            isShowingRenderedOutput: false,
-            text: ""
-        };
-    }
-
-    static uiName = "Text";
-    static identifier = "text";
-    static iconName = "text";
-
-    static disableSettingsConfig = true;
-    static noHeader = true;
-    static supportsSeries = false;
-    static hidden = true;
-
-    static minSize = { width: 4, height: 2 };
-
-    static checkRenderable() {
-        // text can always be rendered, nothing needed here
-    }
-
-    static settings = {
-        "text": {
-            value: "",
-            default: ""
-        }
-    }
-
-    componentWillReceiveProps(newProps: VisualizationProps) {
-        // dashboard is going into edit mode
-        if (!this.props.isEditing && newProps.isEditing) {
-            this.onEdit();
-        }
-    }
-
-    handleTextChange(text: string) {
-        this.props.onUpdateVisualizationSettings({ "text": text });
-    }
-
-    onEdit() {
-        this.setState({ isShowingRenderedOutput: false });
-    }
-
-    onPreview() {
-        this.setState({ isShowingRenderedOutput: true });
+  props: VisualizationProps;
+  state: State;
+
+  constructor(props: VisualizationProps) {
+    super(props);
+
+    this.state = {
+      isShowingRenderedOutput: false,
+      text: "",
+    };
+  }
+
+  static uiName = "Text";
+  static identifier = "text";
+  static iconName = "text";
+
+  static disableSettingsConfig = true;
+  static noHeader = true;
+  static supportsSeries = false;
+  static hidden = true;
+
+  static minSize = { width: 4, height: 2 };
+
+  static checkRenderable() {
+    // text can always be rendered, nothing needed here
+  }
+
+  static settings = {
+    text: {
+      value: "",
+      default: "",
+    },
+  };
+
+  componentWillReceiveProps(newProps: VisualizationProps) {
+    // dashboard is going into edit mode
+    if (!this.props.isEditing && newProps.isEditing) {
+      this.onEdit();
     }
-
-    render() {
-        let { className, actionButtons, gridSize, settings, isEditing } = this.props;
-        let isSmall = gridSize && gridSize.width < 4;
-
-        if (isEditing) {
-            return (
-                <div className={cx(className, styles.Text, styles[isSmall ? "small" : "large"], styles["dashboard-is-editing"])}>
-                    <TextActionButtons
-                        actionButtons={actionButtons}
-                        isShowingRenderedOutput={this.state.isShowingRenderedOutput}
-                        onEdit={this.onEdit.bind(this)}
-                        onPreview={this.onPreview.bind(this)}
-                    />
-                    {this.state.isShowingRenderedOutput ?
-                        <ReactMarkdown
-                            className={cx("full flex-full flex flex-column text-card-markdown", styles["text-card-markdown"])}
-                            source={settings.text}
-                        />
-                    :
-                        <textarea
-                            className={cx("full flex-full flex flex-column bg-grey-0 bordered drag-disabled", styles["text-card-textarea"])}
-                            name="text"
-                            placeholder="Write here, and use Markdown if you'd like"
-                            value={settings.text}
-                            onChange={(e) => this.handleTextChange(e.target.value)}
-                        />
-                    }
-                </div>
-            );
-        } else {
-            return (
-                <div className={cx(className, styles.Text, styles[isSmall ? "small" : "large"])}>
-                    <ReactMarkdown
-                        className={cx("full flex-full flex flex-column text-card-markdown", styles["text-card-markdown"])}
-                        source={settings.text}
-                    />
-                </div>
-            );
-        }
+  }
+
+  handleTextChange(text: string) {
+    this.props.onUpdateVisualizationSettings({ text: text });
+  }
+
+  onEdit() {
+    this.setState({ isShowingRenderedOutput: false });
+  }
+
+  onPreview() {
+    this.setState({ isShowingRenderedOutput: true });
+  }
+
+  render() {
+    let {
+      className,
+      actionButtons,
+      gridSize,
+      settings,
+      isEditing,
+    } = this.props;
+    let isSmall = gridSize && gridSize.width < 4;
+
+    if (isEditing) {
+      return (
+        <div
+          className={cx(
+            className,
+            styles.Text,
+            styles[isSmall ? "small" : "large"],
+            styles["dashboard-is-editing"],
+          )}
+        >
+          <TextActionButtons
+            actionButtons={actionButtons}
+            isShowingRenderedOutput={this.state.isShowingRenderedOutput}
+            onEdit={this.onEdit.bind(this)}
+            onPreview={this.onPreview.bind(this)}
+          />
+          {this.state.isShowingRenderedOutput ? (
+            <ReactMarkdown
+              className={cx(
+                "full flex-full flex flex-column text-card-markdown",
+                styles["text-card-markdown"],
+              )}
+              source={settings.text}
+            />
+          ) : (
+            <textarea
+              className={cx(
+                "full flex-full flex flex-column bg-grey-0 bordered drag-disabled",
+                styles["text-card-textarea"],
+              )}
+              name="text"
+              placeholder="Write here, and use Markdown if you'd like"
+              value={settings.text}
+              onChange={e => this.handleTextChange(e.target.value)}
+            />
+          )}
+        </div>
+      );
+    } else {
+      return (
+        <div
+          className={cx(
+            className,
+            styles.Text,
+            styles[isSmall ? "small" : "large"],
+          )}
+        >
+          <ReactMarkdown
+            className={cx(
+              "full flex-full flex flex-column text-card-markdown",
+              styles["text-card-markdown"],
+            )}
+            source={settings.text}
+          />
+        </div>
+      );
     }
+  }
 }
 
-const TextActionButtons = ({ actionButtons, isShowingRenderedOutput, onEdit, onPreview }) =>
-    <div className="Card-title">
-        <div className="absolute top left p1 px2">
-            <span className="DashCard-actions-persistent flex align-center" style={{ lineHeight: 1 }}>
-                <a
-                    data-metabase-event={"Dashboard;Text;edit"}
-                    className={cx(" cursor-pointer h3 flex-no-shrink relative mr1", { "text-grey-2 text-grey-4-hover": isShowingRenderedOutput, "text-brand": !isShowingRenderedOutput })}
-                    onClick={onEdit}
-                    style={HEADER_ACTION_STYLE}
-                >
-                    <span className="flex align-center">
-                        <span className="flex">
-                            <Icon name="editdocument" style={{ top: 0, left: 0 }} size={HEADER_ICON_SIZE} />
-                        </span>
-                    </span>
-                </a>
-
-                <a
-                    data-metabase-event={"Dashboard;Text;preview"}
-                    className={cx(" cursor-pointer h3 flex-no-shrink relative mr1", { "text-grey-2 text-grey-4-hover": !isShowingRenderedOutput, "text-brand": isShowingRenderedOutput })}
-                    onClick={onPreview}
-                    style={HEADER_ACTION_STYLE}
-                >
-                    <span className="flex align-center">
-                        <span className="flex">
-                            <Icon name="eye" style={{ top: 0, left: 0 }} size={20} />
-                        </span>
-                    </span>
-                </a>
+const TextActionButtons = ({
+  actionButtons,
+  isShowingRenderedOutput,
+  onEdit,
+  onPreview,
+}) => (
+  <div className="Card-title">
+    <div className="absolute top left p1 px2">
+      <span
+        className="DashCard-actions-persistent flex align-center"
+        style={{ lineHeight: 1 }}
+      >
+        <a
+          data-metabase-event={"Dashboard;Text;edit"}
+          className={cx(" cursor-pointer h3 flex-no-shrink relative mr1", {
+            "text-grey-2 text-grey-4-hover": isShowingRenderedOutput,
+            "text-brand": !isShowingRenderedOutput,
+          })}
+          onClick={onEdit}
+          style={HEADER_ACTION_STYLE}
+        >
+          <span className="flex align-center">
+            <span className="flex">
+              <Icon
+                name="editdocument"
+                style={{ top: 0, left: 0 }}
+                size={HEADER_ICON_SIZE}
+              />
             </span>
-        </div>
-        <div className="absolute top right p1 px2">{actionButtons}</div>
+          </span>
+        </a>
+
+        <a
+          data-metabase-event={"Dashboard;Text;preview"}
+          className={cx(" cursor-pointer h3 flex-no-shrink relative mr1", {
+            "text-grey-2 text-grey-4-hover": !isShowingRenderedOutput,
+            "text-brand": isShowingRenderedOutput,
+          })}
+          onClick={onPreview}
+          style={HEADER_ACTION_STYLE}
+        >
+          <span className="flex align-center">
+            <span className="flex">
+              <Icon name="eye" style={{ top: 0, left: 0 }} size={20} />
+            </span>
+          </span>
+        </a>
+      </span>
     </div>
+    <div className="absolute top right p1 px2">{actionButtons}</div>
+  </div>
+);
diff --git a/frontend/src/metabase/xray/Histogram.jsx b/frontend/src/metabase/xray/Histogram.jsx
index 1d53d0b3e72a1ed4edbb7fec3e16493459a181be..e7936ad2ebf809d18f20c6aa842d5f8d58ce1f65 100644
--- a/frontend/src/metabase/xray/Histogram.jsx
+++ b/frontend/src/metabase/xray/Histogram.jsx
@@ -1,36 +1,36 @@
-import React from 'react'
-import Visualization from 'metabase/visualizations/components/Visualization'
+import React from "react";
+import Visualization from "metabase/visualizations/components/Visualization";
 
-import { normal } from 'metabase/lib/colors'
+import { normal } from "metabase/lib/colors";
 
-const Histogram = ({ histogram, color, showAxis }) =>
-    <Visualization
-        className="full-height"
-        rawSeries={[
-            {
-                card: {
-                    display: "bar",
-                    visualization_settings: {
-                        "graph.colors": color,
-                        "graph.x_axis.axis_enabled": showAxis,
-                        "graph.x_axis.labels_enabled": showAxis,
-                        "graph.y_axis.axis_enabled": showAxis,
-                        "graph.y_axis.labels_enabled": showAxis
-                    }
-                },
-                data: {
-                    ...histogram,
-                    rows: histogram.rows.map(row => [row[0], row[1] * 100])
-                }
-            }
-        ]}
-        showTitle={false}
-    />
+const Histogram = ({ histogram, color, showAxis }) => (
+  <Visualization
+    className="full-height"
+    rawSeries={[
+      {
+        card: {
+          display: "bar",
+          visualization_settings: {
+            "graph.colors": color,
+            "graph.x_axis.axis_enabled": showAxis,
+            "graph.x_axis.labels_enabled": showAxis,
+            "graph.y_axis.axis_enabled": showAxis,
+            "graph.y_axis.labels_enabled": showAxis,
+          },
+        },
+        data: {
+          ...histogram,
+          rows: histogram.rows.map(row => [row[0], row[1] * 100]),
+        },
+      },
+    ]}
+    showTitle={false}
+  />
+);
 
 Histogram.defaultProps = {
-    color: [normal.blue],
-    showAxis: true
-}
-
-export default Histogram
+  color: [normal.blue],
+  showAxis: true,
+};
 
+export default Histogram;
diff --git a/frontend/src/metabase/xray/SimpleStat.jsx b/frontend/src/metabase/xray/SimpleStat.jsx
index bc17543e5d378269516b510fa4c78a6c521d72a4..cf3bb263a4d8dc682e2a0f33ba87877c5553fe0d 100644
--- a/frontend/src/metabase/xray/SimpleStat.jsx
+++ b/frontend/src/metabase/xray/SimpleStat.jsx
@@ -1,21 +1,20 @@
-import React from 'react'
-import Tooltip from 'metabase/components/Tooltip'
-import Icon from 'metabase/components/Icon'
+import React from "react";
+import Tooltip from "metabase/components/Tooltip";
+import Icon from "metabase/components/Icon";
 
-const SimpleStat = ({ stat, showDescription }) =>
-    <div>
-        <div className="flex align-center">
-            <h3 className="mr4 text-grey-4">{stat.label}</h3>
-            { showDescription && (
-                <Tooltip tooltip={stat.description}>
-                    <Icon name='infooutlined' />
-                </Tooltip>
-            )}
-        </div>
-        { /* call toString to ensure that values like true / false show up */ }
-        <h1 className="my1">
-            {stat.value ? stat.value.toString() : "No value"}
-        </h1>
+const SimpleStat = ({ stat, showDescription }) => (
+  <div>
+    <div className="flex align-center">
+      <h3 className="mr4 text-grey-4">{stat.label}</h3>
+      {showDescription && (
+        <Tooltip tooltip={stat.description}>
+          <Icon name="infooutlined" />
+        </Tooltip>
+      )}
     </div>
+    {/* call toString to ensure that values like true / false show up */}
+    <h1 className="my1">{stat.value ? stat.value.toString() : "No value"}</h1>
+  </div>
+);
 
-export default SimpleStat
+export default SimpleStat;
diff --git a/frontend/src/metabase/xray/components/ComparisonDropdown.jsx b/frontend/src/metabase/xray/components/ComparisonDropdown.jsx
index 8a033cf7fd7371005978701a9b059a3e303333cc..beedd86c61f7a768fa19754c40f5c6f84c2cfe42 100644
--- a/frontend/src/metabase/xray/components/ComparisonDropdown.jsx
+++ b/frontend/src/metabase/xray/components/ComparisonDropdown.jsx
@@ -1,72 +1,82 @@
-import React, { Component } from 'react'
+import React, { Component } from "react";
 import { Link } from "react-router";
 import Select, { Option } from "metabase/components/Select";
 import Icon from "metabase/components/Icon";
 
 const MODEL_ICONS = {
-    "segment": "segment",
-    "table": "table",
-    "card": "table2"
-}
+  segment: "segment",
+  table: "table",
+  card: "table2",
+};
 
 export class ComparisonDropdown extends Component {
-    props: {
-        // Models of current comparison – you can enter only the left side of comparison with an array of a single model
-        models: any[],
-        comparables: any[],
-        updatingModelAtIndex: number,
-        triggerElement?: any
-    }
+  props: {
+    // Models of current comparison – you can enter only the left side of comparison with an array of a single model
+    models: any[],
+    comparables: any[],
+    updatingModelAtIndex: number,
+    triggerElement?: any,
+  };
 
-    static defaultProps = {
-        updatingModelAtIndex: 1
-    }
+  static defaultProps = {
+    updatingModelAtIndex: 1,
+  };
 
-    getComparisonUrl = (comparableModel) => {
-        const { models, updatingModelAtIndex } = this.props
+  getComparisonUrl = comparableModel => {
+    const { models, updatingModelAtIndex } = this.props;
 
-        let comparisonModels = Object.assign([...models], {
-            [updatingModelAtIndex]: comparableModel
-        })
+    let comparisonModels = Object.assign([...models], {
+      [updatingModelAtIndex]: comparableModel,
+    });
 
-        const isSharedModelType = comparisonModels[0]["type-tag"] === comparisonModels[1]["type-tag"]
-        if (isSharedModelType) {
-            return `/xray/compare/${comparisonModels[0]["type-tag"]}s/${comparisonModels[0].id}/${comparisonModels[1].id}/approximate`
-        } else {
-            return `/xray/compare/${comparisonModels[0]["type-tag"]}/${comparisonModels[0].id}/${comparisonModels[1]["type-tag"]}/${comparisonModels[1].id}/approximate`
-        }
+    const isSharedModelType =
+      comparisonModels[0]["type-tag"] === comparisonModels[1]["type-tag"];
+    if (isSharedModelType) {
+      return `/xray/compare/${comparisonModels[0]["type-tag"]}s/${
+        comparisonModels[0].id
+      }/${comparisonModels[1].id}/approximate`;
+    } else {
+      return `/xray/compare/${comparisonModels[0]["type-tag"]}/${
+        comparisonModels[0].id
+      }/${comparisonModels[1]["type-tag"]}/${
+        comparisonModels[1].id
+      }/approximate`;
     }
+  };
 
-    render() {
-        const { comparables, triggerElement } = this.props;
+  render() {
+    const { comparables, triggerElement } = this.props;
 
-        return (
-            <Select
-                value={null}
-                // TODO Atte Keinänen: Use links instead of this kind of logic
-                triggerElement={
-                    triggerElement ||
-                        <div className="Button bg-white text-brand-hover no-decoration">
-                            <Icon name="compare" className="mr1" />
-                            {`Compare with...`}
-                            <Icon name="chevrondown" size={12} className="ml1" />
-                        </div>
-                }
+    return (
+      <Select
+        value={null}
+        // TODO Atte Keinänen: Use links instead of this kind of logic
+        triggerElement={
+          triggerElement || (
+            <div className="Button bg-white text-brand-hover no-decoration">
+              <Icon name="compare" className="mr1" />
+              {`Compare with...`}
+              <Icon name="chevrondown" size={12} className="ml1" />
+            </div>
+          )
+        }
+      >
+        {comparables.map((comparableModel, index) => (
+          <Link
+            to={this.getComparisonUrl(comparableModel)}
+            className="no-decoration"
+          >
+            <Option
+              key={index}
+              value={comparableModel}
+              icon={MODEL_ICONS[comparableModel["type-tag"]]}
+              iconColor={"#DFE8EA"}
             >
-                { comparables
-                    .map((comparableModel, index) =>
-                        <Link to={this.getComparisonUrl(comparableModel)} className="no-decoration">
-                            <Option
-                                key={index}
-                                value={comparableModel}
-                                icon={MODEL_ICONS[comparableModel["type-tag"]]}
-                                iconColor={"#DFE8EA"}
-                            >
-                                {comparableModel.display_name || comparableModel.name}
-                            </Option>
-                        </Link>
-                    )}
-            </Select>
-        )
-    }
-}
\ No newline at end of file
+              {comparableModel.display_name || comparableModel.name}
+            </Option>
+          </Link>
+        ))}
+      </Select>
+    );
+  }
+}
diff --git a/frontend/src/metabase/xray/components/ComparisonHeader.jsx b/frontend/src/metabase/xray/components/ComparisonHeader.jsx
index e7b7cb1ce6acd31f7b7c31fddc99fe9fb5d41aff..9c36aa2f73f8b174a3141fe39eab03c519e60bea 100644
--- a/frontend/src/metabase/xray/components/ComparisonHeader.jsx
+++ b/frontend/src/metabase/xray/components/ComparisonHeader.jsx
@@ -1,20 +1,19 @@
-import React from 'react'
-import { t } from 'c-3po';
-import Icon from 'metabase/components/Icon'
-import CostSelect from 'metabase/xray/components/CostSelect'
+import React from "react";
+import { t } from "c-3po";
+import Icon from "metabase/components/Icon";
+import CostSelect from "metabase/xray/components/CostSelect";
 
-const ComparisonHeader = ({ cost }) =>
-    <div className="my4 flex align-center">
-        <h1 className="flex align-center">
-            <Icon name="compare" className="mr1" size={32} />
-            {t`Comparing`}
-        </h1>
-        <div className="ml-auto flex align-center">
-            <h3 className="text-grey-3 mr1">{t`Fidelity`}</h3>
-            <CostSelect
-                currentCost={cost}
-            />
-        </div>
+const ComparisonHeader = ({ cost }) => (
+  <div className="my4 flex align-center">
+    <h1 className="flex align-center">
+      <Icon name="compare" className="mr1" size={32} />
+      {t`Comparing`}
+    </h1>
+    <div className="ml-auto flex align-center">
+      <h3 className="text-grey-3 mr1">{t`Fidelity`}</h3>
+      <CostSelect currentCost={cost} />
     </div>
+  </div>
+);
 
-export default ComparisonHeader
+export default ComparisonHeader;
diff --git a/frontend/src/metabase/xray/components/Constituent.jsx b/frontend/src/metabase/xray/components/Constituent.jsx
index 498e09ead3125d179f5a36457c3fb119a517e05c..4f6608ec8cbd868082280c4be18b542f51d91808 100644
--- a/frontend/src/metabase/xray/components/Constituent.jsx
+++ b/frontend/src/metabase/xray/components/Constituent.jsx
@@ -1,40 +1,39 @@
-import React from 'react'
-import { Link } from 'react-router'
+import React from "react";
+import { Link } from "react-router";
 
-import Histogram from 'metabase/xray/Histogram'
-import SimpleStat from 'metabase/xray/SimpleStat'
+import Histogram from "metabase/xray/Histogram";
+import SimpleStat from "metabase/xray/SimpleStat";
 
-const Constituent = ({constituent}) =>
-    <Link
-        to={`xray/field/${constituent.model.id}/approximate`}
-        className="no-decoration"
-    >
-        <div className="Grid my3 bg-white bordered rounded shadowed shadow-hover no-decoration">
-            <div className="Grid-cell Cell--1of3 border-right">
-                <div className="p4">
-                    <h2 className="text-bold text-brand">{constituent.model.display_name}</h2>
-                    <p className="text-measure text-paragraph">{constituent.model.description}</p>
+const Constituent = ({ constituent }) => (
+  <Link
+    to={`xray/field/${constituent.model.id}/approximate`}
+    className="no-decoration"
+  >
+    <div className="Grid my3 bg-white bordered rounded shadowed shadow-hover no-decoration">
+      <div className="Grid-cell Cell--1of3 border-right">
+        <div className="p4">
+          <h2 className="text-bold text-brand">
+            {constituent.model.display_name}
+          </h2>
+          <p className="text-measure text-paragraph">
+            {constituent.model.description}
+          </p>
 
-                    <div className="flex align-center">
-                        { constituent.min && (
-                            <SimpleStat
-                                stat={constituent.min}
-                            />
-                        )}
-                        { constituent.max && (
-                            <SimpleStat
-                                stat={constituent.max}
-                            />
-                        )}
-                    </div>
-                </div>
-            </div>
-            <div className="Grid-cell p3">
-                <div style={{ height: 220 }}>
-                    { constituent.histogram && (<Histogram histogram={constituent.histogram.value} />) }
-                </div>
-            </div>
+          <div className="flex align-center">
+            {constituent.min && <SimpleStat stat={constituent.min} />}
+            {constituent.max && <SimpleStat stat={constituent.max} />}
+          </div>
         </div>
-    </Link>
+      </div>
+      <div className="Grid-cell p3">
+        <div style={{ height: 220 }}>
+          {constituent.histogram && (
+            <Histogram histogram={constituent.histogram.value} />
+          )}
+        </div>
+      </div>
+    </div>
+  </Link>
+);
 
-export default Constituent
+export default Constituent;
diff --git a/frontend/src/metabase/xray/components/CostSelect.jsx b/frontend/src/metabase/xray/components/CostSelect.jsx
index 58f90af02eddf94f2528745357d291bbdaa0cfec..98eadd5ef9ac7afb64b2e347656faea02f4a8130 100644
--- a/frontend/src/metabase/xray/components/CostSelect.jsx
+++ b/frontend/src/metabase/xray/components/CostSelect.jsx
@@ -1,64 +1,60 @@
-import React from 'react'
-import cx from 'classnames'
-import { Link, withRouter } from 'react-router'
-import { connect } from 'react-redux'
-import { getMaxCost } from 'metabase/xray/selectors'
+import React from "react";
+import cx from "classnames";
+import { Link, withRouter } from "react-router";
+import { connect } from "react-redux";
+import { getMaxCost } from "metabase/xray/selectors";
 
-import Icon from 'metabase/components/Icon'
-import Tooltip from 'metabase/components/Tooltip'
+import Icon from "metabase/components/Icon";
+import Tooltip from "metabase/components/Tooltip";
 
-import COSTS from 'metabase/xray/costs'
+import COSTS from "metabase/xray/costs";
 
-const mapStateToProps = (state) => ({
-    maxCost: getMaxCost(state)
-})
+const mapStateToProps = state => ({
+  maxCost: getMaxCost(state),
+});
 
-const getDisabled = (maxCost) => {
-    if(maxCost === 'approximate') {
-        return ['extended', 'exact']
-    } else if (maxCost === 'exact') {
-        return ['extended']
-    }
-    return []
-}
+const getDisabled = maxCost => {
+  if (maxCost === "approximate") {
+    return ["extended", "exact"];
+  } else if (maxCost === "exact") {
+    return ["extended"];
+  }
+  return [];
+};
 
 const CostSelect = ({ currentCost, location, maxCost }) => {
-    const urlWithoutCost = location.pathname.substr(0, location.pathname.lastIndexOf('/'))
-    return (
-        <ol className="bordered rounded shadowed bg-white flex align-center overflow-hidden">
-            { Object.keys(COSTS).map(cost => {
-                const c = COSTS[cost]
-                return (
-                    <Link
-                        to={`${urlWithoutCost}/${cost}`}
-                        className={cx(
-                            'no-decoration',
-                            { 'disabled': getDisabled(maxCost).indexOf(cost) >= 0}
-                        )}
-                        key={cost}
-                    >
-                        <li
-                            key={cost}
-                            className={cx(
-                                "flex align-center justify-center cursor-pointer bg-brand-hover text-white-hover transition-background transition-text text-grey-2",
-                                { 'bg-brand text-white': currentCost === cost },
-                            )}
-                        >
-                            <Tooltip
-                                tooltip={c.description}
-                            >
-                                <Icon
-                                    size={22}
-                                    name={c.icon}
-                                    className="p1 border-right"
-                                />
-                            </Tooltip>
-                        </li>
-                    </Link>
-                )
+  const urlWithoutCost = location.pathname.substr(
+    0,
+    location.pathname.lastIndexOf("/"),
+  );
+  return (
+    <ol className="bordered rounded shadowed bg-white flex align-center overflow-hidden">
+      {Object.keys(COSTS).map(cost => {
+        const c = COSTS[cost];
+        return (
+          <Link
+            to={`${urlWithoutCost}/${cost}`}
+            className={cx("no-decoration", {
+              disabled: getDisabled(maxCost).indexOf(cost) >= 0,
             })}
-        </ol>
-    )
-}
+            key={cost}
+          >
+            <li
+              key={cost}
+              className={cx(
+                "flex align-center justify-center cursor-pointer bg-brand-hover text-white-hover transition-background transition-text text-grey-2",
+                { "bg-brand text-white": currentCost === cost },
+              )}
+            >
+              <Tooltip tooltip={c.description}>
+                <Icon size={22} name={c.icon} className="p1 border-right" />
+              </Tooltip>
+            </li>
+          </Link>
+        );
+      })}
+    </ol>
+  );
+};
 
-export default connect(mapStateToProps)(withRouter(CostSelect))
+export default connect(mapStateToProps)(withRouter(CostSelect));
diff --git a/frontend/src/metabase/xray/components/InsightCard.jsx b/frontend/src/metabase/xray/components/InsightCard.jsx
index 4e902e9ee140c1ecb7c9e925d9cff12563bc3f04..3832837e9939a511156b462e5e1607a0c2c687f2 100644
--- a/frontend/src/metabase/xray/components/InsightCard.jsx
+++ b/frontend/src/metabase/xray/components/InsightCard.jsx
@@ -1,332 +1,385 @@
-import React, { Component } from 'react'
+import React, { Component } from "react";
 import { formatTimeWithUnit } from "metabase/lib/formatting";
 import Icon from "metabase/components/Icon";
 import { Link } from "react-router";
 import Question from "metabase-lib/lib/Question";
 import { TermWithDefinition } from "metabase/components/TermWithDefinition";
-import { t, jt } from 'c-3po'
-
-const InsightText = ({ children }) =>
-    <p className="text-paragraph">
-        {children}
-    </p>
-
-const Feedback = ({ insightType }) =>
-    <div className="flex align-center px1">
-        {t`Was this helpful?`}
-        <div className="ml-auto text-bold">
-            <a className="text-brand-hover" data-metabase-event={`InsightFeedback;${insightType};Yes`}>
-                {t`Yes`}
-            </a>
-            <a className="text-brand-hover ml1" data-metabase-event={`InsightFeedback;${insightType};No`}>
-                {t`No`}
-            </a>
-        </div>
+import { t, jt } from "c-3po";
+
+const InsightText = ({ children }) => (
+  <p className="text-paragraph">{children}</p>
+);
+
+const Feedback = ({ insightType }) => (
+  <div className="flex align-center px1">
+    {t`Was this helpful?`}
+    <div className="ml-auto text-bold">
+      <a
+        className="text-brand-hover"
+        data-metabase-event={`InsightFeedback;${insightType};Yes`}
+      >
+        {t`Yes`}
+      </a>
+      <a
+        className="text-brand-hover ml1"
+        data-metabase-event={`InsightFeedback;${insightType};No`}
+      >
+        {t`No`}
+      </a>
     </div>
+  </div>
+);
 
 export class NormalRangeInsight extends Component {
-    static insightType = "normal-range"
-    static title = t`Normal range of values`
-    static icon = "insight"
+  static insightType = "normal-range";
+  static title = t`Normal range of values`;
+  static icon = "insight";
 
-    render() {
-        const { lower, upper, features: { model } } = this.props
-        return (
-            <InsightText>
-                { jt`Most of the values for ${ model.display_name || model.name } are between ${<b>{ lower }</b>} and ${<b>{ upper }</b>}.` }
-            </InsightText>
-        )
-    }
+  render() {
+    const { lower, upper, features: { model } } = this.props;
+    return (
+      <InsightText>
+        {jt`Most of the values for ${model.display_name ||
+          model.name} are between ${<b>{lower}</b>} and ${<b>{upper}</b>}.`}
+      </InsightText>
+    );
+  }
 }
 
 export class NilsInsight extends Component {
-    static insightType = "nils"
-    static title = t`Missing data`
-    static icon = "warning"
-
-    render() {
-        const { quality, filter, features: { table } } = this.props
-
-        const viewAllRowsUrl = table && Question.create()
-            .query()
-            // imitate the required hydrated metadata format
-            .setTable({ ...table, database: { id: table.db_id }})
-            .addFilter(filter)
-            .question()
-            .getUrl()
-
-        // construct the question with filter
-        return (
-            <InsightText>
-                {t`You have ${ quality } missing (null) values in your data`}.
-                <span> </span>
-                { table && <span><Link to={viewAllRowsUrl}>View all rows</Link> with missing value.</span> }
-            </InsightText>
-        )
-    }
+  static insightType = "nils";
+  static title = t`Missing data`;
+  static icon = "warning";
+
+  render() {
+    const { quality, filter, features: { table } } = this.props;
+
+    const viewAllRowsUrl =
+      table &&
+      Question.create()
+        .query()
+        // imitate the required hydrated metadata format
+        .setTable({ ...table, database: { id: table.db_id } })
+        .addFilter(filter)
+        .question()
+        .getUrl();
+
+    // construct the question with filter
+    return (
+      <InsightText>
+        {t`You have ${quality} missing (null) values in your data`}.
+        <span> </span>
+        {table && (
+          <span>
+            <Link to={viewAllRowsUrl}>View all rows</Link> with missing value.
+          </span>
+        )}
+      </InsightText>
+    );
+  }
 }
 
 export class ZerosInsight extends Component {
-    static insightType = "zeros"
-    static title = t`Zeros in your data`
-    static icon = "warning"
-
-    render() {
-        const { quality, filter, features: { table } } = this.props
-
-        const viewAllRowsUrl = table && Question.create()
-            .query()
-            // imitate the required hydrated metadata format
-            .setTable({ ...table, database: { id: table.db_id }})
-            .addFilter(filter)
-            .question()
-            .getUrl()
-
-        // construct the question with filter
-        return (
-            <InsightText>
-                { t`You have ${ quality } zeros in your data. They may be stand-ins for missing data, or might indicate some other abnormality.` }
-                <span> </span>
-                { table && <span><Link to={viewAllRowsUrl}>View all rows</Link> with zeros.</span> }
-            </InsightText>
-        )
-    }
+  static insightType = "zeros";
+  static title = t`Zeros in your data`;
+  static icon = "warning";
+
+  render() {
+    const { quality, filter, features: { table } } = this.props;
+
+    const viewAllRowsUrl =
+      table &&
+      Question.create()
+        .query()
+        // imitate the required hydrated metadata format
+        .setTable({ ...table, database: { id: table.db_id } })
+        .addFilter(filter)
+        .question()
+        .getUrl();
+
+    // construct the question with filter
+    return (
+      <InsightText>
+        {t`You have ${quality} zeros in your data. They may be stand-ins for missing data, or might indicate some other abnormality.`}
+        <span> </span>
+        {table && (
+          <span>
+            <Link to={viewAllRowsUrl}>View all rows</Link> with zeros.
+          </span>
+        )}
+      </InsightText>
+    );
+  }
 }
 
-const noisinessDefinition = t`Noisy data is highly variable, jumping all over the place with changes carrying relatively little information.`
-const noisinessLink = "https://en.wikipedia.org/wiki/Noisy_data"
+const noisinessDefinition = t`Noisy data is highly variable, jumping all over the place with changes carrying relatively little information.`;
+const noisinessLink = "https://en.wikipedia.org/wiki/Noisy_data";
 
 export class NoisinessInsight extends Component {
-    static insightType = "noisiness"
-    static title = t`Noisy data`
-    static icon = "warning"
+  static insightType = "noisiness";
+  static title = t`Noisy data`;
+  static icon = "warning";
 
-    render() {
-        const { quality, "recommended-resolution": resolution } = this.props
+  render() {
+    const { quality, "recommended-resolution": resolution } = this.props;
 
-        return (
-            <InsightText>
-                Your data is { quality }
-                <span> </span>
-                <TermWithDefinition definition={noisinessDefinition} link={noisinessLink}>
-                    noisy
-                </TermWithDefinition>.
-                { resolution && ` You might consider looking at it by ${resolution}.` }
-            </InsightText>
-        )
-    }
+    return (
+      <InsightText>
+        Your data is {quality}
+        <span> </span>
+        <TermWithDefinition
+          definition={noisinessDefinition}
+          link={noisinessLink}
+        >
+          noisy
+        </TermWithDefinition>.
+        {resolution && ` You might consider looking at it by ${resolution}.`}
+      </InsightText>
+    );
+  }
 }
 
-const autocorrelationDefinition = t`A measure of how much changes in previous values predict future values.`
-const autocorrelationLink = "https://en.wikipedia.org/wiki/Autocorrelation"
+const autocorrelationDefinition = t`A measure of how much changes in previous values predict future values.`;
+const autocorrelationLink = "https://en.wikipedia.org/wiki/Autocorrelation";
 
 export class AutocorrelationInsight extends Component {
-    static insightType = "autocorrelation"
-    static title = t`Autocorrelation`
-    static icon = "insight"
+  static insightType = "autocorrelation";
+  static title = t`Autocorrelation`;
+  static icon = "insight";
 
-    render() {
-        const { quality, lag } = this.props
+  render() {
+    const { quality, lag } = this.props;
 
-        return (
-            <InsightText>
-                Your data has a { quality } <TermWithDefinition definition={autocorrelationDefinition} link={autocorrelationLink}>
-                    autocorrelation
-                </TermWithDefinition> at lag { lag }.
-            </InsightText>
-        )
-    }
+    return (
+      <InsightText>
+        Your data has a {quality}{" "}
+        <TermWithDefinition
+          definition={autocorrelationDefinition}
+          link={autocorrelationLink}
+        >
+          autocorrelation
+        </TermWithDefinition>{" "}
+        at lag {lag}.
+      </InsightText>
+    );
+  }
 }
 
-const variationTrendDefinition = t`How variance in your data is changing over time.`
-const varianceLink = "https://en.wikipedia.org/wiki/Variance"
+const variationTrendDefinition = t`How variance in your data is changing over time.`;
+const varianceLink = "https://en.wikipedia.org/wiki/Variance";
 
 export class VariationTrendInsight extends Component {
-    static insightType = "variation-trend"
-    static title = t`Trending variation`
-    static icon = "insight"
+  static insightType = "variation-trend";
+  static title = t`Trending variation`;
+  static icon = "insight";
 
-    render() {
-        const { mode } = this.props
+  render() {
+    const { mode } = this.props;
 
-        return (
-            <InsightText>
-                It looks like this data has grown { mode }ly  <TermWithDefinition definition={variationTrendDefinition} link={varianceLink}>
-                    varied</TermWithDefinition> over time.
-            </InsightText>
-        )
-    }
+    return (
+      <InsightText>
+        It looks like this data has grown {mode}ly{" "}
+        <TermWithDefinition
+          definition={variationTrendDefinition}
+          link={varianceLink}
+        >
+          varied
+        </TermWithDefinition>{" "}
+        over time.
+      </InsightText>
+    );
+  }
 }
 
 export class SeasonalityInsight extends Component {
-    static insightType = "seasonality"
-    static title = t`Seasonality`
-    static icon = "insight"
+  static insightType = "seasonality";
+  static title = t`Seasonality`;
+  static icon = "insight";
 
-    render() {
-        const { quality } = this.props
+  render() {
+    const { quality } = this.props;
 
-        return (
-            <InsightText>
-                { jt`Your data has a ${ quality } seasonal component.` }
-            </InsightText>
-        )
-    }
+    return (
+      <InsightText>
+        {jt`Your data has a ${quality} seasonal component.`}
+      </InsightText>
+    );
+  }
 }
 
-const multimodalDefinition = t`Data distribution with multiple peaks (modes).`
-const multimodalLink = "https://en.wikipedia.org/wiki/Multimodal_distribution"
+const multimodalDefinition = t`Data distribution with multiple peaks (modes).`;
+const multimodalLink = "https://en.wikipedia.org/wiki/Multimodal_distribution";
 
 export class MultimodalInsight extends Component {
-    static insightType = "multimodal"
-    static title = t`Multimodal`
-    static icon = "warning"
+  static insightType = "multimodal";
+  static title = t`Multimodal`;
+  static icon = "warning";
 
-    render() {
-        return (
-            <InsightText>
-                Your data looks to be <TermWithDefinition definition={multimodalDefinition} link={multimodalLink}>
-                    multimodal
-                </TermWithDefinition>. This is often the case when different segments of data are mixed together.
-            </InsightText>
-        )
-    }
+  render() {
+    return (
+      <InsightText>
+        Your data looks to be{" "}
+        <TermWithDefinition
+          definition={multimodalDefinition}
+          link={multimodalLink}
+        >
+          multimodal
+        </TermWithDefinition>. This is often the case when different segments of
+        data are mixed together.
+      </InsightText>
+    );
+  }
 }
 
 export class OutliersInsight extends Component {
-    static insightType = "outliers"
-    static title = t`Outliers`
-    static icon = "warning"
-
-    render() {
-        const { filter, features: { table } } = this.props
-
-        const viewAllRowsUrl = table && Question.create()
-            .query()
-            // imitate the required hydrated metadata format
-            .setTable({ ...table, database: { id: table.db_id }})
-            .addFilter(filter)
-            .question()
-            .getUrl()
-
-        // construct the question with filter
-        return (
-            <InsightText>
-                You have some outliers.
-                <span> </span>
-                { table && <span><Link to={viewAllRowsUrl}>View all rows</Link> with outliers.</span> }
-            </InsightText>
-        )
-    }
+  static insightType = "outliers";
+  static title = t`Outliers`;
+  static icon = "warning";
+
+  render() {
+    const { filter, features: { table } } = this.props;
+
+    const viewAllRowsUrl =
+      table &&
+      Question.create()
+        .query()
+        // imitate the required hydrated metadata format
+        .setTable({ ...table, database: { id: table.db_id } })
+        .addFilter(filter)
+        .question()
+        .getUrl();
+
+    // construct the question with filter
+    return (
+      <InsightText>
+        You have some outliers.
+        <span> </span>
+        {table && (
+          <span>
+            <Link to={viewAllRowsUrl}>View all rows</Link> with outliers.
+          </span>
+        )}
+      </InsightText>
+    );
+  }
 }
 
 export class StructuralBreaksInsight extends Component {
-    static insightType = "structural-breaks"
-    static title = t`Structural breaks`
-    static icon = "insight"
-
-    render() {
-        const { breaks, features: { resolution } } = this.props
-
-        const breakPoints = breaks.map( (point, idx) => {
-            point = formatTimeWithUnit(point, resolution);
-
-            if (idx == breaks.length - 1 && breaks.length > 1) {
-                return (
-                    <span>, and { point }</span>
-                )
-            } else {
-                return (
-                    <span>{ idx > 0 && <span>, </span>}{ point }</span>
-                )
-            }
-        })
+  static insightType = "structural-breaks";
+  static title = t`Structural breaks`;
+  static icon = "insight";
+
+  render() {
+    const { breaks, features: { resolution } } = this.props;
 
+    const breakPoints = breaks.map((point, idx) => {
+      point = formatTimeWithUnit(point, resolution);
+
+      if (idx == breaks.length - 1 && breaks.length > 1) {
+        return <span>, and {point}</span>;
+      } else {
         return (
-            <InsightText>
-                It looks like your data has
-                { breaks.length > 1 && <span> structural breaks </span>}
-                { breaks.length == 1 && <span> a structural break </span>}
-                at { breakPoints }.
-            </InsightText>
-        )
-    }
+          <span>
+            {idx > 0 && <span>, </span>}
+            {point}
+          </span>
+        );
+      }
+    });
+
+    return (
+      <InsightText>
+        It looks like your data has
+        {breaks.length > 1 && <span> structural breaks </span>}
+        {breaks.length == 1 && <span> a structural break </span>}
+        at {breakPoints}.
+      </InsightText>
+    );
+  }
 }
 
-const stationaryDefinition = t`The mean does not change over time.`
-const stationaryLink = "https://en.wikipedia.org/wiki/Stationary_process"
+const stationaryDefinition = t`The mean does not change over time.`;
+const stationaryLink = "https://en.wikipedia.org/wiki/Stationary_process";
 
 export class StationaryInsight extends Component {
-    static insightType = "stationary"
-    static title = t`Stationary data`
-    static icon = "insight"
+  static insightType = "stationary";
+  static title = t`Stationary data`;
+  static icon = "insight";
 
-    render() {
-        return (
-            <InsightText>
-                Your data looks to be <TermWithDefinition definition={stationaryDefinition} link={stationaryLink}>
-                stationary</TermWithDefinition>.
-            </InsightText>
-        )
-    }
+  render() {
+    return (
+      <InsightText>
+        Your data looks to be{" "}
+        <TermWithDefinition
+          definition={stationaryDefinition}
+          link={stationaryLink}
+        >
+          stationary
+        </TermWithDefinition>.
+      </InsightText>
+    );
+  }
 }
 
 export class TrendInsight extends Component {
-    static insightType = "trend"
-    static title = t`Trend`
-    static icon = "insight"
-
-    render() {
-        const { mode, shape } = this.props
-
-        return(
-            <InsightText>
-                { jt`Your data seems to be ${ mode } ${ shape }.` }
-            </InsightText>
-        )
-    }
-}
+  static insightType = "trend";
+  static title = t`Trend`;
+  static icon = "insight";
 
-const INSIGHT_COMPONENTS = [
-    // any field
-    NilsInsight,
-    // numeric fields
-    NormalRangeInsight,
-    ZerosInsight,
-    MultimodalInsight,
-    OutliersInsight,
-    // timeseries
-    NoisinessInsight,
-    VariationTrendInsight,
-    AutocorrelationInsight,
-    SeasonalityInsight,
-    StructuralBreaksInsight,
-    StationaryInsight,
-    TrendInsight,
-]
-
-export const InsightCard = ({type, props, features}) => {
-    const Insight = INSIGHT_COMPONENTS.find((component) => component.insightType === type)
+  render() {
+    const { mode, shape } = this.props;
 
     return (
-        <div>
-            <div className="bg-white bordered rounded shadowed p3" style={{ height: 180 }}>
-                <header className="flex align-center">
-                    <Icon
-                        name={Insight.icon}
-                        size={24}
-                        className="mr1"
-                        style={{ color: '#93a1ab' }}
-                    />
-                    <span className="text-bold text-uppercase">{Insight.title}</span>
-                </header>
-                <div style={{ lineHeight: '1.4em' }}>
-                    <Insight {...props} features={features} />
-                </div>
-            </div>
-            <div className="mt1">
-                <Feedback insightType={type} />
-            </div>
-        </div>
-    )
+      <InsightText>{jt`Your data seems to be ${mode} ${shape}.`}</InsightText>
+    );
+  }
 }
+
+const INSIGHT_COMPONENTS = [
+  // any field
+  NilsInsight,
+  // numeric fields
+  NormalRangeInsight,
+  ZerosInsight,
+  MultimodalInsight,
+  OutliersInsight,
+  // timeseries
+  NoisinessInsight,
+  VariationTrendInsight,
+  AutocorrelationInsight,
+  SeasonalityInsight,
+  StructuralBreaksInsight,
+  StationaryInsight,
+  TrendInsight,
+];
+
+export const InsightCard = ({ type, props, features }) => {
+  const Insight = INSIGHT_COMPONENTS.find(
+    component => component.insightType === type,
+  );
+
+  return (
+    <div>
+      <div
+        className="bg-white bordered rounded shadowed p3"
+        style={{ height: 180 }}
+      >
+        <header className="flex align-center">
+          <Icon
+            name={Insight.icon}
+            size={24}
+            className="mr1"
+            style={{ color: "#93a1ab" }}
+          />
+          <span className="text-bold text-uppercase">{Insight.title}</span>
+        </header>
+        <div style={{ lineHeight: "1.4em" }}>
+          <Insight {...props} features={features} />
+        </div>
+      </div>
+      <div className="mt1">
+        <Feedback insightType={type} />
+      </div>
+    </div>
+  );
+};
diff --git a/frontend/src/metabase/xray/components/Insights.jsx b/frontend/src/metabase/xray/components/Insights.jsx
index 40ba0494feb87490ee45a2840a0b7fed513d8d46..8583e538527bc27bf801c0b2196ed4008927b14b 100644
--- a/frontend/src/metabase/xray/components/Insights.jsx
+++ b/frontend/src/metabase/xray/components/Insights.jsx
@@ -1,24 +1,29 @@
-import React, { Component } from 'react'
+import React, { Component } from "react";
 import { InsightCard } from "metabase/xray/components/InsightCard";
 
 export class Insights extends Component {
-    props: {
-        features: any,
-    }
+  props: {
+    features: any,
+  };
 
-    render() {
-        const { features } = this.props;
+  render() {
+    const { features } = this.props;
 
-        const parametrizedInsights = Object.entries(features["insights"])
+    const parametrizedInsights = Object.entries(features["insights"]);
 
-        return (
-            <ol className="Grid Grid--gutters Grid--1of4">
-                { parametrizedInsights.map(([type, props], index) =>
-                    <div className="Grid-cell">
-                        <InsightCard key={index} type={type} props={props} features={features} />
-                    </div>
-                )}
-            </ol>
-        )
-    }
+    return (
+      <ol className="Grid Grid--gutters Grid--1of4">
+        {parametrizedInsights.map(([type, props], index) => (
+          <div className="Grid-cell">
+            <InsightCard
+              key={index}
+              type={type}
+              props={props}
+              features={features}
+            />
+          </div>
+        ))}
+      </ol>
+    );
+  }
 }
diff --git a/frontend/src/metabase/xray/components/ItemLink.jsx b/frontend/src/metabase/xray/components/ItemLink.jsx
index a256053a847c2351e4c6a98ea2554947ca761763..b8a3bdaf805e604727266e79da9083ab3823e668 100644
--- a/frontend/src/metabase/xray/components/ItemLink.jsx
+++ b/frontend/src/metabase/xray/components/ItemLink.jsx
@@ -1,24 +1,26 @@
-import React from 'react'
-import { Link } from 'react-router'
+import React from "react";
+import { Link } from "react-router";
 import Icon from "metabase/components/Icon";
 
-const ItemLink = ({ link, item, dropdown }) =>
-    <Link
-        to={link}
-        className="no-decoration flex align-center bordered shadowed bg-white p1 px2 rounded mr1"
-    >
-        <div style={{
-            width: 12,
-            height: 12,
-            backgroundColor: item.color.main,
-            borderRadius: 99,
-            display: 'block'
-        }}>
-        </div>
-        <h2 className="ml1">
-            { item.name }
-            { dropdown && <Icon name="chevrondown" size={12} className="ml1" /> }
-        </h2>
-    </Link>
+const ItemLink = ({ link, item, dropdown }) => (
+  <Link
+    to={link}
+    className="no-decoration flex align-center bordered shadowed bg-white p1 px2 rounded mr1"
+  >
+    <div
+      style={{
+        width: 12,
+        height: 12,
+        backgroundColor: item.color.main,
+        borderRadius: 99,
+        display: "block",
+      }}
+    />
+    <h2 className="ml1">
+      {item.name}
+      {dropdown && <Icon name="chevrondown" size={12} className="ml1" />}
+    </h2>
+  </Link>
+);
 
-export default ItemLink
+export default ItemLink;
diff --git a/frontend/src/metabase/xray/components/LoadingAnimation.jsx b/frontend/src/metabase/xray/components/LoadingAnimation.jsx
index 487c6e8c0749fcb387e0976695d72544bfddf6f4..f15489b25f96a47e408319617f7b9365f2ad4044 100644
--- a/frontend/src/metabase/xray/components/LoadingAnimation.jsx
+++ b/frontend/src/metabase/xray/components/LoadingAnimation.jsx
@@ -1,30 +1,34 @@
-import React from 'react'
-import Icon from 'metabase/components/Icon'
+import React from "react";
+import Icon from "metabase/components/Icon";
 
-const RotatingGear = ({name, speed, size, delay }) =>
-    <div style={{
-        animation: `${name} ${speed}ms linear ${delay}ms infinite`
-    }}>
-        <Icon name='gear' size={size} />
-    </div>
+const RotatingGear = ({ name, speed, size, delay }) => (
+  <div
+    style={{
+      animation: `${name} ${speed}ms linear ${delay}ms infinite`,
+    }}
+  >
+    <Icon name="gear" size={size} />
+  </div>
+);
 
 RotatingGear.defaultProps = {
-    name: 'spin',
-    delay: 0,
-    speed: 5000
-}
+  name: "spin",
+  delay: 0,
+  speed: 5000,
+};
 
-const LoadingAnimation = () =>
-    <div className="relative" style={{ width: 300, height: 180 }}>
-        <div className="absolute" style={{ top: 20, left: 135 }}>
-            <RotatingGear size={90} />
-        </div>
-        <div className="absolute" style={{ top: 60, left: 80 }}>
-            <RotatingGear name='spin-reverse' size={60} speed={6000} />
-        </div>
-        <div className="absolute" style={{ top: 110, left: 125 }}>
-            <RotatingGear speed={7000} size={45} />
-        </div>
+const LoadingAnimation = () => (
+  <div className="relative" style={{ width: 300, height: 180 }}>
+    <div className="absolute" style={{ top: 20, left: 135 }}>
+      <RotatingGear size={90} />
+    </div>
+    <div className="absolute" style={{ top: 60, left: 80 }}>
+      <RotatingGear name="spin-reverse" size={60} speed={6000} />
+    </div>
+    <div className="absolute" style={{ top: 110, left: 125 }}>
+      <RotatingGear speed={7000} size={45} />
     </div>
+  </div>
+);
 
-export default LoadingAnimation
+export default LoadingAnimation;
diff --git a/frontend/src/metabase/xray/components/Periodicity.jsx b/frontend/src/metabase/xray/components/Periodicity.jsx
index b653b29b38d1f8f38069e559068048787a8b3eee..82a518399c42ce1cc7c453842c72e4f41216d293 100644
--- a/frontend/src/metabase/xray/components/Periodicity.jsx
+++ b/frontend/src/metabase/xray/components/Periodicity.jsx
@@ -1,34 +1,35 @@
-import React from 'react'
+import React from "react";
 
-import { PERIODICITY } from 'metabase/xray/stats'
+import { PERIODICITY } from "metabase/xray/stats";
 
-import { Heading } from 'metabase/xray/components/XRayLayout'
-import Histogram from 'metabase/xray/Histogram'
+import { Heading } from "metabase/xray/components/XRayLayout";
+import Histogram from "metabase/xray/Histogram";
 
-const Periodicity = ({ xray }) =>
-    <div>
-        <Heading heading="Time breakdown" />,
-        <div className="bg-white bordered rounded shadowed">
-            <div className="Grid Grid--gutters Grid--1of4">
-                { PERIODICITY.map(period =>
-                    xray[`histogram-${period}`] && xray[`histogram-${period}`].value && (
-                        <div className="Grid-cell">
-                            <div className="p4 border-right border-bottom">
-                                <div style={{ height: 120}}>
-                                    <h4>
-                                        {xray[`histogram-${period}`].label}
-                                    </h4>
-                                    <Histogram
-                                        histogram={xray[`histogram-${period}`].value}
-                                        axis={false}
-                                    />
-                                </div>
-                            </div>
-                        </div>
-                    )
-                )}
-            </div>
-        </div>
+const Periodicity = ({ xray }) => (
+  <div>
+    <Heading heading="Time breakdown" />,
+    <div className="bg-white bordered rounded shadowed">
+      <div className="Grid Grid--gutters Grid--1of4">
+        {PERIODICITY.map(
+          period =>
+            xray[`histogram-${period}`] &&
+            xray[`histogram-${period}`].value && (
+              <div className="Grid-cell">
+                <div className="p4 border-right border-bottom">
+                  <div style={{ height: 120 }}>
+                    <h4>{xray[`histogram-${period}`].label}</h4>
+                    <Histogram
+                      histogram={xray[`histogram-${period}`].value}
+                      axis={false}
+                    />
+                  </div>
+                </div>
+              </div>
+            ),
+        )}
+      </div>
     </div>
+  </div>
+);
 
-export default Periodicity
+export default Periodicity;
diff --git a/frontend/src/metabase/xray/components/PreviewBanner.jsx b/frontend/src/metabase/xray/components/PreviewBanner.jsx
index ade85123e5791d303025a62993d0df20e6f1a644..0c6a6d88d0f68dda1cce95926876ab8615975dfa 100644
--- a/frontend/src/metabase/xray/components/PreviewBanner.jsx
+++ b/frontend/src/metabase/xray/components/PreviewBanner.jsx
@@ -1,12 +1,23 @@
-import React from 'react'
-import Icon from 'metabase/components/Icon'
-import { jt } from 'c-3po';
-const SURVEY_LINK = 'https://docs.google.com/forms/d/e/1FAIpQLSc92WzF76ViiT8l4646lvFSWejNUhh4lhCSMXdZECILVwJG2A/viewform?usp=sf_link'
+import React from "react";
+import Icon from "metabase/components/Icon";
+import { jt } from "c-3po";
+const SURVEY_LINK =
+  "https://docs.google.com/forms/d/e/1FAIpQLSc92WzF76ViiT8l4646lvFSWejNUhh4lhCSMXdZECILVwJG2A/viewform?usp=sf_link";
 
-const PreviewBanner = () =>
-    <div className="full py2 flex align-center justify-center full md-py3 text-centered text-slate text-paragraph bg-white border-bottom">
-        <Icon name='beaker' size={28} className="mr1 text-brand" style={{ marginTop: -5 }} />
-        <span>{jt`Welcome to the x-ray preview! We'd love ${<a className="link" href={SURVEY_LINK} target="_blank">your feedback</a>}`}</span>
-    </div>
+const PreviewBanner = () => (
+  <div className="full py2 flex align-center justify-center full md-py3 text-centered text-slate text-paragraph bg-white border-bottom">
+    <Icon
+      name="beaker"
+      size={28}
+      className="mr1 text-brand"
+      style={{ marginTop: -5 }}
+    />
+    <span>{jt`Welcome to the x-ray preview! We'd love ${(
+      <a className="link" href={SURVEY_LINK} target="_blank">
+        your feedback
+      </a>
+    )}`}</span>
+  </div>
+);
 
-export default PreviewBanner
+export default PreviewBanner;
diff --git a/frontend/src/metabase/xray/components/StatGroup.jsx b/frontend/src/metabase/xray/components/StatGroup.jsx
index 76ef0c8d0f37a615fe19e9473be827b26f76973f..e9bc5190365ebced4be12018d0358e44b382d5c0 100644
--- a/frontend/src/metabase/xray/components/StatGroup.jsx
+++ b/frontend/src/metabase/xray/components/StatGroup.jsx
@@ -1,29 +1,32 @@
-import React from 'react'
-import { Heading } from 'metabase/xray/components/XRayLayout'
-import SimpleStat from 'metabase/xray/SimpleStat'
+import React from "react";
+import { Heading } from "metabase/xray/components/XRayLayout";
+import SimpleStat from "metabase/xray/SimpleStat";
 
-const atLeastOneStat = (xray, stats) =>
-    stats.filter(s => xray[s]).length > 0
+const atLeastOneStat = (xray, stats) => stats.filter(s => xray[s]).length > 0;
 
 const StatGroup = ({ heading, xray, stats, showDescriptions }) =>
-    atLeastOneStat(xray, stats) && (
-        <div className="my4">
-            <Heading heading={heading} />
-            <div className="bordered rounded shadowed bg-white">
-                <ol className="Grid Grid--1of4">
-                    { stats.map(stat =>
-                        (!!xray[stat] && xray[stat].value) ? (
-                            <li className="Grid-cell p1 px2 md-p2 md-px3 lg-p3 lg-px4 border-right border-bottom" key={stat}>
-                                <SimpleStat
-                                    stat={xray[stat]}
-                                    showDescription={showDescriptions}
-                                />
-                            </li>
-                        ) : null
-                    )}
-                </ol>
-            </div>
-        </div>
-    )
+  atLeastOneStat(xray, stats) && (
+    <div className="my4">
+      <Heading heading={heading} />
+      <div className="bordered rounded shadowed bg-white">
+        <ol className="Grid Grid--1of4">
+          {stats.map(
+            stat =>
+              !!xray[stat] && xray[stat].value ? (
+                <li
+                  className="Grid-cell p1 px2 md-p2 md-px3 lg-p3 lg-px4 border-right border-bottom"
+                  key={stat}
+                >
+                  <SimpleStat
+                    stat={xray[stat]}
+                    showDescription={showDescriptions}
+                  />
+                </li>
+              ) : null,
+          )}
+        </ol>
+      </div>
+    </div>
+  );
 
-export default StatGroup
+export default StatGroup;
diff --git a/frontend/src/metabase/xray/components/XRayComparison.jsx b/frontend/src/metabase/xray/components/XRayComparison.jsx
index 150c934eb919f4fb2125f1f3ab4b096af63e6fd6..68ca5a91598c43c4fc260d7d3e62cb4d8b2b98bc 100644
--- a/frontend/src/metabase/xray/components/XRayComparison.jsx
+++ b/frontend/src/metabase/xray/components/XRayComparison.jsx
@@ -1,17 +1,17 @@
-import React from 'react'
-import { Link } from 'react-router'
-import Color from 'color'
-import Visualization from 'metabase/visualizations/components/Visualization'
-import { t } from 'c-3po';
-import Icon from 'metabase/components/Icon'
-import Tooltip from 'metabase/components/Tooltip'
-import { XRayPageWrapper, Heading } from 'metabase/xray/components/XRayLayout'
-import ItemLink from 'metabase/xray/components/ItemLink'
+import React from "react";
+import { Link } from "react-router";
+import Color from "color";
+import Visualization from "metabase/visualizations/components/Visualization";
+import { t } from "c-3po";
+import Icon from "metabase/components/Icon";
+import Tooltip from "metabase/components/Tooltip";
+import { XRayPageWrapper, Heading } from "metabase/xray/components/XRayLayout";
+import ItemLink from "metabase/xray/components/ItemLink";
 
-import ComparisonHeader from 'metabase/xray/components/ComparisonHeader'
+import ComparisonHeader from "metabase/xray/components/ComparisonHeader";
 
-import { getIconForField } from 'metabase/lib/schema_metadata'
-import { distanceToPhrase } from 'metabase/xray/utils'
+import { getIconForField } from "metabase/lib/schema_metadata";
+import { distanceToPhrase } from "metabase/xray/utils";
 import { ComparisonDropdown } from "metabase/xray/components/ComparisonDropdown";
 
 // right now we rely on knowing that itemB is the only one that
@@ -26,74 +26,68 @@ const fieldLinkUrl = (itemA, itemB, fieldName) => {
 }
 */
 
-const itemLinkUrl = (item) =>
-    `/xray/${item["type-tag"]}/${item.id}/approximate`
+const itemLinkUrl = item => `/xray/${item["type-tag"]}/${item.id}/approximate`;
 
-const CompareInts = ({ itemA, itemAColor, itemB, itemBColor }) =>
-    <div className="flex">
-        <div
-            className="p2 text-align-center flex-full"
-            style={{
-                color: itemAColor.text,
-                backgroundColor: Color(itemAColor.main).lighten(0.1)
-            }}
-        >
-            <h3>{itemA}</h3>
-        </div>
-        <div
-            className="p2 text-align-center flex-full"
-            style={{
-                color: itemBColor.text,
-                backgroundColor: Color(itemBColor.main).lighten(0.4)
-            }}
-        >
-            <h3>{itemB}</h3>
-        </div>
+const CompareInts = ({ itemA, itemAColor, itemB, itemBColor }) => (
+  <div className="flex">
+    <div
+      className="p2 text-align-center flex-full"
+      style={{
+        color: itemAColor.text,
+        backgroundColor: Color(itemAColor.main).lighten(0.1),
+      }}
+    >
+      <h3>{itemA}</h3>
     </div>
+    <div
+      className="p2 text-align-center flex-full"
+      style={{
+        color: itemBColor.text,
+        backgroundColor: Color(itemBColor.main).lighten(0.4),
+      }}
+    >
+      <h3>{itemB}</h3>
+    </div>
+  </div>
+);
 
-const Contributor = ({ contributor, itemA, itemB }) =>
-    <div className="full-height">
-        <h3 className="mb2">
-            {contributor.field.model.display_name}
-        </h3>
+const Contributor = ({ contributor, itemA, itemB }) => (
+  <div className="full-height">
+    <h3 className="mb2">{contributor.field.model.display_name}</h3>
 
-        <div className="ComparisonContributor bg-white shadowed rounded bordered">
-                <div>
-                    <div className="p2 flex align-center">
-                        <h4>{contributor.feature.label}</h4>
-                        <Tooltip tooltip={contributor.feature.description}>
-                            <Icon
-                                name="infooutlined"
-                                className="ml1 text-grey-4"
-                                size={14}
-                            />
-                        </Tooltip>
-                    </div>
-                    <div className="py1">
-                        { contributor.feature.type.startsWith('histogram') ? (
-                            <CompareHistograms
-                                itemA={contributor.feature.value.a}
-                                itemB={contributor.feature.value.b}
-                                itemAColor={itemA.color.main}
-                                itemBColor={itemB.color.main}
-                                showAxis={true}
-                                height={120}
-                            />
-                        ) : (
-                            <div className="flex align-center px2 py3">
-                                <h1 className="p2 lg-p3" style={{ color: itemA.color.text }}>
-                                    {contributor.feature.value.a}
-                                </h1>
-                                <h1 className="p2 lg-p3" style={{ color: itemB.color.text }}>
-                                    {contributor.feature.value.b}
-                                </h1>
-                            </div>
-                        )}
-                    </div>
-                </div>
+    <div className="ComparisonContributor bg-white shadowed rounded bordered">
+      <div>
+        <div className="p2 flex align-center">
+          <h4>{contributor.feature.label}</h4>
+          <Tooltip tooltip={contributor.feature.description}>
+            <Icon name="infooutlined" className="ml1 text-grey-4" size={14} />
+          </Tooltip>
+        </div>
+        <div className="py1">
+          {contributor.feature.type.startsWith("histogram") ? (
+            <CompareHistograms
+              itemA={contributor.feature.value.a}
+              itemB={contributor.feature.value.b}
+              itemAColor={itemA.color.main}
+              itemBColor={itemB.color.main}
+              showAxis={true}
+              height={120}
+            />
+          ) : (
+            <div className="flex align-center px2 py3">
+              <h1 className="p2 lg-p3" style={{ color: itemA.color.text }}>
+                {contributor.feature.value.a}
+              </h1>
+              <h1 className="p2 lg-p3" style={{ color: itemB.color.text }}>
+                {contributor.feature.value.b}
+              </h1>
+            </div>
+          )}
+        </div>
+      </div>
 
-            <div className="flex">
-                { /*
+      <div className="flex">
+        {/*
                 <Link
                     to={fieldLinkUrl(itemA, itemB, contributor.field.name)}
                     className="text-grey-3 text-brand-hover no-decoration transition-color ml-auto text-bold px2 pb2"
@@ -101,218 +95,213 @@ const Contributor = ({ contributor, itemA, itemB }) =>
                     View full comparison
                 </Link>
                 */}
-                </div>
-            </div>
+      </div>
     </div>
+  </div>
+);
 
-const CompareHistograms = ({ itemA, itemAColor, itemB, itemBColor, showAxis = false, height = 60}) =>
-    <div className="flex" style={{ height }}>
-        <div className="flex-full">
-            <Visualization
-                className="full-height"
-                rawSeries={[
-                    {
-                        card: {
-                            display: "bar",
-                            visualization_settings: {
-                                "graph.colors": [itemAColor, itemBColor],
-                                "graph.x_axis.axis_enabled": showAxis,
-                                "graph.x_axis.labels_enabled": showAxis,
-                                "graph.y_axis.axis_enabled": showAxis,
-                                "graph.y_axis.labels_enabled": showAxis
-                            }
-                        },
-                        data: itemA
-                    },
-                    {
-                        card: {
-                            display: "bar",
-                            visualization_settings: {
-                                "graph.colors": [itemAColor, itemBColor],
-                                "graph.x_axis.axis_enabled": showAxis,
-                                "graph.x_axis.labels_enabled": showAxis,
-                                "graph.y_axis.axis_enabled": showAxis,
-                                "graph.y_axis.labels_enabled": showAxis
-                            }
-                        },
-                        data: itemB
-                    },
-
-                ]}
-            />
-        </div>
+const CompareHistograms = ({
+  itemA,
+  itemAColor,
+  itemB,
+  itemBColor,
+  showAxis = false,
+  height = 60,
+}) => (
+  <div className="flex" style={{ height }}>
+    <div className="flex-full">
+      <Visualization
+        className="full-height"
+        rawSeries={[
+          {
+            card: {
+              display: "bar",
+              visualization_settings: {
+                "graph.colors": [itemAColor, itemBColor],
+                "graph.x_axis.axis_enabled": showAxis,
+                "graph.x_axis.labels_enabled": showAxis,
+                "graph.y_axis.axis_enabled": showAxis,
+                "graph.y_axis.labels_enabled": showAxis,
+              },
+            },
+            data: itemA,
+          },
+          {
+            card: {
+              display: "bar",
+              visualization_settings: {
+                "graph.colors": [itemAColor, itemBColor],
+                "graph.x_axis.axis_enabled": showAxis,
+                "graph.x_axis.labels_enabled": showAxis,
+                "graph.y_axis.axis_enabled": showAxis,
+                "graph.y_axis.labels_enabled": showAxis,
+              },
+            },
+            data: itemB,
+          },
+        ]}
+      />
     </div>
-
+  </div>
+);
 
 const XRayComparison = ({
-    contributors,
-    comparables,
-    comparison,
-    comparisonFields,
-    itemA,
-    itemB,
-    fields,
-    cost
-}) =>
-    <XRayPageWrapper>
-        <div>
-            <ComparisonHeader
-                cost={cost}
-            />
-            <div className="flex">
-                <ComparisonDropdown
-                    models={[itemA, itemB]}
-                    comparables={
-                        comparables[0].filter((comparableModel) =>
-                            // filter out itemB
-                            !(comparableModel.id === itemB.id && comparableModel["type-tag"] === itemB["type-tag"])
-                        )
-                    }
-                    updatingModelAtIndex={0}
-                    triggerElement={
-                        <ItemLink
-                            item={itemA}
-                            dropdown
-                        />
-                    }
-                />
-                <ComparisonDropdown
-                    models={[itemA, itemB]}
-                    comparables={
-                        comparables[1].filter((comparableModel) =>
-                            // filter out itemA
-                            !(comparableModel.id === itemA.id && comparableModel["type-tag"] === itemA["type-tag"])
-                        )
-                    }
-                    updatingModelAtIndex={1}
-                    triggerElement={
-                        <ItemLink
-                            item={itemB}
-                            dropdown
-                        />
-                    }
-                />
-            </div>
-        </div>
-
-        <Heading heading={t`Overview`} />
-        <div className="bordered rounded bg-white shadowed p4">
-            <h3 className="text-grey-3">{t`Count`}</h3>
-            <div className="flex my1">
-                <h1
-                    className="mr1"
-                    style={{ color: itemA.color.text}}
-                >
-                    {itemA.constituents[fields[0].name].count.value}
-                </h1>
-                <span className="h1 text-grey-1 mr1">/</span>
-                <h1 style={{ color: itemB.color.text}}>
-                    {itemB.constituents[fields[1].name].count.value}
-                </h1>
-            </div>
-        </div>
+  contributors,
+  comparables,
+  comparison,
+  comparisonFields,
+  itemA,
+  itemB,
+  fields,
+  cost,
+}) => (
+  <XRayPageWrapper>
+    <div>
+      <ComparisonHeader cost={cost} />
+      <div className="flex">
+        <ComparisonDropdown
+          models={[itemA, itemB]}
+          comparables={comparables[0].filter(
+            comparableModel =>
+              // filter out itemB
+              !(
+                comparableModel.id === itemB.id &&
+                comparableModel["type-tag"] === itemB["type-tag"]
+              ),
+          )}
+          updatingModelAtIndex={0}
+          triggerElement={<ItemLink item={itemA} dropdown />}
+        />
+        <ComparisonDropdown
+          models={[itemA, itemB]}
+          comparables={comparables[1].filter(
+            comparableModel =>
+              // filter out itemA
+              !(
+                comparableModel.id === itemA.id &&
+                comparableModel["type-tag"] === itemA["type-tag"]
+              ),
+          )}
+          updatingModelAtIndex={1}
+          triggerElement={<ItemLink item={itemB} dropdown />}
+        />
+      </div>
+    </div>
 
-        { contributors && (
-            <div>
-                <Heading heading={t`Potentially interesting differences`} />
-                <ol className="Grid Grid--gutters Grid--1of3">
-                    { contributors.map(contributor =>
-                        <li className="Grid-cell" key={contributor.field.id}>
-                            <Contributor
-                                contributor={contributor}
-                                itemA={itemA}
-                                itemB={itemB}
-                            />
-                        </li>
-                    )}
-                </ol>
-            </div>
-        )}
+    <Heading heading={t`Overview`} />
+    <div className="bordered rounded bg-white shadowed p4">
+      <h3 className="text-grey-3">{t`Count`}</h3>
+      <div className="flex my1">
+        <h1 className="mr1" style={{ color: itemA.color.text }}>
+          {itemA.constituents[fields[0].name].count.value}
+        </h1>
+        <span className="h1 text-grey-1 mr1">/</span>
+        <h1 style={{ color: itemB.color.text }}>
+          {itemB.constituents[fields[1].name].count.value}
+        </h1>
+      </div>
+    </div>
 
-        <Heading heading={t`Full breakdown`} />
-        <div className="bordered rounded bg-white shadowed">
+    {contributors && (
+      <div>
+        <Heading heading={t`Potentially interesting differences`} />
+        <ol className="Grid Grid--gutters Grid--1of3">
+          {contributors.map(contributor => (
+            <li className="Grid-cell" key={contributor.field.id}>
+              <Contributor
+                contributor={contributor}
+                itemA={itemA}
+                itemB={itemB}
+              />
+            </li>
+          ))}
+        </ol>
+      </div>
+    )}
 
-            <div className="flex p2">
-                <Link to={itemLinkUrl(itemA)} className="no-decoration">
-                    <h4 className="mr1" style={{ color: itemA.color.text}}>
-                        {itemA.name}
-                    </h4>
-                </Link>
-                <Link to={itemLinkUrl(itemB)} className="no-decoration">
-                    <h4 style={{ color: itemB.color.text}}>
-                        {itemB.name}
-                    </h4>
-                </Link>
-            </div>
+    <Heading heading={t`Full breakdown`} />
+    <div className="bordered rounded bg-white shadowed">
+      <div className="flex p2">
+        <Link to={itemLinkUrl(itemA)} className="no-decoration">
+          <h4 className="mr1" style={{ color: itemA.color.text }}>
+            {itemA.name}
+          </h4>
+        </Link>
+        <Link to={itemLinkUrl(itemB)} className="no-decoration">
+          <h4 style={{ color: itemB.color.text }}>{itemB.name}</h4>
+        </Link>
+      </div>
 
-            <table className="ComparisonTable full">
-                <thead className="full border-bottom">
-                    <tr>
-                        <th className="px2">{t`Field`}</th>
-                        {comparisonFields.map(c =>
-                            <th
-                                key={c}
-                                className="px2 py2"
-                            >
-                                {c}
-                            </th>
-                        )}
-                    </tr>
-                </thead>
-                <tbody className="full">
-                    { fields.map(field => {
-                        return (
-                            <tr key={field.id}>
-                                <td className="border-right">
-                                    <Link
-                                        to={`/xray/field/${field.id}/approximate`}
-                                        className="px2 no-decoration text-brand flex align-center"
-                                    >
-                                        <Icon name={getIconForField(field)} className="text-grey-2 mr1" />
-                                        <h3>{field.display_name}</h3>
-                                    </Link>
-                                </td>
-                                <td className="border-right px2">
-                                    <h3>{distanceToPhrase(comparison[field.name].distance)}</h3>
-                                </td>
-                                <td className="border-right">
-                                    { itemA.constituents[field.name]['entropy'] && (
-                                        <CompareInts
-                                            itemA={itemA.constituents[field.name]['entropy']['value']}
-                                            itemAColor={itemA.color}
-                                            itemB={itemB.constituents[field.name]['entropy']['value']}
-                                            itemBColor={itemB.color}
-                                        />
-                                    )}
-                                </td>
-                                <td
-                                    className="px2 border-right"
-                                    style={{maxWidth: 200, minHeight: 120 }}
-                                >
-                                    { itemA.constituents[field.name]['histogram'] && (
-                                    <CompareHistograms
-                                        itemA={itemA.constituents[field.name]['histogram'].value}
-                                        itemAColor={itemA.color.main}
-                                        itemB={itemB.constituents[field.name]['histogram'].value}
-                                        itemBColor={itemB.color.main}
-                                    />
-                                    )}
-                                </td>
-                                <td className="px2 h3">
-                                    { itemA.constituents[field.name]['nil%'] && (
-                                        <CompareInts
-                                            itemA={itemA.constituents[field.name]['nil%']['value']}
-                                            itemAColor={itemA.color}
-                                            itemB={itemB.constituents[field.name]['nil%']['value']}
-                                            itemBColor={itemB.color}
-                                        />
-                                    )}
-                                </td>
-                            </tr>
-                        )})}
-                </tbody>
-            </table>
-        </div>
-    </XRayPageWrapper>
+      <table className="ComparisonTable full">
+        <thead className="full border-bottom">
+          <tr>
+            <th className="px2">{t`Field`}</th>
+            {comparisonFields.map(c => (
+              <th key={c} className="px2 py2">
+                {c}
+              </th>
+            ))}
+          </tr>
+        </thead>
+        <tbody className="full">
+          {fields.map(field => {
+            return (
+              <tr key={field.id}>
+                <td className="border-right">
+                  <Link
+                    to={`/xray/field/${field.id}/approximate`}
+                    className="px2 no-decoration text-brand flex align-center"
+                  >
+                    <Icon
+                      name={getIconForField(field)}
+                      className="text-grey-2 mr1"
+                    />
+                    <h3>{field.display_name}</h3>
+                  </Link>
+                </td>
+                <td className="border-right px2">
+                  <h3>{distanceToPhrase(comparison[field.name].distance)}</h3>
+                </td>
+                <td className="border-right">
+                  {itemA.constituents[field.name]["entropy"] && (
+                    <CompareInts
+                      itemA={itemA.constituents[field.name]["entropy"]["value"]}
+                      itemAColor={itemA.color}
+                      itemB={itemB.constituents[field.name]["entropy"]["value"]}
+                      itemBColor={itemB.color}
+                    />
+                  )}
+                </td>
+                <td
+                  className="px2 border-right"
+                  style={{ maxWidth: 200, minHeight: 120 }}
+                >
+                  {itemA.constituents[field.name]["histogram"] && (
+                    <CompareHistograms
+                      itemA={itemA.constituents[field.name]["histogram"].value}
+                      itemAColor={itemA.color.main}
+                      itemB={itemB.constituents[field.name]["histogram"].value}
+                      itemBColor={itemB.color.main}
+                    />
+                  )}
+                </td>
+                <td className="px2 h3">
+                  {itemA.constituents[field.name]["nil%"] && (
+                    <CompareInts
+                      itemA={itemA.constituents[field.name]["nil%"]["value"]}
+                      itemAColor={itemA.color}
+                      itemB={itemB.constituents[field.name]["nil%"]["value"]}
+                      itemBColor={itemB.color}
+                    />
+                  )}
+                </td>
+              </tr>
+            );
+          })}
+        </tbody>
+      </table>
+    </div>
+  </XRayPageWrapper>
+);
 
-export default XRayComparison
+export default XRayComparison;
diff --git a/frontend/src/metabase/xray/components/XRayLayout.jsx b/frontend/src/metabase/xray/components/XRayLayout.jsx
index 9f0d7122e9037cd5cf949d53bb64224c75e7f5e7..77451e15f7f805f8e531ea50e61bf94b93214b9d 100644
--- a/frontend/src/metabase/xray/components/XRayLayout.jsx
+++ b/frontend/src/metabase/xray/components/XRayLayout.jsx
@@ -1,20 +1,20 @@
-import React from 'react'
-import { withBackground } from 'metabase/hoc/Background'
-import PreviewBanner from 'metabase/xray/components/PreviewBanner'
+import React from "react";
+import { withBackground } from "metabase/hoc/Background";
+import PreviewBanner from "metabase/xray/components/PreviewBanner";
 
 // A small wrapper to get consistent page structure
-export const XRayPageWrapper = withBackground('bg-slate-extra-light')(({ children }) =>
+export const XRayPageWrapper = withBackground("bg-slate-extra-light")(
+  ({ children }) => (
     <div className="full-height full">
-        <PreviewBanner />
-        <div className="XRayPageWrapper wrapper pb4 full-height">
-            { children }
-        </div>
+      <PreviewBanner />
+      <div className="XRayPageWrapper wrapper pb4 full-height">{children}</div>
     </div>
-)
-
+  ),
+);
 
 // A unified heading for XRay pages
-export const Heading = ({ heading }) =>
-    <h2 className="py3" style={{ color: '#93A1AB'}}>
-        {heading}
-    </h2>
+export const Heading = ({ heading }) => (
+  <h2 className="py3" style={{ color: "#93A1AB" }}>
+    {heading}
+  </h2>
+);
diff --git a/frontend/src/metabase/xray/containers/CardXRay.jsx b/frontend/src/metabase/xray/containers/CardXRay.jsx
index 25d2ae9e79f868eb74ddfc45f4108484322caeae..c8cd51bf6bfa874b1ae4a7296e2b503c1d786623 100644
--- a/frontend/src/metabase/xray/containers/CardXRay.jsx
+++ b/frontend/src/metabase/xray/containers/CardXRay.jsx
@@ -1,219 +1,224 @@
-import React, { Component } from 'react'
-import cxs from 'cxs'
-import { connect } from 'react-redux'
-import { t } from 'c-3po';
-import { saturated } from 'metabase/lib/colors'
+import React, { Component } from "react";
+import cxs from "cxs";
+import { connect } from "react-redux";
+import { t } from "c-3po";
+import { saturated } from "metabase/lib/colors";
 
-import { fetchCardXray, initialize } from 'metabase/xray/xray'
+import { fetchCardXray, initialize } from "metabase/xray/xray";
 import {
-    getLoadingStatus,
-    getError,
-    getXray,
-    getIsAlreadyFetched
-} from 'metabase/xray/selectors'
-
-import { xrayLoadingMessages } from 'metabase/xray/utils'
-
-import Icon from 'metabase/components/Icon'
-import Tooltip from 'metabase/components/Tooltip'
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
-import Visualization from 'metabase/visualizations/components/Visualization'
-
-import { XRayPageWrapper, Heading } from 'metabase/xray/components/XRayLayout'
-import Periodicity from 'metabase/xray/components/Periodicity'
-import LoadingAnimation from 'metabase/xray/components/LoadingAnimation'
+  getLoadingStatus,
+  getError,
+  getXray,
+  getIsAlreadyFetched,
+} from "metabase/xray/selectors";
+
+import { xrayLoadingMessages } from "metabase/xray/utils";
+
+import Icon from "metabase/components/Icon";
+import Tooltip from "metabase/components/Tooltip";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import Visualization from "metabase/visualizations/components/Visualization";
+
+import { XRayPageWrapper, Heading } from "metabase/xray/components/XRayLayout";
+import Periodicity from "metabase/xray/components/Periodicity";
+import LoadingAnimation from "metabase/xray/components/LoadingAnimation";
 import { Insights } from "metabase/xray/components/Insights";
 
 const mapStateToProps = state => ({
-    xray: getXray(state),
-    isLoading: getLoadingStatus(state),
-    isAlreadyFetched: getIsAlreadyFetched(state),
-    error: getError(state)
-})
+  xray: getXray(state),
+  isLoading: getLoadingStatus(state),
+  isAlreadyFetched: getIsAlreadyFetched(state),
+  error: getError(state),
+});
 
 const mapDispatchToProps = {
-    initialize,
-    fetchCardXray
-}
+  initialize,
+  fetchCardXray,
+};
 
 type Props = {
-    initialize: () => void,
-    initialize: () => {},
-    fetchCardXray: () => void,
-    isLoading: boolean,
-    xray: {}
-}
-
-const GrowthRateDisplay = ({ period }) =>
-    <div className="Grid-cell">
-        <div className="p4 border-right">
-            <h4 className="flex align-center">
-                {period.label}
-                { period.description && (
-                    <Tooltip tooltip={period.description}>
-                        <Icon name="infooutlined" style={{ marginLeft: 8 }} size={14} />
-                    </Tooltip>
-                )}
-            </h4>
-            <h1
-                className={cxs({
-                    color: period.value > 0 ? saturated.green : saturated.red
-                })}
-            >
-                {period.value && (period.value * 100).toFixed(2)}%
-            </h1>
-        </div>
+  initialize: () => void,
+  initialize: () => {},
+  fetchCardXray: () => void,
+  isLoading: boolean,
+  xray: {},
+};
+
+const GrowthRateDisplay = ({ period }) => (
+  <div className="Grid-cell">
+    <div className="p4 border-right">
+      <h4 className="flex align-center">
+        {period.label}
+        {period.description && (
+          <Tooltip tooltip={period.description}>
+            <Icon name="infooutlined" style={{ marginLeft: 8 }} size={14} />
+          </Tooltip>
+        )}
+      </h4>
+      <h1
+        className={cxs({
+          color: period.value > 0 ? saturated.green : saturated.red,
+        })}
+      >
+        {period.value && (period.value * 100).toFixed(2)}%
+      </h1>
     </div>
+  </div>
+);
 
 class CardXRay extends Component {
-    props: Props
-
-    componentWillMount () {
-        const { cardId, cost } = this.props.params
-        this.props.initialize()
-        this.props.fetchCardXray(cardId, cost)
-    }
-
-    componentWillUnmount() {
-        // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
-        // for all xray types and if switching to different kind of xray (= rendering different React container)
-        // without resetting the state fails because `state.xray.xray` subproperty lookups fail
-        this.props.initialize();
-    }
-
-    render () {
-        const { xray, isLoading, isAlreadyFetched, error } = this.props
-
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading || !isAlreadyFetched}
-                error={error}
-                noBackground
-                loadingMessages={xrayLoadingMessages}
-                loadingScenes={[<LoadingAnimation />]}
-            >
-                { () =>
-                    <XRayPageWrapper>
-                        <div className="mt4 mb2">
-                            <h1 className="my3">{xray.features.model.name} X-ray</h1>
-                        </div>
-                        { xray.features["insights"] &&
-                            <div className="mt4">
-                                <Heading heading="Takeaways" />
-                                <Insights features={xray.features} />
-                            </div>
-                        }
-                        <Heading heading="Growth rate" />
-                        <div className="bg-white bordered rounded shadowed">
-                            <div className="Grid Grid--1of4 border-bottom">
-                                { xray.features.DoD.value && (
-                                    <GrowthRateDisplay period={xray.features.DoD} />
-                                )}
-                                { xray.features.WoW.value && (
-                                    <GrowthRateDisplay period={xray.features.WoW} />
-                                )}
-                                { xray.features.MoM.value && (
-                                    <GrowthRateDisplay period={xray.features.MoM} />
-                                )}
-                                { xray.features.YoY.value && (
-                                    <GrowthRateDisplay period={xray.features.YoY} />
-                                )}
-                            </div>
-                            <div className="full">
-                                <div className="py1 px2" style={{ height: 320}}>
-                                    <Visualization
-                                        rawSeries={[
-                                            {
-                                                card: xray.features.model,
-                                                data: xray.features.series
-                                            },
-                                            {
-                                                card: {
-                                                    display: 'line',
-                                                    name: t`Growth Trend`,
-                                                    visualization_settings: {
-
-                                                    }
-                                                },
-                                                data: xray.features['linear-regression'].value
-                                            }
-                                        ]}
-                                        className="full-height"
-                                    />
-                                </div>
-                            </div>
-                        </div>
-
-                        <Heading heading={xray.features['growth-series'].label} />
-                        <div className="full">
-                            <div className="bg-white bordered rounded shadowed" style={{ height: 220}}>
-                                <Visualization
-                                    rawSeries={[
-                                        {
-                                            card: {
-                                                display: 'line',
-                                                name: t`Trend`,
-                                                visualization_settings: {
-
-                                                }
-                                            },
-                                            data: {
-                                                ...xray.features['growth-series'].value,
-                                                // multiple row value by 100 to display as a %
-                                                rows: xray.features['growth-series'].value.rows.map(row =>
-                                                    [row[0], row[1]*100]
-                                                )
-                                            }
-                                        }
-                                    ]}
-                                    className="full-height"
-                                />
-                            </div>
-                        </div>
-
-                        { xray.constituents[0] && (
-                            <Periodicity xray={Object.values(xray.constituents)[0]} />
-                        )}
-
-                        <Heading heading={xray.features['seasonal-decomposition'].label} />
-                        <div className="full">
-                            <div className="bg-white bordered rounded shadowed" style={{ height: 220}}>
-                                <Visualization
-                                    rawSeries={[
-                                        {
-                                            card: {
-                                                display: 'line',
-                                                name: t`Trend`,
-                                                visualization_settings: {}
-                                            },
-                                            data: xray.features['seasonal-decomposition'].value.trend
-                                        },
-                                        {
-                                            card: {
-                                                display: 'line',
-                                                name: t`Seasonal`,
-                                                visualization_settings: {}
-                                            },
-                                            data: xray.features['seasonal-decomposition'].value.seasonal
-                                        },
-                                        {
-                                            card: {
-                                                display: 'line',
-                                                name: t`Residual`,
-                                                visualization_settings: {}
-                                            },
-                                            data: xray.features['seasonal-decomposition'].value.residual
-                                        }
-                                    ]}
-                                    className="full-height"
-                                />
-                            </div>
-                        </div>
-                    </XRayPageWrapper>
-                }
-            </LoadingAndErrorWrapper>
-        )
-    }
+  props: Props;
+
+  componentWillMount() {
+    const { cardId, cost } = this.props.params;
+    this.props.initialize();
+    this.props.fetchCardXray(cardId, cost);
+  }
+
+  componentWillUnmount() {
+    // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
+    // for all xray types and if switching to different kind of xray (= rendering different React container)
+    // without resetting the state fails because `state.xray.xray` subproperty lookups fail
+    this.props.initialize();
+  }
+
+  render() {
+    const { xray, isLoading, isAlreadyFetched, error } = this.props;
+
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading || !isAlreadyFetched}
+        error={error}
+        noBackground
+        loadingMessages={xrayLoadingMessages}
+        loadingScenes={[<LoadingAnimation />]}
+      >
+        {() => (
+          <XRayPageWrapper>
+            <div className="mt4 mb2">
+              <h1 className="my3">{xray.features.model.name} X-ray</h1>
+            </div>
+            {xray.features["insights"] && (
+              <div className="mt4">
+                <Heading heading="Takeaways" />
+                <Insights features={xray.features} />
+              </div>
+            )}
+            <Heading heading="Growth rate" />
+            <div className="bg-white bordered rounded shadowed">
+              <div className="Grid Grid--1of4 border-bottom">
+                {xray.features.DoD.value && (
+                  <GrowthRateDisplay period={xray.features.DoD} />
+                )}
+                {xray.features.WoW.value && (
+                  <GrowthRateDisplay period={xray.features.WoW} />
+                )}
+                {xray.features.MoM.value && (
+                  <GrowthRateDisplay period={xray.features.MoM} />
+                )}
+                {xray.features.YoY.value && (
+                  <GrowthRateDisplay period={xray.features.YoY} />
+                )}
+              </div>
+              <div className="full">
+                <div className="py1 px2" style={{ height: 320 }}>
+                  <Visualization
+                    rawSeries={[
+                      {
+                        card: xray.features.model,
+                        data: xray.features.series,
+                      },
+                      {
+                        card: {
+                          display: "line",
+                          name: t`Growth Trend`,
+                          visualization_settings: {},
+                        },
+                        data: xray.features["linear-regression"].value,
+                      },
+                    ]}
+                    className="full-height"
+                  />
+                </div>
+              </div>
+            </div>
+
+            <Heading heading={xray.features["growth-series"].label} />
+            <div className="full">
+              <div
+                className="bg-white bordered rounded shadowed"
+                style={{ height: 220 }}
+              >
+                <Visualization
+                  rawSeries={[
+                    {
+                      card: {
+                        display: "line",
+                        name: t`Trend`,
+                        visualization_settings: {},
+                      },
+                      data: {
+                        ...xray.features["growth-series"].value,
+                        // multiple row value by 100 to display as a %
+                        rows: xray.features["growth-series"].value.rows.map(
+                          row => [row[0], row[1] * 100],
+                        ),
+                      },
+                    },
+                  ]}
+                  className="full-height"
+                />
+              </div>
+            </div>
+
+            {xray.constituents[0] && (
+              <Periodicity xray={Object.values(xray.constituents)[0]} />
+            )}
+
+            <Heading heading={xray.features["seasonal-decomposition"].label} />
+            <div className="full">
+              <div
+                className="bg-white bordered rounded shadowed"
+                style={{ height: 220 }}
+              >
+                <Visualization
+                  rawSeries={[
+                    {
+                      card: {
+                        display: "line",
+                        name: t`Trend`,
+                        visualization_settings: {},
+                      },
+                      data: xray.features["seasonal-decomposition"].value.trend,
+                    },
+                    {
+                      card: {
+                        display: "line",
+                        name: t`Seasonal`,
+                        visualization_settings: {},
+                      },
+                      data:
+                        xray.features["seasonal-decomposition"].value.seasonal,
+                    },
+                    {
+                      card: {
+                        display: "line",
+                        name: t`Residual`,
+                        visualization_settings: {},
+                      },
+                      data:
+                        xray.features["seasonal-decomposition"].value.residual,
+                    },
+                  ]}
+                  className="full-height"
+                />
+              </div>
+            </div>
+          </XRayPageWrapper>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-export default connect(mapStateToProps, mapDispatchToProps)(CardXRay)
+export default connect(mapStateToProps, mapDispatchToProps)(CardXRay);
diff --git a/frontend/src/metabase/xray/containers/FieldXray.jsx b/frontend/src/metabase/xray/containers/FieldXray.jsx
index 84363a7cc5ea6bb5ef1a0f4aa8d8089ec59b6748..4590f1f0eba91d8ba25acb4cca850a9107245286 100644
--- a/frontend/src/metabase/xray/containers/FieldXray.jsx
+++ b/frontend/src/metabase/xray/containers/FieldXray.jsx
@@ -1,188 +1,189 @@
 /* @flow */
-import React, { Component } from 'react'
-
-import { connect } from 'react-redux'
-import title from 'metabase/hoc/Title'
-import { Link } from 'react-router'
-import { t } from 'c-3po';
-import { isDate } from 'metabase/lib/schema_metadata'
-import { fetchFieldXray, initialize } from 'metabase/xray/xray'
+import React, { Component } from "react";
+
+import { connect } from "react-redux";
+import title from "metabase/hoc/Title";
+import { Link } from "react-router";
+import { t } from "c-3po";
+import { isDate } from "metabase/lib/schema_metadata";
+import { fetchFieldXray, initialize } from "metabase/xray/xray";
 import {
-    getLoadingStatus,
-    getError,
-    getFeatures, getIsAlreadyFetched
-} from 'metabase/xray/selectors'
+  getLoadingStatus,
+  getError,
+  getFeatures,
+  getIsAlreadyFetched,
+} from "metabase/xray/selectors";
 
-import {
-    ROBOTS,
-    STATS_OVERVIEW,
-    VALUES_OVERVIEW
-} from 'metabase/xray/stats'
+import { ROBOTS, STATS_OVERVIEW, VALUES_OVERVIEW } from "metabase/xray/stats";
 
-import Icon from 'metabase/components/Icon'
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
-import CostSelect from 'metabase/xray/components/CostSelect'
-import StatGroup from 'metabase/xray/components/StatGroup'
-import Histogram from 'metabase/xray/Histogram'
-import { Heading, XRayPageWrapper } from 'metabase/xray/components/XRayLayout'
+import Icon from "metabase/components/Icon";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import CostSelect from "metabase/xray/components/CostSelect";
+import StatGroup from "metabase/xray/components/StatGroup";
+import Histogram from "metabase/xray/Histogram";
+import { Heading, XRayPageWrapper } from "metabase/xray/components/XRayLayout";
 
-import { xrayLoadingMessages } from 'metabase/xray/utils'
+import { xrayLoadingMessages } from "metabase/xray/utils";
 
-import Periodicity from 'metabase/xray/components/Periodicity'
-import LoadingAnimation from 'metabase/xray/components/LoadingAnimation'
+import Periodicity from "metabase/xray/components/Periodicity";
+import LoadingAnimation from "metabase/xray/components/LoadingAnimation";
 
-import type { Field } from 'metabase/meta/types/Field'
-import type { Table } from 'metabase/meta/types/Table'
+import type { Field } from "metabase/meta/types/Field";
+import type { Table } from "metabase/meta/types/Table";
 import { Insights } from "metabase/xray/components/Insights";
 
 type Props = {
-    fetchFieldXray: () => void,
-    initialize: () => {},
-    isLoading: boolean,
-    isAlreadyFetched: boolean,
-    features: {
-        model: Field,
-        table: Table,
-        histogram: {
-            value: {}
-        },
-        insights: []
-    },
-    params: {
-        cost: string,
-        fieldId: number
+  fetchFieldXray: () => void,
+  initialize: () => {},
+  isLoading: boolean,
+  isAlreadyFetched: boolean,
+  features: {
+    model: Field,
+    table: Table,
+    histogram: {
+      value: {},
     },
-    error: {}
-}
+    insights: [],
+  },
+  params: {
+    cost: string,
+    fieldId: number,
+  },
+  error: {},
+};
 
 const mapStateToProps = state => ({
-    features: getFeatures(state),
-    isLoading: getLoadingStatus(state),
-    isAlreadyFetched: getIsAlreadyFetched(state),
-    error: getError(state)
-})
+  features: getFeatures(state),
+  isLoading: getLoadingStatus(state),
+  isAlreadyFetched: getIsAlreadyFetched(state),
+  error: getError(state),
+});
 
 const mapDispatchToProps = {
-    initialize,
-    fetchFieldXray
-}
+  initialize,
+  fetchFieldXray,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ features }) => features && features.model.display_name || t`Field`)
+@title(({ features }) => (features && features.model.display_name) || t`Field`)
 class FieldXRay extends Component {
-    props: Props
-
-    componentWillMount () {
-        this.props.initialize()
-        this.fetch()
-    }
-
-    componentWillUnmount() {
-        // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
-        // for all xray types and if switching to different kind of xray (= rendering different React container)
-        // without resetting the state fails because `state.xray.xray` subproperty lookups fail
-        this.props.initialize();
-    }
-
-    fetch() {
-        const { params, fetchFieldXray } = this.props
-        fetchFieldXray(params.fieldId, params.cost)
-    }
-
-    componentDidUpdate (prevProps: Props) {
-        if(prevProps.params.cost !== this.props.params.cost) {
-            this.fetch()
-        }
-    }
-
-    render () {
-        const { features, params, isLoading, isAlreadyFetched, error } = this.props
-
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading || !isAlreadyFetched}
-                error={error}
-                noBackground
-                loadingMessages={xrayLoadingMessages}
-                loadingScenes={[<LoadingAnimation />]}
-            >
-                { () =>
-                    <XRayPageWrapper>
-                        <div className="full">
-                            <div className="my3 flex align-center">
-                                <div className="full">
-                                    <Link
-                                        className="my2 px2 text-bold text-brand-hover inline-block bordered bg-white p1 h4 no-decoration rounded shadowed"
-                                        to={`/xray/table/${features.table.id}/approximate`}
-                                    >
-                                        {features.table.display_name}
-                                    </Link>
-                                    <div className="mt2 flex align-center">
-                                        <h1 className="flex align-center">
-                                            {features.model.display_name}
-                                            <Icon name="chevronright" className="mx1 text-grey-3" size={16} />
-                                            <span className="text-grey-3">{t`X-ray`}</span>
-                                        </h1>
-                                        <div className="ml-auto flex align-center">
-                                            <h3 className="mr2 text-grey-3">{t`Fidelity`}</h3>
-                                            <CostSelect
-                                                xrayType='field'
-                                                id={features.model.id}
-                                                currentCost={params.cost}
-                                            />
-                                        </div>
-                                    </div>
-                                    <p className="mt1 text-paragraph text-measure">
-                                        {features.model.description}
-                                    </p>
-                                </div>
-                            </div>
-                            { features["insights"] &&
-                                <div className="mt4">
-                                    <Heading heading="Takeaways" />
-                                    <Insights features={features} />
-                                </div>
-                            }
-                            <div className="mt4">
-                                <Heading heading={t`Distribution`} />
-                                <div className="bg-white bordered shadowed">
-                                    <div className="lg-p4">
-                                        <div style={{ height: 300 }}>
-                                            { features.histogram.value &&
-                                                <Histogram histogram={features.histogram.value} />
-                                            }
-                                        </div>
-                                    </div>
-                                </div>
-                            </div>
-
-                            { isDate(features.model) && <Periodicity xray={features} /> }
-
-                            <StatGroup
-                                heading={t`Values overview`}
-                                xray={features}
-                                stats={VALUES_OVERVIEW}
-                            />
-
-                            <StatGroup
-                                heading={t`Statistical overview`}
-                                xray={features}
-                                showDescriptions
-                                stats={STATS_OVERVIEW}
-                            />
-
-                            <StatGroup
-                                heading={t`Robots`}
-                                xray={features}
-                                showDescriptions
-                                stats={ROBOTS}
-                            />
-                        </div>
-                    </XRayPageWrapper>
-                }
-            </LoadingAndErrorWrapper>
-        )
+  props: Props;
+
+  componentWillMount() {
+    this.props.initialize();
+    this.fetch();
+  }
+
+  componentWillUnmount() {
+    // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
+    // for all xray types and if switching to different kind of xray (= rendering different React container)
+    // without resetting the state fails because `state.xray.xray` subproperty lookups fail
+    this.props.initialize();
+  }
+
+  fetch() {
+    const { params, fetchFieldXray } = this.props;
+    fetchFieldXray(params.fieldId, params.cost);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.params.cost !== this.props.params.cost) {
+      this.fetch();
     }
+  }
+
+  render() {
+    const { features, params, isLoading, isAlreadyFetched, error } = this.props;
+
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading || !isAlreadyFetched}
+        error={error}
+        noBackground
+        loadingMessages={xrayLoadingMessages}
+        loadingScenes={[<LoadingAnimation />]}
+      >
+        {() => (
+          <XRayPageWrapper>
+            <div className="full">
+              <div className="my3 flex align-center">
+                <div className="full">
+                  <Link
+                    className="my2 px2 text-bold text-brand-hover inline-block bordered bg-white p1 h4 no-decoration rounded shadowed"
+                    to={`/xray/table/${features.table.id}/approximate`}
+                  >
+                    {features.table.display_name}
+                  </Link>
+                  <div className="mt2 flex align-center">
+                    <h1 className="flex align-center">
+                      {features.model.display_name}
+                      <Icon
+                        name="chevronright"
+                        className="mx1 text-grey-3"
+                        size={16}
+                      />
+                      <span className="text-grey-3">{t`X-ray`}</span>
+                    </h1>
+                    <div className="ml-auto flex align-center">
+                      <h3 className="mr2 text-grey-3">{t`Fidelity`}</h3>
+                      <CostSelect
+                        xrayType="field"
+                        id={features.model.id}
+                        currentCost={params.cost}
+                      />
+                    </div>
+                  </div>
+                  <p className="mt1 text-paragraph text-measure">
+                    {features.model.description}
+                  </p>
+                </div>
+              </div>
+              {features["insights"] && (
+                <div className="mt4">
+                  <Heading heading="Takeaways" />
+                  <Insights features={features} />
+                </div>
+              )}
+              <div className="mt4">
+                <Heading heading={t`Distribution`} />
+                <div className="bg-white bordered shadowed">
+                  <div className="lg-p4">
+                    <div style={{ height: 300 }}>
+                      {features.histogram.value && (
+                        <Histogram histogram={features.histogram.value} />
+                      )}
+                    </div>
+                  </div>
+                </div>
+              </div>
+
+              {isDate(features.model) && <Periodicity xray={features} />}
+
+              <StatGroup
+                heading={t`Values overview`}
+                xray={features}
+                stats={VALUES_OVERVIEW}
+              />
+
+              <StatGroup
+                heading={t`Statistical overview`}
+                xray={features}
+                showDescriptions
+                stats={STATS_OVERVIEW}
+              />
+
+              <StatGroup
+                heading={t`Robots`}
+                xray={features}
+                showDescriptions
+                stats={ROBOTS}
+              />
+            </div>
+          </XRayPageWrapper>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-export default FieldXRay
+export default FieldXRay;
diff --git a/frontend/src/metabase/xray/containers/SegmentXRay.jsx b/frontend/src/metabase/xray/containers/SegmentXRay.jsx
index 1576394a1ccfbca45b1822ab9e5d5a004abc94dd..495dea2982be4cd0ee92403114439eb6d84499cb 100644
--- a/frontend/src/metabase/xray/containers/SegmentXRay.jsx
+++ b/frontend/src/metabase/xray/containers/SegmentXRay.jsx
@@ -1,174 +1,189 @@
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import title from 'metabase/hoc/Title'
-import { t } from 'c-3po';
-import { Link } from 'react-router'
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import title from "metabase/hoc/Title";
+import { t } from "c-3po";
+import { Link } from "react-router";
 import { push } from "react-router-redux";
 
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
-import { XRayPageWrapper, Heading } from 'metabase/xray/components/XRayLayout'
-import { fetchSegmentXray, initialize } from 'metabase/xray/xray'
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import { XRayPageWrapper, Heading } from "metabase/xray/components/XRayLayout";
+import { fetchSegmentXray, initialize } from "metabase/xray/xray";
 
-import Icon from 'metabase/components/Icon'
-import CostSelect from 'metabase/xray/components/CostSelect'
+import Icon from "metabase/components/Icon";
+import CostSelect from "metabase/xray/components/CostSelect";
 
 import {
-    getConstituents,
-    getLoadingStatus,
-    getError,
-    getFeatures,
-    getComparables,
-    getIsAlreadyFetched
-} from 'metabase/xray/selectors'
-
-import Constituent from 'metabase/xray/components/Constituent'
-import LoadingAnimation from 'metabase/xray/components/LoadingAnimation'
-
-import { xrayLoadingMessages } from 'metabase/xray/utils'
+  getConstituents,
+  getLoadingStatus,
+  getError,
+  getFeatures,
+  getComparables,
+  getIsAlreadyFetched,
+} from "metabase/xray/selectors";
+
+import Constituent from "metabase/xray/components/Constituent";
+import LoadingAnimation from "metabase/xray/components/LoadingAnimation";
+
+import { xrayLoadingMessages } from "metabase/xray/utils";
 import { ComparisonDropdown } from "metabase/xray/components/ComparisonDropdown";
 
 type Props = {
-    fetchSegmentXray: () => void,
-    initialize: () => {},
-    constituents: [],
-    features: {
-        table: Table,
-        model: Segment,
-    },
-    params: {
-        segmentId: number,
-        cost: string,
-    },
-    isLoading: boolean,
-    push: (string) => void,
-    isAlreadyFetched: boolean,
-    error: {}
-}
+  fetchSegmentXray: () => void,
+  initialize: () => {},
+  constituents: [],
+  features: {
+    table: Table,
+    model: Segment,
+  },
+  params: {
+    segmentId: number,
+    cost: string,
+  },
+  isLoading: boolean,
+  push: string => void,
+  isAlreadyFetched: boolean,
+  error: {},
+};
 
 const mapStateToProps = state => ({
-    features:       getFeatures(state),
-    constituents:   getConstituents(state),
-    comparables:    getComparables(state),
-    isLoading:      getLoadingStatus(state),
-    isAlreadyFetched: getIsAlreadyFetched(state),
-    error:          getError(state)
-})
+  features: getFeatures(state),
+  constituents: getConstituents(state),
+  comparables: getComparables(state),
+  isLoading: getLoadingStatus(state),
+  isAlreadyFetched: getIsAlreadyFetched(state),
+  error: getError(state),
+});
 
 const mapDispatchToProps = {
-    initialize,
-    push,
-    fetchSegmentXray
-}
+  initialize,
+  push,
+  fetchSegmentXray,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ features }) => features && features.model.name || t`Segment` )
+@title(({ features }) => (features && features.model.name) || t`Segment`)
 class SegmentXRay extends Component {
-
-    props: Props
-
-    componentWillMount () {
-        this.props.initialize()
-        this.fetch()
-    }
-
-    componentWillUnmount() {
-        // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.features` isn't same
-        // for all xray types and if switching to different kind of xray (= rendering different React container)
-        // without resetting the state fails because `state.xray.features` subproperty lookups fail
-        this.props.initialize();
-    }
-
-    fetch () {
-        const { params, fetchSegmentXray } = this.props
-        fetchSegmentXray(params.segmentId, params.cost)
+  props: Props;
+
+  componentWillMount() {
+    this.props.initialize();
+    this.fetch();
+  }
+
+  componentWillUnmount() {
+    // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.features` isn't same
+    // for all xray types and if switching to different kind of xray (= rendering different React container)
+    // without resetting the state fails because `state.xray.features` subproperty lookups fail
+    this.props.initialize();
+  }
+
+  fetch() {
+    const { params, fetchSegmentXray } = this.props;
+    fetchSegmentXray(params.segmentId, params.cost);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.params.cost !== this.props.params.cost) {
+      this.fetch();
     }
-
-    componentDidUpdate (prevProps: Props) {
-        if(prevProps.params.cost !== this.props.params.cost) {
-            this.fetch()
-        }
-    }
-
-    navigateToComparison(comparable: any) {
-        const { features, push } = this.props
-
-        const currentModelType = features.model["type-tag"]
-        const comparableModelType = comparable["type-tag"]
-
-        if (currentModelType === comparableModelType) {
-            push(`/xray/compare/${currentModelType}s/${features.model.id}/${comparable.id}/approximate`)
-        } else {
-            push(`/xray/compare/${currentModelType}/${features.model.id}/${comparableModelType}/${comparable.id}/approximate`)
-        }
-    }
-
-    render () {
-        const { comparables, constituents, features, params, isLoading, isAlreadyFetched, error } = this.props
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading || !isAlreadyFetched}
-                error={error}
-                loadingMessages={xrayLoadingMessages}
-                loadingScenes={[
-                    <LoadingAnimation />
-                ]}
-                noBackground
-            >
-                { () =>
-                    <XRayPageWrapper>
-                        <div className="full">
-                            <div className="mt4 mb2 flex align-center py2">
-                                <div>
-                                    <Link
-                                        className="my2 px2 text-bold text-brand-hover inline-block bordered bg-white p1 h4 no-decoration shadowed rounded"
-                                        to={`/xray/table/${features.table.id}/approximate`}
-                                    >
-                                        {features.table.display_name}
-                                    </Link>
-                                    <h1 className="mt2 flex align-center">
-                                        {features.model.name}
-                                        <Icon name="chevronright" className="mx1 text-grey-3" size={16} />
-                                        <span className="text-grey-3">{t`X-ray`}</span>
-                                    </h1>
-                                    <p className="mt1 text-paragraph text-measure">
-                                        {features.model.description}
-                                    </p>
-                                </div>
-                                <div className="ml-auto flex align-center">
-                                   <h3 className="mr2 text-grey-3">{t`Fidelity`}</h3>
-                                    <CostSelect
-                                        currentCost={params.cost}
-                                        xrayType='segment'
-                                        id={features.model.id}
-                                    />
-                                </div>
-                            </div>
-                            { comparables.length > 0 &&
-                            <ComparisonDropdown
-                                models={[features.model]}
-                                comparables={comparables}
-                            />
-                            }
-                            <div className="mt2">
-                                <Heading heading={t`Fields in this segment`} />
-                                <ol>
-                                    { constituents.map((c, i) => {
-                                        return (
-                                            <li key={i}>
-                                                <Constituent
-                                                    constituent={c}
-                                                />
-                                            </li>
-                                        )
-                                    })}
-                                </ol>
-                            </div>
-                        </div>
-                    </XRayPageWrapper>
-                }
-            </LoadingAndErrorWrapper>
-        )
+  }
+
+  navigateToComparison(comparable: any) {
+    const { features, push } = this.props;
+
+    const currentModelType = features.model["type-tag"];
+    const comparableModelType = comparable["type-tag"];
+
+    if (currentModelType === comparableModelType) {
+      push(
+        `/xray/compare/${currentModelType}s/${features.model.id}/${
+          comparable.id
+        }/approximate`,
+      );
+    } else {
+      push(
+        `/xray/compare/${currentModelType}/${
+          features.model.id
+        }/${comparableModelType}/${comparable.id}/approximate`,
+      );
     }
+  }
+
+  render() {
+    const {
+      comparables,
+      constituents,
+      features,
+      params,
+      isLoading,
+      isAlreadyFetched,
+      error,
+    } = this.props;
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading || !isAlreadyFetched}
+        error={error}
+        loadingMessages={xrayLoadingMessages}
+        loadingScenes={[<LoadingAnimation />]}
+        noBackground
+      >
+        {() => (
+          <XRayPageWrapper>
+            <div className="full">
+              <div className="mt4 mb2 flex align-center py2">
+                <div>
+                  <Link
+                    className="my2 px2 text-bold text-brand-hover inline-block bordered bg-white p1 h4 no-decoration shadowed rounded"
+                    to={`/xray/table/${features.table.id}/approximate`}
+                  >
+                    {features.table.display_name}
+                  </Link>
+                  <h1 className="mt2 flex align-center">
+                    {features.model.name}
+                    <Icon
+                      name="chevronright"
+                      className="mx1 text-grey-3"
+                      size={16}
+                    />
+                    <span className="text-grey-3">{t`X-ray`}</span>
+                  </h1>
+                  <p className="mt1 text-paragraph text-measure">
+                    {features.model.description}
+                  </p>
+                </div>
+                <div className="ml-auto flex align-center">
+                  <h3 className="mr2 text-grey-3">{t`Fidelity`}</h3>
+                  <CostSelect
+                    currentCost={params.cost}
+                    xrayType="segment"
+                    id={features.model.id}
+                  />
+                </div>
+              </div>
+              {comparables.length > 0 && (
+                <ComparisonDropdown
+                  models={[features.model]}
+                  comparables={comparables}
+                />
+              )}
+              <div className="mt2">
+                <Heading heading={t`Fields in this segment`} />
+                <ol>
+                  {constituents.map((c, i) => {
+                    return (
+                      <li key={i}>
+                        <Constituent constituent={c} />
+                      </li>
+                    );
+                  })}
+                </ol>
+              </div>
+            </div>
+          </XRayPageWrapper>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-export default SegmentXRay
+export default SegmentXRay;
diff --git a/frontend/src/metabase/xray/containers/TableLikeComparison.jsx b/frontend/src/metabase/xray/containers/TableLikeComparison.jsx
index 93a950394f9f94c937d9cff800ba072a281bc759..06d4d35bc8b40c3f9b8a6f68cf155d09124a1060 100644
--- a/frontend/src/metabase/xray/containers/TableLikeComparison.jsx
+++ b/frontend/src/metabase/xray/containers/TableLikeComparison.jsx
@@ -1,132 +1,143 @@
-import React, { Component } from 'react'
-import { connect } from 'react-redux'
-import _ from 'underscore'
+import React, { Component } from "react";
+import { connect } from "react-redux";
+import _ from "underscore";
 
 import {
-    fetchSharedTypeComparisonXray,
-    fetchTwoTypesComparisonXray,
-    initialize
-} from 'metabase/xray/xray'
+  fetchSharedTypeComparisonXray,
+  fetchTwoTypesComparisonXray,
+  initialize,
+} from "metabase/xray/xray";
 import {
-    getComparison,
-    getComparisonFields,
-    getComparisonContributors,
-    getModelItem,
-    getLoadingStatus,
-    getError,
-    getTitle
-} from 'metabase/xray/selectors'
+  getComparison,
+  getComparisonFields,
+  getComparisonContributors,
+  getModelItem,
+  getLoadingStatus,
+  getError,
+  getTitle,
+} from "metabase/xray/selectors";
 
-import title from 'metabase/hoc/Title'
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
-import XRayComparison from 'metabase/xray/components/XRayComparison'
-import LoadingAnimation from 'metabase/xray/components/LoadingAnimation'
+import title from "metabase/hoc/Title";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import XRayComparison from "metabase/xray/components/XRayComparison";
+import LoadingAnimation from "metabase/xray/components/LoadingAnimation";
 
-import { hasComparison, comparisonLoadingMessages } from 'metabase/xray/utils'
+import { hasComparison, comparisonLoadingMessages } from "metabase/xray/utils";
 
 @connect(null, { fetchSharedTypeComparisonXray })
 export class SharedTypeComparisonXRay extends Component {
-    render() {
-        const { modelTypePlural, modelId1, modelId2, cost } = this.props.params;
+  render() {
+    const { modelTypePlural, modelId1, modelId2, cost } = this.props.params;
 
-        return (
-            <TableLikeComparisonXRay
-                fetchTableLikeComparisonXray={
-                    () => this.props.fetchSharedTypeComparisonXray(modelTypePlural, modelId1, modelId2, cost)
-                }
-            />
-        )
-    }
+    return (
+      <TableLikeComparisonXRay
+        fetchTableLikeComparisonXray={() =>
+          this.props.fetchSharedTypeComparisonXray(
+            modelTypePlural,
+            modelId1,
+            modelId2,
+            cost,
+          )
+        }
+      />
+    );
+  }
 }
 
 @connect(null, { fetchTwoTypesComparisonXray })
 export class TwoTypesComparisonXRay extends Component {
-    render() {
-        const { modelType1, modelId1, modelType2, modelId2, cost } = this.props.params;
+  render() {
+    const {
+      modelType1,
+      modelId1,
+      modelType2,
+      modelId2,
+      cost,
+    } = this.props.params;
 
-        return (
-            <TableLikeComparisonXRay
-                fetchTableLikeComparisonXray={
-                    () => this.props.fetchTwoTypesComparisonXray(modelType1, modelId1, modelType2, modelId2, cost)
-                }
-            />
-        )
-    }
+    return (
+      <TableLikeComparisonXRay
+        fetchTableLikeComparisonXray={() =>
+          this.props.fetchTwoTypesComparisonXray(
+            modelType1,
+            modelId1,
+            modelType2,
+            modelId2,
+            cost,
+          )
+        }
+      />
+    );
+  }
 }
 
-const mapStateToProps = (state) => ({
-    comparison:     getComparison(state),
-    fields:         getComparisonFields(state),
-    contributors:   getComparisonContributors(state),
-    itemA:          getModelItem(state, 0),
-    itemB:          getModelItem(state, 1),
-    isLoading:      getLoadingStatus(state),
-    error:          getError(state)
-})
+const mapStateToProps = state => ({
+  comparison: getComparison(state),
+  fields: getComparisonFields(state),
+  contributors: getComparisonContributors(state),
+  itemA: getModelItem(state, 0),
+  itemB: getModelItem(state, 1),
+  isLoading: getLoadingStatus(state),
+  error: getError(state),
+});
 
 const mapDispatchToProps = {
-    initialize
-}
+  initialize,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
 @title(props => getTitle(props))
 export class TableLikeComparisonXRay extends Component {
-    props: {
-        initialize: () => void,
-        fetchTableLikeComparisonXray: () => void
-    }
-
-    componentWillMount () {
-        this.props.initialize()
-        this.props.fetchTableLikeComparisonXray()
-    }
+  props: {
+    initialize: () => void,
+    fetchTableLikeComparisonXray: () => void,
+  };
 
-    componentWillUnmount() {
-        // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
-        // for all xray types and if switching to different kind of xray (= rendering different React container)
-        // without resetting the state fails because `state.xray.xray` subproperty lookups fail
-        this.props.initialize();
-    }
+  componentWillMount() {
+    this.props.initialize();
+    this.props.fetchTableLikeComparisonXray();
+  }
 
-    render () {
-        const {
-            comparison,
-            contributors,
-            error,
-            fields,
-            isLoading,
-            itemA,
-            itemB,
-            cost
-        } = this.props
+  componentWillUnmount() {
+    // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
+    // for all xray types and if switching to different kind of xray (= rendering different React container)
+    // without resetting the state fails because `state.xray.xray` subproperty lookups fail
+    this.props.initialize();
+  }
 
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading || !hasComparison(comparison)}
-                error={error}
-                noBackground
-                loadingMessages={comparisonLoadingMessages}
-                loadingScenes={[<LoadingAnimation />]}
-            >
-                { () =>
+  render() {
+    const {
+      comparison,
+      contributors,
+      error,
+      fields,
+      isLoading,
+      itemA,
+      itemB,
+      cost,
+    } = this.props;
 
-                    <XRayComparison
-                        cost={cost}
-                        fields={_.sortBy(fields, 'distance').reverse()}
-                        comparisonFields={[
-                            'Difference',
-                            'Entropy',
-                            'Histogram',
-                            'Nil%',
-                        ]}
-                        contributors={contributors}
-                        comparables={comparison.comparables}
-                        comparison={comparison.comparison}
-                        itemA={itemA}
-                        itemB={itemB}
-                    />
-                }
-            </LoadingAndErrorWrapper>
-        )
-    }
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading || !hasComparison(comparison)}
+        error={error}
+        noBackground
+        loadingMessages={comparisonLoadingMessages}
+        loadingScenes={[<LoadingAnimation />]}
+      >
+        {() => (
+          <XRayComparison
+            cost={cost}
+            fields={_.sortBy(fields, "distance").reverse()}
+            comparisonFields={["Difference", "Entropy", "Histogram", "Nil%"]}
+            contributors={contributors}
+            comparables={comparison.comparables}
+            comparison={comparison.comparison}
+            itemA={itemA}
+            itemB={itemB}
+          />
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
diff --git a/frontend/src/metabase/xray/containers/TableXRay.jsx b/frontend/src/metabase/xray/containers/TableXRay.jsx
index 2eb8ec9add4c6ddf91a89efabd1fb3c332b1f299..cb4b2992eed500aaeba15427a07c50ab48e590c6 100644
--- a/frontend/src/metabase/xray/containers/TableXRay.jsx
+++ b/frontend/src/metabase/xray/containers/TableXRay.jsx
@@ -1,144 +1,155 @@
-import React, { Component } from 'react'
-import { t } from 'c-3po';
-import { connect } from 'react-redux'
-import title from 'metabase/hoc/Title'
+import React, { Component } from "react";
+import { t } from "c-3po";
+import { connect } from "react-redux";
+import title from "metabase/hoc/Title";
 
-import { fetchTableXray, initialize } from 'metabase/xray/xray'
-import { XRayPageWrapper } from 'metabase/xray/components/XRayLayout'
+import { fetchTableXray, initialize } from "metabase/xray/xray";
+import { XRayPageWrapper } from "metabase/xray/components/XRayLayout";
 
-import CostSelect from 'metabase/xray/components/CostSelect'
-import Constituent from 'metabase/xray/components/Constituent'
+import CostSelect from "metabase/xray/components/CostSelect";
+import Constituent from "metabase/xray/components/Constituent";
 
 import {
-    getConstituents,
-    getFeatures,
-    getLoadingStatus,
-    getError,
-    getComparables,
-    getIsAlreadyFetched,
-} from 'metabase/xray/selectors'
+  getConstituents,
+  getFeatures,
+  getLoadingStatus,
+  getError,
+  getComparables,
+  getIsAlreadyFetched,
+} from "metabase/xray/selectors";
 
-import Icon from 'metabase/components/Icon'
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
-import LoadingAnimation from 'metabase/xray/components/LoadingAnimation'
+import Icon from "metabase/components/Icon";
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+import LoadingAnimation from "metabase/xray/components/LoadingAnimation";
 
-import type { Table } from 'metabase/meta/types/Table'
+import type { Table } from "metabase/meta/types/Table";
 
-import { xrayLoadingMessages } from 'metabase/xray/utils'
+import { xrayLoadingMessages } from "metabase/xray/utils";
 import { ComparisonDropdown } from "metabase/xray/components/ComparisonDropdown";
 
 type Props = {
-    fetchTableXray: () => void,
-    initialize: () => {},
-    constituents: [],
-    isLoading: boolean,
-    isAlreadyFetched: boolean,
-    xray: {
-        table: Table
-    },
-    params: {
-        tableId: number,
-        cost: string
-    },
-    error: {}
-}
+  fetchTableXray: () => void,
+  initialize: () => {},
+  constituents: [],
+  isLoading: boolean,
+  isAlreadyFetched: boolean,
+  xray: {
+    table: Table,
+  },
+  params: {
+    tableId: number,
+    cost: string,
+  },
+  error: {},
+};
 
 const mapStateToProps = state => ({
-    features: getFeatures(state),
-    constituents: getConstituents(state),
-    comparables: getComparables(state),
-    isLoading: getLoadingStatus(state),
-    isAlreadyFetched: getIsAlreadyFetched(state),
-    error: getError(state)
-})
+  features: getFeatures(state),
+  constituents: getConstituents(state),
+  comparables: getComparables(state),
+  isLoading: getLoadingStatus(state),
+  isAlreadyFetched: getIsAlreadyFetched(state),
+  error: getError(state),
+});
 
 const mapDispatchToProps = {
-    initialize,
-    fetchTableXray
-}
+  initialize,
+  fetchTableXray,
+};
 
 @connect(mapStateToProps, mapDispatchToProps)
-@title(({ features }) => features && features.model.display_name || t`Table`)
+@title(({ features }) => (features && features.model.display_name) || t`Table`)
 class TableXRay extends Component {
-    props: Props
-
-    componentWillMount () {
-        this.props.initialize()
-        this.fetch()
-    }
-
-    componentWillUnmount() {
-        // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
-        // for all xray types and if switching to different kind of xray (= rendering different React container)
-        // without resetting the state fails because `state.xray.xray` subproperty lookups fail
-        this.props.initialize();
-    }
-
-    fetch () {
-        const { params, fetchTableXray } = this.props
-        fetchTableXray(params.tableId, params.cost)
-    }
-
-    componentDidUpdate (prevProps: Props) {
-        if(prevProps.params.cost !== this.props.params.cost) {
-            this.fetch()
-        }
-    }
-
-    render () {
-        const { comparables, constituents, features, params, isLoading, isAlreadyFetched, error } = this.props
-
-        return (
-            <LoadingAndErrorWrapper
-                loading={isLoading || !isAlreadyFetched}
-                error={error}
-                noBackground
-                loadingMessages={xrayLoadingMessages}
-                loadingScenes={[<LoadingAnimation />]}
-            >
-                { () =>
-                    <XRayPageWrapper>
-                        <div className="full">
-                            <div className="my4 flex align-center py2">
-                                <div>
-                                    <h1 className="mt2 flex align-center">
-                                        {features.model.display_name}
-                                        <Icon name="chevronright" className="mx1 text-grey-3" size={16} />
-                                        <span className="text-grey-3">{t`XRay`}</span>
-                                    </h1>
-                                    <p className="m0 text-paragraph text-measure">{features.model.description}</p>
-                                </div>
-                                <div className="ml-auto flex align-center">
-                                    <h3 className="mr2">{t`Fidelity:`}</h3>
-                                    <CostSelect
-                                        xrayType='table'
-                                        currentCost={params.cost}
-                                        id={features.model.id}
-                                    />
-                                </div>
-                            </div>
-                            { comparables.length > 0 &&
-                                <ComparisonDropdown
-                                    models={[features.model]}
-                                    comparables={comparables}
-                                />
-                            }
-                            <ol>
-                                { constituents.map((constituent, index) =>
-                                    <li key={index}>
-                                        <Constituent
-                                            constituent={constituent}
-                                        />
-                                    </li>
-                                )}
-                            </ol>
-                        </div>
-                    </XRayPageWrapper>
-                }
-            </LoadingAndErrorWrapper>
-        )
+  props: Props;
+
+  componentWillMount() {
+    this.props.initialize();
+    this.fetch();
+  }
+
+  componentWillUnmount() {
+    // HACK Atte Keinänen 9/20/17: We need this for now because the structure of `state.xray.xray` isn't same
+    // for all xray types and if switching to different kind of xray (= rendering different React container)
+    // without resetting the state fails because `state.xray.xray` subproperty lookups fail
+    this.props.initialize();
+  }
+
+  fetch() {
+    const { params, fetchTableXray } = this.props;
+    fetchTableXray(params.tableId, params.cost);
+  }
+
+  componentDidUpdate(prevProps: Props) {
+    if (prevProps.params.cost !== this.props.params.cost) {
+      this.fetch();
     }
+  }
+
+  render() {
+    const {
+      comparables,
+      constituents,
+      features,
+      params,
+      isLoading,
+      isAlreadyFetched,
+      error,
+    } = this.props;
+
+    return (
+      <LoadingAndErrorWrapper
+        loading={isLoading || !isAlreadyFetched}
+        error={error}
+        noBackground
+        loadingMessages={xrayLoadingMessages}
+        loadingScenes={[<LoadingAnimation />]}
+      >
+        {() => (
+          <XRayPageWrapper>
+            <div className="full">
+              <div className="my4 flex align-center py2">
+                <div>
+                  <h1 className="mt2 flex align-center">
+                    {features.model.display_name}
+                    <Icon
+                      name="chevronright"
+                      className="mx1 text-grey-3"
+                      size={16}
+                    />
+                    <span className="text-grey-3">{t`XRay`}</span>
+                  </h1>
+                  <p className="m0 text-paragraph text-measure">
+                    {features.model.description}
+                  </p>
+                </div>
+                <div className="ml-auto flex align-center">
+                  <h3 className="mr2">{t`Fidelity:`}</h3>
+                  <CostSelect
+                    xrayType="table"
+                    currentCost={params.cost}
+                    id={features.model.id}
+                  />
+                </div>
+              </div>
+              {comparables.length > 0 && (
+                <ComparisonDropdown
+                  models={[features.model]}
+                  comparables={comparables}
+                />
+              )}
+              <ol>
+                {constituents.map((constituent, index) => (
+                  <li key={index}>
+                    <Constituent constituent={constituent} />
+                  </li>
+                ))}
+              </ol>
+            </div>
+          </XRayPageWrapper>
+        )}
+      </LoadingAndErrorWrapper>
+    );
+  }
 }
 
-
-export default TableXRay
+export default TableXRay;
diff --git a/frontend/src/metabase/xray/costs.js b/frontend/src/metabase/xray/costs.js
index 005cd15ecf6b22ca02b3b82a4bab0400c7248ee6..aee04a3a6dcec710b6c3e23ce103e379a8ba9994 100644
--- a/frontend/src/metabase/xray/costs.js
+++ b/frontend/src/metabase/xray/costs.js
@@ -2,49 +2,49 @@
  * human understandable groupings.
  * for more info on the actual values see src/metabase/fingerprints/costs.clj
  */
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
 const approximate = {
-    display_name: t`Approximate`,
-    description: t`
+  display_name: t`Approximate`,
+  description: t`
         Get a sense for this data by looking at a sample.
         This is faster but less precise.
     `,
-    method: {
-        max_query_cost: 'sample',
-        max_computation_cost: 'linear'
-    },
-    icon: 'costapproximate'
-}
+  method: {
+    max_query_cost: "sample",
+    max_computation_cost: "linear",
+  },
+  icon: "costapproximate",
+};
 
 const exact = {
-    display_name: t`Exact`,
-    description: t`
+  display_name: t`Exact`,
+  description: t`
         Go deeper into this data by performing a full scan.
         This is more precise but slower.
     `,
-    method: {
-        max_query_cost: 'full-scan',
-        max_computation_cost: 'unbounded'
-    },
-    icon: 'costexact'
-}
+  method: {
+    max_query_cost: "full-scan",
+    max_computation_cost: "unbounded",
+  },
+  icon: "costexact",
+};
 
 const extended = {
-    display_name: t`Extended`,
-    description: t`
+  display_name: t`Extended`,
+  description: t`
         Adds additional info about this entity by including related objects.
         This is the slowest but highest fidelity method.
     `,
-    method: {
-        max_query_cost: 'joins',
-        max_computation_cost: 'unbounded'
-    },
-    icon: 'costextended'
-}
+  method: {
+    max_query_cost: "joins",
+    max_computation_cost: "unbounded",
+  },
+  icon: "costextended",
+};
 
 export default {
-    approximate,
-    exact,
-    extended
-}
+  approximate,
+  exact,
+  extended,
+};
diff --git a/frontend/src/metabase/xray/selectors.js b/frontend/src/metabase/xray/selectors.js
index a6dad1461cbe7643fb9b4e0a5efaa7853482fbe1..1e823bc83943c6b9ccb94df50e9cfb42461c60fb 100644
--- a/frontend/src/metabase/xray/selectors.js
+++ b/frontend/src/metabase/xray/selectors.js
@@ -1,148 +1,139 @@
-import { createSelector } from 'reselect'
-import { normal } from 'metabase/lib/colors'
+import { createSelector } from "reselect";
+import { normal } from "metabase/lib/colors";
 
-export const getLoadingStatus = (state) =>
-    state.xray.loading
+export const getLoadingStatus = state => state.xray.loading;
 
-export const getIsAlreadyFetched = (state) =>
-    state.xray.fetched
+export const getIsAlreadyFetched = state => state.xray.fetched;
 
-export const getError = (state) =>
-    state.xray.error
+export const getError = state => state.xray.error;
 
-export const getXray = (state) =>
-    state.xray.xray
+export const getXray = state => state.xray.xray;
 
-export const getFeatures = (state) =>
-    state.xray.xray && state.xray.xray.features
+export const getFeatures = state => state.xray.xray && state.xray.xray.features;
 
-export const getComparables = (state) =>
-    state.xray.xray && state.xray.xray.comparables
+export const getComparables = state =>
+  state.xray.xray && state.xray.xray.comparables;
 
 export const getConstituents = createSelector(
-    [getXray],
-    (xray) => xray && Object.values(xray.constituents)
-)
+  [getXray],
+  xray => xray && Object.values(xray.constituents),
+);
 
-export const getComparison = (state) => state.xray.comparison
+export const getComparison = state => state.xray.comparison;
 
 export const getComparisonFields = createSelector(
-    [getComparison],
-    (comparison) => {
-        if(comparison) {
-            return Object.keys(comparison.constituents[0].constituents)
-                .map(key => {
-                    return {
-                        ...comparison.constituents[0].constituents[key].model,
-                        distance: comparison.comparison[key].distance
-                    }
-                })
-        }
+  [getComparison],
+  comparison => {
+    if (comparison) {
+      return Object.keys(comparison.constituents[0].constituents).map(key => {
+        return {
+          ...comparison.constituents[0].constituents[key].model,
+          distance: comparison.comparison[key].distance,
+        };
+      });
     }
-)
+  },
+);
 
 export const getComparisonContributors = createSelector(
-    [getComparison],
-    (comparison) => {
-        if(comparison) {
-
-            const getValue = (constituent, { field, feature }) => {
-                return constituent.constituents[field][feature] &&
-                    constituent.constituents[field][feature].value
-            }
-
-            const genContributor = ({ field, feature }) => {
-                const featureValue = {
-                    a: getValue(comparison.constituents[0], { field, feature }),
-                    b: getValue(comparison.constituents[1], { field, feature })
-                };
-
-                if (featureValue.a !== null && featureValue.b !== null) {
-                    return {
-                        field: comparison.constituents[0].constituents[field],
-                        feature: {
-                            ...comparison.constituents[0].constituents[field][feature],
-                            value: featureValue,
-                            type: feature
-                        }
-                    }
-                } else {
-                    // NOTE Atte Keinänen: This will become obsolete
-                    return null;
-                }
-            }
-
-            const top = comparison['top-contributors']
-
-            return top && top.map(genContributor).filter((contributor) => contributor !== null)
+  [getComparison],
+  comparison => {
+    if (comparison) {
+      const getValue = (constituent, { field, feature }) => {
+        return (
+          constituent.constituents[field][feature] &&
+          constituent.constituents[field][feature].value
+        );
+      };
+
+      const genContributor = ({ field, feature }) => {
+        const featureValue = {
+          a: getValue(comparison.constituents[0], { field, feature }),
+          b: getValue(comparison.constituents[1], { field, feature }),
+        };
+
+        if (featureValue.a !== null && featureValue.b !== null) {
+          return {
+            field: comparison.constituents[0].constituents[field],
+            feature: {
+              ...comparison.constituents[0].constituents[field][feature],
+              value: featureValue,
+              type: feature,
+            },
+          };
+        } else {
+          // NOTE Atte Keinänen: This will become obsolete
+          return null;
         }
+      };
+
+      const top = comparison["top-contributors"];
+
+      return (
+        top &&
+        top.map(genContributor).filter(contributor => contributor !== null)
+      );
     }
-)
+  },
+);
 
 export const getTitle = ({ comparison, itemA, itemB }) =>
-    comparison && `${itemA.name} / ${itemB.name}`
+  comparison && `${itemA.name} / ${itemB.name}`;
 
-const getItemColor = (index) => ({
-    main: index === 0 ? normal.teal : normal.purple,
-    text: index === 0 ? '#57C5DA' : normal.purple
-})
+const getItemColor = index => ({
+  main: index === 0 ? normal.teal : normal.purple,
+  text: index === 0 ? "#57C5DA" : normal.purple,
+});
 
 const genItem = (item, index) => ({
-    name: item.name,
-    id: item.id,
-    "type-tag": item["type-tag"],
-    color: getItemColor(index),
-})
-
-export const getModelItem = (state, index = 0) => createSelector(
-    [getComparison],
-    (comparison) => {
-        if(comparison) {
-            const item = comparison.constituents[index].features.model
-            return {
-                ...genItem(item, index),
-                constituents: comparison.constituents[index].constituents
-            }
-        }
+  name: item.name,
+  id: item.id,
+  "type-tag": item["type-tag"],
+  color: getItemColor(index),
+});
+
+export const getModelItem = (state, index = 0) =>
+  createSelector([getComparison], comparison => {
+    if (comparison) {
+      const item = comparison.constituents[index].features.model;
+      return {
+        ...genItem(item, index),
+        constituents: comparison.constituents[index].constituents,
+      };
     }
-)(state)
-
-export const getSegmentItem = (state, index = 0) => createSelector(
-    [getComparison],
-    (comparison) => {
-        if(comparison) {
-            const item = comparison.constituents[index].features.segment
-            return {
-                ...genItem(item, index),
-                constituents: comparison.constituents[index].constituents,
-            }
-        }
+  })(state);
+
+export const getSegmentItem = (state, index = 0) =>
+  createSelector([getComparison], comparison => {
+    if (comparison) {
+      const item = comparison.constituents[index].features.segment;
+      return {
+        ...genItem(item, index),
+        constituents: comparison.constituents[index].constituents,
+      };
     }
-)(state)
-
-export const getTableItem = (state, index = 1) => createSelector(
-    [getComparison],
-    (comparison) => {
-        if(comparison) {
-            const item = comparison.constituents[index].features.table
-            return {
-                ...genItem(item, index),
-                name: item.display_name,
-                constituents: comparison.constituents[index].constituents,
-
-            }
-        }
+  })(state);
+
+export const getTableItem = (state, index = 1) =>
+  createSelector([getComparison], comparison => {
+    if (comparison) {
+      const item = comparison.constituents[index].features.table;
+      return {
+        ...genItem(item, index),
+        name: item.display_name,
+        constituents: comparison.constituents[index].constituents,
+      };
     }
-)(state)
+  })(state);
 
 // see if xrays are enabled. unfortunately enabled can equal null so its enabled if its not false
 export const getXrayEnabled = state => {
-    const enabled = state.settings.values && state.settings.values['enable_xrays']
-    if(enabled == null || enabled == true) {
-        return  true
-    }
-    return false
-}
-
-export const getMaxCost = state => state.settings.values['xray_max_cost']
-
+  const enabled =
+    state.settings.values && state.settings.values["enable_xrays"];
+  if (enabled == null || enabled == true) {
+    return true;
+  }
+  return false;
+};
+
+export const getMaxCost = state => state.settings.values["xray_max_cost"];
diff --git a/frontend/src/metabase/xray/stats.js b/frontend/src/metabase/xray/stats.js
index 898eb67d5b38b29045936489ec7087736f430b3c..930e96b47f0ba103c508d350459119a0d7aea49c 100644
--- a/frontend/src/metabase/xray/stats.js
+++ b/frontend/src/metabase/xray/stats.js
@@ -1,34 +1,34 @@
 // keys for common values interesting for most folks
 export const VALUES_OVERVIEW = [
-    'min',
-    'earliest', // date field min is expressed as earliest
-    'max',
-    'latest', // date field max is expressed as latest
-    'count',
-    'sum',
-    'cardinality',
-    'sd',
-    'nil%',
-    'mean',
-    'median',
-    'mean-median-spread'
-]
+  "min",
+  "earliest", // date field min is expressed as earliest
+  "max",
+  "latest", // date field max is expressed as latest
+  "count",
+  "sum",
+  "cardinality",
+  "sd",
+  "nil%",
+  "mean",
+  "median",
+  "mean-median-spread",
+];
 
 // keys for common values interesting for stat folks
 export const STATS_OVERVIEW = [
-    'kurtosis',
-    'skewness',
-    'entropy',
-    'var',
-    'sum-of-square',
-]
+  "kurtosis",
+  "skewness",
+  "entropy",
+  "var",
+  "sum-of-square",
+];
 
 export const ROBOTS = [
-    'cardinality-vs-count',
-    'positive-definite?',
-    'has-nils?',
-    'all-distinct?',
-]
+  "cardinality-vs-count",
+  "positive-definite?",
+  "has-nils?",
+  "all-distinct?",
+];
 
 // periods we care about for showing periodicity
-export const PERIODICITY = ['day', 'week', 'month', 'hour', 'quarter']
+export const PERIODICITY = ["day", "week", "month", "hour", "quarter"];
diff --git a/frontend/src/metabase/xray/utils.js b/frontend/src/metabase/xray/utils.js
index f236ce5b091a0ecef10f50af2b86dbdc1e617f65..e9e5b0fe00797f2aefae3499db10e22970bdec64 100644
--- a/frontend/src/metabase/xray/utils.js
+++ b/frontend/src/metabase/xray/utils.js
@@ -1,34 +1,34 @@
 // takes a distance float and uses it to return a human readable phrase
 // indicating how similar two items in a comparison are
-import { t } from 'c-3po';
+import { t } from "c-3po";
 
-export const distanceToPhrase = (distance) => {
-    if(distance >= 0.75) {
-        return t`Very different`
-    } else if (distance < 0.75 && distance >= 0.5) {
-        return t`Somewhat different`
-    } else if (distance < 0.5 && distance >= 0.25) {
-        return t`Somewhat similar`
-    } else {
-        return t`Very similar`
-    }
-}
+export const distanceToPhrase = distance => {
+  if (distance >= 0.75) {
+    return t`Very different`;
+  } else if (distance < 0.75 && distance >= 0.5) {
+    return t`Somewhat different`;
+  } else if (distance < 0.5 && distance >= 0.25) {
+    return t`Somewhat similar`;
+  } else {
+    return t`Very similar`;
+  }
+};
 
 // Small utilities to determine whether we have an entity yet or not,
 // used for loading status
-function has (entity) {
-    return typeof entity !== 'undefined' ? true : false
+function has(entity) {
+  return typeof entity !== "undefined" ? true : false;
 }
 
-export const hasXray = has
-export const hasComparison = has
+export const hasXray = has;
+export const hasComparison = has;
 
 export const xrayLoadingMessages = [
-    t`Generating your x-ray...`,
-    t`Still working...`,
-]
+  t`Generating your x-ray...`,
+  t`Still working...`,
+];
 
 export const comparisonLoadingMessages = [
-    t`Generating your comparison...`,
-    t`Still working...`,
-]
+  t`Generating your comparison...`,
+  t`Still working...`,
+];
diff --git a/frontend/src/metabase/xray/xray.js b/frontend/src/metabase/xray/xray.js
index 0bd596b4b6a10902ca6658b4b5007e7bfa8c99ef..9dfb1efd245c625478230f5229c721418941a246 100644
--- a/frontend/src/metabase/xray/xray.js
+++ b/frontend/src/metabase/xray/xray.js
@@ -1,84 +1,110 @@
-import COSTS from 'metabase/xray/costs'
+import COSTS from "metabase/xray/costs";
 
 import {
-    createAction,
-    createThunkAction,
-    handleActions
-} from 'metabase/lib/redux'
-import { BackgroundJobRequest/*, RestfulRequest*/ } from "metabase/lib/request";
+  createAction,
+  createThunkAction,
+  handleActions,
+} from "metabase/lib/redux";
+import {
+  BackgroundJobRequest /*, RestfulRequest*/,
+} from "metabase/lib/request";
 
-import { XRayApi } from 'metabase/services'
+import { XRayApi } from "metabase/services";
 
-export const INITIALIZE = 'metabase/xray/INITIALIZE'
+export const INITIALIZE = "metabase/xray/INITIALIZE";
 export const initialize = createAction(INITIALIZE);
 
-export const FETCH_FIELD_XRAY = 'metabase/xray/FETCH_FIELD_XRAY'
+export const FETCH_FIELD_XRAY = "metabase/xray/FETCH_FIELD_XRAY";
 const fieldXrayRequest = new BackgroundJobRequest({
-    creationEndpoint: XRayApi.field_xray,
-    resultPropName: 'xray',
-    actionPrefix: FETCH_FIELD_XRAY
-})
-export const fetchFieldXray = createThunkAction(FETCH_FIELD_XRAY, (fieldId, cost) =>
-    (dispatch) =>
-        dispatch(fieldXrayRequest.trigger({ fieldId, ...COSTS[cost].method }))
-)
+  creationEndpoint: XRayApi.field_xray,
+  resultPropName: "xray",
+  actionPrefix: FETCH_FIELD_XRAY,
+});
+export const fetchFieldXray = createThunkAction(
+  FETCH_FIELD_XRAY,
+  (fieldId, cost) => dispatch =>
+    dispatch(fieldXrayRequest.trigger({ fieldId, ...COSTS[cost].method })),
+);
 
-export const FETCH_TABLE_XRAY = 'metabase/xray/FETCH_TABLE_XRAY'
+export const FETCH_TABLE_XRAY = "metabase/xray/FETCH_TABLE_XRAY";
 const tableXrayRequest = new BackgroundJobRequest({
-    creationEndpoint: XRayApi.table_xray,
-    resultPropName: 'xray',
-    actionPrefix: FETCH_TABLE_XRAY
-})
-export const fetchTableXray = createThunkAction(FETCH_TABLE_XRAY, (tableId, cost) =>
-    (dispatch) =>
-        dispatch(tableXrayRequest.trigger({ tableId, ...COSTS[cost].method }))
-)
+  creationEndpoint: XRayApi.table_xray,
+  resultPropName: "xray",
+  actionPrefix: FETCH_TABLE_XRAY,
+});
+export const fetchTableXray = createThunkAction(
+  FETCH_TABLE_XRAY,
+  (tableId, cost) => dispatch =>
+    dispatch(tableXrayRequest.trigger({ tableId, ...COSTS[cost].method })),
+);
 
-export const FETCH_SEGMENT_XRAY = 'metabase/xray/FETCH_SEGMENT_XRAY'
+export const FETCH_SEGMENT_XRAY = "metabase/xray/FETCH_SEGMENT_XRAY";
 const segmentXrayRequest = new BackgroundJobRequest({
-    creationEndpoint: XRayApi.segment_xray,
-    resultPropName: 'xray',
-    actionPrefix: FETCH_SEGMENT_XRAY
-})
-export const fetchSegmentXray = createThunkAction(FETCH_SEGMENT_XRAY, (segmentId, cost) =>
-    (dispatch) =>
-        dispatch(segmentXrayRequest.trigger({ segmentId, ...COSTS[cost].method }))
-)
+  creationEndpoint: XRayApi.segment_xray,
+  resultPropName: "xray",
+  actionPrefix: FETCH_SEGMENT_XRAY,
+});
+export const fetchSegmentXray = createThunkAction(
+  FETCH_SEGMENT_XRAY,
+  (segmentId, cost) => dispatch =>
+    dispatch(segmentXrayRequest.trigger({ segmentId, ...COSTS[cost].method })),
+);
 
-export const FETCH_CARD_XRAY = 'metabase/xray/FETCH_CARD_XRAY';
+export const FETCH_CARD_XRAY = "metabase/xray/FETCH_CARD_XRAY";
 const cardXrayRequest = new BackgroundJobRequest({
-    creationEndpoint: XRayApi.card_xray,
-    resultPropName: 'xray',
-    actionPrefix: FETCH_CARD_XRAY
-})
-export const fetchCardXray = createThunkAction(FETCH_CARD_XRAY, (cardId, cost) =>
-    (dispatch) =>
-        dispatch(cardXrayRequest.trigger({ cardId, ...COSTS[cost].method }))
-)
+  creationEndpoint: XRayApi.card_xray,
+  resultPropName: "xray",
+  actionPrefix: FETCH_CARD_XRAY,
+});
+export const fetchCardXray = createThunkAction(
+  FETCH_CARD_XRAY,
+  (cardId, cost) => dispatch =>
+    dispatch(cardXrayRequest.trigger({ cardId, ...COSTS[cost].method })),
+);
 
-export const FETCH_SHARED_TYPE_COMPARISON_XRAY = 'metabase/xray/FETCH_SHARED_TYPE_COMPARISON_XRAY';
+export const FETCH_SHARED_TYPE_COMPARISON_XRAY =
+  "metabase/xray/FETCH_SHARED_TYPE_COMPARISON_XRAY";
 const sharedTypeComparisonXrayRequest = new BackgroundJobRequest({
-    creationEndpoint: XRayApi.compare_shared_type,
-    resultPropName: 'comparison',
-    actionPrefix: FETCH_SHARED_TYPE_COMPARISON_XRAY
-})
-export const fetchSharedTypeComparisonXray = createThunkAction(FETCH_SHARED_TYPE_COMPARISON_XRAY, (modelTypePlural, modelId1, modelId2, cost) =>
-    (dispatch) =>
-        dispatch(sharedTypeComparisonXrayRequest.trigger({ modelTypePlural, modelId1, modelId2, ...COSTS[cost].method }))
-)
+  creationEndpoint: XRayApi.compare_shared_type,
+  resultPropName: "comparison",
+  actionPrefix: FETCH_SHARED_TYPE_COMPARISON_XRAY,
+});
+export const fetchSharedTypeComparisonXray = createThunkAction(
+  FETCH_SHARED_TYPE_COMPARISON_XRAY,
+  (modelTypePlural, modelId1, modelId2, cost) => dispatch =>
+    dispatch(
+      sharedTypeComparisonXrayRequest.trigger({
+        modelTypePlural,
+        modelId1,
+        modelId2,
+        ...COSTS[cost].method,
+      }),
+    ),
+);
 
-export const FETCH_TWO_TYPES_COMPARISON_XRAY = 'metabase/xray/FETCH_TWO_TYPES_COMPARISON_XRAY';
+export const FETCH_TWO_TYPES_COMPARISON_XRAY =
+  "metabase/xray/FETCH_TWO_TYPES_COMPARISON_XRAY";
 const twoTypesComparisonXrayRequest = new BackgroundJobRequest({
-    creationEndpoint: XRayApi.compare_two_types,
-    resultPropName: 'comparison',
-    actionPrefix: FETCH_TWO_TYPES_COMPARISON_XRAY
-})
-export const fetchTwoTypesComparisonXray = createThunkAction(FETCH_TWO_TYPES_COMPARISON_XRAY, (modelType1, modelId1, modelType2, modelId2, cost) =>
-    (dispatch) =>
-        dispatch(twoTypesComparisonXrayRequest.trigger({ modelType1, modelId1, modelType2, modelId2, ...COSTS[cost].method }))
-)
+  creationEndpoint: XRayApi.compare_two_types,
+  resultPropName: "comparison",
+  actionPrefix: FETCH_TWO_TYPES_COMPARISON_XRAY,
+});
+export const fetchTwoTypesComparisonXray = createThunkAction(
+  FETCH_TWO_TYPES_COMPARISON_XRAY,
+  (modelType1, modelId1, modelType2, modelId2, cost) => dispatch =>
+    dispatch(
+      twoTypesComparisonXrayRequest.trigger({
+        modelType1,
+        modelId1,
+        modelType2,
+        modelId2,
+        ...COSTS[cost].method,
+      }),
+    ),
+);
 
-export default handleActions({
+export default handleActions(
+  {
     ...fieldXrayRequest.getReducers(),
     ...tableXrayRequest.getReducers(),
     ...segmentXrayRequest.getReducers(),
@@ -86,4 +112,6 @@ export default handleActions({
     ...sharedTypeComparisonXrayRequest.getReducers(),
     ...twoTypesComparisonXrayRequest.getReducers(),
     [INITIALIZE]: () => tableXrayRequest.getDefaultState(),
-}, tableXrayRequest.getDefaultState())
+  },
+  tableXrayRequest.getDefaultState(),
+);
diff --git a/frontend/test/__mocks__/fileMock.js b/frontend/test/__mocks__/fileMock.js
index 86059f36292497436f0fdbd0fb673f6ea9535159..0a445d0600c6d1fbf366969667c9cc7a3977b013 100644
--- a/frontend/test/__mocks__/fileMock.js
+++ b/frontend/test/__mocks__/fileMock.js
@@ -1 +1 @@
-module.exports = 'test-file-stub';
+module.exports = "test-file-stub";
diff --git a/frontend/test/__runner__/backend.js b/frontend/test/__runner__/backend.js
index 6ab64bc09d9bf1bba0b0788852fd26a31d152995..5e58c2c4bcc748daacb9e8c3d43a4ff74d2ccea3 100644
--- a/frontend/test/__runner__/backend.js
+++ b/frontend/test/__runner__/backend.js
@@ -3,146 +3,165 @@ import os from "os";
 import path from "path";
 import { spawn } from "child_process";
 
-import fetch from 'isomorphic-fetch';
-import { delay } from '../../src/metabase/lib/promise';
+import fetch from "isomorphic-fetch";
+import { delay } from "../../src/metabase/lib/promise";
 
 export const DEFAULT_DB = __dirname + "/test_db_fixture.db";
 
 let testDbId = 0;
-const getDbFile = () => path.join(os.tmpdir(), `metabase-test-${process.pid}-${testDbId++}.db`);
+const getDbFile = () =>
+  path.join(os.tmpdir(), `metabase-test-${process.pid}-${testDbId++}.db`);
 
 let port = 4000;
 const getPort = () => port++;
 
 export const BackendResource = createSharedResource("BackendResource", {
-    getKey({ dbKey = DEFAULT_DB }) {
-        return dbKey || {}
-    },
-    create({ dbKey = DEFAULT_DB }) {
-        let dbFile = getDbFile();
-        if (!dbKey) {
-            dbKey = dbFile;
-        }
-        if (process.env["E2E_HOST"] && dbKey === DEFAULT_DB) {
-            return {
-                dbKey: dbKey,
-                host: process.env["E2E_HOST"],
-                process: { kill: () => {} }
-            };
-        } else {
-            let port = getPort();
-            return {
-                dbKey: dbKey,
-                dbFile: dbFile,
-                host: `http://localhost:${port}`,
-                port: port
-            };
-        }
-    },
-    async start(server) {
-        if (!server.process) {
-            if (server.dbKey !== server.dbFile) {
-                await fs.copy(`${server.dbKey}.h2.db`, `${server.dbFile}.h2.db`);
-            }
-            server.process = spawn("java", ["-XX:+IgnoreUnrecognizedVMOptions",     // ignore options not recognized by this Java version (e.g. Java 7/8 should ignore Java 9 options)
-                                            "-Dh2.bindAddress=localhost",           // fix H2 randomly not working (?)
-                                            "-Xmx2g",                               // Hard limit of 2GB size for the heap since Circle is dumb and the JVM tends to go over the limit otherwise
-                                            "-XX:MaxPermSize=256m",                 // (Java 7) Give JVM a little more headroom in the PermGen space. Cloure makes lots of one-off classes!
-                                            "-Xverify:none",                        // Skip bytecode verification for the JAR so it launches faster
-                                            "-XX:+CMSClassUnloadingEnabled",        // (Java 7) Allow GC to collect classes. Clojure makes lots of one-off dynamic classes
-                                            "-XX:+UseConcMarkSweepGC",              // (Java 7) Use Concurrent Mark & Sweep GC which allows classes to be GC'ed
-                                            "-Djava.awt.headless=true",             // when running on macOS prevent little Java icon from popping up in Dock
-                                            "--add-modules=java.xml.bind",          // Tell Java 9 we want to use java.xml stuff
-                                            "-jar", "target/uberjar/metabase.jar"], {
-                env: {
-                    MB_DB_FILE: server.dbFile,
-                    MB_JETTY_PORT: server.port
-                },
-                stdio: "inherit"
-            });
-        }
-        if (!(await isReady(server.host))) {
-            process.stdout.write("Waiting for backend (host=" + server.host + " dbKey=" + server.dbKey + ")");
-            while (!(await isReady(server.host))) {
-                process.stdout.write(".");
-                await delay(500);
-            }
-            process.stdout.write("\n");
-        }
-        console.log("Backend ready (host=" + server.host + " dbKey=" + server.dbKey + ")");
-    },
-    async stop(server) {
-        if (server.process) {
-            server.process.kill('SIGKILL');
-            console.log("Stopped backend (host=" + server.host + " dbKey=" + server.dbKey + ")");
-        }
-        try {
-            if (server.dbFile) {
-                await fs.unlink(`${server.dbFile}.h2.db`);
-            }
-        } catch (e) {
-        }
+  getKey({ dbKey = DEFAULT_DB }) {
+    return dbKey || {};
+  },
+  create({ dbKey = DEFAULT_DB }) {
+    let dbFile = getDbFile();
+    if (!dbKey) {
+      dbKey = dbFile;
+    }
+    if (process.env["E2E_HOST"] && dbKey === DEFAULT_DB) {
+      return {
+        dbKey: dbKey,
+        host: process.env["E2E_HOST"],
+        process: { kill: () => {} },
+      };
+    } else {
+      let port = getPort();
+      return {
+        dbKey: dbKey,
+        dbFile: dbFile,
+        host: `http://localhost:${port}`,
+        port: port,
+      };
+    }
+  },
+  async start(server) {
+    if (!server.process) {
+      if (server.dbKey !== server.dbFile) {
+        await fs.copy(`${server.dbKey}.h2.db`, `${server.dbFile}.h2.db`);
+      }
+      server.process = spawn(
+        "java",
+        [
+          "-XX:+IgnoreUnrecognizedVMOptions", // ignore options not recognized by this Java version (e.g. Java 7/8 should ignore Java 9 options)
+          "-Dh2.bindAddress=localhost", // fix H2 randomly not working (?)
+          "-Xmx2g", // Hard limit of 2GB size for the heap since Circle is dumb and the JVM tends to go over the limit otherwise
+          "-XX:MaxPermSize=256m", // (Java 7) Give JVM a little more headroom in the PermGen space. Cloure makes lots of one-off classes!
+          "-Xverify:none", // Skip bytecode verification for the JAR so it launches faster
+          "-XX:+CMSClassUnloadingEnabled", // (Java 7) Allow GC to collect classes. Clojure makes lots of one-off dynamic classes
+          "-XX:+UseConcMarkSweepGC", // (Java 7) Use Concurrent Mark & Sweep GC which allows classes to be GC'ed
+          "-Djava.awt.headless=true", // when running on macOS prevent little Java icon from popping up in Dock
+          "--add-modules=java.xml.bind", // Tell Java 9 we want to use java.xml stuff
+          "-jar",
+          "target/uberjar/metabase.jar",
+        ],
+        {
+          env: {
+            MB_DB_FILE: server.dbFile,
+            MB_JETTY_PORT: server.port,
+          },
+          stdio: "inherit",
+        },
+      );
+    }
+    if (!await isReady(server.host)) {
+      process.stdout.write(
+        "Waiting for backend (host=" +
+          server.host +
+          " dbKey=" +
+          server.dbKey +
+          ")",
+      );
+      while (!await isReady(server.host)) {
+        process.stdout.write(".");
+        await delay(500);
+      }
+      process.stdout.write("\n");
+    }
+    console.log(
+      "Backend ready (host=" + server.host + " dbKey=" + server.dbKey + ")",
+    );
+  },
+  async stop(server) {
+    if (server.process) {
+      server.process.kill("SIGKILL");
+      console.log(
+        "Stopped backend (host=" + server.host + " dbKey=" + server.dbKey + ")",
+      );
     }
+    try {
+      if (server.dbFile) {
+        await fs.unlink(`${server.dbFile}.h2.db`);
+      }
+    } catch (e) {}
+  },
 });
 
 export async function isReady(host) {
-    try {
-        let response = await fetch(`${host}/api/health`);
-        if (response.status === 200) {
-            return true;
-        }
-    } catch (e) {
+  try {
+    let response = await fetch(`${host}/api/health`);
+    if (response.status === 200) {
+      return true;
     }
-    return false;
+  } catch (e) {}
+  return false;
 }
 
-function createSharedResource(resourceName, {
+function createSharedResource(
+  resourceName,
+  {
     defaultOptions,
-    getKey = (options) => JSON.stringify(options),
-    create = (options) => ({}),
-    start = (resource) => {},
-    stop = (resource) => {},
-}) {
-    let entriesByKey = new Map();
-    let entriesByResource = new Map();
+    getKey = options => JSON.stringify(options),
+    create = options => ({}),
+    start = resource => {},
+    stop = resource => {},
+  },
+) {
+  let entriesByKey = new Map();
+  let entriesByResource = new Map();
 
-    function kill(entry) {
-        if (entriesByKey.has(entry.key)) {
-            entriesByKey.delete(entry.key);
-            entriesByResource.delete(entry.resource);
-            let p = stop(entry.resource).then(null, (err) =>
-                console.log("Error stopping resource", resourceName, entry.key, err)
-            );
-            return p;
-        }
+  function kill(entry) {
+    if (entriesByKey.has(entry.key)) {
+      entriesByKey.delete(entry.key);
+      entriesByResource.delete(entry.resource);
+      let p = stop(entry.resource).then(null, err =>
+        console.log("Error stopping resource", resourceName, entry.key, err),
+      );
+      return p;
     }
+  }
 
-    return {
-        get(options = defaultOptions) {
-            let key = getKey(options);
-            let entry = entriesByKey.get(key);
-            if (!entry) {
-                entry = {
-                    key: key,
-                    references: 0,
-                    resource: create(options)
-                }
-                entriesByKey.set(entry.key, entry);
-                entriesByResource.set(entry.resource, entry);
-            } else {
-            }
-            ++entry.references;
-            return entry.resource;
-        },
-        async start(resource) {
-            let entry = entriesByResource.get(resource);
-            return start(entry.resource);
-        },
-        async stop(resource) {
-            let entry = entriesByResource.get(resource);
-            if (entry && --entry.references <= 0) {
-                await kill(entry);
-            }
-        }
-    }
+  return {
+    get(options = defaultOptions) {
+      let key = getKey(options);
+      let entry = entriesByKey.get(key);
+      if (!entry) {
+        entry = {
+          key: key,
+          references: 0,
+          resource: create(options),
+        };
+        entriesByKey.set(entry.key, entry);
+        entriesByResource.set(entry.resource, entry);
+      } else {
+      }
+      ++entry.references;
+      return entry.resource;
+    },
+    async start(resource) {
+      let entry = entriesByResource.get(resource);
+      return start(entry.resource);
+    },
+    async stop(resource) {
+      let entry = entriesByResource.get(resource);
+      if (entry && --entry.references <= 0) {
+        await kill(entry);
+      }
+    },
+  };
 }
diff --git a/frontend/test/__runner__/run_integrated_tests.js b/frontend/test/__runner__/run_integrated_tests.js
index 0851a9e3977754471bb6110c064457dac4a01b21..e77edaff7558f1bb78d6168a877cc278619a4115 100755
--- a/frontend/test/__runner__/run_integrated_tests.js
+++ b/frontend/test/__runner__/run_integrated_tests.js
@@ -1,13 +1,15 @@
 // Provide custom afterAll implementation for letting shared-resouce.js set method for doing cleanup
-let jasmineAfterAllCleanup = async () => {}
-global.afterAll = (method) => { jasmineAfterAllCleanup = method; }
+let jasmineAfterAllCleanup = async () => {};
+global.afterAll = method => {
+  jasmineAfterAllCleanup = method;
+};
 
 import { spawn } from "child_process";
 import fs from "fs";
 import chalk from "chalk";
 
 // Use require for BackendResource to run it after the mock afterAll has been set
-const BackendResource = require("./backend.js").BackendResource
+const BackendResource = require("./backend.js").BackendResource;
 
 // Backend that uses a test fixture database
 const serverWithTestDbFixture = BackendResource.get({});
@@ -17,127 +19,160 @@ const serverWithPlainDb = BackendResource.get({ dbKey: "" });
 const plainBackendHost = serverWithPlainDb.host;
 
 const userArgs = process.argv.slice(2);
-const isJestWatchMode = userArgs[0] === "--watch"
+const isJestWatchMode = userArgs[0] === "--watch";
 
 function readFile(fileName) {
-    return new Promise(function(resolve, reject){
-        fs.readFile(fileName, 'utf8', (err, data) => {
-            if (err) { reject(err); }
-            resolve(data);
-        })
+  return new Promise(function(resolve, reject) {
+    fs.readFile(fileName, "utf8", (err, data) => {
+      if (err) {
+        reject(err);
+      }
+      resolve(data);
     });
+  });
 }
 
 const login = async (apiHost, user) => {
-    const loginFetchOptions = {
-        method: "POST",
-        headers: new Headers({
-            "Accept": "application/json",
-            "Content-Type": "application/json"
-        }),
-        body: JSON.stringify(user)
-    };
-    const result = await fetch(apiHost + "/api/session", loginFetchOptions);
-
-    let resultBody = null
-    try {
-        resultBody = await result.text();
-        resultBody = JSON.parse(resultBody);
-    } catch (e) {}
-
-    if (result.status >= 200 && result.status <= 299) {
-        console.log(`Successfully created a shared login with id ${resultBody.id}`)
-        return resultBody
-    } else {
-        const error = {status: result.status, data: resultBody }
-        console.log('A shared login attempt failed with the following error:');
-        console.log(error, {depth: null});
-        throw error
-    }
-}
-
-const init = async() => {
-    if (!isJestWatchMode) {
-        console.log(chalk.yellow('If you are developing locally, prefer using `lein run test-integrated-watch` instead.\n'));
-    }
-
-    try {
-        const version = await readFile(__dirname + "/../../../resources/version.properties")
-        console.log(chalk.bold('Running integrated test runner with this build:'));
-        process.stdout.write(chalk.cyan(version))
-        console.log(chalk.bold('If that version seems too old, please run `./bin/build version uberjar`.\n'));
-    } catch(e) {
-        console.log(chalk.bold('No version file found. Please run `./bin/build version uberjar`.'));
-        process.exit(1)
-    }
-
-    console.log(chalk.bold('1/4 Starting first backend with test H2 database fixture'));
-    console.log(chalk.cyan('You can update the fixture by running a local instance against it:\n`MB_DB_TYPE=h2 MB_DB_FILE=frontend/test/__runner__/test_db_fixture.db lein run`'))
-    await BackendResource.start(serverWithTestDbFixture)
-    console.log(chalk.bold('2/4 Starting second backend with plain database'));
-    await BackendResource.start(serverWithPlainDb)
-
-    console.log(chalk.bold('3/4 Creating a shared login session for backend 1'));
-    const sharedAdminLoginSession = await login(
-        testFixtureBackendHost,
-        { username: "bob@metabase.com", password: "12341234" }
-    )
-    const sharedNormalLoginSession = await login(
-        testFixtureBackendHost,
-        { username: "robert@metabase.com", password: "12341234" }
-    )
-
-    console.log(chalk.bold('4/4 Starting Jest'));
-    const env = {
-        ...process.env,
-        "TEST_FIXTURE_BACKEND_HOST": testFixtureBackendHost,
-        "PLAIN_BACKEND_HOST": plainBackendHost,
-        "TEST_FIXTURE_SHARED_ADMIN_LOGIN_SESSION_ID": sharedAdminLoginSession.id,
-        "TEST_FIXTURE_SHARED_NORMAL_LOGIN_SESSION_ID": sharedNormalLoginSession.id,
-    }
-
-    const jestProcess = spawn(
-        "yarn",
-        ["run", "jest", "--", "--maxWorkers=1", "--config", "jest.integ.conf.json", ...userArgs],
-        {
-            env,
-            stdio: "inherit"
-        }
+  const loginFetchOptions = {
+    method: "POST",
+    headers: new Headers({
+      Accept: "application/json",
+      "Content-Type": "application/json",
+    }),
+    body: JSON.stringify(user),
+  };
+  const result = await fetch(apiHost + "/api/session", loginFetchOptions);
+
+  let resultBody = null;
+  try {
+    resultBody = await result.text();
+    resultBody = JSON.parse(resultBody);
+  } catch (e) {}
+
+  if (result.status >= 200 && result.status <= 299) {
+    console.log(`Successfully created a shared login with id ${resultBody.id}`);
+    return resultBody;
+  } else {
+    const error = { status: result.status, data: resultBody };
+    console.log("A shared login attempt failed with the following error:");
+    console.log(error, { depth: null });
+    throw error;
+  }
+};
+
+const init = async () => {
+  if (!isJestWatchMode) {
+    console.log(
+      chalk.yellow(
+        "If you are developing locally, prefer using `lein run test-integrated-watch` instead.\n",
+      ),
     );
+  }
 
-    return new Promise((resolve, reject) => {
-        jestProcess.on('exit', resolve)
-    })
-}
+  try {
+    const version = await readFile(
+      __dirname + "/../../../resources/version.properties",
+    );
+    console.log(chalk.bold("Running integrated test runner with this build:"));
+    process.stdout.write(chalk.cyan(version));
+    console.log(
+      chalk.bold(
+        "If that version seems too old, please run `./bin/build version uberjar`.\n",
+      ),
+    );
+  } catch (e) {
+    console.log(
+      chalk.bold(
+        "No version file found. Please run `./bin/build version uberjar`.",
+      ),
+    );
+    process.exit(1);
+  }
+
+  console.log(
+    chalk.bold("1/4 Starting first backend with test H2 database fixture"),
+  );
+  console.log(
+    chalk.cyan(
+      "You can update the fixture by running a local instance against it:\n`MB_DB_TYPE=h2 MB_DB_FILE=frontend/test/__runner__/test_db_fixture.db lein run`",
+    ),
+  );
+  await BackendResource.start(serverWithTestDbFixture);
+  console.log(chalk.bold("2/4 Starting second backend with plain database"));
+  await BackendResource.start(serverWithPlainDb);
+
+  console.log(chalk.bold("3/4 Creating a shared login session for backend 1"));
+  const sharedAdminLoginSession = await login(testFixtureBackendHost, {
+    username: "bob@metabase.com",
+    password: "12341234",
+  });
+  const sharedNormalLoginSession = await login(testFixtureBackendHost, {
+    username: "robert@metabase.com",
+    password: "12341234",
+  });
+
+  console.log(chalk.bold("4/4 Starting Jest"));
+  const env = {
+    ...process.env,
+    TEST_FIXTURE_BACKEND_HOST: testFixtureBackendHost,
+    PLAIN_BACKEND_HOST: plainBackendHost,
+    TEST_FIXTURE_SHARED_ADMIN_LOGIN_SESSION_ID: sharedAdminLoginSession.id,
+    TEST_FIXTURE_SHARED_NORMAL_LOGIN_SESSION_ID: sharedNormalLoginSession.id,
+  };
+
+  const jestProcess = spawn(
+    "yarn",
+    [
+      "run",
+      "jest",
+      "--",
+      "--maxWorkers=1",
+      "--config",
+      "jest.integ.conf.json",
+      ...userArgs,
+    ],
+    {
+      env,
+      stdio: "inherit",
+    },
+  );
+
+  return new Promise((resolve, reject) => {
+    jestProcess.on("exit", resolve);
+  });
+};
 
 const cleanup = async (exitCode = 0) => {
-    console.log(chalk.bold('Cleaning up...'))
-    await jasmineAfterAllCleanup();
-    await BackendResource.stop(serverWithTestDbFixture);
-    await BackendResource.stop(serverWithPlainDb);
-    process.exit(exitCode);
+  console.log(chalk.bold("Cleaning up..."));
+  await jasmineAfterAllCleanup();
+  await BackendResource.stop(serverWithTestDbFixture);
+  await BackendResource.stop(serverWithPlainDb);
+  process.exit(exitCode);
+};
+
+const askWhetherToQuit = exitCode => {
+  console.log(
+    chalk.bold(
+      "Jest process exited. Press [ctrl-c] to quit the integrated test runner or any other key to restart Jest.",
+    ),
+  );
+  process.stdin.once("data", launch);
+};
 
-}
+const launch = () =>
+  init()
+    .then(isJestWatchMode ? askWhetherToQuit : cleanup)
+    .catch(e => {
+      console.error(e);
+      cleanup(1);
+    });
 
-const askWhetherToQuit = (exitCode) => {
-    console.log(chalk.bold('Jest process exited. Press [ctrl-c] to quit the integrated test runner or any other key to restart Jest.'));
-    process.stdin.once('data', launch);
-}
+launch();
 
-const launch = () =>
-    init()
-        .then(isJestWatchMode ? askWhetherToQuit : cleanup)
-        .catch((e) => {
-            console.error(e);
-            cleanup(1);
-        })
-
-launch()
-
-process.on('SIGTERM', () => {
-    cleanup();
-})
-
-process.on('SIGINT', () => {
-    cleanup()
-})
\ No newline at end of file
+process.on("SIGTERM", () => {
+  cleanup();
+});
+
+process.on("SIGINT", () => {
+  cleanup();
+});
diff --git a/frontend/test/__support__/enzyme_utils.js b/frontend/test/__support__/enzyme_utils.js
index edb5433fa1cf724685b02c1762e4d4f3cdc77fba..b72044669542e38ed8fc3033b1bce1711d10ba56 100644
--- a/frontend/test/__support__/enzyme_utils.js
+++ b/frontend/test/__support__/enzyme_utils.js
@@ -5,73 +5,79 @@ import Button from "metabase/components/Button";
 
 // Triggers events that are being listened to with `window.addEventListener` or `document.addEventListener`
 export const dispatchBrowserEvent = (eventName, ...args) => {
-    if (eventListeners[eventName]) {
-        eventListeners[eventName].forEach(listener => listener(...args))
-    } else {
-        throw new Error(
-            `No event listeners are currently attached to event '${eventName}'. List of event listeners:\n` +
-            Object.entries(eventListeners).map(([name, funcs]) => `${name} (${funcs.length} listeners)`).join('\n')
-        )
-    }
-}
+  if (eventListeners[eventName]) {
+    eventListeners[eventName].forEach(listener => listener(...args));
+  } else {
+    throw new Error(
+      `No event listeners are currently attached to event '${eventName}'. List of event listeners:\n` +
+        Object.entries(eventListeners)
+          .map(([name, funcs]) => `${name} (${funcs.length} listeners)`)
+          .join("\n"),
+    );
+  }
+};
 
-export const click = (enzymeWrapper) => {
-    if (enzymeWrapper.length === 0) {
-        throw new Error("The wrapper you provided for `click(wrapper)` is empty.")
-    }
-    const nodeType = enzymeWrapper.type();
-    if (nodeType === Button || nodeType === "button") {
-        console.trace(
-            'You are calling `click` for a button; you would probably want to use `clickButton` instead as ' +
-            'it takes all button click scenarios into account.'
-        )
-    }
-    // Normal click event. Works for both `onClick` React event handlers and react-router <Link> objects.
-    // We simulate a left button click with `{ button: 0 }` because react-router requires that.
-    enzymeWrapper.simulate('click', { button: 0 });
-}
+export const click = enzymeWrapper => {
+  if (enzymeWrapper.length === 0) {
+    throw new Error("The wrapper you provided for `click(wrapper)` is empty.");
+  }
+  const nodeType = enzymeWrapper.type();
+  if (nodeType === Button || nodeType === "button") {
+    console.trace(
+      "You are calling `click` for a button; you would probably want to use `clickButton` instead as " +
+        "it takes all button click scenarios into account.",
+    );
+  }
+  // Normal click event. Works for both `onClick` React event handlers and react-router <Link> objects.
+  // We simulate a left button click with `{ button: 0 }` because react-router requires that.
+  enzymeWrapper.simulate("click", { button: 0 });
+};
 
-export const clickButton = (enzymeWrapper) => {
-    if (enzymeWrapper.length === 0) {
-        throw new Error("The wrapper you provided for `clickButton(wrapper)` is empty.")
-    }
-    // `clickButton` is separate from `click` because `wrapper.closest(..)` sometimes results in error
-    // if the parent element isn't found, https://github.com/airbnb/enzyme/issues/410
+export const clickButton = enzymeWrapper => {
+  if (enzymeWrapper.length === 0) {
+    throw new Error(
+      "The wrapper you provided for `clickButton(wrapper)` is empty.",
+    );
+  }
+  // `clickButton` is separate from `click` because `wrapper.closest(..)` sometimes results in error
+  // if the parent element isn't found, https://github.com/airbnb/enzyme/issues/410
 
-    // Submit event must be called on the button component itself (not its child components), otherwise it won't work
-    const closestButton = enzymeWrapper.closest("button");
+  // Submit event must be called on the button component itself (not its child components), otherwise it won't work
+  const closestButton = enzymeWrapper.closest("button");
 
-    if (closestButton.length === 1) {
-        closestButton.simulate("submit"); // for forms with onSubmit
-        closestButton.simulate("click", { button: 0 }); // for lone buttons / forms without onSubmit
-    } else {
-        // Assume that the current component wraps a button element
-        enzymeWrapper.simulate("submit");
+  if (closestButton.length === 1) {
+    closestButton.simulate("submit"); // for forms with onSubmit
+    closestButton.simulate("click", { button: 0 }); // for lone buttons / forms without onSubmit
+  } else {
+    // Assume that the current component wraps a button element
+    enzymeWrapper.simulate("submit");
 
-        // For some reason the click sometimes fails when using a Button component
-        try {
-            enzymeWrapper.simulate("click", { button: 0 });
-        } catch(e) {
-
-        }
-    }
-}
+    // For some reason the click sometimes fails when using a Button component
+    try {
+      enzymeWrapper.simulate("click", { button: 0 });
+    } catch (e) {}
+  }
+};
 
 export const setInputValue = (inputWrapper, value, { blur = true } = {}) => {
-    if (inputWrapper.length === 0) {
-        throw new Error("The wrapper you provided for `setInputValue(...)` is empty.")
-    }
+  if (inputWrapper.length === 0) {
+    throw new Error(
+      "The wrapper you provided for `setInputValue(...)` is empty.",
+    );
+  }
 
-    inputWrapper.simulate('change', { target: { value: value } });
-    if (blur) inputWrapper.simulate("blur")
-}
+  inputWrapper.simulate("change", { target: { value: value } });
+  if (blur) inputWrapper.simulate("blur");
+};
 
-export const chooseSelectOption = (optionWrapper) => {
-    if (optionWrapper.length === 0) {
-        throw new Error("The wrapper you provided for `chooseSelectOption(...)` is empty.")
-    }
+export const chooseSelectOption = optionWrapper => {
+  if (optionWrapper.length === 0) {
+    throw new Error(
+      "The wrapper you provided for `chooseSelectOption(...)` is empty.",
+    );
+  }
 
-    const optionValue = optionWrapper.prop('value');
-    const parentSelect = optionWrapper.closest("select");
-    parentSelect.simulate('change', { target: { value: optionValue } });
-}
+  const optionValue = optionWrapper.prop("value");
+  const parentSelect = optionWrapper.closest("select");
+  parentSelect.simulate("change", { target: { value: optionValue } });
+};
diff --git a/frontend/test/__support__/integrated_tests.js b/frontend/test/__support__/integrated_tests.js
index b64767f4272d2fb2aa16263b7055b16acaf45a64..5514281e3f13fcd7c7d646923ab19a3dabff0a64 100644
--- a/frontend/test/__support__/integrated_tests.js
+++ b/frontend/test/__support__/integrated_tests.js
@@ -12,20 +12,20 @@ import { format as urlFormat } from "url";
 import api from "metabase/lib/api";
 import { DashboardApi, SessionApi } from "metabase/services";
 import { METABASE_SESSION_COOKIE } from "metabase/lib/cookies";
-import normalReducers from 'metabase/reducers-main';
-import publicReducers from 'metabase/reducers-public';
+import normalReducers from "metabase/reducers-main";
+import publicReducers from "metabase/reducers-public";
 
-import React from 'react'
-import { Provider } from 'react-redux';
+import React from "react";
+import { Provider } from "react-redux";
 
-import { createMemoryHistory } from 'history'
+import { createMemoryHistory } from "history";
 import { getStore } from "metabase/store";
 import { createRoutes, Router, useRouterHistory } from "react-router";
-import _ from 'underscore';
+import _ from "underscore";
 import chalk from "chalk";
 
 // Importing isomorphic-fetch sets the global `fetch` and `Headers` objects that are used here
-import fetch from 'isomorphic-fetch';
+import fetch from "isomorphic-fetch";
 
 import { refreshSiteSettings } from "metabase/redux/settings";
 
@@ -36,14 +36,13 @@ import { getRoutes as getEmbedRoutes } from "metabase/routes-embed";
 import moment from "moment";
 
 let hasStartedCreatingStore = false;
-let hasFinishedCreatingStore = false
+let hasFinishedCreatingStore = false;
 let loginSession = null; // Stores the current login session
 let previousLoginSession = null;
 let simulateOfflineMode = false;
 let apiRequestCompletedCallback = null;
 let skippedApiRequests = [];
 
-
 // These i18n settings are same is beginning of app.js
 
 // make the i18n function "t" global so we don't have to import it in basically every file
@@ -54,82 +53,89 @@ global.jt = jt;
 // set the locale before loading anything else
 import { setLocalization } from "metabase/lib/i18n";
 if (window.MetabaseLocalization) {
-    setLocalization(window.MetabaseLocalization)
+  setLocalization(window.MetabaseLocalization);
 }
 
 const warnAboutCreatingStoreBeforeLogin = () => {
-    if (!loginSession && hasStartedCreatingStore) {
-        console.warn(
-            "Warning: You have created a test store before calling logging in which means that up-to-date site settings " +
-            "won't be in the store unless you call `refreshSiteSettings` action manually. Please prefer " +
-            "logging in before all tests and creating the store inside an individual test or describe block."
-        )
-    }
-}
+  if (!loginSession && hasStartedCreatingStore) {
+    console.warn(
+      "Warning: You have created a test store before calling logging in which means that up-to-date site settings " +
+        "won't be in the store unless you call `refreshSiteSettings` action manually. Please prefer " +
+        "logging in before all tests and creating the store inside an individual test or describe block.",
+    );
+  }
+};
 /**
  * Login to the Metabase test instance with default credentials
  */
-export async function login({ username = "bob@metabase.com", password = "12341234" } = {}) {
-    warnAboutCreatingStoreBeforeLogin()
-    loginSession = await SessionApi.create({ username, password });
+export async function login({
+  username = "bob@metabase.com",
+  password = "12341234",
+} = {}) {
+  warnAboutCreatingStoreBeforeLogin();
+  loginSession = await SessionApi.create({ username, password });
 }
 
 export function useSharedAdminLogin() {
-    warnAboutCreatingStoreBeforeLogin()
-    loginSession = { id: process.env.TEST_FIXTURE_SHARED_ADMIN_LOGIN_SESSION_ID }
+  warnAboutCreatingStoreBeforeLogin();
+  loginSession = { id: process.env.TEST_FIXTURE_SHARED_ADMIN_LOGIN_SESSION_ID };
 }
 export function useSharedNormalLogin() {
-    warnAboutCreatingStoreBeforeLogin()
-    loginSession = { id: process.env.TEST_FIXTURE_SHARED_NORMAL_LOGIN_SESSION_ID }
-}
-export const forBothAdminsAndNormalUsers = async (tests) => {
-    useSharedAdminLogin()
-    await tests()
-    useSharedNormalLogin()
-    await tests()
+  warnAboutCreatingStoreBeforeLogin();
+  loginSession = {
+    id: process.env.TEST_FIXTURE_SHARED_NORMAL_LOGIN_SESSION_ID,
+  };
 }
+export const forBothAdminsAndNormalUsers = async tests => {
+  useSharedAdminLogin();
+  await tests();
+  useSharedNormalLogin();
+  await tests();
+};
 
 export function logout() {
-    previousLoginSession = loginSession
-    loginSession = null
+  previousLoginSession = loginSession;
+  loginSession = null;
 }
 
 /**
  * Lets you recover the previous login session after calling logout
  */
 export function restorePreviousLogin() {
-    if (previousLoginSession) {
-        loginSession = previousLoginSession
-    } else {
-        console.warn("There is no previous login that could be restored!")
-    }
+  if (previousLoginSession) {
+    loginSession = previousLoginSession;
+  } else {
+    console.warn("There is no previous login that could be restored!");
+  }
 }
 
 /**
  * Calls the provided function while simulating that the browser is offline
  */
 export async function whenOffline(callWhenOffline) {
-    simulateOfflineMode = true;
-    return callWhenOffline()
-        .then((result) => {
-            simulateOfflineMode = false;
-            return result;
-        })
-        .catch((e) => {
-            simulateOfflineMode = false;
-            throw e;
-        });
+  simulateOfflineMode = true;
+  return callWhenOffline()
+    .then(result => {
+      simulateOfflineMode = false;
+      return result;
+    })
+    .catch(e => {
+      simulateOfflineMode = false;
+      throw e;
+    });
 }
 
 export function switchToPlainDatabase() {
-    api.basename = process.env.PLAIN_BACKEND_HOST;
+  api.basename = process.env.PLAIN_BACKEND_HOST;
 }
 export function switchToTestFixtureDatabase() {
-    api.basename = process.env.TEST_FIXTURE_BACKEND_HOST;
+  api.basename = process.env.TEST_FIXTURE_BACKEND_HOST;
 }
 
-export const isPlainDatabase = () => api.basename === process.env.PLAIN_BACKEND_HOST;
-export const isTestFixtureDatabase = () => api.basename === process.env.TEST_FIXTURE_BACKEND_HOST;
+export const isPlainDatabase = () =>
+  api.basename === process.env.PLAIN_BACKEND_HOST;
+export const isTestFixtureDatabase = () =>
+  api.basename === process.env.TEST_FIXTURE_BACKEND_HOST;
 
 /**
  * Creates an augmented Redux store for testing the whole app including browser history manipulation. Includes:
@@ -139,349 +145,413 @@ export const isTestFixtureDatabase = () => api.basename === process.env.TEST_FIX
  *     * waiting until specific Redux actions have been dispatched
  *     * getting a React container subtree for the current route
  */
-export const createTestStore = async ({ publicApp = false, embedApp = false } = {}) => {
-    hasFinishedCreatingStore = false;
-    hasStartedCreatingStore = true;
-
-    const history = useRouterHistory(createMemoryHistory)();
-    const getRoutes = publicApp ? getPublicRoutes : (embedApp ? getEmbedRoutes : getNormalRoutes);
-    const reducers = (publicApp || embedApp) ? publicReducers : normalReducers;
-    const store = getStore(reducers, history, undefined, (createStore) => testStoreEnhancer(createStore, history, getRoutes));
-    store._setFinalStoreInstance(store);
-
-    if (!publicApp) {
-        await store.dispatch(refreshSiteSettings());
-    }
-
-    hasFinishedCreatingStore = true;
-
-    return store;
-}
+export const createTestStore = async ({
+  publicApp = false,
+  embedApp = false,
+} = {}) => {
+  hasFinishedCreatingStore = false;
+  hasStartedCreatingStore = true;
+
+  const history = useRouterHistory(createMemoryHistory)();
+  const getRoutes = publicApp
+    ? getPublicRoutes
+    : embedApp ? getEmbedRoutes : getNormalRoutes;
+  const reducers = publicApp || embedApp ? publicReducers : normalReducers;
+  const store = getStore(reducers, history, undefined, createStore =>
+    testStoreEnhancer(createStore, history, getRoutes),
+  );
+  store._setFinalStoreInstance(store);
+
+  if (!publicApp) {
+    await store.dispatch(refreshSiteSettings());
+  }
+
+  hasFinishedCreatingStore = true;
+
+  return store;
+};
 
 /**
  * History state change events you can listen to in tests
  */
-export const BROWSER_HISTORY_PUSH = `integrated-tests/BROWSER_HISTORY_PUSH`
-export const BROWSER_HISTORY_REPLACE = `integrated-tests/BROWSER_HISTORY_REPLACE`
-export const BROWSER_HISTORY_POP = `integrated-tests/BROWSER_HISTORY_POP`
+export const BROWSER_HISTORY_PUSH = `integrated-tests/BROWSER_HISTORY_PUSH`;
+export const BROWSER_HISTORY_REPLACE = `integrated-tests/BROWSER_HISTORY_REPLACE`;
+export const BROWSER_HISTORY_POP = `integrated-tests/BROWSER_HISTORY_POP`;
 
 const testStoreEnhancer = (createStore, history, getRoutes) => {
+  return (...args) => {
+    const store = createStore(...args);
+
+    // Because we don't have an access to internal actions of react-router,
+    // let's create synthetic actions from actual history changes instead
+    history.listen(location => {
+      store.dispatch({
+        type: `integrated-tests/BROWSER_HISTORY_${location.action}`,
+        location: location,
+      });
+    });
+
+    const testStoreExtensions = {
+      _originalDispatch: store.dispatch,
+      _onActionDispatched: null,
+      _allDispatchedActions: [],
+      _latestDispatchedActions: [],
+      _finalStoreInstance: null,
+
+      /**
+       * Redux dispatch method middleware that records all dispatched actions
+       */
+      dispatch: action => {
+        const result = store._originalDispatch(action);
+
+        const actionWithTimestamp = [
+          {
+            ...action,
+            timestamp: Date.now(),
+          },
+        ];
+        store._allDispatchedActions = store._allDispatchedActions.concat(
+          actionWithTimestamp,
+        );
+        store._latestDispatchedActions = store._latestDispatchedActions.concat(
+          actionWithTimestamp,
+        );
+
+        if (store._onActionDispatched) store._onActionDispatched();
+        return result;
+      },
+
+      /**
+       * Waits until all actions with given type identifiers have been called or fails if the maximum waiting
+       * time defined in `timeout` is exceeded.
+       *
+       * Convenient in tests for waiting specific actions to be executed after mounting a React container.
+       */
+      waitForActions: (actionTypes, { timeout = 8000 } = {}) => {
+        if (store._onActionDispatched) {
+          return Promise.reject(
+            new Error(
+              "You have an earlier `store.waitForActions(...)` still in progress – have you forgotten to prepend `await` to the method call?",
+            ),
+          );
+        }
 
-    return (...args) => {
-        const store = createStore(...args);
-
-        // Because we don't have an access to internal actions of react-router,
-        // let's create synthetic actions from actual history changes instead
-        history.listen((location) => {
-            store.dispatch({
-                type: `integrated-tests/BROWSER_HISTORY_${location.action}`,
-                location: location
-            })
-        });
-
-        const testStoreExtensions = {
-            _originalDispatch: store.dispatch,
-            _onActionDispatched: null,
-            _allDispatchedActions: [],
-            _latestDispatchedActions: [],
-            _finalStoreInstance: null,
-
-            /**
-             * Redux dispatch method middleware that records all dispatched actions
-             */
-            dispatch: (action) => {
-                const result = store._originalDispatch(action);
-
-                const actionWithTimestamp = [{
-                    ...action,
-                    timestamp: Date.now()
-                }]
-                store._allDispatchedActions = store._allDispatchedActions.concat(actionWithTimestamp);
-                store._latestDispatchedActions = store._latestDispatchedActions.concat(actionWithTimestamp);
-
-                if (store._onActionDispatched) store._onActionDispatched();
-                return result;
-            },
-
-            /**
-             * Waits until all actions with given type identifiers have been called or fails if the maximum waiting
-             * time defined in `timeout` is exceeded.
-             *
-             * Convenient in tests for waiting specific actions to be executed after mounting a React container.
-             */
-            waitForActions: (actionTypes, {timeout = 8000} = {}) => {
-                if (store._onActionDispatched) {
-                    return Promise.reject(new Error("You have an earlier `store.waitForActions(...)` still in progress – have you forgotten to prepend `await` to the method call?"))
-                }
-
-                actionTypes = Array.isArray(actionTypes) ? actionTypes : [actionTypes]
-
-                // Returns all actions that are triggered after the last action which belongs to `actionTypes
-                const getRemainingActions = () => {
-                    const lastActionIndex = _.findLastIndex(store._latestDispatchedActions, (action) => actionTypes.includes(action.type))
-                    return store._latestDispatchedActions.slice(lastActionIndex + 1)
-                }
-
-                const allActionsAreTriggered = () => _.every(actionTypes, actionType =>
-                    store._latestDispatchedActions.filter((action) => action.type === actionType).length > 0
-                );
-
-                if (allActionsAreTriggered()) {
-                    // Short-circuit if all action types are already in the history of dispatched actions
-                    store._latestDispatchedActions = getRemainingActions();
-                    return Promise.resolve();
-                } else {
-                    return new Promise((resolve, reject) => {
-                        const timeoutID = setTimeout(() => {
-                            store._onActionDispatched = null;
-
-                            return reject(
-                                new Error(
-                                    `All these actions were not dispatched within ${timeout}ms:\n` +
-                                    chalk.cyan(actionTypes.join("\n")) +
-                                    "\n\nDispatched actions since the last call of `waitForActions`:\n" +
-                                    (store._latestDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") +
-                                    "\n\nDispatched actions since the initialization of test suite:\n" +
-                                    (store._allDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions")
-                                )
-                            )
-                        }, timeout)
-
-                        store._onActionDispatched = () => {
-                            if (allActionsAreTriggered()) {
-                                store._latestDispatchedActions = getRemainingActions();
-                                store._onActionDispatched = null;
-                                clearTimeout(timeoutID);
-                                resolve()
-                            }
-                        };
-                    });
-                }
-            },
-
-            /**
-             * Logs the actions that have been dispatched so far
-             */
-            debug: () => {
-                if (store._onActionDispatched) {
-                    console.log("You have `store.waitForActions(...)` still in progress – have you forgotten to prepend `await` to the method call?")
-                }
-
-                console.log(
-                    chalk.bold("Dispatched actions since last call of `waitForActions`:\n") +
-                    (store._latestDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions") +
-                    chalk.bold("\n\nDispatched actions since initialization of test suite:\n") +
-                    store._allDispatchedActions.map(store._formatDispatchedAction).join("\n") || "No dispatched actions"
-                )
-            },
-
-            /**
-             * Methods for manipulating the simulated browser history
-             */
-            pushPath: (path) => history.push(path),
-            goBack: () => history.goBack(),
-            getPath: () => urlFormat(history.getCurrentLocation()),
-
-            warnIfStoreCreationNotComplete: () => {
-                if (!hasFinishedCreatingStore) {
-                    console.warn(
-                        "Seems that you haven't waited until the store creation has completely finished. " +
-                        "This means that site settings might not have been completely loaded. " +
-                        "Please add `await` in front of createTestStore call.")
-                }
-            },
-
-            /**
-             * For testing an individual component that is rendered to the router context.
-             * The component will receive the same router props as it would if it was part of the complete app component tree.
-             *
-             * This is usually a lot faster than `getAppContainer` but doesn't work well with react-router links.
-             */
-            connectContainer: (reactContainer) => {
-                store.warnIfStoreCreationNotComplete();
-
-                const routes = createRoutes(getRoutes(store._finalStoreInstance))
-                return store._connectWithStore(
-                    <Router
-                        routes={routes}
-                        history={history}
-                        render={(props) => React.cloneElement(reactContainer, props)}
-                    />
-                );
-            },
-
-            /**
-             * Renders the whole app tree.
-             * Useful if you want to navigate between different sections of your app in your tests.
-             */
-            getAppContainer: () => {
-                store.warnIfStoreCreationNotComplete();
-
-                return store._connectWithStore(
-                    <Router history={history}>
-                        {getRoutes(store._finalStoreInstance)}
-                    </Router>
-                )
-            },
-
-            /** For having internally access to the store with all middlewares included **/
-            _setFinalStoreInstance: (finalStore) => {
-                store._finalStoreInstance = finalStore;
-            },
-
-            _formatDispatchedAction: (action) =>
-                moment(action.timestamp).format("hh:mm:ss.SSS") + " " + chalk.cyan(action.type),
-
-            // eslint-disable-next-line react/display-name
-            _connectWithStore: (reactContainer) =>
-                <Provider store={store._finalStoreInstance}>
-                    {reactContainer}
-                </Provider>
+        actionTypes = Array.isArray(actionTypes) ? actionTypes : [actionTypes];
+
+        // Returns all actions that are triggered after the last action which belongs to `actionTypes
+        const getRemainingActions = () => {
+          const lastActionIndex = _.findLastIndex(
+            store._latestDispatchedActions,
+            action => actionTypes.includes(action.type),
+          );
+          return store._latestDispatchedActions.slice(lastActionIndex + 1);
+        };
+
+        const allActionsAreTriggered = () =>
+          _.every(
+            actionTypes,
+            actionType =>
+              store._latestDispatchedActions.filter(
+                action => action.type === actionType,
+              ).length > 0,
+          );
+
+        if (allActionsAreTriggered()) {
+          // Short-circuit if all action types are already in the history of dispatched actions
+          store._latestDispatchedActions = getRemainingActions();
+          return Promise.resolve();
+        } else {
+          return new Promise((resolve, reject) => {
+            const timeoutID = setTimeout(() => {
+              store._onActionDispatched = null;
 
+              return reject(
+                new Error(
+                  `All these actions were not dispatched within ${timeout}ms:\n` +
+                    chalk.cyan(actionTypes.join("\n")) +
+                    "\n\nDispatched actions since the last call of `waitForActions`:\n" +
+                    (store._latestDispatchedActions
+                      .map(store._formatDispatchedAction)
+                      .join("\n") || "No dispatched actions") +
+                    "\n\nDispatched actions since the initialization of test suite:\n" +
+                    (store._allDispatchedActions
+                      .map(store._formatDispatchedAction)
+                      .join("\n") || "No dispatched actions"),
+                ),
+              );
+            }, timeout);
+
+            store._onActionDispatched = () => {
+              if (allActionsAreTriggered()) {
+                store._latestDispatchedActions = getRemainingActions();
+                store._onActionDispatched = null;
+                clearTimeout(timeoutID);
+                resolve();
+              }
+            };
+          });
+        }
+      },
+
+      /**
+       * Logs the actions that have been dispatched so far
+       */
+      debug: () => {
+        if (store._onActionDispatched) {
+          console.log(
+            "You have `store.waitForActions(...)` still in progress – have you forgotten to prepend `await` to the method call?",
+          );
         }
 
-        return Object.assign(store, testStoreExtensions);
-    }
-}
+        console.log(
+          chalk.bold(
+            "Dispatched actions since last call of `waitForActions`:\n",
+          ) +
+            (store._latestDispatchedActions
+              .map(store._formatDispatchedAction)
+              .join("\n") || "No dispatched actions") +
+            chalk.bold(
+              "\n\nDispatched actions since initialization of test suite:\n",
+            ) +
+            store._allDispatchedActions
+              .map(store._formatDispatchedAction)
+              .join("\n") || "No dispatched actions",
+        );
+      },
+
+      /**
+       * Methods for manipulating the simulated browser history
+       */
+      pushPath: path => history.push(path),
+      goBack: () => history.goBack(),
+      getPath: () => urlFormat(history.getCurrentLocation()),
+
+      warnIfStoreCreationNotComplete: () => {
+        if (!hasFinishedCreatingStore) {
+          console.warn(
+            "Seems that you haven't waited until the store creation has completely finished. " +
+              "This means that site settings might not have been completely loaded. " +
+              "Please add `await` in front of createTestStore call.",
+          );
+        }
+      },
+
+      /**
+       * For testing an individual component that is rendered to the router context.
+       * The component will receive the same router props as it would if it was part of the complete app component tree.
+       *
+       * This is usually a lot faster than `getAppContainer` but doesn't work well with react-router links.
+       */
+      connectContainer: reactContainer => {
+        store.warnIfStoreCreationNotComplete();
+
+        const routes = createRoutes(getRoutes(store._finalStoreInstance));
+        return store._connectWithStore(
+          <Router
+            routes={routes}
+            history={history}
+            render={props => React.cloneElement(reactContainer, props)}
+          />,
+        );
+      },
+
+      /**
+       * Renders the whole app tree.
+       * Useful if you want to navigate between different sections of your app in your tests.
+       */
+      getAppContainer: () => {
+        store.warnIfStoreCreationNotComplete();
+
+        return store._connectWithStore(
+          <Router history={history}>
+            {getRoutes(store._finalStoreInstance)}
+          </Router>,
+        );
+      },
+
+      /** For having internally access to the store with all middlewares included **/
+      _setFinalStoreInstance: finalStore => {
+        store._finalStoreInstance = finalStore;
+      },
+
+      _formatDispatchedAction: action =>
+        moment(action.timestamp).format("hh:mm:ss.SSS") +
+        " " +
+        chalk.cyan(action.type),
+
+      // eslint-disable-next-line react/display-name
+      _connectWithStore: reactContainer => (
+        <Provider store={store._finalStoreInstance}>{reactContainer}</Provider>
+      ),
+    };
+
+    return Object.assign(store, testStoreExtensions);
+  };
+};
 
 // Commonly used question helpers that are temporarily here
 // TODO Atte Keinänen 6/27/17: Put all metabase-lib -related test helpers to one file
-export const createSavedQuestion = async (unsavedQuestion) => {
-    const savedQuestion = await unsavedQuestion.apiCreate()
-    savedQuestion._card = { ...savedQuestion.card(), original_card_id: savedQuestion.id() }
-    return savedQuestion
-}
-
-export const createDashboard = async (details) => {
-    let savedDashboard = await DashboardApi.create(details)
-    return savedDashboard
-}
+export const createSavedQuestion = async unsavedQuestion => {
+  const savedQuestion = await unsavedQuestion.apiCreate();
+  savedQuestion._card = {
+    ...savedQuestion.card(),
+    original_card_id: savedQuestion.id(),
+  };
+  return savedQuestion;
+};
+
+export const createDashboard = async details => {
+  let savedDashboard = await DashboardApi.create(details);
+  return savedDashboard;
+};
 
 /**
  * Waits for a API request with a given method (GET/POST/PUT...) and a url which matches the given regural expression.
  * Useful in those relatively rare situations where React components do API requests inline instead of using Redux actions.
  */
-export const waitForRequestToComplete = (method, urlRegex, { timeout = 5000 } = {}) => {
-    skippedApiRequests = []
-    return new Promise((resolve, reject) => {
-        const completionTimeoutId = setTimeout(() => {
-            reject(
-                new Error(
-                    `API request ${method} ${urlRegex} wasn't completed within ${timeout}ms.\n` +
-                    `Other requests during that time period:\n${skippedApiRequests.join("\n") || "No requests"}`
-                )
-            )
-        }, timeout)
-
-        apiRequestCompletedCallback = (requestMethod, requestUrl) => {
-            if (requestMethod === method && urlRegex.test(requestUrl)) {
-                clearTimeout(completionTimeoutId)
-                resolve()
-            } else {
-                skippedApiRequests.push(`${requestMethod} ${requestUrl}`)
-            }
-        }
-    })
-}
+export const waitForRequestToComplete = (
+  method,
+  urlRegex,
+  { timeout = 5000 } = {},
+) => {
+  skippedApiRequests = [];
+  return new Promise((resolve, reject) => {
+    const completionTimeoutId = setTimeout(() => {
+      reject(
+        new Error(
+          `API request ${method} ${urlRegex} wasn't completed within ${timeout}ms.\n` +
+            `Other requests during that time period:\n${skippedApiRequests.join(
+              "\n",
+            ) || "No requests"}`,
+        ),
+      );
+    }, timeout);
+
+    apiRequestCompletedCallback = (requestMethod, requestUrl) => {
+      if (requestMethod === method && urlRegex.test(requestUrl)) {
+        clearTimeout(completionTimeoutId);
+        resolve();
+      } else {
+        skippedApiRequests.push(`${requestMethod} ${requestUrl}`);
+      }
+    };
+  });
+};
 
 /**
  * Lets you replace given API endpoints with mocked implementations for the lifetime of a test
  */
 export async function withApiMocks(mocks, test) {
-    if (!mocks.every(([apiService, endpointName, mockMethod]) =>
-            _.isObject(apiService) && _.isString(endpointName) && _.isFunction(mockMethod)
-        )
-    ) {
-        throw new Error(
-            "Seems that you are calling \`withApiMocks\` with invalid parameters. " +
-            "The calls should be in format \`withApiMocks([[ApiService, endpointName, mockMethod], ...], tests)\`."
-        )
-    }
-
-    const originals = mocks.map(([apiService, endpointName]) => apiService[endpointName])
-
-    // Replace real API endpoints with mocks
-    mocks.forEach(([apiService, endpointName, mockMethod]) => {
-        apiService[endpointName] = mockMethod
-    })
-
-    try {
-        await test();
-    } finally {
-        // Restore original endpoints after tests, even in case of an exception
-        mocks.forEach(([apiService, endpointName], index) => {
-            apiService[endpointName] = originals[index]
-        })
-    }
+  if (
+    !mocks.every(
+      ([apiService, endpointName, mockMethod]) =>
+        _.isObject(apiService) &&
+        _.isString(endpointName) &&
+        _.isFunction(mockMethod),
+    )
+  ) {
+    throw new Error(
+      "Seems that you are calling `withApiMocks` with invalid parameters. " +
+        "The calls should be in format `withApiMocks([[ApiService, endpointName, mockMethod], ...], tests)`.",
+    );
+  }
+
+  const originals = mocks.map(
+    ([apiService, endpointName]) => apiService[endpointName],
+  );
+
+  // Replace real API endpoints with mocks
+  mocks.forEach(([apiService, endpointName, mockMethod]) => {
+    apiService[endpointName] = mockMethod;
+  });
+
+  try {
+    await test();
+  } finally {
+    // Restore original endpoints after tests, even in case of an exception
+    mocks.forEach(([apiService, endpointName], index) => {
+      apiService[endpointName] = originals[index];
+    });
+  }
 }
 
 // Patches the metabase/lib/api module so that all API queries contain the login credential cookie.
 // Needed because we are not in a real web browser environment.
 api._makeRequest = async (method, url, headers, requestBody, data, options) => {
-    const headersWithSessionCookie = {
-        ...headers,
-        ...(loginSession ? {"Cookie": `${METABASE_SESSION_COOKIE}=${loginSession.id}`} : {})
+  const headersWithSessionCookie = {
+    ...headers,
+    ...(loginSession
+      ? { Cookie: `${METABASE_SESSION_COOKIE}=${loginSession.id}` }
+      : {}),
+  };
+
+  const fetchOptions = {
+    credentials: "include",
+    method,
+    headers: new Headers(headersWithSessionCookie),
+    ...(requestBody ? { body: requestBody } : {}),
+  };
+
+  let isCancelled = false;
+  if (options.cancelled) {
+    options.cancelled.then(() => {
+      isCancelled = true;
+    });
+  }
+  const result = simulateOfflineMode
+    ? { status: 0, responseText: "" }
+    : await fetch(api.basename + url, fetchOptions);
+
+  if (isCancelled) {
+    throw { status: 0, data: "", isCancelled: true };
+  }
+
+  let resultBody = null;
+  try {
+    resultBody = await result.text();
+    // Even if the result conversion to JSON fails, we still return the original text
+    // This is 1-to-1 with the real _makeRequest implementation
+    resultBody = JSON.parse(resultBody);
+  } catch (e) {}
+
+  apiRequestCompletedCallback &&
+    setTimeout(() => apiRequestCompletedCallback(method, url), 0);
+
+  if (result.status >= 200 && result.status <= 299) {
+    if (options.transformResponse) {
+      return options.transformResponse(resultBody, { data });
+    } else {
+      return resultBody;
     }
-
-    const fetchOptions = {
-        credentials: "include",
-        method,
-        headers: new Headers(headersWithSessionCookie),
-        ...(requestBody ? { body: requestBody } : {})
+  } else {
+    const error = {
+      status: result.status,
+      data: resultBody,
+      isCancelled: false,
     };
-
-    let isCancelled = false
-    if (options.cancelled) {
-        options.cancelled.then(() => {
-            isCancelled = true;
-        });
+    if (!simulateOfflineMode) {
+      console.log("A request made in a test failed with the following error:");
+      console.log(error, { depth: null });
+      console.log(`The original request: ${method} ${url}`);
+      if (requestBody) console.log(`Original payload: ${requestBody}`);
     }
-    const result = simulateOfflineMode
-        ? { status: 0, responseText: '' }
-        : (await fetch(api.basename + url, fetchOptions));
 
-    if (isCancelled) {
-        throw { status: 0, data: '', isCancelled: true}
-    }
-
-    let resultBody = null
-    try {
-        resultBody = await result.text();
-        // Even if the result conversion to JSON fails, we still return the original text
-        // This is 1-to-1 with the real _makeRequest implementation
-        resultBody = JSON.parse(resultBody);
-    } catch (e) {}
-
-    apiRequestCompletedCallback && setTimeout(() => apiRequestCompletedCallback(method, url), 0)
-
-    if (result.status >= 200 && result.status <= 299) {
-        if (options.transformResponse) {
-            return options.transformResponse(resultBody, { data });
-        } else {
-            return resultBody
-        }
-    } else {
-        const error = { status: result.status, data: resultBody, isCancelled: false }
-        if (!simulateOfflineMode) {
-            console.log('A request made in a test failed with the following error:');
-            console.log(error, { depth: null });
-            console.log(`The original request: ${method} ${url}`);
-            if (requestBody) console.log(`Original payload: ${requestBody}`);
-        }
-
-        throw error
-    }
-}
+    throw error;
+  }
+};
 
 // Set the correct base url to metabase/lib/api module
-if (process.env.TEST_FIXTURE_BACKEND_HOST && process.env.TEST_FIXTURE_BACKEND_HOST) {
-    // Default to the test db fixture
-    api.basename = process.env.TEST_FIXTURE_BACKEND_HOST;
+if (
+  process.env.TEST_FIXTURE_BACKEND_HOST &&
+  process.env.TEST_FIXTURE_BACKEND_HOST
+) {
+  // Default to the test db fixture
+  api.basename = process.env.TEST_FIXTURE_BACKEND_HOST;
 } else {
-    console.log(
-        'Please use `yarn run test-integrated` or `yarn run test-integrated-watch` for running integration tests.'
-    )
-    process.quit(0)
+  console.log(
+    "Please use `yarn run test-integrated` or `yarn run test-integrated-watch` for running integration tests.",
+  );
+  process.quit(0);
 }
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 20000;
diff --git a/frontend/test/__support__/mocks.js b/frontend/test/__support__/mocks.js
index a519aa5c8d6c52d232c1bcc3db551d3549af8c02..9dbe9c99289643f62be3e0e4afb1609244f50af9 100644
--- a/frontend/test/__support__/mocks.js
+++ b/frontend/test/__support__/mocks.js
@@ -1,59 +1,67 @@
-global.ga = () => {}
-global.ace.define = () => {}
-global.ace.require = () => {}
+global.ga = () => {};
+global.ace.define = () => {};
+global.ace.require = () => {};
 
-global.window.matchMedia = () => ({ addListener: () => {}, removeListener: () => {} })
+global.window.matchMedia = () => ({
+  addListener: () => {},
+  removeListener: () => {},
+});
 
 // Disable analytics
-jest.mock('metabase/lib/analytics');
+jest.mock("metabase/lib/analytics");
 
 // Suppress ace import errors
-jest.mock("ace/ace", () => {}, {virtual: true});
-jest.mock("ace/mode-plain_text", () => {}, {virtual: true});
-jest.mock("ace/mode-javascript", () => {}, {virtual: true});
-jest.mock("ace/mode-json", () => {}, {virtual: true});
-jest.mock("ace/mode-clojure", () => {}, {virtual: true});
-jest.mock("ace/mode-ruby", () => {}, {virtual: true});
-jest.mock("ace/mode-html", () => {}, {virtual: true});
-jest.mock("ace/mode-jsx", () => {}, {virtual: true});
-jest.mock("ace/mode-sql", () => {}, {virtual: true});
-jest.mock("ace/mode-mysql", () => {}, {virtual: true});
-jest.mock("ace/mode-pgsql", () => {}, {virtual: true});
-jest.mock("ace/mode-sqlserver", () => {}, {virtual: true});
-jest.mock("ace/snippets/sql", () => {}, {virtual: true});
-jest.mock("ace/snippets/mysql", () => {}, {virtual: true});
-jest.mock("ace/snippets/pgsql", () => {}, {virtual: true});
-jest.mock("ace/snippets/sqlserver", () => {}, {virtual: true});
-jest.mock("ace/snippets/json", () => {}, {virtual: true});
-jest.mock("ace/snippets/json", () => {}, {virtual: true});
-jest.mock("ace/ext-language_tools", () => {}, {virtual: true});
+jest.mock("ace/ace", () => {}, { virtual: true });
+jest.mock("ace/mode-plain_text", () => {}, { virtual: true });
+jest.mock("ace/mode-javascript", () => {}, { virtual: true });
+jest.mock("ace/mode-json", () => {}, { virtual: true });
+jest.mock("ace/mode-clojure", () => {}, { virtual: true });
+jest.mock("ace/mode-ruby", () => {}, { virtual: true });
+jest.mock("ace/mode-html", () => {}, { virtual: true });
+jest.mock("ace/mode-jsx", () => {}, { virtual: true });
+jest.mock("ace/mode-sql", () => {}, { virtual: true });
+jest.mock("ace/mode-mysql", () => {}, { virtual: true });
+jest.mock("ace/mode-pgsql", () => {}, { virtual: true });
+jest.mock("ace/mode-sqlserver", () => {}, { virtual: true });
+jest.mock("ace/snippets/sql", () => {}, { virtual: true });
+jest.mock("ace/snippets/mysql", () => {}, { virtual: true });
+jest.mock("ace/snippets/pgsql", () => {}, { virtual: true });
+jest.mock("ace/snippets/sqlserver", () => {}, { virtual: true });
+jest.mock("ace/snippets/json", () => {}, { virtual: true });
+jest.mock("ace/snippets/json", () => {}, { virtual: true });
+jest.mock("ace/ext-language_tools", () => {}, { virtual: true });
 
 // Use test versions of components that are normally rendered to document root or use unsupported browser APIs
 import * as modal from "metabase/components/Modal";
 modal.default = modal.TestModal;
 
 import * as tooltip from "metabase/components/Tooltip";
-tooltip.default = tooltip.TestTooltip
+tooltip.default = tooltip.TestTooltip;
 
-import * as popover from "metabase/components/Popover";
-popover.default = popover.TestPopover
+jest.mock("metabase/components/Popover.jsx");
 
 import * as bodyComponent from "metabase/components/BodyComponent";
-bodyComponent.default = bodyComponent.TestBodyComponent
+bodyComponent.default = bodyComponent.TestBodyComponent;
 
 import * as table from "metabase/visualizations/visualizations/Table";
-table.default = table.TestTable
+table.default = table.TestTable;
+
+jest.mock("metabase/hoc/Remapped");
 
 // Replace addEventListener with a test implementation which collects all event listeners to `eventListeners` map
 export let eventListeners = {};
 const testAddEventListener = jest.fn((event, listener) => {
-    eventListeners[event] = eventListeners[event] ? [...eventListeners[event], listener] : [listener]
-})
+  eventListeners[event] = eventListeners[event]
+    ? [...eventListeners[event], listener]
+    : [listener];
+});
 const testRemoveEventListener = jest.fn((event, listener) => {
-    eventListeners[event] = (eventListeners[event] || []).filter(l => l !== listener)
-})
+  eventListeners[event] = (eventListeners[event] || []).filter(
+    l => l !== listener,
+  );
+});
 
-global.document.addEventListener = testAddEventListener
-global.window.addEventListener = testAddEventListener
-global.document.removeEventListener = testRemoveEventListener
-global.window.removeEventListener = testRemoveEventListener
+global.document.addEventListener = testAddEventListener;
+global.window.addEventListener = testAddEventListener;
+global.document.removeEventListener = testRemoveEventListener;
+global.window.removeEventListener = testRemoveEventListener;
diff --git a/frontend/test/__support__/sample_dataset_fixture.js b/frontend/test/__support__/sample_dataset_fixture.js
index 3dbaa2a5f50d7eb4f6249f4731a9f12b20735527..32295c1284211896e268b2071c0ac5e1e7b5160a 100644
--- a/frontend/test/__support__/sample_dataset_fixture.js
+++ b/frontend/test/__support__/sample_dataset_fixture.js
@@ -1,3 +1,7 @@
+import React from "react";
+import { Provider } from "react-redux";
+import { getStore } from "metabase/store";
+
 import Question from "metabase-lib/lib/Question";
 import { getMetadata } from "metabase/selectors/metadata";
 import { assocIn } from "icepick";
@@ -29,347 +33,281 @@ export const PEOPLE_STATE_FIELD_ID = 19;
 export const state = {
   metadata: {
     metrics: {
-      '1': {
-        description: 'Because we want to know the total I ugess',
+      "1": {
+        description: "Because we want to know the total I ugess",
         table_id: 1,
         definition: {
-          aggregation: [
-            [
-              'sum',
-              [
-                'field-id',
-                6
-              ]
-            ]
-          ],
-          source_table: 1
+          aggregation: [["sum", ["field-id", 6]]],
+          source_table: 1,
         },
         creator: {
-          email: 'sameer@metabase.com',
-          first_name: 'Sameer',
-          last_login: '2017-06-14T23:23:59.582Z',
+          email: "sameer@metabase.com",
+          first_name: "Sameer",
+          last_login: "2017-06-14T23:23:59.582Z",
           is_qbnewb: true,
           is_superuser: true,
           id: 1,
-          last_name: 'Al-Sakran',
-          date_joined: '2017-06-14T23:23:59.409Z',
-          common_name: 'Sameer Al-Sakran'
+          last_name: "Al-Sakran",
+          date_joined: "2017-06-14T23:23:59.409Z",
+          common_name: "Sameer Al-Sakran",
         },
         database_id: 1,
         show_in_getting_started: false,
-        name: 'Total Order Value',
+        name: "Total Order Value",
         is_active: true,
         caveats: null,
         creator_id: 1,
-        updated_at: '2017-06-14T23:32:12.266Z',
+        updated_at: "2017-06-14T23:32:12.266Z",
         id: 1,
         how_is_this_calculated: null,
-        created_at: '2017-06-14T23:32:12.267Z',
-        points_of_interest: null
-      }
+        created_at: "2017-06-14T23:32:12.267Z",
+        points_of_interest: null,
+      },
     },
     segments: {
-      '1': {
-        description: 'If it\'s more expensive it must be better',
+      "1": {
+        description: "If it's more expensive it must be better",
         table_id: 1,
         definition: {
-          filter: [
-            '>',
-            [
-              'field-id',
-              6
-            ],
-            30
-          ],
-          source_table: 1
+          filter: [">", ["field-id", 6], 30],
+          source_table: 1,
         },
         creator: {
-          email: 'sameer@metabase.com',
-          first_name: 'Sameer',
-          last_login: '2017-06-14T23:23:59.582Z',
+          email: "sameer@metabase.com",
+          first_name: "Sameer",
+          last_login: "2017-06-14T23:23:59.582Z",
           is_qbnewb: true,
           is_superuser: true,
           id: 1,
-          last_name: 'Al-Sakran',
-          date_joined: '2017-06-14T23:23:59.409Z',
-          common_name: 'Sameer Al-Sakran'
+          last_name: "Al-Sakran",
+          date_joined: "2017-06-14T23:23:59.409Z",
+          common_name: "Sameer Al-Sakran",
         },
         show_in_getting_started: false,
-        name: 'Expensive Things',
+        name: "Expensive Things",
         is_active: true,
         caveats: null,
         creator_id: 1,
-        updated_at: '2017-06-14T23:31:46.480Z',
+        updated_at: "2017-06-14T23:31:46.480Z",
         id: 1,
-        created_at: '2017-06-14T23:31:46.480Z',
-        points_of_interest: null
-      }
+        created_at: "2017-06-14T23:31:46.480Z",
+        points_of_interest: null,
+      },
     },
     databases: {
-      '1': {
+      "1": {
         description: null,
         features: [
-          'basic-aggregations',
-          'standard-deviation-aggregations',
-          'expression-aggregations',
-          'foreign-keys',
-          'native-parameters',
-          'expressions'
+          "basic-aggregations",
+          "standard-deviation-aggregations",
+          "expression-aggregations",
+          "foreign-keys",
+          "native-parameters",
+          "expressions",
         ],
-        name: 'Sample Dataset',
+        name: "Sample Dataset",
         caveats: null,
-        tables: [
-          1,
-          2,
-          3,
-          4
-        ],
+        tables: [1, 2, 3, 4],
         is_full_sync: true,
-        updated_at: '2017-06-14T23:22:55.349Z',
-        native_permissions: 'write',
+        updated_at: "2017-06-14T23:22:55.349Z",
+        native_permissions: "write",
         details: {
-          db: 'zip:/private/tmp/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest'
+          db:
+            "zip:/private/tmp/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest",
         },
         is_sample: true,
         id: 1,
-        engine: 'h2',
-        created_at: '2017-06-14T23:22:55.349Z',
-        points_of_interest: null
+        engine: "h2",
+        created_at: "2017-06-14T23:22:55.349Z",
+        points_of_interest: null,
       },
-       '2': {
+      "2": {
         description: null,
         features: [
-          'basic-aggregations',
-          'standard-deviation-aggregations',
-          'expression-aggregations',
-          'foreign-keys',
-          'native-parameters',
-          'expressions'
+          "basic-aggregations",
+          "standard-deviation-aggregations",
+          "expression-aggregations",
+          "foreign-keys",
+          "native-parameters",
+          "expressions",
         ],
-        name: 'Sample Empty Dataset',
+        name: "Sample Empty Dataset",
         caveats: null,
         tables: [],
         is_full_sync: true,
-        updated_at: '2017-06-14T23:22:55.349Z',
-        native_permissions: 'write',
+        updated_at: "2017-06-14T23:22:55.349Z",
+        native_permissions: "write",
         details: {
-          db: 'zip:/private/tmp/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest'
+          db:
+            "zip:/private/tmp/metabase.jar!/sample-dataset.db;USER=GUEST;PASSWORD=guest",
         },
         is_sample: true,
         id: 2,
-        engine: 'h2',
-        created_at: '2017-06-14T23:22:55.349Z',
-        points_of_interest: null
+        engine: "h2",
+        created_at: "2017-06-14T23:22:55.349Z",
+        points_of_interest: null,
+      },
+      "3": {
+        description: null,
+        features: ["basic-aggregations", "nested-fields", "dynamic-schema"],
+        name: "test-data",
+        caveats: null,
+        tables: [],
+        is_full_sync: true,
+        updated_at: "2017-06-22T00:33:36.681Z",
+        details: {
+          dbname: "test-data",
+          host: "localhost",
+        },
+        is_sample: false,
+        id: 3,
+        engine: "mongo",
+        created_at: "2017-06-22T00:33:36.681Z",
+        points_of_interest: null,
       },
-      '3':{
-          description: null,
-          features: [
-              "basic-aggregations",
-              "nested-fields",
-              "dynamic-schema"
-          ],
-          name: "test-data",
-          caveats: null,
-          tables: [],
-          is_full_sync: true,
-          updated_at: "2017-06-22T00:33:36.681Z",
-          details: {
-              dbname: "test-data",
-              host: "localhost"
-          },
-          is_sample: false,
-          id: 3,
-          engine: "mongo",
-          created_at: "2017-06-22T00:33:36.681Z",
-          points_of_interest: null
-      }
     },
     tables: {
-      '1': {
-        description: 'This is a confirmed order for a product from a user.',
+      "1": {
+        description: "This is a confirmed order for a product from a user.",
         entity_type: null,
-        schema: 'PUBLIC',
+        schema: "PUBLIC",
         raw_table_id: 2,
         show_in_getting_started: false,
-        name: 'ORDERS',
+        name: "ORDERS",
         caveats: null,
         rows: 17624,
-        updated_at: '2017-06-14T23:22:56.818Z',
+        updated_at: "2017-06-14T23:22:56.818Z",
         entity_name: null,
         active: true,
         id: 1,
         db_id: 1,
         visibility_type: null,
-        display_name: 'Orders',
-        created_at: '2017-06-14T23:22:55.758Z',
+        display_name: "Orders",
+        created_at: "2017-06-14T23:22:55.758Z",
         points_of_interest: null,
         db: 1,
-        fields: [
-          1,
-          2,
-          3,
-          4,
-          5,
-          6,
-          7
-        ],
-        segments: [
-          1
-        ],
+        fields: [1, 2, 3, 4, 5, 6, 7],
+        segments: [1],
         field_values: {},
-        metrics: [
-          1
-        ]
+        metrics: [1],
       },
-      '2': {
-        description: 'This is a user account. Note that employees and customer support staff will have accounts.',
+      "2": {
+        description:
+          "This is a user account. Note that employees and customer support staff will have accounts.",
         entity_type: null,
-        schema: 'PUBLIC',
+        schema: "PUBLIC",
         raw_table_id: 3,
         show_in_getting_started: false,
-        name: 'PEOPLE',
+        name: "PEOPLE",
         caveats: null,
         rows: 2500,
-        updated_at: '2017-06-14T23:22:57.664Z',
+        updated_at: "2017-06-14T23:22:57.664Z",
         entity_name: null,
         active: true,
         id: 2,
         db_id: 1,
         visibility_type: null,
-        display_name: 'People',
-        created_at: '2017-06-14T23:22:55.779Z',
+        display_name: "People",
+        created_at: "2017-06-14T23:22:55.779Z",
         points_of_interest: null,
         db: 1,
-        fields: [
-          8,
-          9,
-          10,
-          11,
-          12,
-          13,
-          14,
-          15,
-          16,
-          17,
-          18,
-          19,
-          20
-        ],
+        fields: [8, 9, 10, 11, 12, 13, 14, 15, 16, 17, 18, 19, 20],
         segments: [],
         field_values: {
-          '18': [
-            'Affiliate',
-            'Facebook',
-            'Google',
-            'Organic',
-            'Twitter'
+          "18": ["Affiliate", "Facebook", "Google", "Organic", "Twitter"],
+          "19": [
+            "AA",
+            "AE",
+            "AK",
+            "AL",
+            "AP",
+            "AR",
+            "AS",
+            "AZ",
+            "CA",
+            "CO",
+            "CT",
+            "DC",
+            "DE",
+            "FL",
+            "FM",
+            "GA",
+            "GU",
+            "HI",
+            "IA",
+            "ID",
+            "IL",
+            "IN",
+            "KS",
+            "KY",
+            "LA",
+            "MA",
+            "MD",
+            "ME",
+            "MH",
+            "MI",
+            "MN",
+            "MO",
+            "MP",
+            "MS",
+            "MT",
+            "NC",
+            "ND",
+            "NE",
+            "NH",
+            "NJ",
+            "NM",
+            "NV",
+            "NY",
+            "OH",
+            "OK",
+            "OR",
+            "PA",
+            "PR",
+            "PW",
+            "RI",
+            "SC",
+            "SD",
+            "TN",
+            "TX",
+            "UT",
+            "VA",
+            "VI",
+            "VT",
+            "WA",
+            "WI",
+            "WV",
+            "WY",
           ],
-          '19': [
-            'AA',
-            'AE',
-            'AK',
-            'AL',
-            'AP',
-            'AR',
-            'AS',
-            'AZ',
-            'CA',
-            'CO',
-            'CT',
-            'DC',
-            'DE',
-            'FL',
-            'FM',
-            'GA',
-            'GU',
-            'HI',
-            'IA',
-            'ID',
-            'IL',
-            'IN',
-            'KS',
-            'KY',
-            'LA',
-            'MA',
-            'MD',
-            'ME',
-            'MH',
-            'MI',
-            'MN',
-            'MO',
-            'MP',
-            'MS',
-            'MT',
-            'NC',
-            'ND',
-            'NE',
-            'NH',
-            'NJ',
-            'NM',
-            'NV',
-            'NY',
-            'OH',
-            'OK',
-            'OR',
-            'PA',
-            'PR',
-            'PW',
-            'RI',
-            'SC',
-            'SD',
-            'TN',
-            'TX',
-            'UT',
-            'VA',
-            'VI',
-            'VT',
-            'WA',
-            'WI',
-            'WV',
-            'WY'
-          ]
         },
-        metrics: []
+        metrics: [],
       },
-      '3': {
-        description: 'This is our product catalog. It includes all products ever sold by the Sample Company.',
+      "3": {
+        description:
+          "This is our product catalog. It includes all products ever sold by the Sample Company.",
         entity_type: null,
-        schema: 'PUBLIC',
+        schema: "PUBLIC",
         raw_table_id: 1,
         show_in_getting_started: false,
-        name: 'PRODUCTS',
+        name: "PRODUCTS",
         caveats: null,
         rows: 200,
-        updated_at: '2017-06-14T23:22:57.756Z',
+        updated_at: "2017-06-14T23:22:57.756Z",
         entity_name: null,
         active: true,
         id: 3,
         db_id: 1,
         visibility_type: null,
-        display_name: 'Products',
-        created_at: '2017-06-14T23:22:55.809Z',
+        display_name: "Products",
+        created_at: "2017-06-14T23:22:55.809Z",
         points_of_interest: null,
         db: 1,
-        fields: [
-          21,
-          22,
-          23,
-          24,
-          25,
-          26,
-          27,
-          28
-        ],
+        fields: [21, 22, 23, 24, 25, 26, 27, 28],
         segments: [],
         field_values: {
-          '21': [
-            'Doohickey',
-            'Gadget',
-            'Gizmo',
-            'Widget'
-          ],
+          "21": ["Doohickey", "Gadget", "Gizmo", "Widget"],
 
-          '26': [
+          "26": [
             0,
             1,
             1.6,
@@ -394,758 +332,757 @@ export const state = {
             4.5,
             4.6,
             4.7,
-            5
-          ]
+            5,
+          ],
         },
-        metrics: []
+        metrics: [],
       },
-      '4': {
-        description: 'These are reviews our customers have left on products. Note that these are not tied to orders so it is possible people have reviewed products they did not purchase from us.',
+      "4": {
+        description:
+          "These are reviews our customers have left on products. Note that these are not tied to orders so it is possible people have reviewed products they did not purchase from us.",
         entity_type: null,
-        schema: 'PUBLIC',
+        schema: "PUBLIC",
         raw_table_id: 5,
         show_in_getting_started: false,
-        name: 'REVIEWS',
+        name: "REVIEWS",
         caveats: null,
         rows: 1078,
-        updated_at: '2017-06-14T23:22:58.024Z',
+        updated_at: "2017-06-14T23:22:58.024Z",
         entity_name: null,
         active: true,
         id: 4,
         db_id: 1,
         visibility_type: null,
-        display_name: 'Reviews',
-        created_at: '2017-06-14T23:22:55.825Z',
+        display_name: "Reviews",
+        created_at: "2017-06-14T23:22:55.825Z",
         points_of_interest: null,
-        fields: [
-          29,
-          30,
-          31,
-          32,
-          33,
-          34
-        ],
+        fields: [29, 30, 31, 32, 33, 34],
         segments: [],
-        metrics: []
-      }
+        metrics: [],
+      },
     },
     fields: {
-      '1': {
-        description: 'The date and time an order was submitted.',
+      "1": {
+        description: "The date and time an order was submitted.",
         table_id: 1,
         special_type: null,
-        name: 'CREATED_AT',
+        name: "CREATED_AT",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.766Z',
+        updated_at: "2017-06-14T23:22:55.766Z",
         active: true,
         parent_id: null,
         id: 1,
         raw_column_id: 9,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Created At',
-        created_at: '2017-06-14T23:22:55.766Z',
-        base_type: 'type/DateTime',
+        display_name: "Created At",
+        created_at: "2017-06-14T23:22:55.766Z",
+        base_type: "type/DateTime",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '2': {
-        description: 'This is a unique ID for the product. It is also called the “Invoice number” or “Confirmation number” in customer facing emails and screens.',
+      "2": {
+        description:
+          "This is a unique ID for the product. It is also called the “Invoice number” or “Confirmation number” in customer facing emails and screens.",
         table_id: 1,
-        special_type: 'type/PK',
-        name: 'ID',
+        special_type: "type/PK",
+        name: "ID",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.769Z',
+        updated_at: "2017-06-14T23:22:55.769Z",
         active: true,
         parent_id: null,
         id: 2,
         raw_column_id: 10,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'ID',
-        created_at: '2017-06-14T23:22:55.769Z',
-        base_type: 'type/BigInteger',
+        display_name: "ID",
+        created_at: "2017-06-14T23:22:55.769Z",
+        base_type: "type/BigInteger",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '3': {
-        description: 'The product ID. This is an internal identifier for the product, NOT the SKU.',
+      "3": {
+        description:
+          "The product ID. This is an internal identifier for the product, NOT the SKU.",
         table_id: 1,
-        special_type: 'type/FK',
-        name: 'PRODUCT_ID',
+        special_type: "type/FK",
+        name: "PRODUCT_ID",
         caveats: null,
         fk_target_field_id: 24,
-        updated_at: '2017-06-14T23:22:55.838Z',
+        updated_at: "2017-06-14T23:22:55.838Z",
         active: true,
         parent_id: null,
         id: 3,
         raw_column_id: 11,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: 24,
         preview_display: true,
-        display_name: 'Product ID',
-        created_at: '2017-06-14T23:22:55.770Z',
-        base_type: 'type/Integer',
+        display_name: "Product ID",
+        created_at: "2017-06-14T23:22:55.770Z",
+        base_type: "type/Integer",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '4': {
-        description: 'The raw, pre-tax cost of the order. Note that this might be different in the future from the product price due to promotions, credits, etc.',
+      "4": {
+        description:
+          "The raw, pre-tax cost of the order. Note that this might be different in the future from the product price due to promotions, credits, etc.",
         table_id: 1,
-        special_type: 'type/Category',
-        name: 'SUBTOTAL',
+        special_type: "type/Category",
+        name: "SUBTOTAL",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:56.822Z',
+        updated_at: "2017-06-14T23:22:56.822Z",
         active: true,
         parent_id: null,
         id: 4,
         raw_column_id: 12,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Subtotal',
-        created_at: '2017-06-14T23:22:55.772Z',
-        base_type: 'type/Float',
+        display_name: "Subtotal",
+        created_at: "2017-06-14T23:22:55.772Z",
+        base_type: "type/Float",
         points_of_interest: null,
         values: {
           id: 1,
-          created_at: '2017-06-14T23:22:56.827Z',
-          updated_at: '2017-06-14T23:22:56.827Z',
+          created_at: "2017-06-14T23:22:56.827Z",
+          updated_at: "2017-06-14T23:22:56.827Z",
           values: [],
           human_readable_values: {},
-          field_id: 4
-        }
+          field_id: 4,
+        },
       },
-      '5': {
-        description: 'This is the amount of local and federal taxes that are collected on the purchase. Note that other governmental fees on some products are not included here, but instead are accounted for in the subtotal.',
+      "5": {
+        description:
+          "This is the amount of local and federal taxes that are collected on the purchase. Note that other governmental fees on some products are not included here, but instead are accounted for in the subtotal.",
         table_id: 1,
         special_type: null,
-        name: 'TAX',
+        name: "TAX",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.774Z',
+        updated_at: "2017-06-14T23:22:55.774Z",
         active: true,
         parent_id: null,
         id: 5,
         raw_column_id: 13,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Tax',
-        created_at: '2017-06-14T23:22:55.774Z',
-        base_type: 'type/Float',
+        display_name: "Tax",
+        created_at: "2017-06-14T23:22:55.774Z",
+        base_type: "type/Float",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '6': {
-        description: 'The total billed amount.',
+      "6": {
+        description: "The total billed amount.",
         table_id: 1,
         special_type: null,
-        name: 'TOTAL',
+        name: "TOTAL",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.775Z',
+        updated_at: "2017-06-14T23:22:55.775Z",
         active: true,
         parent_id: null,
         id: 6,
         raw_column_id: 14,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Total',
-        created_at: '2017-06-14T23:22:55.775Z',
-        base_type: 'type/Float',
+        display_name: "Total",
+        created_at: "2017-06-14T23:22:55.775Z",
+        base_type: "type/Float",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '7': {
-        description: 'The id of the user who made this order. Note that in some cases where an order was created on behalf of a customer who phoned the order in, this might be the employee who handled the request.',
+      "7": {
+        description:
+          "The id of the user who made this order. Note that in some cases where an order was created on behalf of a customer who phoned the order in, this might be the employee who handled the request.",
         table_id: 1,
-        special_type: 'type/FK',
-        name: 'USER_ID',
+        special_type: "type/FK",
+        name: "USER_ID",
         caveats: null,
         fk_target_field_id: 13,
-        updated_at: '2017-06-14T23:22:55.839Z',
+        updated_at: "2017-06-14T23:22:55.839Z",
         active: true,
         parent_id: null,
         id: 7,
         raw_column_id: 15,
-        last_analyzed: '2017-06-14T23:22:56.832Z',
+        last_analyzed: "2017-06-14T23:22:56.832Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: 13,
         preview_display: true,
-        display_name: 'User ID',
-        created_at: '2017-06-14T23:22:55.777Z',
-        base_type: 'type/Integer',
+        display_name: "User ID",
+        created_at: "2017-06-14T23:22:55.777Z",
+        base_type: "type/Integer",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '8': {
-        description: 'The street address of the account’s billing address',
+      "8": {
+        description: "The street address of the account’s billing address",
         table_id: 2,
         special_type: null,
-        name: 'ADDRESS',
+        name: "ADDRESS",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.785Z',
+        updated_at: "2017-06-14T23:22:55.785Z",
         active: true,
         parent_id: null,
         id: 8,
         raw_column_id: 16,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Address',
-        created_at: '2017-06-14T23:22:55.785Z',
-        base_type: 'type/Text',
+        display_name: "Address",
+        created_at: "2017-06-14T23:22:55.785Z",
+        base_type: "type/Text",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '9': {
-        description: 'The date of birth of the user',
+      "9": {
+        description: "The date of birth of the user",
         table_id: 2,
         special_type: null,
-        name: 'BIRTH_DATE',
+        name: "BIRTH_DATE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.787Z',
+        updated_at: "2017-06-14T23:22:55.787Z",
         active: true,
         parent_id: null,
         id: 9,
         raw_column_id: 17,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Birth Date',
-        created_at: '2017-06-14T23:22:55.787Z',
-        base_type: 'type/Date',
+        display_name: "Birth Date",
+        created_at: "2017-06-14T23:22:55.787Z",
+        base_type: "type/Date",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '10': {
-        description: 'The city of the account’s billing address',
+      "10": {
+        description: "The city of the account’s billing address",
         table_id: 2,
-        special_type: 'type/City',
-        name: 'CITY',
+        special_type: "type/City",
+        name: "CITY",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.788Z',
+        updated_at: "2017-06-14T23:22:55.788Z",
         active: true,
         parent_id: null,
         id: 10,
         raw_column_id: 18,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'City',
-        created_at: '2017-06-14T23:22:55.789Z',
-        base_type: 'type/Text',
+        display_name: "City",
+        created_at: "2017-06-14T23:22:55.789Z",
+        base_type: "type/Text",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '11': {
-        description: 'The date the user record was created. Also referred to as the user’s "join date"',
+      "11": {
+        description:
+          'The date the user record was created. Also referred to as the user’s "join date"',
         table_id: 2,
         special_type: null,
-        name: 'CREATED_AT',
+        name: "CREATED_AT",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.791Z',
+        updated_at: "2017-06-14T23:22:55.791Z",
         active: true,
         parent_id: null,
         id: 11,
         raw_column_id: 19,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Created At',
-        created_at: '2017-06-14T23:22:55.791Z',
-        base_type: 'type/DateTime',
+        display_name: "Created At",
+        created_at: "2017-06-14T23:22:55.791Z",
+        base_type: "type/DateTime",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '12': {
-        description: 'The contact email for the account.',
+      "12": {
+        description: "The contact email for the account.",
         table_id: 2,
-        special_type: 'type/Email',
-        name: 'EMAIL',
+        special_type: "type/Email",
+        name: "EMAIL",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.666Z',
+        updated_at: "2017-06-14T23:22:57.666Z",
         active: true,
         parent_id: null,
         id: 12,
         raw_column_id: 20,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Email',
-        created_at: '2017-06-14T23:22:55.793Z',
-        base_type: 'type/Text',
+        display_name: "Email",
+        created_at: "2017-06-14T23:22:55.793Z",
+        base_type: "type/Text",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '13': {
-        description: 'A unique identifier given to each user.',
+      "13": {
+        description: "A unique identifier given to each user.",
         table_id: 2,
-        special_type: 'type/PK',
-        name: 'ID',
+        special_type: "type/PK",
+        name: "ID",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.795Z',
+        updated_at: "2017-06-14T23:22:55.795Z",
         active: true,
         parent_id: null,
         id: 13,
         raw_column_id: 21,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         preview_display: true,
-        display_name: 'ID',
-        created_at: '2017-06-14T23:22:55.795Z',
-        base_type: 'type/BigInteger',
+        display_name: "ID",
+        created_at: "2017-06-14T23:22:55.795Z",
+        base_type: "type/BigInteger",
         points_of_interest: null,
         target: null,
-        values: []
+        values: [],
       },
-      '14': {
-        description: 'This is the latitude of the user on sign-up. It might be updated in the future to the last seen location.',
+      "14": {
+        description:
+          "This is the latitude of the user on sign-up. It might be updated in the future to the last seen location.",
         table_id: 2,
-        special_type: 'type/Latitude',
-        name: 'LATITUDE',
+        special_type: "type/Latitude",
+        name: "LATITUDE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.797Z',
+        updated_at: "2017-06-14T23:22:55.797Z",
         active: true,
         parent_id: null,
         id: 14,
         raw_column_id: 22,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Latitude',
-        created_at: '2017-06-14T23:22:55.797Z',
-        base_type: 'type/Float',
+        display_name: "Latitude",
+        created_at: "2017-06-14T23:22:55.797Z",
+        base_type: "type/Float",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '15': {
-        description: 'This is the longitude of the user on sign-up. It might be updated in the future to the last seen location.',
+      "15": {
+        description:
+          "This is the longitude of the user on sign-up. It might be updated in the future to the last seen location.",
         table_id: 2,
-        special_type: 'type/Longitude',
-        name: 'LONGITUDE',
+        special_type: "type/Longitude",
+        name: "LONGITUDE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.798Z',
+        updated_at: "2017-06-14T23:22:55.798Z",
         active: true,
         parent_id: null,
         id: 15,
         raw_column_id: 23,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Longitude',
-        created_at: '2017-06-14T23:22:55.798Z',
-        base_type: 'type/Float',
+        display_name: "Longitude",
+        created_at: "2017-06-14T23:22:55.798Z",
+        base_type: "type/Float",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '16': {
-        description: 'The name of the user who owns an account',
+      "16": {
+        description: "The name of the user who owns an account",
         table_id: 2,
-        special_type: 'type/Name',
-        name: 'NAME',
+        special_type: "type/Name",
+        name: "NAME",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.800Z',
+        updated_at: "2017-06-14T23:22:55.800Z",
         active: true,
         parent_id: null,
         id: 16,
         raw_column_id: 24,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Name',
-        created_at: '2017-06-14T23:22:55.800Z',
-        base_type: 'type/Text',
+        display_name: "Name",
+        created_at: "2017-06-14T23:22:55.800Z",
+        base_type: "type/Text",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '17': {
-        description: 'This is the salted password of the user. It should not be visible',
+      "17": {
+        description:
+          "This is the salted password of the user. It should not be visible",
         table_id: 2,
         special_type: null,
-        name: 'PASSWORD',
+        name: "PASSWORD",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.802Z',
+        updated_at: "2017-06-14T23:22:55.802Z",
         active: true,
         parent_id: null,
         id: 17,
         raw_column_id: 25,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Password',
-        created_at: '2017-06-14T23:22:55.802Z',
-        base_type: 'type/Text',
+        display_name: "Password",
+        created_at: "2017-06-14T23:22:55.802Z",
+        base_type: "type/Text",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '18': {
-        description: 'The channel through which we acquired this user. Valid values include: Affiliate, Facebook, Google, Organic and Twitter',
+      "18": {
+        description:
+          "The channel through which we acquired this user. Valid values include: Affiliate, Facebook, Google, Organic and Twitter",
         table_id: 2,
-        special_type: 'type/Category',
-        name: 'SOURCE',
+        special_type: "type/Category",
+        name: "SOURCE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.667Z',
+        updated_at: "2017-06-14T23:22:57.667Z",
         active: true,
         parent_id: null,
         id: 18,
         raw_column_id: 26,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Source',
-        created_at: '2017-06-14T23:22:55.803Z',
-        base_type: 'type/Text',
+        display_name: "Source",
+        created_at: "2017-06-14T23:22:55.803Z",
+        base_type: "type/Text",
         points_of_interest: null,
         values: {
           id: 2,
-          created_at: '2017-06-14T23:22:57.668Z',
-          updated_at: '2017-06-14T23:22:57.668Z',
-          values: [
-            'Affiliate',
-            'Facebook',
-            'Google',
-            'Organic',
-            'Twitter'
-          ],
+          created_at: "2017-06-14T23:22:57.668Z",
+          updated_at: "2017-06-14T23:22:57.668Z",
+          values: ["Affiliate", "Facebook", "Google", "Organic", "Twitter"],
           human_readable_values: {},
-          field_id: 18
-        }
+          field_id: 18,
+        },
       },
-      '19': {
-        description: 'The state or province of the account’s billing address',
+      "19": {
+        description: "The state or province of the account’s billing address",
         table_id: 2,
-        special_type: 'type/State',
-        name: 'STATE',
+        special_type: "type/State",
+        name: "STATE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.805Z',
+        updated_at: "2017-06-14T23:22:55.805Z",
         active: true,
         parent_id: null,
         id: 19,
         raw_column_id: 27,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'State',
-        created_at: '2017-06-14T23:22:55.805Z',
-        base_type: 'type/Text',
+        display_name: "State",
+        created_at: "2017-06-14T23:22:55.805Z",
+        base_type: "type/Text",
         points_of_interest: null,
         values: {
           id: 3,
-          created_at: '2017-06-14T23:22:57.669Z',
-          updated_at: '2017-06-14T23:22:57.669Z',
+          created_at: "2017-06-14T23:22:57.669Z",
+          updated_at: "2017-06-14T23:22:57.669Z",
           values: [
-            'AA',
-            'AE',
-            'AK',
-            'AL',
-            'AP',
-            'AR',
-            'AS',
-            'AZ',
-            'CA',
-            'CO',
-            'CT',
-            'DC',
-            'DE',
-            'FL',
-            'FM',
-            'GA',
-            'GU',
-            'HI',
-            'IA',
-            'ID',
-            'IL',
-            'IN',
-            'KS',
-            'KY',
-            'LA',
-            'MA',
-            'MD',
-            'ME',
-            'MH',
-            'MI',
-            'MN',
-            'MO',
-            'MP',
-            'MS',
-            'MT',
-            'NC',
-            'ND',
-            'NE',
-            'NH',
-            'NJ',
-            'NM',
-            'NV',
-            'NY',
-            'OH',
-            'OK',
-            'OR',
-            'PA',
-            'PR',
-            'PW',
-            'RI',
-            'SC',
-            'SD',
-            'TN',
-            'TX',
-            'UT',
-            'VA',
-            'VI',
-            'VT',
-            'WA',
-            'WI',
-            'WV',
-            'WY'
+            "AA",
+            "AE",
+            "AK",
+            "AL",
+            "AP",
+            "AR",
+            "AS",
+            "AZ",
+            "CA",
+            "CO",
+            "CT",
+            "DC",
+            "DE",
+            "FL",
+            "FM",
+            "GA",
+            "GU",
+            "HI",
+            "IA",
+            "ID",
+            "IL",
+            "IN",
+            "KS",
+            "KY",
+            "LA",
+            "MA",
+            "MD",
+            "ME",
+            "MH",
+            "MI",
+            "MN",
+            "MO",
+            "MP",
+            "MS",
+            "MT",
+            "NC",
+            "ND",
+            "NE",
+            "NH",
+            "NJ",
+            "NM",
+            "NV",
+            "NY",
+            "OH",
+            "OK",
+            "OR",
+            "PA",
+            "PR",
+            "PW",
+            "RI",
+            "SC",
+            "SD",
+            "TN",
+            "TX",
+            "UT",
+            "VA",
+            "VI",
+            "VT",
+            "WA",
+            "WI",
+            "WV",
+            "WY",
           ],
           human_readable_values: {},
-          field_id: 19
-        }
+          field_id: 19,
+        },
       },
-      '20': {
-        description: 'The postal code of the account’s billing address',
+      "20": {
+        description: "The postal code of the account’s billing address",
         table_id: 2,
-        special_type: 'type/ZipCode',
-        name: 'ZIP',
+        special_type: "type/ZipCode",
+        name: "ZIP",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.807Z',
+        updated_at: "2017-06-14T23:22:55.807Z",
         active: true,
         parent_id: null,
         id: 20,
         raw_column_id: 28,
-        last_analyzed: '2017-06-14T23:22:57.670Z',
+        last_analyzed: "2017-06-14T23:22:57.670Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Zip',
-        created_at: '2017-06-14T23:22:55.807Z',
-        base_type: 'type/Text',
+        display_name: "Zip",
+        created_at: "2017-06-14T23:22:55.807Z",
+        base_type: "type/Text",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '21': {
-        description: 'The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget',
+      "21": {
+        description:
+          "The type of product, valid values include: Doohicky, Gadget, Gizmo and Widget",
         table_id: 3,
-        special_type: 'type/Category',
-        name: 'CATEGORY',
+        special_type: "type/Category",
+        name: "CATEGORY",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.757Z',
+        updated_at: "2017-06-14T23:22:57.757Z",
         active: true,
         parent_id: null,
         id: 21,
         raw_column_id: 1,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Category',
-        created_at: '2017-06-14T23:22:55.813Z',
-        base_type: 'type/Text',
+        display_name: "Category",
+        created_at: "2017-06-14T23:22:55.813Z",
+        base_type: "type/Text",
         points_of_interest: null,
         values: {
           id: 4,
-          created_at: '2017-06-14T23:22:57.758Z',
-          updated_at: '2017-06-14T23:22:57.758Z',
-          values: [
-            'Doohickey',
-            'Gadget',
-            'Gizmo',
-            'Widget'
-          ],
+          created_at: "2017-06-14T23:22:57.758Z",
+          updated_at: "2017-06-14T23:22:57.758Z",
+          values: ["Doohickey", "Gadget", "Gizmo", "Widget"],
           human_readable_values: {},
-          field_id: 21
-        }
+          field_id: 21,
+        },
+        has_field_values: "list",
       },
-      '22': {
-        description: 'The date the product was added to our catalog.',
+      "22": {
+        description: "The date the product was added to our catalog.",
         table_id: 3,
         special_type: null,
-        name: 'CREATED_AT',
+        name: "CREATED_AT",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.814Z',
+        updated_at: "2017-06-14T23:22:55.814Z",
         active: true,
         parent_id: null,
         id: 22,
         raw_column_id: 2,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Created At',
-        created_at: '2017-06-14T23:22:55.814Z',
-        base_type: 'type/DateTime',
+        display_name: "Created At",
+        created_at: "2017-06-14T23:22:55.814Z",
+        base_type: "type/DateTime",
         points_of_interest: null,
-        values: []
+        values: [],
       },
-      '23': {
-        description: 'The international article number. A 13 digit number uniquely identifying the product.',
+      "23": {
+        description:
+          "The international article number. A 13 digit number uniquely identifying the product.",
         table_id: 3,
-        special_type: 'type/Category',
-        name: 'EAN',
+        special_type: "type/Category",
+        name: "EAN",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.759Z',
+        updated_at: "2017-06-14T23:22:57.759Z",
         active: true,
         parent_id: null,
         id: 23,
         raw_column_id: 3,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Ean',
-        created_at: '2017-06-14T23:22:55.816Z',
-        base_type: 'type/Text',
+        display_name: "Ean",
+        created_at: "2017-06-14T23:22:55.816Z",
+        base_type: "type/Text",
         points_of_interest: null,
         values: {
           id: 5,
-          created_at: '2017-06-14T23:22:57.760Z',
-          updated_at: '2017-06-14T23:22:57.760Z',
+          created_at: "2017-06-14T23:22:57.760Z",
+          updated_at: "2017-06-14T23:22:57.760Z",
           values: [],
           human_readable_values: {},
-          field_id: 23
-        }
+          field_id: 23,
+        },
       },
-      '24': {
-        description: 'The numerical product number. Only used internally. All external communication should use the title or EAN.',
+      "24": {
+        description:
+          "The numerical product number. Only used internally. All external communication should use the title or EAN.",
         table_id: 3,
-        special_type: 'type/PK',
-        name: 'ID',
+        special_type: "type/PK",
+        name: "ID",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.817Z',
+        updated_at: "2017-06-14T23:22:55.817Z",
         active: true,
         parent_id: null,
         id: 24,
         raw_column_id: 4,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         preview_display: true,
-        display_name: 'ID',
-        created_at: '2017-06-14T23:22:55.817Z',
-        base_type: 'type/BigInteger',
+        display_name: "ID",
+        created_at: "2017-06-14T23:22:55.817Z",
+        base_type: "type/BigInteger",
         points_of_interest: null,
         target: null,
-        values: []
+        values: [],
       },
-      '25': {
-        description: 'The list price of the product. Note that this is not always the price the product sold for due to discounts, promotions, etc.',
+      "25": {
+        description:
+          "The list price of the product. Note that this is not always the price the product sold for due to discounts, promotions, etc.",
         table_id: 3,
-        special_type: 'type/Category',
-        name: 'PRICE',
+        special_type: "type/Category",
+        name: "PRICE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.762Z',
+        updated_at: "2017-06-14T23:22:57.762Z",
         active: true,
         parent_id: null,
         id: 25,
         raw_column_id: 5,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Price',
-        created_at: '2017-06-14T23:22:55.818Z',
-        base_type: 'type/Float',
+        display_name: "Price",
+        created_at: "2017-06-14T23:22:55.818Z",
+        base_type: "type/Float",
         points_of_interest: null,
         values: {
           id: 6,
-          created_at: '2017-06-14T23:22:57.764Z',
-          updated_at: '2017-06-14T23:22:57.764Z',
+          created_at: "2017-06-14T23:22:57.764Z",
+          updated_at: "2017-06-14T23:22:57.764Z",
           values: [],
           human_readable_values: {},
-          field_id: 25
-        }
+          field_id: 25,
+        },
       },
-      '26': {
-        description: 'The average rating users have given the product. This ranges from 1 - 5',
+      "26": {
+        description:
+          "The average rating users have given the product. This ranges from 1 - 5",
         table_id: 3,
-        special_type: 'type/Category',
-        name: 'RATING',
+        special_type: "type/Category",
+        name: "RATING",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.765Z',
+        updated_at: "2017-06-14T23:22:57.765Z",
         active: true,
         parent_id: null,
         id: 26,
         raw_column_id: 6,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Rating',
-        created_at: '2017-06-14T23:22:55.820Z',
-        base_type: 'type/Float',
+        display_name: "Rating",
+        created_at: "2017-06-14T23:22:55.820Z",
+        base_type: "type/Float",
         points_of_interest: null,
         values: {
           id: 7,
-          created_at: '2017-06-14T23:22:57.765Z',
-          updated_at: '2017-06-14T23:22:57.765Z',
+          created_at: "2017-06-14T23:22:57.765Z",
+          updated_at: "2017-06-14T23:22:57.765Z",
           values: [
             0,
             1,
@@ -1171,422 +1108,422 @@ export const state = {
             4.5,
             4.6,
             4.7,
-            5
+            5,
           ],
           human_readable_values: {},
-          field_id: 26
-        }
+          field_id: 26,
+        },
       },
-      '27': {
-        description: 'The name of the product as it should be displayed to customers.',
+      "27": {
+        description:
+          "The name of the product as it should be displayed to customers.",
         table_id: 3,
-        special_type: 'type/Category',
-        name: 'TITLE',
+        special_type: "type/Category",
+        name: "TITLE",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.766Z',
+        updated_at: "2017-06-14T23:22:57.766Z",
         active: true,
         parent_id: null,
         id: 27,
         raw_column_id: 7,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Title',
-        created_at: '2017-06-14T23:22:55.821Z',
-        base_type: 'type/Text',
+        display_name: "Title",
+        created_at: "2017-06-14T23:22:55.821Z",
+        base_type: "type/Text",
         points_of_interest: null,
         values: {
           id: 8,
-          created_at: '2017-06-14T23:22:57.767Z',
-          updated_at: '2017-06-14T23:22:57.767Z',
+          created_at: "2017-06-14T23:22:57.767Z",
+          updated_at: "2017-06-14T23:22:57.767Z",
           values: [],
           human_readable_values: {},
-          field_id: 27
-        }
+          field_id: 27,
+        },
       },
-      '28': {
-        description: 'The source of the product.',
+      "28": {
+        description: "The source of the product.",
         table_id: 3,
-        special_type: 'type/Category',
-        name: 'VENDOR',
+        special_type: "type/Category",
+        name: "VENDOR",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:57.768Z',
+        updated_at: "2017-06-14T23:22:57.768Z",
         active: true,
         parent_id: null,
         id: 28,
         raw_column_id: 8,
-        last_analyzed: '2017-06-14T23:22:57.771Z',
+        last_analyzed: "2017-06-14T23:22:57.771Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Vendor',
-        created_at: '2017-06-14T23:22:55.823Z',
-        base_type: 'type/Text',
+        display_name: "Vendor",
+        created_at: "2017-06-14T23:22:55.823Z",
+        base_type: "type/Text",
         points_of_interest: null,
         values: {
           id: 9,
-          created_at: '2017-06-14T23:22:57.769Z',
-          updated_at: '2017-06-14T23:22:57.769Z',
+          created_at: "2017-06-14T23:22:57.769Z",
+          updated_at: "2017-06-14T23:22:57.769Z",
           values: [],
           human_readable_values: {},
-          field_id: 28
-        }
+          field_id: 28,
+        },
       },
-      '29': {
-        description: 'The review the user left. Limited to 2000 characters.',
+      "29": {
+        description: "The review the user left. Limited to 2000 characters.",
         table_id: 4,
-        special_type: 'type/Description',
-        name: 'BODY',
+        special_type: "type/Description",
+        name: "BODY",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.828Z',
+        updated_at: "2017-06-14T23:22:55.828Z",
         active: true,
         parent_id: null,
         id: 29,
         values: [],
         raw_column_id: 31,
-        last_analyzed: '2017-06-14T23:22:58.030Z',
+        last_analyzed: "2017-06-14T23:22:58.030Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Body',
-        created_at: '2017-06-14T23:22:55.828Z',
-        base_type: 'type/Text',
-        points_of_interest: null
+        display_name: "Body",
+        created_at: "2017-06-14T23:22:55.828Z",
+        base_type: "type/Text",
+        points_of_interest: null,
       },
-      '30': {
-        description: 'The day and time a review was written by a user.',
+      "30": {
+        description: "The day and time a review was written by a user.",
         table_id: 4,
         special_type: null,
-        name: 'CREATED_AT',
+        name: "CREATED_AT",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.830Z',
+        updated_at: "2017-06-14T23:22:55.830Z",
         active: true,
         parent_id: null,
         id: 30,
         values: [],
         raw_column_id: 32,
-        last_analyzed: '2017-06-14T23:22:58.030Z',
+        last_analyzed: "2017-06-14T23:22:58.030Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Created At',
-        created_at: '2017-06-14T23:22:55.830Z',
-        base_type: 'type/DateTime',
-        points_of_interest: null
+        display_name: "Created At",
+        created_at: "2017-06-14T23:22:55.830Z",
+        base_type: "type/DateTime",
+        points_of_interest: null,
       },
-      '31': {
-        description: 'A unique internal identifier for the review. Should not be used externally.',
+      "31": {
+        description:
+          "A unique internal identifier for the review. Should not be used externally.",
         table_id: 4,
-        special_type: 'type/PK',
-        name: 'ID',
+        special_type: "type/PK",
+        name: "ID",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.831Z',
+        updated_at: "2017-06-14T23:22:55.831Z",
         active: true,
         parent_id: null,
         id: 31,
         values: [],
         raw_column_id: 33,
-        last_analyzed: '2017-06-14T23:22:58.030Z',
+        last_analyzed: "2017-06-14T23:22:58.030Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'ID',
-        created_at: '2017-06-14T23:22:55.831Z',
-        base_type: 'type/BigInteger',
-        points_of_interest: null
+        display_name: "ID",
+        created_at: "2017-06-14T23:22:55.831Z",
+        base_type: "type/BigInteger",
+        points_of_interest: null,
       },
-      '32': {
-        description: 'The product the review was for',
+      "32": {
+        description: "The product the review was for",
         table_id: 4,
-        special_type: 'type/FK',
-        name: 'PRODUCT_ID',
+        special_type: "type/FK",
+        name: "PRODUCT_ID",
         caveats: null,
         fk_target_field_id: 24,
-        updated_at: '2017-06-14T23:22:55.840Z',
+        updated_at: "2017-06-14T23:22:55.840Z",
         active: true,
         parent_id: null,
         id: 32,
         values: [],
         raw_column_id: 34,
-        last_analyzed: '2017-06-14T23:22:58.030Z',
+        last_analyzed: "2017-06-14T23:22:58.030Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: 24,
         preview_display: true,
-        display_name: 'Product ID',
-        created_at: '2017-06-14T23:22:55.832Z',
-        base_type: 'type/Integer',
-        points_of_interest: null
+        display_name: "Product ID",
+        created_at: "2017-06-14T23:22:55.832Z",
+        base_type: "type/Integer",
+        points_of_interest: null,
       },
-      '33': {
-        description: 'The rating (on a scale of 1-5) the user left.',
+      "33": {
+        description: "The rating (on a scale of 1-5) the user left.",
         table_id: 4,
-        special_type: 'type/Category',
-        name: 'RATING',
+        special_type: "type/Category",
+        name: "RATING",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.834Z',
+        updated_at: "2017-06-14T23:22:55.834Z",
         active: true,
         parent_id: null,
         id: 33,
         values: {
           id: 10,
-          created_at: '2017-06-14T23:22:58.028Z',
-          updated_at: '2017-06-14T23:22:58.028Z',
-          values: [
-            1,
-            2,
-            3,
-            4,
-            5
-          ],
+          created_at: "2017-06-14T23:22:58.028Z",
+          updated_at: "2017-06-14T23:22:58.028Z",
+          values: [1, 2, 3, 4, 5],
           human_readable_values: {},
-          field_id: 33
+          field_id: 33,
         },
         raw_column_id: 35,
-        last_analyzed: '2017-06-14T23:22:58.030Z',
+        last_analyzed: "2017-06-14T23:22:58.030Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Rating',
-        created_at: '2017-06-14T23:22:55.834Z',
-        base_type: 'type/Integer',
-        points_of_interest: null
+        display_name: "Rating",
+        created_at: "2017-06-14T23:22:55.834Z",
+        base_type: "type/Integer",
+        points_of_interest: null,
       },
-      '34': {
-        description: 'The user who left the review',
+      "34": {
+        description: "The user who left the review",
         table_id: 4,
         special_type: null,
-        name: 'REVIEWER',
+        name: "REVIEWER",
         caveats: null,
         fk_target_field_id: null,
-        updated_at: '2017-06-14T23:22:55.835Z',
+        updated_at: "2017-06-14T23:22:55.835Z",
         active: true,
         parent_id: null,
         id: 34,
         values: [],
         raw_column_id: 36,
-        last_analyzed: '2017-06-14T23:22:58.030Z',
+        last_analyzed: "2017-06-14T23:22:58.030Z",
         position: 0,
-        visibility_type: 'normal',
+        visibility_type: "normal",
         target: null,
         preview_display: true,
-        display_name: 'Reviewer',
-        created_at: '2017-06-14T23:22:55.835Z',
-        base_type: 'type/Text',
-        points_of_interest: null
-      }
+        display_name: "Reviewer",
+        created_at: "2017-06-14T23:22:55.835Z",
+        base_type: "type/Text",
+        points_of_interest: null,
+      },
     },
     revisions: {},
-    databasesList: [
-      1
-    ]
+    databasesList: [1],
   },
-}
+};
 
 export const metadata = getMetadata(state);
 
 export const card = {
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "query",
-        database: DATABASE_ID,
-        query: {
-            source_table: ORDERS_TABLE_ID
-        }
-    }
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "query",
+    database: DATABASE_ID,
+    query: {
+      source_table: ORDERS_TABLE_ID,
+    },
+  },
 };
 
 export const product_card = {
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "query",
-        database: DATABASE_ID,
-        query: {
-            source_table: PRODUCT_TABLE_ID
-        }
-    }
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "query",
+    database: DATABASE_ID,
+    query: {
+      source_table: PRODUCT_TABLE_ID,
+    },
+  },
 };
 
 export const orders_raw_card = {
-    id: 1,
-    name: "Raw orders data",
-    display: 'table',
-    visualization_settings: {},
-    can_write: true,
-    dataset_query: {
-        type: "query",
-        database: DATABASE_ID,
-        query: {
-            source_table: ORDERS_TABLE_ID
-        }
-    }
+  id: 1,
+  name: "Raw orders data",
+  display: "table",
+  visualization_settings: {},
+  can_write: true,
+  dataset_query: {
+    type: "query",
+    database: DATABASE_ID,
+    query: {
+      source_table: ORDERS_TABLE_ID,
+    },
+  },
 };
 
 export const orders_count_card = {
-    id: 2,
-    name: "# orders data",
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "query",
-        database: DATABASE_ID,
-        query: {
-            aggregation: [["count"]],
-            source_table: ORDERS_TABLE_ID
-        }
-    }
+  id: 2,
+  name: "# orders data",
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "query",
+    database: DATABASE_ID,
+    query: {
+      aggregation: [["count"]],
+      source_table: ORDERS_TABLE_ID,
+    },
+  },
 };
 
 export const native_orders_count_card = {
-    id: 3,
-    name: "# orders data",
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "native",
-        database: DATABASE_ID,
-        native: {
-            query: "SELECT count(*) FROM orders"
-        }
-    }
+  id: 3,
+  name: "# orders data",
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "native",
+    database: DATABASE_ID,
+    native: {
+      query: "SELECT count(*) FROM orders",
+    },
+  },
 };
 
 export const unsaved_native_orders_count_card = {
-    name: "# orders data",
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "native",
-        database: DATABASE_ID,
-        native: {
-            query: "SELECT count(*) FROM orders"
-        }
-    }
+  name: "# orders data",
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "native",
+    database: DATABASE_ID,
+    native: {
+      query: "SELECT count(*) FROM orders",
+    },
+  },
 };
 
 export const invalid_orders_count_card = {
-    id: 2,
-    name: "# orders data",
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "nosuchqueryprocessor",
-        database: DATABASE_ID,
-        query: {
-            query: "SELECT count(*) FROM orders"
-        }
-    }
+  id: 2,
+  name: "# orders data",
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "nosuchqueryprocessor",
+    database: DATABASE_ID,
+    query: {
+      query: "SELECT count(*) FROM orders",
+    },
+  },
 };
 
 export const orders_count_by_id_card = {
-    id: 2,
-    name: "# orders data",
-    can_write: false,
-    display: 'table',
-    visualization_settings: {},
-    dataset_query: {
-        type: "query",
-        database: DATABASE_ID,
-        query: {
-            aggregation: [["count"]],
-            source_table: ORDERS_TABLE_ID,
-            breakout: [["field-id", ORDERS_PK_FIELD_ID]]
-        }
-    }
+  id: 2,
+  name: "# orders data",
+  can_write: false,
+  display: "table",
+  visualization_settings: {},
+  dataset_query: {
+    type: "query",
+    database: DATABASE_ID,
+    query: {
+      aggregation: [["count"]],
+      source_table: ORDERS_TABLE_ID,
+      breakout: [["field-id", ORDERS_PK_FIELD_ID]],
+    },
+  },
 };
 
 export const clickedFloatHeader = {
-    column: {
-        ...metadata.fields[ORDERS_TOTAL_FIELD_ID],
-        source: "fields"
-    }
+  column: {
+    ...metadata.fields[ORDERS_TOTAL_FIELD_ID],
+    source: "fields",
+  },
 };
 
 export const clickedCategoryHeader = {
-    column: {
-        ...metadata.fields[PRODUCT_CATEGORY_FIELD_ID],
-        source: "fields"
-    }
+  column: {
+    ...metadata.fields[PRODUCT_CATEGORY_FIELD_ID],
+    source: "fields",
+  },
 };
 
 export const clickedFloatValue = {
-    column: {
-        ...metadata.fields[ORDERS_TOTAL_FIELD_ID],
-        source: "fields"
-    },
-    value: 1234
+  column: {
+    ...metadata.fields[ORDERS_TOTAL_FIELD_ID],
+    source: "fields",
+  },
+  value: 1234,
 };
 
 export const clickedPKValue = {
-    column: {
-        ...metadata.fields[ORDERS_PK_FIELD_ID],
-        source: "fields"
-    },
-    value: 42
+  column: {
+    ...metadata.fields[ORDERS_PK_FIELD_ID],
+    source: "fields",
+  },
+  value: 42,
 };
 
 export const clickedFKValue = {
-    column: {
-        ...metadata.fields[ORDERS_PRODUCT_FK_FIELD_ID],
-        source: "fields"
-    },
-    value: 43
+  column: {
+    ...metadata.fields[ORDERS_PRODUCT_FK_FIELD_ID],
+    source: "fields",
+  },
+  value: 43,
 };
 
 export const tableMetadata = metadata.tables[ORDERS_TABLE_ID];
 
 export function makeQuestion(fn = (card, state) => ({ card, state })) {
-    const result = fn(card, state);
-    return new Question(getMetadata(result.state), result.card);
+  const result = fn(card, state);
+  return new Question(getMetadata(result.state), result.card);
 }
 
 export const question = new Question(metadata, card);
-export const unsavedOrderCountQuestion = new Question(metadata, _.omit(orders_count_card, 'id'));
+export const unsavedOrderCountQuestion = new Question(
+  metadata,
+  _.omit(orders_count_card, "id"),
+);
 export const productQuestion = new Question(metadata, product_card);
-const NoFieldsMetadata = getMetadata(assocIn(state, ["metadata", "tables", ORDERS_TABLE_ID, "fields"], []))
+const NoFieldsMetadata = getMetadata(
+  assocIn(state, ["metadata", "tables", ORDERS_TABLE_ID, "fields"], []),
+);
 export const questionNoFields = new Question(NoFieldsMetadata, card);
 
 export const orders_past_300_days_segment = {
-    "id": null,
-    "name": "Past 300 days",
-    "description": "Past 300 days created at",
-    "table_id": 1,
-    "definition": {
-        "source_table": 1,
-        "filter": ["time-interval", ["field-id", 1], -300, "day"]
-    }
+  id: null,
+  name: "Past 300 days",
+  description: "Past 300 days created at",
+  table_id: 1,
+  definition: {
+    source_table: 1,
+    filter: ["time-interval", ["field-id", 1], -300, "day"],
+  },
 };
 
 export const vendor_count_metric = {
-    "id": null,
-    "name": "Vendor count",
-    "description": "Tells how many vendors we have",
-    "table_id": 3,
-    "definition": {
-        "aggregation": [
-            [
-                "distinct",
-                [
-                    "field-id",
-                    28
-                ]
-            ]
-        ],
-        "source_table": 3
-    }
+  id: null,
+  name: "Vendor count",
+  description: "Tells how many vendors we have",
+  table_id: 3,
+  definition: {
+    aggregation: [["distinct", ["field-id", 28]]],
+    source_table: 3,
+  },
 };
+
+const nopMetadataReducer = (s = state.metadata, a) => s;
+
+// simple provider which only supports static metadata defined above, no actions will take effect
+export const StaticMetadataProvider = ({ children }) => (
+  <Provider store={getStore({ metadata: nopMetadataReducer }, null, state)}>
+    {children}
+  </Provider>
+);
diff --git a/frontend/test/admin/databases/DatabaseEditApp.integ.spec.js b/frontend/test/admin/databases/DatabaseEditApp.integ.spec.js
index 683a5007b03f47ef0dc38a6bb39383220a03de65..c451d0a729551023461e1e7352a96852cd192dd4 100644
--- a/frontend/test/admin/databases/DatabaseEditApp.integ.spec.js
+++ b/frontend/test/admin/databases/DatabaseEditApp.integ.spec.js
@@ -1,21 +1,26 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 
 import React from "react";
 import { mount } from "enzyme";
 import {
-    INITIALIZE_DATABASE,
-    RESCAN_DATABASE_FIELDS,
-    SYNC_DATABASE_SCHEMA,
-    DISCARD_SAVED_FIELD_VALUES,
-    UPDATE_DATABASE,
-    MIGRATE_TO_NEW_SCHEDULING_SETTINGS, DEFAULT_SCHEDULES
+  INITIALIZE_DATABASE,
+  RESCAN_DATABASE_FIELDS,
+  SYNC_DATABASE_SCHEMA,
+  DISCARD_SAVED_FIELD_VALUES,
+  UPDATE_DATABASE,
+  MIGRATE_TO_NEW_SCHEDULING_SETTINGS,
+  DEFAULT_SCHEDULES,
 } from "metabase/admin/databases/database";
-import DatabaseEditApp, { Tab } from "metabase/admin/databases/containers/DatabaseEditApp";
+import DatabaseEditApp, {
+  Tab,
+} from "metabase/admin/databases/containers/DatabaseEditApp";
 import DatabaseEditForms from "metabase/admin/databases/components/DatabaseEditForms";
-import DatabaseSchedulingForm, { SyncOption } from "metabase/admin/databases/components/DatabaseSchedulingForm";
+import DatabaseSchedulingForm, {
+  SyncOption,
+} from "metabase/admin/databases/components/DatabaseSchedulingForm";
 import FormField from "metabase/components/form/FormField";
 import Toggle from "metabase/components/Toggle";
 import { TestModal } from "metabase/components/Modal";
@@ -30,289 +35,327 @@ import _ from "underscore";
 
 // Currently a lot of duplication with SegmentPane tests
 describe("DatabaseEditApp", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("Connection tab", () => {
+    it("shows the connection settings for sample dataset correctly", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      const editForm = dbEditApp.find(DatabaseEditForms);
+      expect(editForm.length).toBe(1);
+      expect(editForm.find("select").props().defaultValue).toBe("h2");
+      expect(editForm.find('input[name="name"]').props().value).toBe(
+        "Sample Dataset",
+      );
+      expect(editForm.find('input[name="db"]').props().value).toEqual(
+        expect.stringContaining("sample-dataset.db;USER=GUEST;PASSWORD=guest"),
+      );
+    });
+
+    it("lets you modify the connection settings", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      const editForm = dbEditApp.find(DatabaseEditForms);
+      const letUserControlSchedulingField = editForm
+        .find(FormField)
+        .filterWhere(
+          f => f.props().fieldName === "let-user-control-scheduling",
+        );
+      expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(
+        false,
+      );
+      click(letUserControlSchedulingField.find(Toggle));
+
+      // Connection and Scheduling tabs shouldn't be visible yet
+      expect(dbEditApp.find(Tab).length).toBe(0);
+
+      clickButton(editForm.find('button[children="Save"]'));
+
+      await store.waitForActions([UPDATE_DATABASE]);
+
+      // Tabs should be now visible as user-controlled scheduling is enabled
+      expect(dbEditApp.find(Tab).length).toBe(2);
+    });
+
+    // NOTE Atte Keinänen 8/17/17: See migrateDatabaseToNewSchedulingSettings for more information about migration process
+    it("shows the analysis toggle correctly for non-migrated analysis settings when `is_full_sync` is true", async () => {
+      // Set is_full_sync to false here inline and remove the let-user-control-scheduling setting
+      const database = await MetabaseApi.db_get({ dbId: 1 });
+      await MetabaseApi.db_update({
+        ...database,
+        is_full_sync: true,
+        details: _.omit(database.details, "let-user-control-scheduling"),
+      });
+
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([
+        INITIALIZE_DATABASE,
+        MIGRATE_TO_NEW_SCHEDULING_SETTINGS,
+      ]);
+
+      const editForm = dbEditApp.find(DatabaseEditForms);
+      expect(editForm.length).toBe(1);
+      expect(editForm.find("select").props().defaultValue).toBe("h2");
+      expect(editForm.find('input[name="name"]').props().value).toBe(
+        "Sample Dataset",
+      );
+      expect(editForm.find('input[name="db"]').props().value).toEqual(
+        expect.stringContaining("sample-dataset.db;USER=GUEST;PASSWORD=guest"),
+      );
+
+      const letUserControlSchedulingField = editForm
+        .find(FormField)
+        .filterWhere(
+          f => f.props().fieldName === "let-user-control-scheduling",
+        );
+      expect(letUserControlSchedulingField.length).toBe(1);
+      expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(
+        false,
+      );
+      expect(dbEditApp.find(Tab).length).toBe(0);
+    });
+
+    it("shows the analysis toggle correctly for non-migrated analysis settings when `is_full_sync` is false", async () => {
+      // Set is_full_sync to true here inline and remove the let-user-control-scheduling setting
+      const database = await MetabaseApi.db_get({ dbId: 1 });
+      await MetabaseApi.db_update({
+        ...database,
+        is_full_sync: false,
+        details: _.omit(database.details, "let-user-control-scheduling"),
+      });
+
+      // Start the actual interaction test
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([
+        INITIALIZE_DATABASE,
+        MIGRATE_TO_NEW_SCHEDULING_SETTINGS,
+      ]);
+
+      const editForm = dbEditApp.find(DatabaseEditForms);
+      const letUserControlSchedulingField = editForm
+        .find(FormField)
+        .filterWhere(
+          f => f.props().fieldName === "let-user-control-scheduling",
+        );
+      expect(letUserControlSchedulingField.length).toBe(1);
+      expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(
+        true,
+      );
+      expect(dbEditApp.find(Tab).length).toBe(2);
+    });
+
+    afterAll(async () => {
+      // revert all changes that have been made
+      // use a direct API call for the sake of simplicity / reliability
+      const database = await MetabaseApi.db_get({ dbId: 1 });
+      await MetabaseApi.db_update({
+        ...database,
+        is_full_sync: true,
+        details: {
+          ...database.details,
+          "let-user-control-scheduling": false,
+        },
+      });
+    });
+  });
+
+  describe("Scheduling tab", () => {
     beforeAll(async () => {
-        useSharedAdminLogin();
-    })
-
-    describe("Connection tab", () => {
-        it("shows the connection settings for sample dataset correctly", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp/>));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            const editForm = dbEditApp.find(DatabaseEditForms)
-            expect(editForm.length).toBe(1)
-            expect(editForm.find("select").props().defaultValue).toBe("h2")
-            expect(editForm.find('input[name="name"]').props().value).toBe("Sample Dataset")
-            expect(editForm.find('input[name="db"]').props().value).toEqual(
-                expect.stringContaining("sample-dataset.db;USER=GUEST;PASSWORD=guest")
-            )
-        });
-
-        it("lets you modify the connection settings", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            const editForm = dbEditApp.find(DatabaseEditForms)
-            const letUserControlSchedulingField =
-                editForm.find(FormField).filterWhere((f) => f.props().fieldName === "let-user-control-scheduling");
-            expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(false);
-            click(letUserControlSchedulingField.find(Toggle))
-
-            // Connection and Scheduling tabs shouldn't be visible yet
-            expect(dbEditApp.find(Tab).length).toBe(0)
-
-            clickButton(editForm.find('button[children="Save"]'));
-
-            await store.waitForActions([UPDATE_DATABASE])
-
-            // Tabs should be now visible as user-controlled scheduling is enabled
-            expect(dbEditApp.find(Tab).length).toBe(2)
-        });
-
-        // NOTE Atte Keinänen 8/17/17: See migrateDatabaseToNewSchedulingSettings for more information about migration process
-        it("shows the analysis toggle correctly for non-migrated analysis settings when `is_full_sync` is true", async () => {
-            // Set is_full_sync to false here inline and remove the let-user-control-scheduling setting
-            const database = await MetabaseApi.db_get({"dbId": 1})
-            await MetabaseApi.db_update({
-                ...database,
-                is_full_sync: true,
-                details: _.omit(database.details, "let-user-control-scheduling")
-            });
-
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp/>));
-            await store.waitForActions([INITIALIZE_DATABASE, MIGRATE_TO_NEW_SCHEDULING_SETTINGS])
-
-            const editForm = dbEditApp.find(DatabaseEditForms)
-            expect(editForm.length).toBe(1)
-            expect(editForm.find("select").props().defaultValue).toBe("h2")
-            expect(editForm.find('input[name="name"]').props().value).toBe("Sample Dataset")
-            expect(editForm.find('input[name="db"]').props().value).toEqual(
-                expect.stringContaining("sample-dataset.db;USER=GUEST;PASSWORD=guest")
-            )
-
-            const letUserControlSchedulingField =
-                editForm.find(FormField).filterWhere((f) => f.props().fieldName === "let-user-control-scheduling");
-            expect(letUserControlSchedulingField.length).toBe(1);
-            expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(false);
-            expect(dbEditApp.find(Tab).length).toBe(0)
-        });
-
-        it("shows the analysis toggle correctly for non-migrated analysis settings when `is_full_sync` is false", async () => {
-            // Set is_full_sync to true here inline and remove the let-user-control-scheduling setting
-            const database = await MetabaseApi.db_get({"dbId": 1})
-            await MetabaseApi.db_update({
-                ...database,
-                is_full_sync: false,
-                details: _.omit(database.details, "let-user-control-scheduling")
-            });
-
-            // Start the actual interaction test
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp/>));
-            await store.waitForActions([INITIALIZE_DATABASE, MIGRATE_TO_NEW_SCHEDULING_SETTINGS])
-
-            const editForm = dbEditApp.find(DatabaseEditForms)
-            const letUserControlSchedulingField =
-                editForm.find(FormField).filterWhere((f) => f.props().fieldName === "let-user-control-scheduling");
-            expect(letUserControlSchedulingField.length).toBe(1);
-            expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(true);
-            expect(dbEditApp.find(Tab).length).toBe(2)
-        })
-
-        afterAll(async () => {
-            // revert all changes that have been made
-            // use a direct API call for the sake of simplicity / reliability
-            const database = await MetabaseApi.db_get({"dbId": 1})
-            await MetabaseApi.db_update({
-                ...database,
-                is_full_sync: true,
-                details: {
-                    ...database.details,
-                    "let-user-control-scheduling": false
-                }
-            });
-        })
-    })
-
-    describe("Scheduling tab", () => {
-        beforeAll(async () => {
-            // Enable the user-controlled scheduling for these tests
-            const database = await MetabaseApi.db_get({"dbId": 1})
-            await MetabaseApi.db_update({
-                ...database,
-                details: {
-                    ...database.details,
-                    "let-user-control-scheduling": true
-                }
-            });
-        })
-
-        it("shows the initial scheduling settings correctly", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            const editForm = dbEditApp.find(DatabaseEditForms)
-            expect(editForm.length).toBe(1)
-            click(dbEditApp.find(Tab).last());
-
-            const schedulingForm = dbEditApp.find(DatabaseSchedulingForm)
-            expect(schedulingForm.length).toBe(1)
-
-            expect(schedulingForm.find(Select).first().text()).toEqual("Hourly");
-
-            const syncOptions = schedulingForm.find(SyncOption);
-            const syncOptionOften = syncOptions.first();
-
-            expect(syncOptionOften.props().name).toEqual("Regularly, on a schedule");
-            expect(syncOptionOften.props().selected).toEqual(true);
-        });
-
-        it("lets you change the db sync period", async () => {
-            const store = await createTestStore()
-
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            click(dbEditApp.find(Tab).last());
-            const schedulingForm = dbEditApp.find(DatabaseSchedulingForm)
-            const dbSyncSelect = schedulingForm.find(Select).first()
-            click(dbSyncSelect)
-
-            const dailyOption = schedulingForm.find(ColumnarSelector).find("li").at(1).children();
-            expect(dailyOption.text()).toEqual("Daily")
-            click(dailyOption);
-
-            expect(dbSyncSelect.text()).toEqual("Daily");
-
-            clickButton(schedulingForm.find('button[children="Save changes"]'));
-
-            await store.waitForActions([UPDATE_DATABASE])
-        });
-
-        it("lets you change the table change frequency to Never", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            click(dbEditApp.find(Tab).last())
-            const schedulingForm = dbEditApp.find(DatabaseSchedulingForm)
-            const dbSyncSelect = schedulingForm.find(Select).first()
-            click(dbSyncSelect)
-
-            const syncOptions = schedulingForm.find(SyncOption);
-            const syncOptionsNever = syncOptions.at(1);
-
-            expect(syncOptionsNever.props().selected).toEqual(false);
-            click(syncOptionsNever)
-            expect(syncOptionsNever.props().selected).toEqual(true);
-
-            clickButton(schedulingForm.find('button[children="Save changes"]'));
-            await store.waitForActions([UPDATE_DATABASE])
-
-        });
-
-        it("shows the modified scheduling settings correctly", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            click(dbEditApp.find(Tab).last())
-            const schedulingForm = dbEditApp.find(DatabaseSchedulingForm)
-            expect(schedulingForm.length).toBe(1)
-
-            expect(schedulingForm.find(Select).first().text()).toEqual("Daily");
-
-            const syncOptions = schedulingForm.find(SyncOption);
-            const syncOptionOften = syncOptions.first();
-            const syncOptionNever = syncOptions.at(1);
-            expect(syncOptionOften.props().selected).toEqual(false);
-            expect(syncOptionNever.props().selected).toEqual(true);
-        })
-
-        afterAll(async () => {
-            // revert all changes that have been made
-            const database = await MetabaseApi.db_get({"dbId": 1})
-            await MetabaseApi.db_update({
-                ...database,
-                is_full_sync: true,
-                schedules: DEFAULT_SCHEDULES,
-                details: {
-                    ...database.details,
-                    "let-user-control-scheduling": false
-                }
-            });
-        })
-    })
-
-    describe("Actions sidebar", () => {
-        it("lets you trigger the manual database schema sync", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            clickButton(dbEditApp.find(".Button--syncDbSchema"))
-            await store.waitForActions([SYNC_DATABASE_SCHEMA])
-            // TODO: do we have any way to see that the sync is actually in progress in the backend?
-        });
-
-        it("lets you trigger the manual rescan of field values", async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            clickButton(dbEditApp.find(".Button--rescanFieldValues"))
-            await store.waitForActions([RESCAN_DATABASE_FIELDS])
-            // TODO: do we have any way to see that the field rescanning is actually in progress in the backend?
-        });
-
-        // TODO Atte Keinänen 8/15/17: Does losing field values potentially cause test failures in other test suites?
-        it("lets you discard saved field values", async () => {
-            // To be safe, let's mock the API method
-            MetabaseApi.db_discard_values = jest.fn();
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-            const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            await store.waitForActions([INITIALIZE_DATABASE])
-
-            click(dbEditApp.find(".Button--discardSavedFieldValues"))
-            clickButton(dbEditApp.find(TestModal).find(".Button--danger"))
-            await store.waitForActions([DISCARD_SAVED_FIELD_VALUES])
-
-            expect(MetabaseApi.db_discard_values.mock.calls.length).toBe(1);
-        })
-
-        // Disabled because removal&recovery causes the db id to change
-        it("lets you remove the dataset", () => {
-            pending();
-
-            // const store = await createTestStore()
-            // store.pushPath("/admin/databases/1");
-            // const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
-            // await store.waitForActions([INITIALIZE_DATABASE])
-            //
-            // try {
-            //     click(dbEditApp.find(".Button--deleteDatabase"))
-            //     console.log(dbEditApp.debug());
-            //     await store.waitForActions([DELETE_DATABASE])
-            //     await store.dispatch(addSampleDataset())
-            // } catch(e) {
-            //     throw e;
-            // } finally {
-            // }
-        });
-    })
+      // Enable the user-controlled scheduling for these tests
+      const database = await MetabaseApi.db_get({ dbId: 1 });
+      await MetabaseApi.db_update({
+        ...database,
+        details: {
+          ...database.details,
+          "let-user-control-scheduling": true,
+        },
+      });
+    });
+
+    it("shows the initial scheduling settings correctly", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      const editForm = dbEditApp.find(DatabaseEditForms);
+      expect(editForm.length).toBe(1);
+      click(dbEditApp.find(Tab).last());
+
+      const schedulingForm = dbEditApp.find(DatabaseSchedulingForm);
+      expect(schedulingForm.length).toBe(1);
+
+      expect(
+        schedulingForm
+          .find(Select)
+          .first()
+          .text(),
+      ).toEqual("Hourly");
+
+      const syncOptions = schedulingForm.find(SyncOption);
+      const syncOptionOften = syncOptions.first();
+
+      expect(syncOptionOften.props().name).toEqual("Regularly, on a schedule");
+      expect(syncOptionOften.props().selected).toEqual(true);
+    });
+
+    it("lets you change the db sync period", async () => {
+      const store = await createTestStore();
+
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      click(dbEditApp.find(Tab).last());
+      const schedulingForm = dbEditApp.find(DatabaseSchedulingForm);
+      const dbSyncSelect = schedulingForm.find(Select).first();
+      click(dbSyncSelect);
+
+      const dailyOption = schedulingForm
+        .find(ColumnarSelector)
+        .find("li")
+        .at(1)
+        .children();
+      expect(dailyOption.text()).toEqual("Daily");
+      click(dailyOption);
+
+      expect(dbSyncSelect.text()).toEqual("Daily");
+
+      clickButton(schedulingForm.find('button[children="Save changes"]'));
+
+      await store.waitForActions([UPDATE_DATABASE]);
+    });
+
+    it("lets you change the table change frequency to Never", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      click(dbEditApp.find(Tab).last());
+      const schedulingForm = dbEditApp.find(DatabaseSchedulingForm);
+      const dbSyncSelect = schedulingForm.find(Select).first();
+      click(dbSyncSelect);
+
+      const syncOptions = schedulingForm.find(SyncOption);
+      const syncOptionsNever = syncOptions.at(1);
+
+      expect(syncOptionsNever.props().selected).toEqual(false);
+      click(syncOptionsNever);
+      expect(syncOptionsNever.props().selected).toEqual(true);
+
+      clickButton(schedulingForm.find('button[children="Save changes"]'));
+      await store.waitForActions([UPDATE_DATABASE]);
+    });
+
+    it("shows the modified scheduling settings correctly", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      click(dbEditApp.find(Tab).last());
+      const schedulingForm = dbEditApp.find(DatabaseSchedulingForm);
+      expect(schedulingForm.length).toBe(1);
+
+      expect(
+        schedulingForm
+          .find(Select)
+          .first()
+          .text(),
+      ).toEqual("Daily");
+
+      const syncOptions = schedulingForm.find(SyncOption);
+      const syncOptionOften = syncOptions.first();
+      const syncOptionNever = syncOptions.at(1);
+      expect(syncOptionOften.props().selected).toEqual(false);
+      expect(syncOptionNever.props().selected).toEqual(true);
+    });
+
+    afterAll(async () => {
+      // revert all changes that have been made
+      const database = await MetabaseApi.db_get({ dbId: 1 });
+      await MetabaseApi.db_update({
+        ...database,
+        is_full_sync: true,
+        schedules: DEFAULT_SCHEDULES,
+        details: {
+          ...database.details,
+          "let-user-control-scheduling": false,
+        },
+      });
+    });
+  });
+
+  describe("Actions sidebar", () => {
+    it("lets you trigger the manual database schema sync", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      clickButton(dbEditApp.find(".Button--syncDbSchema"));
+      await store.waitForActions([SYNC_DATABASE_SCHEMA]);
+      // TODO: do we have any way to see that the sync is actually in progress in the backend?
+    });
+
+    it("lets you trigger the manual rescan of field values", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      clickButton(dbEditApp.find(".Button--rescanFieldValues"));
+      await store.waitForActions([RESCAN_DATABASE_FIELDS]);
+      // TODO: do we have any way to see that the field rescanning is actually in progress in the backend?
+    });
+
+    // TODO Atte Keinänen 8/15/17: Does losing field values potentially cause test failures in other test suites?
+    it("lets you discard saved field values", async () => {
+      // To be safe, let's mock the API method
+      MetabaseApi.db_discard_values = jest.fn();
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
+      const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      click(dbEditApp.find(".Button--discardSavedFieldValues"));
+      clickButton(dbEditApp.find(TestModal).find(".Button--danger"));
+      await store.waitForActions([DISCARD_SAVED_FIELD_VALUES]);
+
+      expect(MetabaseApi.db_discard_values.mock.calls.length).toBe(1);
+    });
+
+    // Disabled because removal&recovery causes the db id to change
+    it("lets you remove the dataset", () => {
+      pending();
+
+      // const store = await createTestStore()
+      // store.pushPath("/admin/databases/1");
+      // const dbEditApp = mount(store.connectContainer(<DatabaseEditApp />));
+      // await store.waitForActions([INITIALIZE_DATABASE])
+      //
+      // try {
+      //     click(dbEditApp.find(".Button--deleteDatabase"))
+      //     console.log(dbEditApp.debug());
+      //     await store.waitForActions([DELETE_DATABASE])
+      //     await store.dispatch(addSampleDataset())
+      // } catch(e) {
+      //     throw e;
+      // } finally {
+      // }
+    });
+  });
 });
diff --git a/frontend/test/admin/databases/DatabaseListApp.integ.spec.js b/frontend/test/admin/databases/DatabaseListApp.integ.spec.js
index 246efbc177ce730f6ec9fed7314d28b93a941cda..c5cbd5e9c69bbb36cdbdf52ca4e67516280e1315 100644
--- a/frontend/test/admin/databases/DatabaseListApp.integ.spec.js
+++ b/frontend/test/admin/databases/DatabaseListApp.integ.spec.js
@@ -1,434 +1,485 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton,
-    setInputValue
-} from "__support__/enzyme_utils";
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 
 import { mount } from "enzyme";
 import {
-    FETCH_DATABASES,
-    initializeDatabase,
-    INITIALIZE_DATABASE,
-    DELETE_DATABASE_FAILED,
-    DELETE_DATABASE,
-    CREATE_DATABASE_STARTED,
-    CREATE_DATABASE_FAILED,
-    CREATE_DATABASE,
-    UPDATE_DATABASE_STARTED,
-    UPDATE_DATABASE_FAILED,
-    UPDATE_DATABASE, VALIDATE_DATABASE_STARTED, SET_DATABASE_CREATION_STEP, VALIDATE_DATABASE_FAILED,
-} from "metabase/admin/databases/database"
+  FETCH_DATABASES,
+  initializeDatabase,
+  INITIALIZE_DATABASE,
+  DELETE_DATABASE_FAILED,
+  DELETE_DATABASE,
+  CREATE_DATABASE_STARTED,
+  CREATE_DATABASE_FAILED,
+  CREATE_DATABASE,
+  UPDATE_DATABASE_STARTED,
+  UPDATE_DATABASE_FAILED,
+  UPDATE_DATABASE,
+  VALIDATE_DATABASE_STARTED,
+  SET_DATABASE_CREATION_STEP,
+  VALIDATE_DATABASE_FAILED,
+} from "metabase/admin/databases/database";
 
 import DatabaseListApp from "metabase/admin/databases/containers/DatabaseListApp";
 
-import { MetabaseApi } from 'metabase/services'
+import { MetabaseApi } from "metabase/services";
 import DatabaseEditApp from "metabase/admin/databases/containers/DatabaseEditApp";
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 import { getEditingDatabase } from "metabase/admin/databases/selectors";
-import FormMessage, { SERVER_ERROR_MESSAGE } from "metabase/components/form/FormMessage";
+import FormMessage, {
+  SERVER_ERROR_MESSAGE,
+} from "metabase/components/form/FormMessage";
 import CreatedDatabaseModal from "metabase/admin/databases/components/CreatedDatabaseModal";
 import FormField from "metabase/components/form/FormField";
 import Toggle from "metabase/components/Toggle";
-import DatabaseSchedulingForm, { SyncOption } from "metabase/admin/databases/components/DatabaseSchedulingForm";
-
-describe('dashboard list', () => {
-
-    beforeAll(async () => {
-        useSharedAdminLogin()
-    })
-
-    it('should render', async () => {
-        const store = await createTestStore()
-        store.pushPath("/admin/databases");
-
-        const app = mount(store.getAppContainer())
-
-        await store.waitForActions([FETCH_DATABASES])
-
-        const wrapper = app.find(DatabaseListApp)
-        expect(wrapper.length).toEqual(1)
-    })
-
-    describe('adds', () => {
-        it("should work and shouldn't let you accidentally add db twice", async () => {
-            MetabaseApi.db_create = async (db) => { await delay(10); return {...db, id: 10}; };
-
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
-
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
-
-            const listAppBeforeAdd = app.find(DatabaseListApp)
+import DatabaseSchedulingForm, {
+  SyncOption,
+} from "metabase/admin/databases/components/DatabaseSchedulingForm";
 
-            const addDbButton = listAppBeforeAdd.find('.Button.Button--primary').first()
-            click(addDbButton)
+describe("dashboard list", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
+  it("should render", async () => {
+    const store = await createTestStore();
+    store.pushPath("/admin/databases");
 
-            await store.waitForActions([INITIALIZE_DATABASE]);
+    const app = mount(store.getAppContainer());
 
-            expect(dbDetailsForm.find('button[children="Save"]').props().disabled).toBe(true)
+    await store.waitForActions([FETCH_DATABASES]);
 
-            const updateInputValue = (name, value) =>
-                setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
+    const wrapper = app.find(DatabaseListApp);
+    expect(wrapper.length).toEqual(1);
+  });
 
-            updateInputValue("name", "Test db name");
-            updateInputValue("dbname", "test_postgres_db");
-            updateInputValue("user", "uberadmin");
+  describe("adds", () => {
+    it("should work and shouldn't let you accidentally add db twice", async () => {
+      MetabaseApi.db_create = async db => {
+        await delay(10);
+        return { ...db, id: 10 };
+      };
 
-            const saveButton = dbDetailsForm.find('button[children="Save"]')
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
 
-            expect(saveButton.props().disabled).toBe(false)
-            clickButton(saveButton)
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
 
-            // Now the submit button should be disabled so that you aren't able to trigger the db creation action twice
-            await store.waitForActions([CREATE_DATABASE_STARTED])
-            expect(saveButton.text()).toBe("Saving...");
-            expect(saveButton.props().disabled).toBe(true);
+      const listAppBeforeAdd = app.find(DatabaseListApp);
 
-            await store.waitForActions([CREATE_DATABASE]);
+      const addDbButton = listAppBeforeAdd
+        .find(".Button.Button--primary")
+        .first();
+      click(addDbButton);
+
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
+
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      expect(
+        dbDetailsForm.find('button[children="Save"]').props().disabled,
+      ).toBe(true);
+
+      const updateInputValue = (name, value) =>
+        setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
 
-            expect(store.getPath()).toEqual("/admin/databases?created=10")
-            expect(app.find(CreatedDatabaseModal).length).toBe(1);
-        })
+      updateInputValue("name", "Test db name");
+      updateInputValue("dbname", "test_postgres_db");
+      updateInputValue("user", "uberadmin");
+
+      const saveButton = dbDetailsForm.find('button[children="Save"]');
 
-        it("should show validation error if you enable scheduling toggle and enter invalid db connection info", async () => {
-            MetabaseApi.db_create = async (db) => { await delay(10); return {...db, id: 10}; };
+      expect(saveButton.props().disabled).toBe(false);
+      clickButton(saveButton);
 
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
+      // Now the submit button should be disabled so that you aren't able to trigger the db creation action twice
+      await store.waitForActions([CREATE_DATABASE_STARTED]);
+      expect(saveButton.text()).toBe("Saving...");
+      expect(saveButton.props().disabled).toBe(true);
 
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
+      await store.waitForActions([CREATE_DATABASE]);
 
-            const listAppBeforeAdd = app.find(DatabaseListApp)
+      expect(store.getPath()).toEqual("/admin/databases?created=10");
+      expect(app.find(CreatedDatabaseModal).length).toBe(1);
+    });
 
-            const addDbButton = listAppBeforeAdd.find('.Button.Button--primary').first()
-            click(addDbButton)
+    it("should show validation error if you enable scheduling toggle and enter invalid db connection info", async () => {
+      MetabaseApi.db_create = async db => {
+        await delay(10);
+        return { ...db, id: 10 };
+      };
 
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
-
-            await store.waitForActions([INITIALIZE_DATABASE]);
-
-            expect(dbDetailsForm.find('button[children="Save"]').props().disabled).toBe(true)
-
-            const updateInputValue = (name, value) =>
-                setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
-
-            updateInputValue("name", "Test db name");
-            updateInputValue("dbname", "test_postgres_db");
-            updateInputValue("user", "uberadmin");
-
-            const letUserControlSchedulingField =
-                dbDetailsForm.find(FormField).filterWhere((f) => f.props().fieldName === "let-user-control-scheduling");
-            expect(letUserControlSchedulingField.length).toBe(1);
-            expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(false);
-            click(letUserControlSchedulingField.find(Toggle))
-
-            const nextStepButton = dbDetailsForm.find('button[children="Next"]')
-            expect(nextStepButton.props().disabled).toBe(false)
-            clickButton(nextStepButton)
-
-            await store.waitForActions([VALIDATE_DATABASE_STARTED, VALIDATE_DATABASE_FAILED])
-            expect(app.find(FormMessage).text()).toMatch(/Couldn't connect to the database./);
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
+
+      const listAppBeforeAdd = app.find(DatabaseListApp);
+
+      const addDbButton = listAppBeforeAdd
+        .find(".Button.Button--primary")
+        .first();
+      click(addDbButton);
+
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
+
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      expect(
+        dbDetailsForm.find('button[children="Save"]').props().disabled,
+      ).toBe(true);
+
+      const updateInputValue = (name, value) =>
+        setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
+
+      updateInputValue("name", "Test db name");
+      updateInputValue("dbname", "test_postgres_db");
+      updateInputValue("user", "uberadmin");
+
+      const letUserControlSchedulingField = dbDetailsForm
+        .find(FormField)
+        .filterWhere(
+          f => f.props().fieldName === "let-user-control-scheduling",
+        );
+      expect(letUserControlSchedulingField.length).toBe(1);
+      expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(
+        false,
+      );
+      click(letUserControlSchedulingField.find(Toggle));
+
+      const nextStepButton = dbDetailsForm.find('button[children="Next"]');
+      expect(nextStepButton.props().disabled).toBe(false);
+      clickButton(nextStepButton);
+
+      await store.waitForActions([
+        VALIDATE_DATABASE_STARTED,
+        VALIDATE_DATABASE_FAILED,
+      ]);
+      expect(app.find(FormMessage).text()).toMatch(
+        /Couldn't connect to the database./,
+      );
+    });
+
+    it("should direct you to scheduling settings if you enable the toggle", async () => {
+      MetabaseApi.db_create = async db => {
+        await delay(10);
+        return { ...db, id: 10 };
+      };
+      // mock the validate API now because we need a positive response
+      // TODO Atte Keinänen 8/17/17: Could we at some point connect to some real H2 instance here?
+      // Maybe the test fixture would be a good fit as tests are anyway using a copy of it (no connection conflicts expected)
+      MetabaseApi.db_validate = async db => {
+        await delay(10);
+        return { valid: true };
+      };
+
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
+
+      const listAppBeforeAdd = app.find(DatabaseListApp);
+
+      const addDbButton = listAppBeforeAdd
+        .find(".Button.Button--primary")
+        .first();
+      click(addDbButton);
+
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
+
+      await store.waitForActions([INITIALIZE_DATABASE]);
+
+      expect(
+        dbDetailsForm.find('button[children="Save"]').props().disabled,
+      ).toBe(true);
+
+      const updateInputValue = (name, value) =>
+        setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
+
+      updateInputValue("name", "Test db name");
+      updateInputValue("dbname", "test_postgres_db");
+      updateInputValue("user", "uberadmin");
+
+      const letUserControlSchedulingField = dbDetailsForm
+        .find(FormField)
+        .filterWhere(
+          f => f.props().fieldName === "let-user-control-scheduling",
+        );
+      expect(letUserControlSchedulingField.length).toBe(1);
+      expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(
+        false,
+      );
+      click(letUserControlSchedulingField.find(Toggle));
+
+      const nextStepButton = dbDetailsForm.find('button[children="Next"]');
+      expect(nextStepButton.props().disabled).toBe(false);
+      clickButton(nextStepButton);
+
+      await store.waitForActions([
+        VALIDATE_DATABASE_STARTED,
+        SET_DATABASE_CREATION_STEP,
+      ]);
+
+      // Change the sync period to never in scheduling settings
+      const schedulingForm = app.find(DatabaseSchedulingForm);
+      expect(schedulingForm.length).toBe(1);
+      const syncOptions = schedulingForm.find(SyncOption);
+      const syncOptionsNever = syncOptions.at(1);
+      expect(syncOptionsNever.props().selected).toEqual(false);
+      click(syncOptionsNever);
+      expect(syncOptionsNever.props().selected).toEqual(true);
+
+      const saveButton = dbDetailsForm.find('button[children="Save"]');
+      expect(saveButton.props().disabled).toBe(false);
+      clickButton(saveButton);
+
+      // Now the submit button should be disabled so that you aren't able to trigger the db creation action twice
+      await store.waitForActions([CREATE_DATABASE_STARTED]);
+      expect(saveButton.text()).toBe("Saving...");
+
+      await store.waitForActions([CREATE_DATABASE]);
+
+      expect(store.getPath()).toEqual("/admin/databases?created=10");
+      expect(app.find(CreatedDatabaseModal).length).toBe(1);
+    });
+
+    it("should show error correctly on failure", async () => {
+      MetabaseApi.db_create = async () => {
+        await delay(10);
+        return Promise.reject({
+          status: 400,
+          data: {},
+          isCancelled: false,
         });
+      };
 
-        it("should direct you to scheduling settings if you enable the toggle", async () => {
-            MetabaseApi.db_create = async (db) => { await delay(10); return {...db, id: 10}; };
-            // mock the validate API now because we need a positive response
-            // TODO Atte Keinänen 8/17/17: Could we at some point connect to some real H2 instance here?
-            // Maybe the test fixture would be a good fit as tests are anyway using a copy of it (no connection conflicts expected)
-            MetabaseApi.db_validate = async (db) => { await delay(10); return { valid: true }; };
-
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
-
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
-
-            const listAppBeforeAdd = app.find(DatabaseListApp)
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
 
-            const addDbButton = listAppBeforeAdd.find('.Button.Button--primary').first()
-            click(addDbButton)
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
 
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
+      const listAppBeforeAdd = app.find(DatabaseListApp);
 
-            await store.waitForActions([INITIALIZE_DATABASE]);
+      const addDbButton = listAppBeforeAdd
+        .find(".Button.Button--primary")
+        .first();
 
-            expect(dbDetailsForm.find('button[children="Save"]').props().disabled).toBe(true)
+      click(addDbButton); // ROUTER LINK
 
-            const updateInputValue = (name, value) =>
-                setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
 
-            updateInputValue("name", "Test db name");
-            updateInputValue("dbname", "test_postgres_db");
-            updateInputValue("user", "uberadmin");
+      await store.waitForActions([INITIALIZE_DATABASE]);
 
-            const letUserControlSchedulingField =
-                dbDetailsForm.find(FormField).filterWhere((f) => f.props().fieldName === "let-user-control-scheduling");
-            expect(letUserControlSchedulingField.length).toBe(1);
-            expect(letUserControlSchedulingField.find(Toggle).props().value).toBe(false);
-            click(letUserControlSchedulingField.find(Toggle))
+      const saveButton = dbDetailsForm.find('button[children="Save"]');
+      expect(saveButton.props().disabled).toBe(true);
 
-            const nextStepButton = dbDetailsForm.find('button[children="Next"]')
-            expect(nextStepButton.props().disabled).toBe(false)
-            clickButton(nextStepButton)
+      // TODO: Apply change method here
+      const updateInputValue = (name, value) =>
+        setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
 
-            await store.waitForActions([VALIDATE_DATABASE_STARTED, SET_DATABASE_CREATION_STEP])
+      updateInputValue("name", "Test db name");
+      updateInputValue("dbname", "test_postgres_db");
+      updateInputValue("user", "uberadmin");
 
-            // Change the sync period to never in scheduling settings
-            const schedulingForm = app.find(DatabaseSchedulingForm)
-            expect(schedulingForm.length).toBe(1);
-            const syncOptions = schedulingForm.find(SyncOption);
-            const syncOptionsNever = syncOptions.at(1);
-            expect(syncOptionsNever.props().selected).toEqual(false);
-            click(syncOptionsNever)
-            expect(syncOptionsNever.props().selected).toEqual(true);
+      // TODO: Apply button submit thing here
+      expect(saveButton.props().disabled).toBe(false);
+      clickButton(saveButton);
 
-            const saveButton = dbDetailsForm.find('button[children="Save"]')
-            expect(saveButton.props().disabled).toBe(false)
-            clickButton(saveButton)
+      await store.waitForActions([CREATE_DATABASE_STARTED]);
+      expect(saveButton.text()).toBe("Saving...");
 
-            // Now the submit button should be disabled so that you aren't able to trigger the db creation action twice
-            await store.waitForActions([CREATE_DATABASE_STARTED])
-            expect(saveButton.text()).toBe("Saving...");
+      await store.waitForActions([CREATE_DATABASE_FAILED]);
+      expect(dbDetailsForm.find(FormMessage).text()).toEqual(
+        SERVER_ERROR_MESSAGE,
+      );
+      expect(saveButton.text()).toBe("Save");
+    });
+  });
 
-            await store.waitForActions([CREATE_DATABASE]);
+  describe("deletes", () => {
+    it("should not block deletes", async () => {
+      MetabaseApi.db_delete = async () => await delay(10);
 
-            expect(store.getPath()).toEqual("/admin/databases?created=10")
-            expect(app.find(CreatedDatabaseModal).length).toBe(1);
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
 
-        })
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
 
-        it('should show error correctly on failure', async () => {
-            MetabaseApi.db_create = async () => {
-                await delay(10);
-                return Promise.reject({
-                    status: 400,
-                    data: {},
-                    isCancelled: false
-                })
-            }
+      const wrapper = app.find(DatabaseListApp);
+      const dbCount = wrapper.find("tr").length;
 
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
+      const deleteButton = wrapper.find(".Button.Button--danger").first();
 
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
+      click(deleteButton);
 
-            const listAppBeforeAdd = app.find(DatabaseListApp)
+      const deleteModal = wrapper.find(".test-modal");
+      setInputValue(deleteModal.find(".Form-input"), "DELETE");
+      clickButton(deleteModal.find(".Button.Button--danger"));
 
-            const addDbButton = listAppBeforeAdd.find('.Button.Button--primary').first()
+      // test that the modal is gone
+      expect(wrapper.find(".test-modal").length).toEqual(0);
 
-            click(addDbButton) // ROUTER LINK
+      // we should now have a disabled db row during delete
+      expect(wrapper.find("tr.disabled").length).toEqual(1);
 
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
+      // db delete finishes
+      await store.waitForActions([DELETE_DATABASE]);
 
-            await store.waitForActions([INITIALIZE_DATABASE]);
+      // there should be no disabled db rows now
+      expect(wrapper.find("tr.disabled").length).toEqual(0);
 
-            const saveButton = dbDetailsForm.find('button[children="Save"]')
-            expect(saveButton.props().disabled).toBe(true)
+      // we should now have one database less in the list
+      expect(wrapper.find("tr").length).toEqual(dbCount - 1);
+    });
 
-            // TODO: Apply change method here
-            const updateInputValue = (name, value) =>
-                setInputValue(dbDetailsForm.find(`input[name="${name}"]`), value);
-
-            updateInputValue("name", "Test db name");
-            updateInputValue("dbname", "test_postgres_db");
-            updateInputValue("user", "uberadmin");
-
-            // TODO: Apply button submit thing here
-            expect(saveButton.props().disabled).toBe(false)
-            clickButton(saveButton)
-
-            await store.waitForActions([CREATE_DATABASE_STARTED])
-            expect(saveButton.text()).toBe("Saving...");
-
-            await store.waitForActions([CREATE_DATABASE_FAILED]);
-            expect(dbDetailsForm.find(FormMessage).text()).toEqual(SERVER_ERROR_MESSAGE);
-            expect(saveButton.text()).toBe("Save");
+    it("should show error correctly on failure", async () => {
+      MetabaseApi.db_delete = async () => {
+        await delay(10);
+        return Promise.reject({
+          status: 400,
+          data: {},
+          isCancelled: false,
         });
-    })
-
-    describe('deletes', () => {
-        it('should not block deletes', async () => {
-            MetabaseApi.db_delete = async () => await delay(10)
-
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
-
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
-
-            const wrapper = app.find(DatabaseListApp)
-            const dbCount = wrapper.find('tr').length
-
-            const deleteButton = wrapper.find('.Button.Button--danger').first()
-
-            click(deleteButton);
+      };
 
-            const deleteModal = wrapper.find('.test-modal')
-            setInputValue(deleteModal.find('.Form-input'), "DELETE")
-            clickButton(deleteModal.find('.Button.Button--danger'));
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
 
-            // test that the modal is gone
-            expect(wrapper.find('.test-modal').length).toEqual(0)
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
 
-            // we should now have a disabled db row during delete
-            expect(wrapper.find('tr.disabled').length).toEqual(1)
+      const wrapper = app.find(DatabaseListApp);
+      const dbCount = wrapper.find("tr").length;
 
-            // db delete finishes
-            await store.waitForActions([DELETE_DATABASE])
+      const deleteButton = wrapper.find(".Button.Button--danger").first();
+      click(deleteButton);
 
-            // there should be no disabled db rows now
-            expect(wrapper.find('tr.disabled').length).toEqual(0)
+      const deleteModal = wrapper.find(".test-modal");
 
-            // we should now have one database less in the list
-            expect(wrapper.find('tr').length).toEqual(dbCount - 1)
-        })
+      setInputValue(deleteModal.find(".Form-input"), "DELETE");
+      clickButton(deleteModal.find(".Button.Button--danger"));
 
-        it('should show error correctly on failure', async () => {
-            MetabaseApi.db_delete = async () => {
-                await delay(10);
-                return Promise.reject({
-                    status: 400,
-                    data: {},
-                    isCancelled: false
-                })
-            }
+      // test that the modal is gone
+      expect(wrapper.find(".test-modal").length).toEqual(0);
 
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
+      // we should now have a disabled db row during delete
+      expect(wrapper.find("tr.disabled").length).toEqual(1);
 
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
+      // db delete fails
+      await store.waitForActions([DELETE_DATABASE_FAILED]);
 
-            const wrapper = app.find(DatabaseListApp)
-            const dbCount = wrapper.find('tr').length
+      // there should be no disabled db rows now
+      expect(wrapper.find("tr.disabled").length).toEqual(0);
 
-            const deleteButton = wrapper.find('.Button.Button--danger').first()
-            click(deleteButton)
+      // the db count should be same as before
+      expect(wrapper.find("tr").length).toEqual(dbCount);
 
-            const deleteModal = wrapper.find('.test-modal')
+      expect(wrapper.find(FormMessage).text()).toBe(SERVER_ERROR_MESSAGE);
+    });
+  });
 
-            setInputValue(deleteModal.find('.Form-input'), "DELETE");
-            clickButton(deleteModal.find('.Button.Button--danger'))
+  describe("editing", () => {
+    const newName = "Ex-Sample Data Set";
 
-            // test that the modal is gone
-            expect(wrapper.find('.test-modal').length).toEqual(0)
+    it("should be able to edit database name", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases");
 
-            // we should now have a disabled db row during delete
-            expect(wrapper.find('tr.disabled').length).toEqual(1)
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DATABASES]);
 
-            // db delete fails
-            await store.waitForActions([DELETE_DATABASE_FAILED])
+      const wrapper = app.find(DatabaseListApp);
+      const sampleDatasetEditLink = wrapper
+        .find('a[children="Sample Dataset"]')
+        .first();
+      click(sampleDatasetEditLink); // ROUTER LINK
 
-            // there should be no disabled db rows now
-            expect(wrapper.find('tr.disabled').length).toEqual(0)
+      expect(store.getPath()).toEqual("/admin/databases/1");
+      await store.waitForActions([INITIALIZE_DATABASE]);
 
-            // the db count should be same as before
-            expect(wrapper.find('tr').length).toEqual(dbCount)
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
 
-            expect(wrapper.find(FormMessage).text()).toBe(SERVER_ERROR_MESSAGE);
-        })
-    })
+      const nameField = dbDetailsForm.find(`input[name="name"]`);
+      expect(nameField.props().value).toEqual("Sample Dataset");
 
-    describe('editing', () => {
-        const newName = "Ex-Sample Data Set";
+      setInputValue(nameField, newName);
 
-        it('should be able to edit database name', async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases");
+      const saveButton = dbDetailsForm.find('button[children="Save"]');
+      clickButton(saveButton);
 
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_DATABASES])
+      await store.waitForActions([UPDATE_DATABASE_STARTED]);
+      expect(saveButton.text()).toBe("Saving...");
+      expect(saveButton.props().disabled).toBe(true);
 
-            const wrapper = app.find(DatabaseListApp)
-            const sampleDatasetEditLink = wrapper.find('a[children="Sample Dataset"]').first()
-            click(sampleDatasetEditLink); // ROUTER LINK
+      await store.waitForActions([UPDATE_DATABASE]);
+      expect(saveButton.props().disabled).toBe(undefined);
+      expect(dbDetailsForm.find(FormMessage).text()).toEqual(
+        "Successfully saved!",
+      );
+    });
 
-            expect(store.getPath()).toEqual("/admin/databases/1")
-            await store.waitForActions([INITIALIZE_DATABASE]);
+    it("should show the updated database name", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
 
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([INITIALIZE_DATABASE]);
 
-            const nameField = dbDetailsForm.find(`input[name="name"]`);
-            expect(nameField.props().value).toEqual("Sample Dataset")
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
 
-            setInputValue(nameField, newName);
+      const nameField = dbDetailsForm.find(`input[name="name"]`);
+      expect(nameField.props().value).toEqual(newName);
+    });
 
-            const saveButton = dbDetailsForm.find('button[children="Save"]')
-            clickButton(saveButton)
+    it("should show an error if saving fails", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/databases/1");
 
-            await store.waitForActions([UPDATE_DATABASE_STARTED]);
-            expect(saveButton.text()).toBe("Saving...");
-            expect(saveButton.props().disabled).toBe(true);
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([INITIALIZE_DATABASE]);
 
-            await store.waitForActions([UPDATE_DATABASE]);
-            expect(saveButton.props().disabled).toBe(undefined);
-            expect(dbDetailsForm.find(FormMessage).text()).toEqual("Successfully saved!");
-        })
+      const dbDetailsForm = app.find(DatabaseEditApp);
+      expect(dbDetailsForm.length).toBe(1);
 
-        it('should show the updated database name', async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
+      const tooLongName = "too long name ".repeat(100);
+      const nameField = dbDetailsForm.find(`input[name="name"]`);
+      setInputValue(nameField, tooLongName);
 
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([INITIALIZE_DATABASE]);
+      const saveButton = dbDetailsForm.find('button[children="Save"]');
+      clickButton(saveButton);
 
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
+      await store.waitForActions([UPDATE_DATABASE_STARTED]);
+      expect(saveButton.text()).toBe("Saving...");
+      expect(saveButton.props().disabled).toBe(true);
 
-            const nameField = dbDetailsForm.find(`input[name="name"]`);
-            expect(nameField.props().value).toEqual(newName)
-        });
-
-        it('should show an error if saving fails', async () => {
-            const store = await createTestStore()
-            store.pushPath("/admin/databases/1");
-
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([INITIALIZE_DATABASE]);
-
-            const dbDetailsForm = app.find(DatabaseEditApp);
-            expect(dbDetailsForm.length).toBe(1);
-
-            const tooLongName = "too long name ".repeat(100);
-            const nameField = dbDetailsForm.find(`input[name="name"]`);
-            setInputValue(nameField, tooLongName);
+      await store.waitForActions([UPDATE_DATABASE_FAILED]);
+      expect(saveButton.props().disabled).toBe(undefined);
+      expect(dbDetailsForm.find(".Form-message.text-error").length).toBe(1);
+    });
 
-            const saveButton = dbDetailsForm.find('button[children="Save"]')
-            clickButton(saveButton)
+    afterAll(async () => {
+      const store = await createTestStore();
+      store.dispatch(initializeDatabase(1));
+      await store.waitForActions([INITIALIZE_DATABASE]);
+      const sampleDatasetDb = getEditingDatabase(store.getState());
 
-            await store.waitForActions([UPDATE_DATABASE_STARTED]);
-            expect(saveButton.text()).toBe("Saving...");
-            expect(saveButton.props().disabled).toBe(true);
-
-            await store.waitForActions([UPDATE_DATABASE_FAILED]);
-            expect(saveButton.props().disabled).toBe(undefined);
-            expect(dbDetailsForm.find(".Form-message.text-error").length).toBe(1);
-        });
-
-        afterAll(async () => {
-            const store = await createTestStore()
-            store.dispatch(initializeDatabase(1));
-            await store.waitForActions([INITIALIZE_DATABASE])
-            const sampleDatasetDb = getEditingDatabase(store.getState())
-
-            await MetabaseApi.db_update({
-                ...sampleDatasetDb,
-                name: "Sample Dataset"
-            });
-        });
-    })
-})
+      await MetabaseApi.db_update({
+        ...sampleDatasetDb,
+        name: "Sample Dataset",
+      });
+    });
+  });
+});
diff --git a/frontend/test/admin/datamodel/FieldApp.integ.spec.js b/frontend/test/admin/datamodel/FieldApp.integ.spec.js
index 793168b92535f83247eeb34e2d7b7258bfd3b1d4..313b844329eaabdcdf71895fdaf3fa1bae2ac24e 100644
--- a/frontend/test/admin/datamodel/FieldApp.integ.spec.js
+++ b/frontend/test/admin/datamodel/FieldApp.integ.spec.js
@@ -1,50 +1,52 @@
 import {
-    useSharedAdminLogin,
-    createTestStore,
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 
 import {
-    clickButton,
-    setInputValue,
-    click, dispatchBrowserEvent
-} from "__support__/enzyme_utils"
+  clickButton,
+  setInputValue,
+  click,
+  dispatchBrowserEvent,
+} from "__support__/enzyme_utils";
 import {
-    DELETE_FIELD_DIMENSION,
-    deleteFieldDimension,
-    FETCH_TABLE_METADATA,
-    fetchTableMetadata,
-    UPDATE_FIELD,
-    UPDATE_FIELD_DIMENSION,
-    UPDATE_FIELD_VALUES,
-    updateField,
-    updateFieldValues
-} from "metabase/redux/metadata"
-
-import { metadata as staticFixtureMetadata } from "__support__/sample_dataset_fixture"
-
-import React from 'react';
+  DELETE_FIELD_DIMENSION,
+  deleteFieldDimension,
+  FETCH_TABLE_METADATA,
+  fetchTableMetadata,
+  UPDATE_FIELD,
+  UPDATE_FIELD_DIMENSION,
+  UPDATE_FIELD_VALUES,
+  updateField,
+  updateFieldValues,
+} from "metabase/redux/metadata";
+
+import { metadata as staticFixtureMetadata } from "__support__/sample_dataset_fixture";
+
+import React from "react";
 import { mount } from "enzyme";
 import { FETCH_IDFIELDS } from "metabase/admin/datamodel/datamodel";
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 import FieldApp, {
-    FieldHeader,
-    FieldRemapping,
-    FieldValueMapping,
-    RemappingNamingTip,
-    ValueRemappings
+  FieldHeader,
+  FieldRemapping,
+  FieldValueMapping,
+  RemappingNamingTip,
+  ValueRemappings,
 } from "metabase/admin/datamodel/containers/FieldApp";
 import Input from "metabase/components/Input";
 import {
-    FieldVisibilityPicker,
-    SpecialTypeAndTargetPicker
+  FieldVisibilityPicker,
+  SpecialTypeAndTargetPicker,
 } from "metabase/admin/datamodel/components/database/ColumnItem";
-import { TestPopover } from "metabase/components/Popover";
+import Popover from "metabase/components/Popover";
 import Select from "metabase/components/Select";
 import SelectButton from "metabase/components/SelectButton";
 import ButtonWithStatus from "metabase/components/ButtonWithStatus";
 import { getMetadata } from "metabase/selectors/metadata";
 
-const getRawFieldWithId = (store, fieldId) => store.getState().metadata.fields[fieldId];
+const getRawFieldWithId = (store, fieldId) =>
+  store.getState().metadata.fields[fieldId];
 
 // TODO: Should we use the metabase/lib/urls methods for constructing urls also here?
 
@@ -61,360 +63,456 @@ const PRODUCT_RATING_TABLE_ID = 4;
 const PRODUCT_RATING_ID = 33;
 
 const initFieldApp = async ({ tableId = 1, fieldId }) => {
-    const store = await createTestStore()
-    store.pushPath(`/admin/datamodel/database/1/table/${tableId}/${fieldId}`);
-    const fieldApp = mount(store.connectContainer(<FieldApp />));
-    await store.waitForActions([FETCH_IDFIELDS]);
-    return { store, fieldApp }
-}
+  const store = await createTestStore();
+  store.pushPath(`/admin/datamodel/database/1/table/${tableId}/${fieldId}`);
+  const fieldApp = mount(store.connectContainer(<FieldApp />));
+  await store.waitForActions([FETCH_IDFIELDS]);
+  return { store, fieldApp };
+};
 
 describe("FieldApp", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin()
-    })
-
-    describe("name settings", () => {
-        const newTitle = 'Brought Into Existence At'
-        const newDescription = 'The point in space-time when this order saw the light.'
-
-        it("lets you change field name and description", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-
-            const header = fieldApp.find(FieldHeader)
-            expect(header.length).toBe(1)
-
-            const nameInput = header.find(Input).at(0);
-            expect(nameInput.props().value).toBe(staticFixtureMetadata.fields['1'].display_name);
-            const descriptionInput = header.find(Input).at(1);
-            expect(descriptionInput.props().value).toBe(staticFixtureMetadata.fields['1'].description);
-
-            setInputValue(nameInput, newTitle);
-            await store.waitForActions([UPDATE_FIELD])
-
-            setInputValue(descriptionInput, newDescription);
-            await store.waitForActions([UPDATE_FIELD])
-        })
-
-        it("should show the entered values after a page reload", async () => {
-            const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-
-            const header = fieldApp.find(FieldHeader)
-            expect(header.length).toBe(1)
-            const nameInput = header.find(Input).at(0);
-            const descriptionInput = header.find(Input).at(1);
-
-            expect(nameInput.props().value).toBe(newTitle);
-            expect(descriptionInput.props().value).toBe(newDescription);
-        })
-
-        afterAll(async () => {
-            const store = await createTestStore()
-            await store.dispatch(fetchTableMetadata(1));
-            const createdAtField = getRawFieldWithId(store, CREATED_AT_ID)
-
-            await store.dispatch(updateField({
-                ...createdAtField,
-                display_name: staticFixtureMetadata.fields[1].display_name,
-                description: staticFixtureMetadata.fields[1].description,
-            }))
-        })
-    })
-
-    describe("visibility settings", () => {
-        it("shows correct default visibility", async () => {
-            const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-            const visibilitySelect = fieldApp.find(FieldVisibilityPicker);
-            expect(visibilitySelect.text()).toMatch(/Everywhere/);
-        })
-
-        it("lets you change field visibility", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-
-            const visibilitySelect = fieldApp.find(FieldVisibilityPicker);
-            click(visibilitySelect);
-            click(visibilitySelect.find(TestPopover).find("li").at(1).children().first());
-
-            await store.waitForActions([UPDATE_FIELD])
-        })
-
-        it("should show the updated visibility setting after a page reload", async () => {
-            const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-
-            const picker = fieldApp.find(FieldVisibilityPicker);
-            expect(picker.text()).toMatch(/Only in Detail Views/);
-        })
-
-        afterAll(async () => {
-            const store = await createTestStore()
-            await store.dispatch(fetchTableMetadata(1));
-            const createdAtField = getRawFieldWithId(store, CREATED_AT_ID)
-
-            await store.dispatch(updateField({
-                ...createdAtField,
-                visibility_type: "normal",
-            }))
-        })
-    })
-
-    describe("special type and target settings", () => {
-        it("shows the correct default special type for a foreign key", async () => {
-            const { fieldApp } = await initFieldApp({ fieldId: PRODUCT_ID_FK_ID });
-            const picker = fieldApp.find(SpecialTypeAndTargetPicker).text()
-            expect(picker).toMatch(/Foreign KeyProducts → ID/);
-        })
-
-        it("lets you change the type to 'No special type'", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-            const picker = fieldApp.find(SpecialTypeAndTargetPicker)
-            const typeSelect = picker.find(Select).at(0)
-            click(typeSelect);
-
-            const noSpecialTypeButton = typeSelect.find(TestPopover).find("li").last().children().first()
-            click(noSpecialTypeButton);
-
-            await store.waitForActions([UPDATE_FIELD])
-            expect(picker.text()).toMatch(/Select a special type/);
-        })
-
-        it("lets you change the type to 'Number'", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-            const picker = fieldApp.find(SpecialTypeAndTargetPicker)
-            const typeSelect = picker.find(Select).at(0)
-            click(typeSelect);
-
-            const noSpecialTypeButton = typeSelect.find(TestPopover)
-                .find("li")
-                .filterWhere(li => li.text() === "Number").first()
-                .children().first();
-
-            click(noSpecialTypeButton);
-
-            await store.waitForActions([UPDATE_FIELD])
-            expect(picker.text()).toMatch(/Number/);
-        })
-
-        it("lets you change the type to 'Foreign key' and choose the target field", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-            const picker = fieldApp.find(SpecialTypeAndTargetPicker)
-            const typeSelect = picker.find(Select).at(0)
-            click(typeSelect);
-
-            const foreignKeyButton = typeSelect.find(TestPopover).find("li").at(2).children().first();
-            click(foreignKeyButton);
-            await store.waitForActions([UPDATE_FIELD])
-
-            expect(picker.text()).toMatch(/Foreign KeySelect a target/);
-            const fkFieldSelect = picker.find(Select).at(1)
-            click(fkFieldSelect);
-
-            const productIdField = fkFieldSelect.find(TestPopover)
-                .find("li")
-                .filterWhere(li => /The numerical product number./.test(li.text()))
-                .first().children().first();
-
-            click(productIdField)
-            await store.waitForActions([UPDATE_FIELD])
-            expect(picker.text()).toMatch(/Foreign KeyProducts → ID/);
-        })
-
-        afterAll(async () => {
-            const store = await createTestStore()
-            await store.dispatch(fetchTableMetadata(1));
-            const createdAtField = getRawFieldWithId(store, CREATED_AT_ID)
-
-            await store.dispatch(updateField({
-                ...createdAtField,
-                special_type: null,
-                fk_target_field_id: null
-            }))
-        })
-    })
-
-    describe("display value / remapping settings", () => {
-        it("shows only 'Use original value' for fields without fk and values", async () => {
-            const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select).first();
-            expect(mappingTypePicker.text()).toBe('Use original value')
-
-            click(mappingTypePicker);
-            const pickerOptions = mappingTypePicker.find(TestPopover).find("li");
-            expect(pickerOptions.length).toBe(1);
-        })
-
-        it("lets you change to 'Use foreign key' and change the target for field with fk", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: USER_ID_FK_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-            expect(mappingTypePicker.text()).toBe('Use original value')
-
-            click(mappingTypePicker);
-            const pickerOptions = mappingTypePicker.find(TestPopover).find("li");
-            expect(pickerOptions.length).toBe(2);
-
-            const useFKButton = pickerOptions.at(1).children().first()
-            click(useFKButton);
-            store.waitForActions([UPDATE_FIELD_DIMENSION, FETCH_TABLE_METADATA])
-            // TODO: Figure out a way to avoid using delay – the use of delays may lead to occasional CI failures
-            await delay(500);
-
-            const fkFieldSelect = section.find(SelectButton);
-
-            expect(fkFieldSelect.text()).toBe("Name");
-            click(fkFieldSelect);
-
-            const sourceField = fkFieldSelect.parent().find(TestPopover)
-                .find(".List-item")
-                .filterWhere(li => /Source/.test(li.text()))
-                .first().children().first();
-
-            click(sourceField)
-            store.waitForActions([FETCH_TABLE_METADATA])
-            // TODO: Figure out a way to avoid using delay – the use of delays may lead to occasional CI failures
-            await delay(500);
-            expect(fkFieldSelect.text()).toBe("Source");
-        })
-
-        it("doesn't show date fields in fk options", async () => {
-            const { fieldApp } = await initFieldApp({ fieldId: USER_ID_FK_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-            expect(mappingTypePicker.text()).toBe('Use foreign key')
-
-            const fkFieldSelect = section.find(SelectButton);
-            click(fkFieldSelect);
-
-            const popover = fkFieldSelect.parent().find(TestPopover);
-            expect(popover.length).toBe(1);
-
-            const dateFieldIcons = popover.find("svg.Icon-calendar")
-            expect(dateFieldIcons.length).toBe(0);
-        })
-
-        it("lets you switch back to Use original value after changing to some other value", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: USER_ID_FK_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-            expect(mappingTypePicker.text()).toBe('Use foreign key')
-
-            click(mappingTypePicker);
-            const pickerOptions = mappingTypePicker.find(TestPopover).find("li");
-            const useOriginalValue = pickerOptions.first().children().first()
-            click(useOriginalValue);
-
-            store.waitForActions([DELETE_FIELD_DIMENSION, FETCH_TABLE_METADATA]);
-        })
-
-        it("forces you to choose the FK field manually if there is no field with Field Name special type", async () => {
-            const { store, fieldApp } = await initFieldApp({ fieldId: USER_ID_FK_ID });
-
-            // Set FK id to `Reviews -> ID`  with a direct metadata update call
-            const field = getMetadata(store.getState()).fields[USER_ID_FK_ID]
-            await store.dispatch(updateField({
-                ...field.getPlainObject(),
-                fk_target_field_id: 31
-            }));
-
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-            expect(mappingTypePicker.text()).toBe('Use original value')
-            click(mappingTypePicker);
-            const pickerOptions = mappingTypePicker.find(TestPopover).find("li");
-            expect(pickerOptions.length).toBe(2);
-
-            const useFKButton = pickerOptions.at(1).children().first()
-            click(useFKButton);
-            store.waitForActions([UPDATE_FIELD_DIMENSION, FETCH_TABLE_METADATA])
-            // TODO: Figure out a way to avoid using delay – the use of delays may lead to occasional CI failures
-            await delay(500);
-
-            expect(section.find(RemappingNamingTip).length).toBe(1)
-
-            dispatchBrowserEvent('mousedown', { e: { target: document.documentElement }})
-            await delay(300); // delay needed because of setState in FieldApp; app.update() does not work for whatever reason
-            expect(section.find(".text-danger").length).toBe(1) // warning that you should choose a column
-        })
-
-        it("doesn't let you enter custom remappings for a field with string values", async () => {
-            const { fieldApp } = await initFieldApp({ tableId: USER_SOURCE_TABLE_ID, fieldId: USER_SOURCE_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-
-            expect(mappingTypePicker.text()).toBe('Use original value')
-            click(mappingTypePicker);
-            const pickerOptions = mappingTypePicker.find(TestPopover).find("li");
-            expect(pickerOptions.length).toBe(1);
-        });
-
-        // TODO: Make sure that product rating is a Category and that a sync has been run
-        it("lets you enter custom remappings for a field with numeral values", async () => {
-            const { store, fieldApp } = await initFieldApp({ tableId: PRODUCT_RATING_TABLE_ID, fieldId: PRODUCT_RATING_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-
-            expect(mappingTypePicker.text()).toBe('Use original value')
-            click(mappingTypePicker);
-            const pickerOptions = mappingTypePicker.find(TestPopover).find("li");
-            expect(pickerOptions.length).toBe(2);
-
-            const customMappingButton = pickerOptions.at(1).children().first()
-            click(customMappingButton);
-
-            store.waitForActions([UPDATE_FIELD_DIMENSION, FETCH_TABLE_METADATA])
-            // TODO: Figure out a way to avoid using delay – using delays may lead to occasional CI failures
-            await delay(500);
-
-            const valueRemappingsSection = section.find(ValueRemappings);
-            expect(valueRemappingsSection.length).toBe(1);
-
-            const fieldValueMappings = valueRemappingsSection.find(FieldValueMapping);
-            expect(fieldValueMappings.length).toBe(5);
-
-            const firstMapping = fieldValueMappings.at(0);
-            expect(firstMapping.find("h3").text()).toBe("1");
-            expect(firstMapping.find(Input).props().value).toBe("1");
-            setInputValue(firstMapping.find(Input), "Terrible")
-
-            const lastMapping = fieldValueMappings.last();
-            expect(lastMapping.find("h3").text()).toBe("5");
-            expect(lastMapping.find(Input).props().value).toBe("5");
-            setInputValue(lastMapping.find(Input), "Extraordinarily awesome")
-
-            const saveButton = valueRemappingsSection.find(ButtonWithStatus)
-            clickButton(saveButton)
-
-            store.waitForActions([UPDATE_FIELD_VALUES]);
-        });
-
-        it("shows the updated values after page reload", async () => {
-            const { fieldApp } = await initFieldApp({ tableId: PRODUCT_RATING_TABLE_ID, fieldId: PRODUCT_RATING_ID });
-            const section = fieldApp.find(FieldRemapping)
-            const mappingTypePicker = section.find(Select);
-
-            expect(mappingTypePicker.text()).toBe('Custom mapping');
-            const fieldValueMappings = section.find(FieldValueMapping);
-            expect(fieldValueMappings.first().find(Input).props().value).toBe("Terrible");
-            expect(fieldValueMappings.last().find(Input).props().value).toBe("Extraordinarily awesome");
-        });
-
-        afterAll(async () => {
-            const store = await createTestStore()
-            await store.dispatch(fetchTableMetadata(1))
-
-            const field = getMetadata(store.getState()).fields[USER_ID_FK_ID]
-            await store.dispatch(updateField({
-                ...field.getPlainObject(),
-                fk_target_field_id: 13 // People -> ID
-            }));
-
-            await store.dispatch(deleteFieldDimension(USER_ID_FK_ID));
-            await store.dispatch(deleteFieldDimension(PRODUCT_RATING_ID));
-
-            // TODO: This is a little hacky – could there be a way to simply reset the user-defined valued?
-            await store.dispatch(updateFieldValues(PRODUCT_RATING_ID, [
-                [1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5']
-            ]));
-        })
-    })
-
-})
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("name settings", () => {
+    const newTitle = "Brought Into Existence At";
+    const newDescription =
+      "The point in space-time when this order saw the light.";
+
+    it("lets you change field name and description", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: CREATED_AT_ID,
+      });
+
+      const header = fieldApp.find(FieldHeader);
+      expect(header.length).toBe(1);
+
+      const nameInput = header.find(Input).at(0);
+      expect(nameInput.props().value).toBe(
+        staticFixtureMetadata.fields["1"].display_name,
+      );
+      const descriptionInput = header.find(Input).at(1);
+      expect(descriptionInput.props().value).toBe(
+        staticFixtureMetadata.fields["1"].description,
+      );
+
+      setInputValue(nameInput, newTitle);
+      await store.waitForActions([UPDATE_FIELD]);
+
+      setInputValue(descriptionInput, newDescription);
+      await store.waitForActions([UPDATE_FIELD]);
+    });
+
+    it("should show the entered values after a page reload", async () => {
+      const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
+
+      const header = fieldApp.find(FieldHeader);
+      expect(header.length).toBe(1);
+      const nameInput = header.find(Input).at(0);
+      const descriptionInput = header.find(Input).at(1);
+
+      expect(nameInput.props().value).toBe(newTitle);
+      expect(descriptionInput.props().value).toBe(newDescription);
+    });
+
+    afterAll(async () => {
+      const store = await createTestStore();
+      await store.dispatch(fetchTableMetadata(1));
+      const createdAtField = getRawFieldWithId(store, CREATED_AT_ID);
+
+      await store.dispatch(
+        updateField({
+          ...createdAtField,
+          display_name: staticFixtureMetadata.fields[1].display_name,
+          description: staticFixtureMetadata.fields[1].description,
+        }),
+      );
+    });
+  });
+
+  describe("visibility settings", () => {
+    it("shows correct default visibility", async () => {
+      const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
+      const visibilitySelect = fieldApp.find(FieldVisibilityPicker);
+      expect(visibilitySelect.text()).toMatch(/Everywhere/);
+    });
+
+    it("lets you change field visibility", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: CREATED_AT_ID,
+      });
+
+      const visibilitySelect = fieldApp.find(FieldVisibilityPicker);
+      click(visibilitySelect);
+      click(
+        visibilitySelect
+          .find(Popover)
+          .find("li")
+          .at(1)
+          .children()
+          .first(),
+      );
+
+      await store.waitForActions([UPDATE_FIELD]);
+    });
+
+    it("should show the updated visibility setting after a page reload", async () => {
+      const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
+
+      const picker = fieldApp.find(FieldVisibilityPicker);
+      expect(picker.text()).toMatch(/Only in Detail Views/);
+    });
+
+    afterAll(async () => {
+      const store = await createTestStore();
+      await store.dispatch(fetchTableMetadata(1));
+      const createdAtField = getRawFieldWithId(store, CREATED_AT_ID);
+
+      await store.dispatch(
+        updateField({
+          ...createdAtField,
+          visibility_type: "normal",
+        }),
+      );
+    });
+  });
+
+  describe("special type and target settings", () => {
+    it("shows the correct default special type for a foreign key", async () => {
+      const { fieldApp } = await initFieldApp({ fieldId: PRODUCT_ID_FK_ID });
+      const picker = fieldApp.find(SpecialTypeAndTargetPicker).text();
+      expect(picker).toMatch(/Foreign KeyProducts → ID/);
+    });
+
+    it("lets you change the type to 'No special type'", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: CREATED_AT_ID,
+      });
+      const picker = fieldApp.find(SpecialTypeAndTargetPicker);
+      const typeSelect = picker.find(Select).at(0);
+      click(typeSelect);
+
+      const noSpecialTypeButton = typeSelect
+        .find(Popover)
+        .find("li")
+        .last()
+        .children()
+        .first();
+      click(noSpecialTypeButton);
+
+      await store.waitForActions([UPDATE_FIELD]);
+      expect(picker.text()).toMatch(/Select a special type/);
+    });
+
+    it("lets you change the type to 'Number'", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: CREATED_AT_ID,
+      });
+      const picker = fieldApp.find(SpecialTypeAndTargetPicker);
+      const typeSelect = picker.find(Select).at(0);
+      click(typeSelect);
+
+      const noSpecialTypeButton = typeSelect
+        .find(Popover)
+        .find("li")
+        .filterWhere(li => li.text() === "Number")
+        .first()
+        .children()
+        .first();
+
+      click(noSpecialTypeButton);
+
+      await store.waitForActions([UPDATE_FIELD]);
+      expect(picker.text()).toMatch(/Number/);
+    });
+
+    it("lets you change the type to 'Foreign key' and choose the target field", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: CREATED_AT_ID,
+      });
+      const picker = fieldApp.find(SpecialTypeAndTargetPicker);
+      const typeSelect = picker.find(Select).at(0);
+      click(typeSelect);
+
+      const foreignKeyButton = typeSelect
+        .find(Popover)
+        .find("li")
+        .at(2)
+        .children()
+        .first();
+      click(foreignKeyButton);
+      await store.waitForActions([UPDATE_FIELD]);
+
+      expect(picker.text()).toMatch(/Foreign KeySelect a target/);
+      const fkFieldSelect = picker.find(Select).at(1);
+      click(fkFieldSelect);
+
+      const productIdField = fkFieldSelect
+        .find(Popover)
+        .find("li")
+        .filterWhere(li => /The numerical product number./.test(li.text()))
+        .first()
+        .children()
+        .first();
+
+      click(productIdField);
+      await store.waitForActions([UPDATE_FIELD]);
+      expect(picker.text()).toMatch(/Foreign KeyProducts → ID/);
+    });
+
+    afterAll(async () => {
+      const store = await createTestStore();
+      await store.dispatch(fetchTableMetadata(1));
+      const createdAtField = getRawFieldWithId(store, CREATED_AT_ID);
+
+      await store.dispatch(
+        updateField({
+          ...createdAtField,
+          special_type: null,
+          fk_target_field_id: null,
+        }),
+      );
+    });
+  });
+
+  describe("display value / remapping settings", () => {
+    it("shows only 'Use original value' for fields without fk and values", async () => {
+      const { fieldApp } = await initFieldApp({ fieldId: CREATED_AT_ID });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select).first();
+      expect(mappingTypePicker.text()).toBe("Use original value");
+
+      click(mappingTypePicker);
+      const pickerOptions = mappingTypePicker.find(Popover).find("li");
+      expect(pickerOptions.length).toBe(1);
+    });
+
+    it("lets you change to 'Use foreign key' and change the target for field with fk", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: USER_ID_FK_ID,
+      });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+      expect(mappingTypePicker.text()).toBe("Use original value");
+
+      click(mappingTypePicker);
+      const pickerOptions = mappingTypePicker.find(Popover).find("li");
+      expect(pickerOptions.length).toBe(2);
+
+      const useFKButton = pickerOptions
+        .at(1)
+        .children()
+        .first();
+      click(useFKButton);
+      store.waitForActions([UPDATE_FIELD_DIMENSION, FETCH_TABLE_METADATA]);
+      // TODO: Figure out a way to avoid using delay – the use of delays may lead to occasional CI failures
+      await delay(500);
+
+      const fkFieldSelect = section.find(SelectButton);
+
+      expect(fkFieldSelect.text()).toBe("Name");
+      click(fkFieldSelect);
+
+      const sourceField = fkFieldSelect
+        .parent()
+        .find(Popover)
+        .find(".List-item")
+        .filterWhere(li => /Source/.test(li.text()))
+        .first()
+        .children()
+        .first();
+
+      click(sourceField);
+      store.waitForActions([FETCH_TABLE_METADATA]);
+      // TODO: Figure out a way to avoid using delay – the use of delays may lead to occasional CI failures
+      await delay(500);
+      expect(fkFieldSelect.text()).toBe("Source");
+    });
+
+    it("doesn't show date fields in fk options", async () => {
+      const { fieldApp } = await initFieldApp({ fieldId: USER_ID_FK_ID });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+      expect(mappingTypePicker.text()).toBe("Use foreign key");
+
+      const fkFieldSelect = section.find(SelectButton);
+      click(fkFieldSelect);
+
+      const popover = fkFieldSelect.parent().find(Popover);
+      expect(popover.length).toBe(1);
+
+      const dateFieldIcons = popover.find("svg.Icon-calendar");
+      expect(dateFieldIcons.length).toBe(0);
+    });
+
+    it("lets you switch back to Use original value after changing to some other value", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: USER_ID_FK_ID,
+      });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+      expect(mappingTypePicker.text()).toBe("Use foreign key");
+
+      click(mappingTypePicker);
+      const pickerOptions = mappingTypePicker.find(Popover).find("li");
+      const useOriginalValue = pickerOptions
+        .first()
+        .children()
+        .first();
+      click(useOriginalValue);
+
+      store.waitForActions([DELETE_FIELD_DIMENSION, FETCH_TABLE_METADATA]);
+    });
+
+    it("forces you to choose the FK field manually if there is no field with Field Name special type", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        fieldId: USER_ID_FK_ID,
+      });
+
+      // Set FK id to `Reviews -> ID`  with a direct metadata update call
+      const field = getMetadata(store.getState()).fields[USER_ID_FK_ID];
+      await store.dispatch(
+        updateField({
+          ...field.getPlainObject(),
+          fk_target_field_id: 31,
+        }),
+      );
+
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+      expect(mappingTypePicker.text()).toBe("Use original value");
+      click(mappingTypePicker);
+      const pickerOptions = mappingTypePicker.find(Popover).find("li");
+      expect(pickerOptions.length).toBe(2);
+
+      const useFKButton = pickerOptions
+        .at(1)
+        .children()
+        .first();
+      click(useFKButton);
+      store.waitForActions([UPDATE_FIELD_DIMENSION, FETCH_TABLE_METADATA]);
+      // TODO: Figure out a way to avoid using delay – the use of delays may lead to occasional CI failures
+      await delay(500);
+
+      expect(section.find(RemappingNamingTip).length).toBe(1);
+
+      dispatchBrowserEvent("mousedown", {
+        e: { target: document.documentElement },
+      });
+      await delay(300); // delay needed because of setState in FieldApp; app.update() does not work for whatever reason
+      expect(section.find(".text-danger").length).toBe(1); // warning that you should choose a column
+    });
+
+    it("doesn't let you enter custom remappings for a field with string values", async () => {
+      const { fieldApp } = await initFieldApp({
+        tableId: USER_SOURCE_TABLE_ID,
+        fieldId: USER_SOURCE_ID,
+      });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+
+      expect(mappingTypePicker.text()).toBe("Use original value");
+      click(mappingTypePicker);
+      const pickerOptions = mappingTypePicker.find(Popover).find("li");
+      expect(pickerOptions.length).toBe(1);
+    });
+
+    // TODO: Make sure that product rating is a Category and that a sync has been run
+    it("lets you enter custom remappings for a field with numeral values", async () => {
+      const { store, fieldApp } = await initFieldApp({
+        tableId: PRODUCT_RATING_TABLE_ID,
+        fieldId: PRODUCT_RATING_ID,
+      });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+
+      expect(mappingTypePicker.text()).toBe("Use original value");
+      click(mappingTypePicker);
+      const pickerOptions = mappingTypePicker.find(Popover).find("li");
+      expect(pickerOptions.length).toBe(2);
+
+      const customMappingButton = pickerOptions
+        .at(1)
+        .children()
+        .first();
+      click(customMappingButton);
+
+      store.waitForActions([UPDATE_FIELD_DIMENSION, FETCH_TABLE_METADATA]);
+      // TODO: Figure out a way to avoid using delay – using delays may lead to occasional CI failures
+      await delay(500);
+
+      const valueRemappingsSection = section.find(ValueRemappings);
+      expect(valueRemappingsSection.length).toBe(1);
+
+      const fieldValueMappings = valueRemappingsSection.find(FieldValueMapping);
+      expect(fieldValueMappings.length).toBe(5);
+
+      const firstMapping = fieldValueMappings.at(0);
+      expect(firstMapping.find("h3").text()).toBe("1");
+      expect(firstMapping.find(Input).props().value).toBe("1");
+      setInputValue(firstMapping.find(Input), "Terrible");
+
+      const lastMapping = fieldValueMappings.last();
+      expect(lastMapping.find("h3").text()).toBe("5");
+      expect(lastMapping.find(Input).props().value).toBe("5");
+      setInputValue(lastMapping.find(Input), "Extraordinarily awesome");
+
+      const saveButton = valueRemappingsSection.find(ButtonWithStatus);
+      clickButton(saveButton);
+
+      store.waitForActions([UPDATE_FIELD_VALUES]);
+    });
+
+    it("shows the updated values after page reload", async () => {
+      const { fieldApp } = await initFieldApp({
+        tableId: PRODUCT_RATING_TABLE_ID,
+        fieldId: PRODUCT_RATING_ID,
+      });
+      const section = fieldApp.find(FieldRemapping);
+      const mappingTypePicker = section.find(Select);
+
+      expect(mappingTypePicker.text()).toBe("Custom mapping");
+      const fieldValueMappings = section.find(FieldValueMapping);
+      expect(
+        fieldValueMappings
+          .first()
+          .find(Input)
+          .props().value,
+      ).toBe("Terrible");
+      expect(
+        fieldValueMappings
+          .last()
+          .find(Input)
+          .props().value,
+      ).toBe("Extraordinarily awesome");
+    });
+
+    afterAll(async () => {
+      const store = await createTestStore();
+      await store.dispatch(fetchTableMetadata(1));
+
+      const field = getMetadata(store.getState()).fields[USER_ID_FK_ID];
+      await store.dispatch(
+        updateField({
+          ...field.getPlainObject(),
+          fk_target_field_id: 13, // People -> ID
+        }),
+      );
+
+      await store.dispatch(deleteFieldDimension(USER_ID_FK_ID));
+      await store.dispatch(deleteFieldDimension(PRODUCT_RATING_ID));
+
+      // TODO: This is a little hacky – could there be a way to simply reset the user-defined valued?
+      await store.dispatch(
+        updateFieldValues(PRODUCT_RATING_ID, [
+          [1, "1"],
+          [2, "2"],
+          [3, "3"],
+          [4, "4"],
+          [5, "5"],
+        ]),
+      );
+    });
+  });
+});
diff --git a/frontend/test/admin/datamodel/datamodel.integ.spec.js b/frontend/test/admin/datamodel/datamodel.integ.spec.js
index 333c3d00a77ba0a22a30154bb5d31219fdfd0866..e030d5cc3ddb7cc1456c61b44420f99ae4b08757 100644
--- a/frontend/test/admin/datamodel/datamodel.integ.spec.js
+++ b/frontend/test/admin/datamodel/datamodel.integ.spec.js
@@ -1,23 +1,19 @@
 // Converted from an old Selenium E2E test
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton,
-    setInputValue
-} from "__support__/enzyme_utils"
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 import { mount } from "enzyme";
 import {
-    CREATE_METRIC,
-    CREATE_SEGMENT,
-    FETCH_IDFIELDS,
-    INITIALIZE_METADATA,
-    SELECT_TABLE,
-    UPDATE_FIELD,
-    UPDATE_PREVIEW_SUMMARY,
-    UPDATE_TABLE
+  CREATE_METRIC,
+  CREATE_SEGMENT,
+  FETCH_IDFIELDS,
+  INITIALIZE_METADATA,
+  SELECT_TABLE,
+  UPDATE_FIELD,
+  UPDATE_PREVIEW_SUMMARY,
+  UPDATE_TABLE,
 } from "metabase/admin/datamodel/datamodel";
 import { FETCH_TABLE_METADATA } from "metabase/redux/metadata";
 
@@ -34,146 +30,183 @@ import MetricItem from "metabase/admin/datamodel/components/database/MetricItem"
 import { MetabaseApi } from "metabase/services";
 
 describe("admin/datamodel", () => {
-    beforeAll(async () =>
-        useSharedAdminLogin()
-    );
-
-    describe("data model editor", () => {
-        it("should allow admin to edit data model", async () => {
-            const store = await createTestStore();
-
-            store.pushPath('/admin/datamodel/database');
-            const app = mount(store.getAppContainer())
-
-            await store.waitForActions([INITIALIZE_METADATA, FETCH_IDFIELDS]);
-
-            // Open "Orders" table section
-            const adminListItems = app.find(".AdminList-item");
-            click(adminListItems.at(0));
-            await store.waitForActions([SELECT_TABLE]);
-
-            // Toggle its visibility to "Hidden"
-            click(app.find("#VisibilityTypes > span").at(1))
-            await store.waitForActions([UPDATE_TABLE]);
-
-            // Toggle "Why hide" to "Irrelevant/Cruft"
-            click(app.find("#VisibilitySubTypes > span").at(2))
-            await store.waitForActions([UPDATE_TABLE]);
-
-            // Unhide
-            click(app.find("#VisibilityTypes > span").at(0))
-
-            // Open "People" table section
-            click(adminListItems.at(1));
-            await store.waitForActions([SELECT_TABLE]);
-
-            // hide fields from people table
-            // Set Address field to "Only in Detail Views"
-            const columnsListItems = app.find(ColumnsList).find("li")
-
-            click(columnsListItems.first().find(".TableEditor-field-visibility"));
-            const onlyInDetailViewsRow = app.find(ColumnarSelector).find(".ColumnarSelector-row").at(1)
-            expect(onlyInDetailViewsRow.text()).toMatch(/Only in Detail Views/);
-            click(onlyInDetailViewsRow);
-            await store.waitForActions([UPDATE_FIELD]);
-
-            // Set Birth Date field to "Do Not Include"
-            click(columnsListItems.at(1).find(".TableEditor-field-visibility"));
-            // different ColumnarSelector than before so do a new lookup
-            const doNotIncludeRow = app.find(ColumnarSelector).find(".ColumnarSelector-row").at(2)
-            expect(doNotIncludeRow.text()).toMatch(/Do Not Include/);
-            click(doNotIncludeRow);
-
-            await store.waitForActions([UPDATE_FIELD]);
-
-            // modify special type for address field
-            click(columnsListItems.first().find(".TableEditor-field-special-type"))
-            const entityNameTypeRow = app.find(ColumnarSelector).find(".ColumnarSelector-row").at(1)
-            expect(entityNameTypeRow.text()).toMatch(/Entity Name/);
-            click(entityNameTypeRow);
-            await store.waitForActions([UPDATE_FIELD]);
-
-            // TODO Atte Keinänen 8/9/17: Currently this test doesn't validate that the updates actually are reflected in QB
-        });
-
-        it("should allow admin to create segments", async () => {
-            const store = await createTestStore();
-
-            // Open the People table admin page
-            store.pushPath('/admin/datamodel/database/1/table/2');
-            const app = mount(store.getAppContainer())
-
-            await store.waitForActions([INITIALIZE_METADATA, FETCH_IDFIELDS]);
-
-            // Click the new segment button and check that we get properly redirected
-            click(app.find(SegmentsList).find(Link));
-            expect(store.getPath()).toBe('/admin/datamodel/segment/create?table=2')
-            await store.waitForActions([FETCH_TABLE_METADATA, UPDATE_PREVIEW_SUMMARY]);
-
-            // Add "Email Is Not gmail" filter
-            click(app.find(".GuiBuilder-filtered-by a").first())
-
-            const filterPopover = app.find(FilterPopover);
-            click(filterPopover.find(FieldList).find('h4[children="Email"]'));
-
-            const operatorSelector = filterPopover.find(OperatorSelector);
-            clickButton(operatorSelector.find('button[children="Is not"]'));
-
-            const addFilterButton = filterPopover.find(".Button.disabled");
+  beforeAll(async () => useSharedAdminLogin());
+
+  describe("data model editor", () => {
+    it("should allow admin to edit data model", async () => {
+      const store = await createTestStore();
+
+      store.pushPath("/admin/datamodel/database");
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([INITIALIZE_METADATA, FETCH_IDFIELDS]);
+
+      // Open "Orders" table section
+      const adminListItems = app.find(".AdminList-item");
+      click(adminListItems.at(0));
+      await store.waitForActions([SELECT_TABLE]);
+
+      // Toggle its visibility to "Hidden"
+      click(app.find("#VisibilityTypes > span").at(1));
+      await store.waitForActions([UPDATE_TABLE]);
+
+      // Toggle "Why hide" to "Irrelevant/Cruft"
+      click(app.find("#VisibilitySubTypes > span").at(2));
+      await store.waitForActions([UPDATE_TABLE]);
+
+      // Unhide
+      click(app.find("#VisibilityTypes > span").at(0));
+
+      // Open "People" table section
+      click(adminListItems.at(1));
+      await store.waitForActions([SELECT_TABLE]);
+
+      // hide fields from people table
+      // Set Address field to "Only in Detail Views"
+      const columnsListItems = app.find(ColumnsList).find("li");
+
+      click(columnsListItems.first().find(".TableEditor-field-visibility"));
+      const onlyInDetailViewsRow = app
+        .find(ColumnarSelector)
+        .find(".ColumnarSelector-row")
+        .at(1);
+      expect(onlyInDetailViewsRow.text()).toMatch(/Only in Detail Views/);
+      click(onlyInDetailViewsRow);
+      await store.waitForActions([UPDATE_FIELD]);
+
+      // Set Birth Date field to "Do Not Include"
+      click(columnsListItems.at(1).find(".TableEditor-field-visibility"));
+      // different ColumnarSelector than before so do a new lookup
+      const doNotIncludeRow = app
+        .find(ColumnarSelector)
+        .find(".ColumnarSelector-row")
+        .at(2);
+      expect(doNotIncludeRow.text()).toMatch(/Do Not Include/);
+      click(doNotIncludeRow);
+
+      await store.waitForActions([UPDATE_FIELD]);
+
+      // modify special type for address field
+      click(columnsListItems.first().find(".TableEditor-field-special-type"));
+      const entityNameTypeRow = app
+        .find(ColumnarSelector)
+        .find(".ColumnarSelector-row")
+        .at(1);
+      expect(entityNameTypeRow.text()).toMatch(/Entity Name/);
+      click(entityNameTypeRow);
+      await store.waitForActions([UPDATE_FIELD]);
+
+      // TODO Atte Keinänen 8/9/17: Currently this test doesn't validate that the updates actually are reflected in QB
+    });
 
-            setInputValue(filterPopover.find('textarea.border-purple'), "gmail");
-            await clickButton(addFilterButton);
+    it("should allow admin to create segments", async () => {
+      const store = await createTestStore();
 
-            await store.waitForActions([UPDATE_PREVIEW_SUMMARY]);
+      // Open the People table admin page
+      store.pushPath("/admin/datamodel/database/1/table/2");
+      const app = mount(store.getAppContainer());
 
-            // Add name and description
-            setInputValue(app.find("input[name='name']"), "Gmail users")
-            setInputValue(app.find("textarea[name='description']"), "change")
+      await store.waitForActions([INITIALIZE_METADATA, FETCH_IDFIELDS]);
 
-            // Save the segment
-            click(app.find('button[children="Save changes"]'))
+      // Click the new segment button and check that we get properly redirected
+      click(app.find(SegmentsList).find(Link));
+      expect(store.getPath()).toBe("/admin/datamodel/segment/create?table=2");
+      await store.waitForActions([
+        FETCH_TABLE_METADATA,
+        UPDATE_PREVIEW_SUMMARY,
+      ]);
 
-            await store.waitForActions([CREATE_SEGMENT, INITIALIZE_METADATA]);
-            expect(store.getPath()).toBe("/admin/datamodel/database/1/table/2")
+      // Add "Email Is Not gmail" filter
+      click(app.find(".GuiBuilder-filtered-by a").first());
 
-            // Validate that the segment got actually added
-            expect(app.find(SegmentsList).find(SegmentItem).first().text()).toEqual("Gmail usersFiltered by Email");
-        })
+      const filterPopover = app.find(FilterPopover);
+      click(filterPopover.find(FieldList).find('h4[children="Email"]'));
 
-        it("should allow admin to create metrics", async () => {
-            const store = await createTestStore();
+      // click to expand options
+      const operatorSelector = filterPopover.find(OperatorSelector);
+      click(operatorSelector);
+      // click "Is Not"
+      clickButton(operatorSelector.find('[children="Is not"]'));
 
-            // Open the People table admin page
-            store.pushPath('/admin/datamodel/database/1/table/2');
-            const app = mount(store.getAppContainer())
+      const addFilterButton = filterPopover.find(".Button.disabled");
 
-            await store.waitForActions([INITIALIZE_METADATA, FETCH_IDFIELDS]);
+      setInputValue(filterPopover.find("input"), "gmail");
+      await clickButton(addFilterButton);
 
-            // Click the new metric button and check that we get properly redirected
-            click(app.find(MetricsList).find(Link));
-            expect(store.getPath()).toBe('/admin/datamodel/metric/create?table=2')
-            await store.waitForActions([FETCH_TABLE_METADATA, UPDATE_PREVIEW_SUMMARY]);
+      await store.waitForActions([UPDATE_PREVIEW_SUMMARY]);
 
-            click(app.find("#Query-section-aggregation"));
-            click(app.find("#AggregationPopover").find('h4[children="Count of rows"]'))
+      // Add name and description
+      setInputValue(app.find("input[name='name']"), "Gmail users");
+      setInputValue(app.find("textarea[name='description']"), "change");
 
-            setInputValue(app.find("input[name='name']"), 'User count');
-            setInputValue(app.find("textarea[name='description']"), 'Total number of users');
+      // Save the segment
+      click(app.find('button[children="Save changes"]'));
 
-            // Save the metric
-            click(app.find('button[children="Save changes"]'))
+      await store.waitForActions([CREATE_SEGMENT, INITIALIZE_METADATA]);
+      expect(store.getPath()).toBe("/admin/datamodel/database/1/table/2");
 
-            await store.waitForActions([CREATE_METRIC, INITIALIZE_METADATA]);
-            expect(store.getPath()).toBe("/admin/datamodel/database/1/table/2")
+      // Validate that the segment got actually added
+      expect(
+        app
+          .find(SegmentsList)
+          .find(SegmentItem)
+          .first()
+          .text(),
+      ).toEqual("Gmail usersFiltered by Email");
+    });
 
-            // Validate that the segment got actually added
-            expect(app.find(MetricsList).find(MetricItem).first().text()).toEqual("User countCount");
-        });
+    it("should allow admin to create metrics", async () => {
+      const store = await createTestStore();
+
+      // Open the People table admin page
+      store.pushPath("/admin/datamodel/database/1/table/2");
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([INITIALIZE_METADATA, FETCH_IDFIELDS]);
+
+      // Click the new metric button and check that we get properly redirected
+      click(app.find(MetricsList).find(Link));
+      expect(store.getPath()).toBe("/admin/datamodel/metric/create?table=2");
+      await store.waitForActions([
+        FETCH_TABLE_METADATA,
+        UPDATE_PREVIEW_SUMMARY,
+      ]);
+
+      click(app.find("#Query-section-aggregation"));
+      click(
+        app.find("#AggregationPopover").find('h4[children="Count of rows"]'),
+      );
+
+      setInputValue(app.find("input[name='name']"), "User count");
+      setInputValue(
+        app.find("textarea[name='description']"),
+        "Total number of users",
+      );
+
+      // Save the metric
+      click(app.find('button[children="Save changes"]'));
+
+      await store.waitForActions([CREATE_METRIC, INITIALIZE_METADATA]);
+      expect(store.getPath()).toBe("/admin/datamodel/database/1/table/2");
+
+      // Validate that the segment got actually added
+      expect(
+        app
+          .find(MetricsList)
+          .find(MetricItem)
+          .first()
+          .text(),
+      ).toEqual("User countCount");
+    });
 
-        afterAll(async () => {
-            await MetabaseApi.table_update({ id: 1, visibility_type: null}); // Sample Dataset
-            await MetabaseApi.field_update({ id: 8, visibility_type: "normal", special_type: null }) // Address
-            await MetabaseApi.field_update({ id: 9, visibility_type: "normal"}) // Address
-        })
+    afterAll(async () => {
+      await MetabaseApi.table_update({ id: 1, visibility_type: null }); // Sample Dataset
+      await MetabaseApi.field_update({
+        id: 8,
+        visibility_type: "normal",
+        special_type: null,
+      }); // Address
+      await MetabaseApi.field_update({ id: 9, visibility_type: "normal" }); // Address
     });
+  });
 });
diff --git a/frontend/test/admin/people/people.integ.spec.js b/frontend/test/admin/people/people.integ.spec.js
index 9d594f6af8c806b85e97c1edcec6a1de5d4e77aa..5ccb235b755385defc3558db1e741b1a3ca83254 100644
--- a/frontend/test/admin/people/people.integ.spec.js
+++ b/frontend/test/admin/people/people.integ.spec.js
@@ -1,18 +1,18 @@
 // Converted from a Selenium E2E test
 import {
-    createTestStore,
-    useSharedAdminLogin
+  createTestStore,
+  useSharedAdminLogin,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton,
-    setInputValue
-} from "__support__/enzyme_utils"
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 import { mount } from "enzyme";
 import {
-    CREATE_MEMBERSHIP,
-    CREATE_USER, FETCH_USERS, LOAD_GROUPS, LOAD_MEMBERSHIPS,
-    SHOW_MODAL, UPDATE_USER
+  CREATE_MEMBERSHIP,
+  CREATE_USER,
+  FETCH_USERS,
+  LOAD_GROUPS,
+  LOAD_MEMBERSHIPS,
+  SHOW_MODAL,
+  UPDATE_USER,
 } from "metabase/admin/people/people";
 import ModalContent from "metabase/components/ModalContent";
 import { delay } from "metabase/lib/promise";
@@ -24,99 +24,133 @@ import { UserApi } from "metabase/services";
 import UserActionsSelect from "metabase/admin/people/components/UserActionsSelect";
 
 describe("admin/people", () => {
-    let createdUserId = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    })
-
-    describe("user management", () => {
-        it("should allow admin to create new users", async () => {
-            const store = await createTestStore();
-            store.pushPath("/admin/people");
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_USERS, LOAD_GROUPS, LOAD_MEMBERSHIPS])
-
-            const email = "testy" + Math.round(Math.random()*10000) + "@metabase.com";
-            const firstName = "Testy";
-            const lastName = "McTestFace";
-
-            click(app.find('button[children="Add someone"]'));
-            await store.waitForActions([SHOW_MODAL])
-            await delay(1000);
-
-            const addUserModal = app.find(ModalContent);
-            const addButton = addUserModal.find('div[children="Add"]').closest(Button)
-            expect(addButton.props().disabled).toBe(true);
-
-            setInputValue(addUserModal.find("input[name='firstName']"), firstName)
-            setInputValue(addUserModal.find("input[name='lastName']"), lastName)
-            setInputValue(addUserModal.find("input[name='email']"), email)
-
-            expect(addButton.props().disabled).toBe(false);
-            clickButton(addButton)
-
-            await store.waitForActions([CREATE_USER])
-
-            // it should be a pretty safe assumption in test environment that the user that was just created has the biggest ID
-            const userIds = Object.keys(getUsers(store.getState()))
-            createdUserId = Math.max.apply(null, userIds.map((key) => parseInt(key)))
-
-            click(addUserModal.find('a[children="Show"]'))
-            const password = addUserModal.find("input").prop("value");
-
-            // "Done" button
-            click(addUserModal.find(".Button.Button--primary"))
-
-            const usersTable = app.find('.ContentTable')
-            const userRow = usersTable.find(`td[children="${email}"]`).closest("tr")
-            expect(userRow.find("td").first().find("span").last().text()).toBe(`${firstName} ${lastName}`);
-
-            // add admin permissions
-            const userGroupSelect = userRow.find(UserGroupSelect);
-            expect(userGroupSelect.text()).toBe("Default");
-            click(userGroupSelect)
-
-            click(app.find(".TestPopover").find(GroupOption).first());
-            await store.waitForActions([CREATE_MEMBERSHIP])
-
-            // edit user details
-            click(userRow.find(UserActionsSelect))
-            click(app.find(".TestPopover").find('li[children="Edit Details"]'))
-
-            const editDetailsModal = app.find(ModalContent);
-
-            const saveButton = editDetailsModal.find('div[children="Save changes"]').closest(Button)
-            expect(saveButton.props().disabled).toBe(true);
-
-            setInputValue(editDetailsModal.find("input[name='firstName']"), firstName + "x")
-            setInputValue(editDetailsModal.find("input[name='lastName']"), lastName + "x")
-            setInputValue(editDetailsModal.find("input[name='email']"), email + "x")
-            expect(saveButton.props().disabled).toBe(false);
-
-            await clickButton(saveButton)
-            await store.waitForActions([UPDATE_USER])
-
-            const updatedUserRow = usersTable.find(`td[children="${email}x"]`).closest("tr")
-            expect(updatedUserRow.find("td").first().find("span").last().text()).toBe(`${firstName}x ${lastName}x`);
-
-            click(userRow.find(UserActionsSelect))
-            click(app.find(".TestPopover").find('li[children="Reset Password"]'))
-
-            const resetPasswordModal = app.find(ModalContent);
-            const resetButton = resetPasswordModal.find('div[children="Reset"]').closest(Button)
-            click(resetButton);
-            click(resetPasswordModal.find('a[children="Show"]'))
-            const newPassword = resetPasswordModal.find("input").prop("value");
-
-            expect(newPassword).not.toEqual(password);
-        });
+  let createdUserId = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("user management", () => {
+    it("should allow admin to create new users", async () => {
+      const store = await createTestStore();
+      store.pushPath("/admin/people");
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_USERS, LOAD_GROUPS, LOAD_MEMBERSHIPS]);
+
+      const email =
+        "testy" + Math.round(Math.random() * 10000) + "@metabase.com";
+      const firstName = "Testy";
+      const lastName = "McTestFace";
+
+      click(app.find('button[children="Add someone"]'));
+      await store.waitForActions([SHOW_MODAL]);
+      await delay(1000);
+
+      const addUserModal = app.find(ModalContent);
+      const addButton = addUserModal
+        .find('div[children="Add"]')
+        .closest(Button);
+      expect(addButton.props().disabled).toBe(true);
+
+      setInputValue(addUserModal.find("input[name='firstName']"), firstName);
+      setInputValue(addUserModal.find("input[name='lastName']"), lastName);
+      setInputValue(addUserModal.find("input[name='email']"), email);
+
+      expect(addButton.props().disabled).toBe(false);
+      clickButton(addButton);
+
+      await store.waitForActions([CREATE_USER]);
+
+      // it should be a pretty safe assumption in test environment that the user that was just created has the biggest ID
+      const userIds = Object.keys(getUsers(store.getState()));
+      createdUserId = Math.max.apply(null, userIds.map(key => parseInt(key)));
+
+      click(addUserModal.find('a[children="Show"]'));
+      const password = addUserModal.find("input").prop("value");
+
+      // "Done" button
+      click(addUserModal.find(".Button.Button--primary"));
+
+      const usersTable = app.find(".ContentTable");
+      const userRow = usersTable.find(`td[children="${email}"]`).closest("tr");
+      expect(
+        userRow
+          .find("td")
+          .first()
+          .find("span")
+          .last()
+          .text(),
+      ).toBe(`${firstName} ${lastName}`);
+
+      // add admin permissions
+      const userGroupSelect = userRow.find(UserGroupSelect);
+      expect(userGroupSelect.text()).toBe("Default");
+      click(userGroupSelect);
+
+      click(
+        app
+          .find(".TestPopover")
+          .find(GroupOption)
+          .first(),
+      );
+      await store.waitForActions([CREATE_MEMBERSHIP]);
+
+      // edit user details
+      click(userRow.find(UserActionsSelect));
+      click(app.find(".TestPopover").find('li[children="Edit Details"]'));
+
+      const editDetailsModal = app.find(ModalContent);
+
+      const saveButton = editDetailsModal
+        .find('div[children="Save changes"]')
+        .closest(Button);
+      expect(saveButton.props().disabled).toBe(true);
+
+      setInputValue(
+        editDetailsModal.find("input[name='firstName']"),
+        firstName + "x",
+      );
+      setInputValue(
+        editDetailsModal.find("input[name='lastName']"),
+        lastName + "x",
+      );
+      setInputValue(editDetailsModal.find("input[name='email']"), email + "x");
+      expect(saveButton.props().disabled).toBe(false);
+
+      await clickButton(saveButton);
+      await store.waitForActions([UPDATE_USER]);
+
+      const updatedUserRow = usersTable
+        .find(`td[children="${email}x"]`)
+        .closest("tr");
+      expect(
+        updatedUserRow
+          .find("td")
+          .first()
+          .find("span")
+          .last()
+          .text(),
+      ).toBe(`${firstName}x ${lastName}x`);
+
+      click(userRow.find(UserActionsSelect));
+      click(app.find(".TestPopover").find('li[children="Reset Password"]'));
+
+      const resetPasswordModal = app.find(ModalContent);
+      const resetButton = resetPasswordModal
+        .find('div[children="Reset"]')
+        .closest(Button);
+      click(resetButton);
+      click(resetPasswordModal.find('a[children="Show"]'));
+      const newPassword = resetPasswordModal.find("input").prop("value");
+
+      expect(newPassword).not.toEqual(password);
+    });
 
-        afterAll(async () => {
-            // Test cleanup
-            if (createdUserId) {
-                await UserApi.delete({ userId: createdUserId });
-            }
-        })
+    afterAll(async () => {
+      // Test cleanup
+      if (createdUserId) {
+        await UserApi.delete({ userId: createdUserId });
+      }
     });
+  });
 });
diff --git a/frontend/test/admin/permissions/selectors.unit.spec.fixtures.js b/frontend/test/admin/permissions/selectors.unit.spec.fixtures.js
index e6e4109acd2a636a20a1ebb4a95a82d3544c0b2e..c32096c4175f69cc944010fc5990a3415e80f13a 100644
--- a/frontend/test/admin/permissions/selectors.unit.spec.fixtures.js
+++ b/frontend/test/admin/permissions/selectors.unit.spec.fixtures.js
@@ -3,91 +3,87 @@
 // (A single-schema database was originally Database 1 but it got removed as testing against it felt redundant)
 
 export const normalizedMetadata = {
-    "metrics": {},
-    "segments": {},
-    "databases": {
-        "2": {
-            "name": "Imaginary Multi-Schema Dataset",
-            "tables": [
-                // In schema_1
-                5,
-                6,
-                // In schema_2
-                7,
-                8,
-                9
-            ],
-            "id": 2
-        },
-        "3": {
-            "name": "Imaginary Schemaless Dataset",
-            "tables": [
-                10,
-                11,
-                12,
-                13
-            ],
-            "id": 3
-        }
+  metrics: {},
+  segments: {},
+  databases: {
+    "2": {
+      name: "Imaginary Multi-Schema Dataset",
+      tables: [
+        // In schema_1
+        5,
+        6,
+        // In schema_2
+        7,
+        8,
+        9,
+      ],
+      id: 2,
     },
-    "tables": {
-        "5": {
-            "schema": "schema_1",
-            "name": "Avian Singles Messages",
-            "id": 5,
-            "db_id": 2
-        },
-        "6": {
-            "schema": "schema_1",
-            "name": "Avian Singles Users",
-            "id": 6,
-            "db_id": 2
-        },
-        "7": {
-            "schema": "schema_2",
-            "name": "Tupac Sightings Sightings",
-            "id": 7,
-            "db_id": 2
-        },
-        "8": {
-            "schema": "schema_2",
-            "name": "Tupac Sightings Categories",
-            "id": 8,
-            "db_id": 2
-        },
-        "9": {
-            "schema": "schema_2",
-            "name": "Tupac Sightings Cities",
-            "id": 9,
-            "db_id": 2
-        },
-        "10": {
-            "schema": null,
-            "name": "Badminton Men's Double Results",
-            "id": 10,
-            "db_id": 3
-        },
-        "11": {
-            "schema": null,
-            "name": "Badminton Mixed Double Results",
-            "id": 11,
-            "db_id": 3
-        },
-        "12": {
-            "schema": null,
-            "name": "Badminton Women's Singles Results",
-            "id": 12,
-            "db_id": 3
-        },
-        "13": {
-            "schema": null,
-            "name": "Badminton Mixed Singles Results",
-            "id": 13,
-            "db_id": 3
-        },
+    "3": {
+      name: "Imaginary Schemaless Dataset",
+      tables: [10, 11, 12, 13],
+      id: 3,
     },
-    "fields": {/* stripped out */},
-    "revisions": {},
-    "databasesList": [2, 3]
+  },
+  tables: {
+    "5": {
+      schema: "schema_1",
+      name: "Avian Singles Messages",
+      id: 5,
+      db_id: 2,
+    },
+    "6": {
+      schema: "schema_1",
+      name: "Avian Singles Users",
+      id: 6,
+      db_id: 2,
+    },
+    "7": {
+      schema: "schema_2",
+      name: "Tupac Sightings Sightings",
+      id: 7,
+      db_id: 2,
+    },
+    "8": {
+      schema: "schema_2",
+      name: "Tupac Sightings Categories",
+      id: 8,
+      db_id: 2,
+    },
+    "9": {
+      schema: "schema_2",
+      name: "Tupac Sightings Cities",
+      id: 9,
+      db_id: 2,
+    },
+    "10": {
+      schema: null,
+      name: "Badminton Men's Double Results",
+      id: 10,
+      db_id: 3,
+    },
+    "11": {
+      schema: null,
+      name: "Badminton Mixed Double Results",
+      id: 11,
+      db_id: 3,
+    },
+    "12": {
+      schema: null,
+      name: "Badminton Women's Singles Results",
+      id: 12,
+      db_id: 3,
+    },
+    "13": {
+      schema: null,
+      name: "Badminton Mixed Singles Results",
+      id: 13,
+      db_id: 3,
+    },
+  },
+  fields: {
+    /* stripped out */
+  },
+  revisions: {},
+  databasesList: [2, 3],
 };
-
diff --git a/frontend/test/admin/permissions/selectors.unit.spec.js b/frontend/test/admin/permissions/selectors.unit.spec.js
index e057b6d20143c4735b1fc635fb7ea16511476656..8e34bd81d096e4545ca65082bb0dc9b2de274d31 100644
--- a/frontend/test/admin/permissions/selectors.unit.spec.js
+++ b/frontend/test/admin/permissions/selectors.unit.spec.js
@@ -7,462 +7,624 @@
 
 import { setIn } from "icepick";
 
-jest.mock('metabase/lib/analytics');
+jest.mock("metabase/lib/analytics");
 
-import {GroupsPermissions} from "metabase/meta/types/Permissions";
+import { GroupsPermissions } from "metabase/meta/types/Permissions";
 import { normalizedMetadata } from "./selectors.unit.spec.fixtures";
-import { getTablesPermissionsGrid, getSchemasPermissionsGrid, getDatabasesPermissionsGrid } from "metabase/admin/permissions/selectors";
+import {
+  getTablesPermissionsGrid,
+  getSchemasPermissionsGrid,
+  getDatabasesPermissionsGrid,
+} from "metabase/admin/permissions/selectors";
 
 /******** INITIAL TEST STATE ********/
 
-const groups = [{
+const groups = [
+  {
     id: 1,
     name: "Group starting with full access",
-}, {
+  },
+  {
     id: 2,
     name: "Group starting with no access at all",
-}];
+  },
+];
 
 const initialPermissions: GroupsPermissions = {
+  1: {
+    // Sample dataset
     1: {
-        // Sample dataset
-        1: {
-            "native": "write",
-            "schemas": "all"
-        },
-        // Imaginary multi-schema
-        2: {
-            "native": "write",
-            "schemas": "all"
-        },
-        // Imaginary schemaless
-        3: {
-            "native": "write",
-            "schemas": "all"
-        }
+      native: "write",
+      schemas: "all",
     },
+    // Imaginary multi-schema
     2: {
-        // Sample dataset
-        1: {
-            "native": "none",
-            "schemas": "none"
-        },
-        // Imaginary multi-schema
-        2: {
-            "native": "none",
-            "schemas": "none"
-        },
-        // Imaginary schemaless
-        3: {
-            "native": "none",
-            "schemas": "none"
-        }
-    }
+      native: "write",
+      schemas: "all",
+    },
+    // Imaginary schemaless
+    3: {
+      native: "write",
+      schemas: "all",
+    },
+  },
+  2: {
+    // Sample dataset
+    1: {
+      native: "none",
+      schemas: "none",
+    },
+    // Imaginary multi-schema
+    2: {
+      native: "none",
+      schemas: "none",
+    },
+    // Imaginary schemaless
+    3: {
+      native: "none",
+      schemas: "none",
+    },
+  },
 };
 
-
 /******** MANAGING THE CURRENT (SIMULATED) STATE TREE ********/
 
 const initialState = {
-    admin: {
-        permissions: {
-            permissions: initialPermissions,
-            originalPermissions: initialPermissions,
-            groups
-        }
+  admin: {
+    permissions: {
+      permissions: initialPermissions,
+      originalPermissions: initialPermissions,
+      groups,
     },
-    metadata: normalizedMetadata
+  },
+  metadata: normalizedMetadata,
 };
 
 var state = initialState;
-const resetState = () => { state = initialState };
+const resetState = () => {
+  state = initialState;
+};
 const getPermissionsTree = () => state.admin.permissions.permissions;
-const getPermissionsForDb = ({ entityId, groupId }) => getPermissionsTree()[groupId][entityId.databaseId];
+const getPermissionsForDb = ({ entityId, groupId }) =>
+  getPermissionsTree()[groupId][entityId.databaseId];
 
-const updatePermissionsInState = (permissions) => {
-    state = setIn(state, ["admin", "permissions", "permissions"], permissions);
+const updatePermissionsInState = permissions => {
+  state = setIn(state, ["admin", "permissions", "permissions"], permissions);
 };
 
 const getProps = ({ databaseId, schemaName }) => ({
-    params: {
-        databaseId,
-            schemaName
-    }
+  params: {
+    databaseId,
+    schemaName,
+  },
 });
 
 /******** HIGH-LEVEL METHODS FOR UPDATING PERMISSIONS ********/
 
-const changePermissionsForEntityInGrid = ({ grid, category, entityId, groupId, permission }) => {
-    const newPermissions = grid.permissions[category].updater(groupId, entityId, permission);
-    updatePermissionsInState(newPermissions);
-    return newPermissions;
+const changePermissionsForEntityInGrid = ({
+  grid,
+  category,
+  entityId,
+  groupId,
+  permission,
+}) => {
+  const newPermissions = grid.permissions[category].updater(
+    groupId,
+    entityId,
+    permission,
+  );
+  updatePermissionsInState(newPermissions);
+  return newPermissions;
 };
 
-const changeDbNativePermissionsForEntity = ({ entityId, groupId, permission }) => {
-    const grid = getDatabasesPermissionsGrid(state, getProps(entityId));
-    return changePermissionsForEntityInGrid({ grid, category: "native", entityId, groupId, permission });
+const changeDbNativePermissionsForEntity = ({
+  entityId,
+  groupId,
+  permission,
+}) => {
+  const grid = getDatabasesPermissionsGrid(state, getProps(entityId));
+  return changePermissionsForEntityInGrid({
+    grid,
+    category: "native",
+    entityId,
+    groupId,
+    permission,
+  });
 };
 
-const changeDbDataPermissionsForEntity = ({ entityId, groupId, permission }) => {
-    const grid = getDatabasesPermissionsGrid(state, getProps(entityId));
-    return changePermissionsForEntityInGrid({ grid, category: "schemas", entityId, groupId, permission });
+const changeDbDataPermissionsForEntity = ({
+  entityId,
+  groupId,
+  permission,
+}) => {
+  const grid = getDatabasesPermissionsGrid(state, getProps(entityId));
+  return changePermissionsForEntityInGrid({
+    grid,
+    category: "schemas",
+    entityId,
+    groupId,
+    permission,
+  });
 };
 
-const changeSchemaPermissionsForEntity = ({ entityId, groupId, permission }) => {
-    const grid = getSchemasPermissionsGrid(state, getProps(entityId));
-    return changePermissionsForEntityInGrid({ grid, category: "tables", entityId, groupId, permission });
+const changeSchemaPermissionsForEntity = ({
+  entityId,
+  groupId,
+  permission,
+}) => {
+  const grid = getSchemasPermissionsGrid(state, getProps(entityId));
+  return changePermissionsForEntityInGrid({
+    grid,
+    category: "tables",
+    entityId,
+    groupId,
+    permission,
+  });
 };
 
 const changeTablePermissionsForEntity = ({ entityId, groupId, permission }) => {
-    const grid = getTablesPermissionsGrid(state, getProps(entityId));
-    return changePermissionsForEntityInGrid({ grid, category: "fields", entityId, groupId, permission });
+  const grid = getTablesPermissionsGrid(state, getProps(entityId));
+  return changePermissionsForEntityInGrid({
+    grid,
+    category: "fields",
+    entityId,
+    groupId,
+    permission,
+  });
 };
 
-const getMethodsForDbAndSchema = (entityId) => ({
-    changeDbNativePermissions: ({ groupId, permission }) =>
-        changeDbNativePermissionsForEntity({ entityId, groupId, permission }),
-    changeDbDataPermissions: ({ groupId, permission }) =>
-        changeDbDataPermissionsForEntity({ entityId, groupId, permission }),
-    changeSchemaPermissions: ({ groupId, permission }) =>
-        changeSchemaPermissionsForEntity({ entityId, groupId, permission }),
-    changeTablePermissions: ({ tableId, groupId, permission }) =>
-        changeTablePermissionsForEntity({ entityId: {...entityId, tableId}, groupId, permission }),
-    getPermissions: ({ groupId }) =>
-        getPermissionsForDb({ entityId, groupId })
+const getMethodsForDbAndSchema = entityId => ({
+  changeDbNativePermissions: ({ groupId, permission }) =>
+    changeDbNativePermissionsForEntity({ entityId, groupId, permission }),
+  changeDbDataPermissions: ({ groupId, permission }) =>
+    changeDbDataPermissionsForEntity({ entityId, groupId, permission }),
+  changeSchemaPermissions: ({ groupId, permission }) =>
+    changeSchemaPermissionsForEntity({ entityId, groupId, permission }),
+  changeTablePermissions: ({ tableId, groupId, permission }) =>
+    changeTablePermissionsForEntity({
+      entityId: { ...entityId, tableId },
+      groupId,
+      permission,
+    }),
+  getPermissions: ({ groupId }) => getPermissionsForDb({ entityId, groupId }),
 });
 
 /******** ACTUAL TESTS ********/
 
 describe("permissions selectors", () => {
-    beforeEach(resetState);
-
-    describe("for a schemaless dataset", () => {
-        // Schema "name" (better description would be a "permission path identifier") is simply an empty string
-        // for databases where the metadata value for table schema is `null`
-        const schemalessDataset = getMethodsForDbAndSchema({ databaseId: 3, schemaName: "" });
-
-        it("should restrict access correctly on table level", () => {
-            // Revoking access to one table should downgrade the native permissions to "read"
-            schemalessDataset.changeTablePermissions({ tableId: 10, groupId: 1, permission: "none" });
-            expect(schemalessDataset.getPermissions({ groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": {
-                    "": {
-                        "10": "none",
-                        "11": "all",
-                        "12": "all",
-                        "13": "all"
-                    }
-                }
-            });
-
-            // Revoking access to the rest of tables one-by-one...
-            schemalessDataset.changeTablePermissions({ tableId: 11, groupId: 1, permission: "none" });
-            schemalessDataset.changeTablePermissions({ tableId: 12, groupId: 1, permission: "none" });
-            schemalessDataset.changeTablePermissions({ tableId: 13, groupId: 1, permission: "none" });
-
-            expect(schemalessDataset.getPermissions({groupId: 1})).toMatchObject({
-                // ...should revoke all permissions for that database
-                "native": "none",
-                "schemas": "none"
-            });
-
-        });
-
-        it("should restrict access correctly on db level", () => {
-            // Should let change the native permission to "read"
-            schemalessDataset.changeDbNativePermissions({ groupId: 1, permission: "read" });
-            expect(schemalessDataset.getPermissions({groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": "all"
-            });
-
-            // Should not let change the native permission to none
-            schemalessDataset.changeDbNativePermissions({ groupId: 1, permission: "none" });
-            expect(schemalessDataset.getPermissions({groupId: 1})).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-
-            resetState(); // ad-hoc state reset for the next test
-            // Revoking the data access to the database at once should revoke all permissions for that database
-            schemalessDataset.changeDbDataPermissions({ groupId: 1, permission: "none" });
-            expect(schemalessDataset.getPermissions({groupId: 1})).toMatchObject({
-                "native": "none",
-                "schemas": "none"
-            });
-        });
-
-        it("should grant more access correctly on table level", () => {
-            // Simply grant an access to a single table
-            schemalessDataset.changeTablePermissions({ tableId: 12, groupId: 2, permission: "all" });
-            expect(schemalessDataset.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "": {
-                        "10": "none",
-                        "11": "none",
-                        "12": "all",
-                        "13": "none"
-                    }
-                }
-            });
-
-            // Grant the access to rest of tables
-            schemalessDataset.changeTablePermissions({ tableId: 10, groupId: 2, permission: "all" });
-            schemalessDataset.changeTablePermissions({ tableId: 11, groupId: 2, permission: "all" });
-            schemalessDataset.changeTablePermissions({ tableId: 13, groupId: 2, permission: "all" });
-            expect(schemalessDataset.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-
-
-            // Should pass changes to native permissions through
-            schemalessDataset.changeDbNativePermissions({ groupId: 2, permission: "read" });
-            expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "read",
-                "schemas": "all"
-            });
-        });
-
-        it("should grant more access correctly on db level", () => {
-            // Setting limited access should produce a permission tree where each schema has "none" access
-            // (this is a strange, rather no-op edge case but the UI currently enables this)
-            schemalessDataset.changeDbDataPermissions({ groupId: 2, permission: "controlled" });
-            expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "": "none"
-                }
-            });
-
-            // Granting native access should also grant a full write access
-            schemalessDataset.changeDbNativePermissions({ groupId: 2, permission: "write" });
-            expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "write",
-                "schemas": "all"
-            });
-
-            resetState(); // ad-hoc reset (normally run before tests)
-            // test that setting full access works too
-            schemalessDataset.changeDbDataPermissions({ groupId: 2, permission: "all" });
-            expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-        })
+  beforeEach(resetState);
+
+  describe("for a schemaless dataset", () => {
+    // Schema "name" (better description would be a "permission path identifier") is simply an empty string
+    // for databases where the metadata value for table schema is `null`
+    const schemalessDataset = getMethodsForDbAndSchema({
+      databaseId: 3,
+      schemaName: "",
+    });
+
+    it("should restrict access correctly on table level", () => {
+      // Revoking access to one table should downgrade the native permissions to "read"
+      schemalessDataset.changeTablePermissions({
+        tableId: 10,
+        groupId: 1,
+        permission: "none",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: {
+          "": {
+            "10": "none",
+            "11": "all",
+            "12": "all",
+            "13": "all",
+          },
+        },
+      });
+
+      // Revoking access to the rest of tables one-by-one...
+      schemalessDataset.changeTablePermissions({
+        tableId: 11,
+        groupId: 1,
+        permission: "none",
+      });
+      schemalessDataset.changeTablePermissions({
+        tableId: 12,
+        groupId: 1,
+        permission: "none",
+      });
+      schemalessDataset.changeTablePermissions({
+        tableId: 13,
+        groupId: 1,
+        permission: "none",
+      });
+
+      expect(schemalessDataset.getPermissions({ groupId: 1 })).toMatchObject({
+        // ...should revoke all permissions for that database
+        native: "none",
+        schemas: "none",
+      });
+    });
+
+    it("should restrict access correctly on db level", () => {
+      // Should let change the native permission to "read"
+      schemalessDataset.changeDbNativePermissions({
+        groupId: 1,
+        permission: "read",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: "all",
+      });
+
+      // Should not let change the native permission to none
+      schemalessDataset.changeDbNativePermissions({
+        groupId: 1,
+        permission: "none",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
+
+      resetState(); // ad-hoc state reset for the next test
+      // Revoking the data access to the database at once should revoke all permissions for that database
+      schemalessDataset.changeDbDataPermissions({
+        groupId: 1,
+        permission: "none",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "none",
+        schemas: "none",
+      });
+    });
+
+    it("should grant more access correctly on table level", () => {
+      // Simply grant an access to a single table
+      schemalessDataset.changeTablePermissions({
+        tableId: 12,
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          "": {
+            "10": "none",
+            "11": "none",
+            "12": "all",
+            "13": "none",
+          },
+        },
+      });
+
+      // Grant the access to rest of tables
+      schemalessDataset.changeTablePermissions({
+        tableId: 10,
+        groupId: 2,
+        permission: "all",
+      });
+      schemalessDataset.changeTablePermissions({
+        tableId: 11,
+        groupId: 2,
+        permission: "all",
+      });
+      schemalessDataset.changeTablePermissions({
+        tableId: 13,
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
+
+      // Should pass changes to native permissions through
+      schemalessDataset.changeDbNativePermissions({
+        groupId: 2,
+        permission: "read",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "read",
+        schemas: "all",
+      });
+    });
+
+    it("should grant more access correctly on db level", () => {
+      // Setting limited access should produce a permission tree where each schema has "none" access
+      // (this is a strange, rather no-op edge case but the UI currently enables this)
+      schemalessDataset.changeDbDataPermissions({
+        groupId: 2,
+        permission: "controlled",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          "": "none",
+        },
+      });
+
+      // Granting native access should also grant a full write access
+      schemalessDataset.changeDbNativePermissions({
+        groupId: 2,
+        permission: "write",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "write",
+        schemas: "all",
+      });
+
+      resetState(); // ad-hoc reset (normally run before tests)
+      // test that setting full access works too
+      schemalessDataset.changeDbDataPermissions({
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schemalessDataset.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
+    });
+  });
+
+  describe("for a dataset with multiple schemas", () => {
+    const schema1 = getMethodsForDbAndSchema({
+      databaseId: 2,
+      schemaName: "schema_1",
+    });
+    const schema2 = getMethodsForDbAndSchema({
+      databaseId: 2,
+      schemaName: "schema_2",
+    });
+
+    it("should restrict access correctly on table level", () => {
+      // Revoking access to one table should downgrade the native permissions to "read"
+      schema1.changeTablePermissions({
+        tableId: 5,
+        groupId: 1,
+        permission: "none",
+      });
+      expect(schema1.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: {
+          schema_1: {
+            "5": "none",
+            "6": "all",
+          },
+          schema_2: "all",
+        },
+      });
+
+      // State where both schemas have mixed permissions
+      schema2.changeTablePermissions({
+        tableId: 8,
+        groupId: 1,
+        permission: "none",
+      });
+      schema2.changeTablePermissions({
+        tableId: 9,
+        groupId: 1,
+        permission: "none",
+      });
+      expect(schema2.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: {
+          schema_1: {
+            "5": "none",
+            "6": "all",
+          },
+          schema_2: {
+            "7": "all",
+            "8": "none",
+            "9": "none",
+          },
+        },
+      });
+
+      // Completely revoke access to the first schema with table-level changes
+      schema1.changeTablePermissions({
+        tableId: 6,
+        groupId: 1,
+        permission: "none",
+      });
+
+      expect(schema1.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: {
+          schema_1: "none",
+          schema_2: {
+            "7": "all",
+            "8": "none",
+            "9": "none",
+          },
+        },
+      });
+
+      // Revoking all permissions of the other schema should revoke all db permissions too
+      schema2.changeTablePermissions({
+        tableId: 7,
+        groupId: 1,
+        permission: "none",
+      });
+      expect(schema2.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "none",
+        schemas: "none",
+      });
+    });
+
+    it("should restrict access correctly on schema level", () => {
+      // Revoking access to one schema
+      schema2.changeSchemaPermissions({ groupId: 1, permission: "none" });
+      expect(schema2.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: {
+          schema_1: "all",
+          schema_2: "none",
+        },
+      });
+
+      // Revoking access to other too
+      schema1.changeSchemaPermissions({ groupId: 1, permission: "none" });
+      expect(schema1.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "none",
+        schemas: "none",
+      });
     });
 
-    describe("for a dataset with multiple schemas", () => {
-        const schema1 = getMethodsForDbAndSchema({ databaseId: 2, schemaName: "schema_1" });
-        const schema2 = getMethodsForDbAndSchema({ databaseId: 2, schemaName: "schema_2" });
-
-        it("should restrict access correctly on table level", () => {
-            // Revoking access to one table should downgrade the native permissions to "read"
-            schema1.changeTablePermissions({ tableId: 5, groupId: 1, permission: "none" });
-            expect(schema1.getPermissions({ groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": {
-                    "schema_1": {
-                        "5": "none",
-                        "6": "all"
-                    },
-                    "schema_2": "all"
-                }
-            });
-
-            // State where both schemas have mixed permissions
-            schema2.changeTablePermissions({ tableId: 8, groupId: 1, permission: "none" });
-            schema2.changeTablePermissions({ tableId: 9, groupId: 1, permission: "none" });
-            expect(schema2.getPermissions({groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": {
-                    "schema_1": {
-                        "5": "none",
-                        "6": "all"
-                    },
-                    "schema_2": {
-                        "7": "all",
-                        "8": "none",
-                        "9": "none"
-                    }
-                }
-            });
-
-            // Completely revoke access to the first schema with table-level changes
-            schema1.changeTablePermissions({ tableId: 6, groupId: 1, permission: "none" });
-
-            expect(schema1.getPermissions({groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": {
-                    "schema_1": "none",
-                    "schema_2": {
-                        "7": "all",
-                        "8": "none",
-                        "9": "none"
-                    }
-                }
-            });
-
-            // Revoking all permissions of the other schema should revoke all db permissions too
-            schema2.changeTablePermissions({ tableId: 7, groupId: 1, permission: "none" });
-            expect(schema2.getPermissions({groupId: 1})).toMatchObject({
-                "native": "none",
-                "schemas": "none"
-            });
-        });
-
-        it("should restrict access correctly on schema level", () => {
-            // Revoking access to one schema
-            schema2.changeSchemaPermissions({ groupId: 1, permission: "none" });
-            expect(schema2.getPermissions({groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": {
-                    "schema_1": "all",
-                    "schema_2": "none"
-                }
-            });
-
-            // Revoking access to other too
-            schema1.changeSchemaPermissions({ groupId: 1, permission: "none" });
-            expect(schema1.getPermissions({groupId: 1})).toMatchObject({
-                "native": "none",
-                "schemas": "none"
-            });
-        });
-
-        it("should restrict access correctly on db level", () => {
-            // Should let change the native permission to "read"
-            schema1.changeDbNativePermissions({ groupId: 1, permission: "read" });
-            expect(schema1.getPermissions({groupId: 1})).toMatchObject({
-                "native": "read",
-                "schemas": "all"
-            });
-
-            // Should let change the native permission to none
-            schema1.changeDbNativePermissions({ groupId: 1, permission: "none" });
-            expect(schema1.getPermissions({groupId: 1})).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-
-            resetState(); // ad-hoc state reset for the next test
-            // Revoking the data access to the database at once should revoke all permissions for that database
-            schema1.changeDbDataPermissions({ groupId: 1, permission: "none" });
-            expect(schema1.getPermissions({groupId: 1})).toMatchObject({
-                "native": "none",
-                "schemas": "none"
-            });
-        });
-
-        it("should grant more access correctly on table level", () => {
-            // Simply grant an access to a single table
-            schema2.changeTablePermissions({ tableId: 7, groupId: 2, permission: "all" });
-            expect(schema2.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "schema_1": "none",
-                    "schema_2": {
-                        "7": "all",
-                        "8": "none",
-                        "9": "none"
-                    }
-                }
-            });
-
-            // State where both schemas have mixed permissions
-            schema1.changeTablePermissions({ tableId: 5, groupId: 2, permission: "all" });
-            expect(schema1.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "schema_1": {
-                        "5": "all",
-                        "6": "none"
-                    },
-                    "schema_2": {
-                        "7": "all",
-                        "8": "none",
-                        "9": "none"
-                    }
-                }
-            });
-
-            // Grant full access to the second schema
-            schema2.changeTablePermissions({ tableId: 8, groupId: 2, permission: "all" });
-            schema2.changeTablePermissions({ tableId: 9, groupId: 2, permission: "all" });
-            expect(schema2.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "schema_1": {
-                        "5": "all",
-                        "6": "none"
-                    },
-                    "schema_2": "all"
-                }
-            });
-
-            // Grant the access to whole db (no native yet)
-            schema1.changeTablePermissions({ tableId: 5, groupId: 2, permission: "all" });
-            schema1.changeTablePermissions({ tableId: 6, groupId: 2, permission: "all" });
-            expect(schema1.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-
-            // Should pass changes to native permissions through
-            schema1.changeDbNativePermissions({ groupId: 2, permission: "read" });
-            expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "read",
-                "schemas": "all"
-            });
-        });
-
-        it("should grant more access correctly on schema level", () => {
-            // Granting full access to one schema
-            schema1.changeSchemaPermissions({ groupId: 2, permission: "all" });
-            expect(schema1.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "schema_1": "all",
-                    "schema_2": "none"
-                }
-            });
-
-            // Granting access to the other as well
-            schema2.changeSchemaPermissions({ groupId: 2, permission: "all" });
-            expect(schema2.getPermissions({groupId: 2})).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-        });
-
-        it("should grant more access correctly on db level", () => {
-            // Setting limited access should produce a permission tree where each schema has "none" access
-            // (this is a strange, rather no-op edge case but the UI currently enables this)
-            schema1.changeDbDataPermissions({ groupId: 2, permission: "controlled" });
-            expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "none",
-                "schemas": {
-                    "schema_1": "none",
-                    "schema_2": "none"
-                }
-            });
-
-            // Granting native access should also grant a full write access
-            schema1.changeDbNativePermissions({ groupId: 2, permission: "write" });
-            expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "write",
-                "schemas": "all"
-            });
-
-            resetState(); // ad-hoc reset (normally run before tests)
-            // test that setting full access works too
-            schema1.changeDbDataPermissions({ groupId: 2, permission: "all" });
-            expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
-                "native": "none",
-                "schemas": "all"
-            });
-        })
+    it("should restrict access correctly on db level", () => {
+      // Should let change the native permission to "read"
+      schema1.changeDbNativePermissions({ groupId: 1, permission: "read" });
+      expect(schema1.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "read",
+        schemas: "all",
+      });
+
+      // Should let change the native permission to none
+      schema1.changeDbNativePermissions({ groupId: 1, permission: "none" });
+      expect(schema1.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
+
+      resetState(); // ad-hoc state reset for the next test
+      // Revoking the data access to the database at once should revoke all permissions for that database
+      schema1.changeDbDataPermissions({ groupId: 1, permission: "none" });
+      expect(schema1.getPermissions({ groupId: 1 })).toMatchObject({
+        native: "none",
+        schemas: "none",
+      });
+    });
+
+    it("should grant more access correctly on table level", () => {
+      // Simply grant an access to a single table
+      schema2.changeTablePermissions({
+        tableId: 7,
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schema2.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          schema_1: "none",
+          schema_2: {
+            "7": "all",
+            "8": "none",
+            "9": "none",
+          },
+        },
+      });
+
+      // State where both schemas have mixed permissions
+      schema1.changeTablePermissions({
+        tableId: 5,
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          schema_1: {
+            "5": "all",
+            "6": "none",
+          },
+          schema_2: {
+            "7": "all",
+            "8": "none",
+            "9": "none",
+          },
+        },
+      });
+
+      // Grant full access to the second schema
+      schema2.changeTablePermissions({
+        tableId: 8,
+        groupId: 2,
+        permission: "all",
+      });
+      schema2.changeTablePermissions({
+        tableId: 9,
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schema2.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          schema_1: {
+            "5": "all",
+            "6": "none",
+          },
+          schema_2: "all",
+        },
+      });
+
+      // Grant the access to whole db (no native yet)
+      schema1.changeTablePermissions({
+        tableId: 5,
+        groupId: 2,
+        permission: "all",
+      });
+      schema1.changeTablePermissions({
+        tableId: 6,
+        groupId: 2,
+        permission: "all",
+      });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
+
+      // Should pass changes to native permissions through
+      schema1.changeDbNativePermissions({ groupId: 2, permission: "read" });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "read",
+        schemas: "all",
+      });
+    });
+
+    it("should grant more access correctly on schema level", () => {
+      // Granting full access to one schema
+      schema1.changeSchemaPermissions({ groupId: 2, permission: "all" });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          schema_1: "all",
+          schema_2: "none",
+        },
+      });
+
+      // Granting access to the other as well
+      schema2.changeSchemaPermissions({ groupId: 2, permission: "all" });
+      expect(schema2.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
+    });
+
+    it("should grant more access correctly on db level", () => {
+      // Setting limited access should produce a permission tree where each schema has "none" access
+      // (this is a strange, rather no-op edge case but the UI currently enables this)
+      schema1.changeDbDataPermissions({ groupId: 2, permission: "controlled" });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: {
+          schema_1: "none",
+          schema_2: "none",
+        },
+      });
+
+      // Granting native access should also grant a full write access
+      schema1.changeDbNativePermissions({ groupId: 2, permission: "write" });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "write",
+        schemas: "all",
+      });
+
+      resetState(); // ad-hoc reset (normally run before tests)
+      // test that setting full access works too
+      schema1.changeDbDataPermissions({ groupId: 2, permission: "all" });
+      expect(schema1.getPermissions({ groupId: 2 })).toMatchObject({
+        native: "none",
+        schemas: "all",
+      });
     });
+  });
 });
diff --git a/frontend/test/admin/settings/SettingsAuthenticationOptions.integ.spec.js b/frontend/test/admin/settings/SettingsAuthenticationOptions.integ.spec.js
index 18e9b7da8127480b55affe179cc997c116e9d57a..4cf306d89822cafb2cf962e4f8b9eb99179180e0 100644
--- a/frontend/test/admin/settings/SettingsAuthenticationOptions.integ.spec.js
+++ b/frontend/test/admin/settings/SettingsAuthenticationOptions.integ.spec.js
@@ -1,48 +1,50 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
 import { mount } from "enzyme";
 
-import SettingsEditorApp from "metabase/admin/settings/containers/SettingsEditorApp"
-import SettingsAuthenticationOptions from "metabase/admin/settings/components/SettingsAuthenticationOptions"
+import SettingsEditorApp from "metabase/admin/settings/containers/SettingsEditorApp";
+import SettingsAuthenticationOptions from "metabase/admin/settings/components/SettingsAuthenticationOptions";
 import SettingsSingleSignOnForm from "metabase/admin/settings/components/SettingsSingleSignOnForm.jsx";
 import SettingsLdapForm from "metabase/admin/settings/components/SettingsLdapForm.jsx";
 
-import { INITIALIZE_SETTINGS } from "metabase/admin/settings/settings"
+import { INITIALIZE_SETTINGS } from "metabase/admin/settings/settings";
 
-describe('Admin Auth Options', () => {
-    beforeAll(async () => {
-        useSharedAdminLogin()
-    })
+describe("Admin Auth Options", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-    it('it should render the proper configuration form', async () => {
-        const store = await createTestStore()
+  it("it should render the proper configuration form", async () => {
+    const store = await createTestStore();
 
-        store.pushPath("/admin/settings");
+    store.pushPath("/admin/settings");
 
-        const app = mount(store.getAppContainer())
-        await store.waitForActions([INITIALIZE_SETTINGS])
-        const settingsWrapper = app.find(SettingsEditorApp)
-        const authListItem = settingsWrapper.find('span[children="Authentication"]')
+    const app = mount(store.getAppContainer());
+    await store.waitForActions([INITIALIZE_SETTINGS]);
+    const settingsWrapper = app.find(SettingsEditorApp);
+    const authListItem = settingsWrapper.find(
+      'span[children="Authentication"]',
+    );
 
-        click(authListItem)
+    click(authListItem);
 
-        expect(settingsWrapper.find(SettingsAuthenticationOptions).length).toBe(1)
+    expect(settingsWrapper.find(SettingsAuthenticationOptions).length).toBe(1);
 
-        // test google
-        const googleConfigButton = settingsWrapper.find('.Button').first()
-        click(googleConfigButton)
+    // test google
+    const googleConfigButton = settingsWrapper.find(".Button").first();
+    click(googleConfigButton);
 
-        expect(settingsWrapper.find(SettingsSingleSignOnForm).length).toBe(1)
+    expect(settingsWrapper.find(SettingsSingleSignOnForm).length).toBe(1);
 
-        store.goBack()
+    store.goBack();
 
-        // test ldap
-        const ldapConfigButton = settingsWrapper.find('.Button').last()
-        click(ldapConfigButton)
-        expect(settingsWrapper.find(SettingsLdapForm).length).toBe(1)
-    })
-})
+    // test ldap
+    const ldapConfigButton = settingsWrapper.find(".Button").last();
+    click(ldapConfigButton);
+    expect(settingsWrapper.find(SettingsLdapForm).length).toBe(1);
+  });
+});
diff --git a/frontend/test/admin/settings/settings.integ.spec.js b/frontend/test/admin/settings/settings.integ.spec.js
index df4a7b4349ba4b674122bef9aad6e1d7c9f4741c..8509aa8f3880f0a2e9eb736893c530680ce633a7 100644
--- a/frontend/test/admin/settings/settings.integ.spec.js
+++ b/frontend/test/admin/settings/settings.integ.spec.js
@@ -1,52 +1,59 @@
 // Converted from an old Selenium E2E test
 import {
-    useSharedAdminLogin,
-    createTestStore,
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 import { mount } from "enzyme";
 import SettingInput from "metabase/admin/settings/components/widgets/SettingInput";
-import { INITIALIZE_SETTINGS, UPDATE_SETTING } from "metabase/admin/settings/settings";
+import {
+  INITIALIZE_SETTINGS,
+  UPDATE_SETTING,
+} from "metabase/admin/settings/settings";
 import { LOAD_CURRENT_USER } from "metabase/redux/user";
 import { setInputValue } from "__support__/enzyme_utils";
 
 describe("admin/settings", () => {
-    beforeAll(async () =>
-        useSharedAdminLogin()
-    );
+  beforeAll(async () => useSharedAdminLogin());
 
-    // TODO Atte Keinänen 6/22/17: Disabled because we already have converted this to Jest&Enzyme in other branch
-    describe("admin settings", () => {
-        // pick a random site name to try updating it to
-        const siteName = "Metabase" + Math.random();
+  // TODO Atte Keinänen 6/22/17: Disabled because we already have converted this to Jest&Enzyme in other branch
+  describe("admin settings", () => {
+    // pick a random site name to try updating it to
+    const siteName = "Metabase" + Math.random();
 
-        it("should save the setting", async () => {
-            const store = await createTestStore();
+    it("should save the setting", async () => {
+      const store = await createTestStore();
 
-            store.pushPath('/admin/settings/general');
-            const app = mount(store.getAppContainer())
+      store.pushPath("/admin/settings/general");
+      const app = mount(store.getAppContainer());
 
-            await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS])
+      await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
 
-            // first just make sure the site name isn't already set (it shouldn't since we're using a random name)
-            const input = app.find(SettingInput).first().find("input");
-            expect(input.prop("value")).not.toBe(siteName)
+      // first just make sure the site name isn't already set (it shouldn't since we're using a random name)
+      const input = app
+        .find(SettingInput)
+        .first()
+        .find("input");
+      expect(input.prop("value")).not.toBe(siteName);
 
-            // clear the site name input, send the keys corresponding to the site name, then blur to trigger the update
-            setInputValue(input, siteName)
+      // clear the site name input, send the keys corresponding to the site name, then blur to trigger the update
+      setInputValue(input, siteName);
 
-            await store.waitForActions([UPDATE_SETTING])
-        });
+      await store.waitForActions([UPDATE_SETTING]);
+    });
 
-        it("should show the updated name after page reload", async () => {
-            const store = await createTestStore();
+    it("should show the updated name after page reload", async () => {
+      const store = await createTestStore();
 
-            store.pushPath('/admin/settings/general');
-            const app = mount(store.getAppContainer())
+      store.pushPath("/admin/settings/general");
+      const app = mount(store.getAppContainer());
 
-            await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS])
+      await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
 
-            const input = app.find(SettingInput).first().find("input");
-            expect(input.prop("value")).toBe(siteName)
-        })
+      const input = app
+        .find(SettingInput)
+        .first()
+        .find("input");
+      expect(input.prop("value")).toBe(siteName);
     });
+  });
 });
diff --git a/frontend/test/admin/settings/utils.unit.spec.js b/frontend/test/admin/settings/utils.unit.spec.js
index 00acbdeec721edbc836a66380c7aa3956e694ef1..583b4f7925473187e7b024ee70c63525dfbd7017 100644
--- a/frontend/test/admin/settings/utils.unit.spec.js
+++ b/frontend/test/admin/settings/utils.unit.spec.js
@@ -1,20 +1,23 @@
-import { prepareAnalyticsValue } from '../../../src/metabase/admin/settings/utils'
+import { prepareAnalyticsValue } from "../../../src/metabase/admin/settings/utils";
 
-describe('prepareAnalyticsValue', () => {
-    const defaultSetting = { value: 120, type: 'number' }
+describe("prepareAnalyticsValue", () => {
+  const defaultSetting = { value: 120, type: "number" };
 
-    const checkResult = (setting = defaultSetting, expected = "success") =>
-        expect(prepareAnalyticsValue(setting)).toEqual(expected)
+  const checkResult = (setting = defaultSetting, expected = "success") =>
+    expect(prepareAnalyticsValue(setting)).toEqual(expected);
 
-    it('should return a non identifying value by default ', () => {
-        checkResult()
-    })
+  it("should return a non identifying value by default ", () => {
+    checkResult();
+  });
 
-    it('should return the value of a setting marked collectable', () => {
-        checkResult({ ...defaultSetting, allowValueCollection: true }, defaultSetting.value)
-    })
+  it("should return the value of a setting marked collectable", () => {
+    checkResult(
+      { ...defaultSetting, allowValueCollection: true },
+      defaultSetting.value,
+    );
+  });
 
-    it('should return the value of a setting with a type of "boolean" collectable', () => {
-        checkResult({ ...defaultSetting, type: 'boolean'}, defaultSetting.value)
-    })
-})
+  it('should return the value of a setting with a type of "boolean" collectable', () => {
+    checkResult({ ...defaultSetting, type: "boolean" }, defaultSetting.value);
+  });
+});
diff --git a/frontend/test/alert/alert.integ.spec.js b/frontend/test/alert/alert.integ.spec.js
index 69c9daa731136f245e67ae488e4d729c365bb0f9..0e3bd4805526bcfaf5cf82bf004bf81db8cb18fa 100644
--- a/frontend/test/alert/alert.integ.spec.js
+++ b/frontend/test/alert/alert.integ.spec.js
@@ -1,13 +1,11 @@
 import {
-    createSavedQuestion,
-    createTestStore,
-    forBothAdminsAndNormalUsers,
-    useSharedAdminLogin,
-    useSharedNormalLogin
+  createSavedQuestion,
+  createTestStore,
+  forBothAdminsAndNormalUsers,
+  useSharedAdminLogin,
+  useSharedNormalLogin,
 } from "__support__/integrated_tests";
-import {
-    click, clickButton
-} from "__support__/enzyme_utils"
+import { click, clickButton } from "__support__/enzyme_utils";
 
 import { fetchTableMetadata } from "metabase/redux/metadata";
 import { mount } from "enzyme";
@@ -21,20 +19,20 @@ import EntityMenu from "metabase/components/EntityMenu";
 import { delay } from "metabase/lib/promise";
 import Icon from "metabase/components/Icon";
 import {
-    AlertEducationalScreen,
-    AlertSettingToggle,
-    CreateAlertModalContent,
-    MultiSeriesAlertTip,
-    NormalAlertTip,
-    RawDataAlertTip,
-    UpdateAlertModalContent
+  AlertEducationalScreen,
+  AlertSettingToggle,
+  CreateAlertModalContent,
+  MultiSeriesAlertTip,
+  NormalAlertTip,
+  RawDataAlertTip,
+  UpdateAlertModalContent,
 } from "metabase/query_builder/components/AlertModals";
 import Button from "metabase/components/Button";
 import {
-    CREATE_ALERT,
-    FETCH_ALERTS_FOR_QUESTION,
-    UNSUBSCRIBE_FROM_ALERT,
-    UPDATE_ALERT,
+  CREATE_ALERT,
+  FETCH_ALERTS_FOR_QUESTION,
+  UNSUBSCRIBE_FROM_ALERT,
+  UPDATE_ALERT,
 } from "metabase/alert/alert";
 import MetabaseCookies from "metabase/lib/cookies";
 import Radio from "metabase/components/Radio";
@@ -43,432 +41,493 @@ import { FETCH_PULSE_FORM_INPUT, FETCH_USERS } from "metabase/pulse/actions";
 import ChannelSetupModal from "metabase/components/ChannelSetupModal";
 import { getDefaultAlert } from "metabase-lib/lib/Alert";
 import { getMetadata } from "metabase/selectors/metadata";
-import { AlertListItem, AlertListPopoverContent } from "metabase/query_builder/components/AlertListPopoverContent";
-
+import {
+  AlertListItem,
+  AlertListPopoverContent,
+} from "metabase/query_builder/components/AlertListPopoverContent";
 
 async function removeAllCreatedAlerts() {
-    useSharedAdminLogin()
-    const alerts = await AlertApi.list()
-    await Promise.all(alerts.map((alert) => AlertApi.delete({ id: alert.id })))
+  useSharedAdminLogin();
+  const alerts = await AlertApi.list();
+  await Promise.all(alerts.map(alert => AlertApi.delete({ id: alert.id })));
 }
 
-const initQbWithAlertMenuItemClicked = async (question, { hasSeenAlertSplash = true } = {}) => {
-    MetabaseCookies.getHasSeenAlertSplash = () => hasSeenAlertSplash
+const initQbWithAlertMenuItemClicked = async (
+  question,
+  { hasSeenAlertSplash = true } = {},
+) => {
+  MetabaseCookies.getHasSeenAlertSplash = () => hasSeenAlertSplash;
 
-    const store = await createTestStore()
-    store.pushPath(Urls.question(question.id()))
-    const app = mount(store.getAppContainer());
+  const store = await createTestStore();
+  store.pushPath(Urls.question(question.id()));
+  const app = mount(store.getAppContainer());
 
-    await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED, FETCH_ALERTS_FOR_QUESTION])
-    await delay(500);
+  await store.waitForActions([
+    INITIALIZE_QB,
+    QUERY_COMPLETED,
+    FETCH_ALERTS_FOR_QUESTION,
+  ]);
+  await delay(500);
 
-    const actionsMenu = app.find(QueryHeader).find(EntityMenu)
-    click(actionsMenu.childAt(0))
+  const actionsMenu = app.find(QueryHeader).find(EntityMenu);
+  click(actionsMenu.childAt(0));
 
-    const alertsMenuItem = actionsMenu.find(Icon).filterWhere(i => i.prop("name") === "alert")
-    click(alertsMenuItem)
+  const alertsMenuItem = actionsMenu
+    .find(Icon)
+    .filterWhere(i => i.prop("name") === "alert");
+  click(alertsMenuItem);
 
-    return { store, app }
-}
+  return { store, app };
+};
 
 describe("Alerts", () => {
-    let rawDataQuestion = null;
-    let timeSeriesQuestion = null;
-    let timeSeriesWithGoalQuestion = null;
-    let timeMultiSeriesWithGoalQuestion = null;
-    let progressBarQuestion = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin()
-
-        const store = await createTestStore()
-
-        // table metadata is needed for `Question.alertType()` calls
-        await store.dispatch(fetchTableMetadata(1))
-        const metadata = getMetadata(store.getState())
-
-        rawDataQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata })
-                .query()
-                .addFilter(["=", ["field-id", 4], 123456])
-                .question()
-                .setDisplayName("Just raw, untamed data")
-        )
-
-        timeSeriesQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata })
-                .query()
-                .addAggregation(["count"])
-                .addBreakout(["datetime-field", ["field-id", 1], "month"])
-                .question()
-                .setDisplay("line")
-                .setVisualizationSettings({
-                    "graph.dimensions": ["CREATED_AT"],
-                    "graph.metrics": ["count"]
-                })
-                .setDisplayName("Time series line")
-        )
-
-        timeSeriesWithGoalQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata })
-                .query()
-                .addAggregation(["count"])
-                .addBreakout(["datetime-field", ["field-id", 1], "month"])
-                .question()
-                .setDisplay("line")
-                .setVisualizationSettings({
-                    "graph.show_goal": true,
-                    "graph.goal_value": 10,
-                    "graph.dimensions": ["CREATED_AT"],
-                    "graph.metrics": ["count"]
-                })
-                .setDisplayName("Time series line with goal")
-        )
-
-        timeMultiSeriesWithGoalQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata })
-                .query()
-                .addAggregation(["count"])
-                .addAggregation(["sum", ["field-id", 6]])
-                .addBreakout(["datetime-field", ["field-id", 1], "month"])
-                .question()
-                .setDisplay("line")
-                .setVisualizationSettings({
-                    "graph.show_goal": true,
-                    "graph.goal_value": 10,
-                    "graph.dimensions": ["CREATED_AT"],
-                    "graph.metrics": ["count", "sum"]
-                })
-                .setDisplayName("Time multiseries line with goal")
-        )
-        progressBarQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata })
-                .query()
-                .addAggregation(["count"])
-                .question()
-                .setDisplay("progress")
-                .setVisualizationSettings({ "progress.goal": 50 })
-                .setDisplayName("Progress bar question")
-        )
-    })
-
-    afterAll(async () => {
-        await CardApi.delete({cardId: rawDataQuestion.id()})
-        await CardApi.delete({cardId: timeSeriesQuestion.id()})
-        await CardApi.delete({cardId: timeSeriesWithGoalQuestion.id()})
-        await CardApi.delete({cardId: timeMultiSeriesWithGoalQuestion.id()})
-        await CardApi.delete({cardId: progressBarQuestion.id()})
-    })
-
-    describe("missing email/slack credentials", () => {
-        it("should prompt you to add email/slack credentials", async () => {
-            await forBothAdminsAndNormalUsers(async () => {
-                MetabaseCookies.getHasSeenAlertSplash = () => false
-
-                const store = await createTestStore()
-                store.pushPath(Urls.question(rawDataQuestion.id()))
-                const app = mount(store.getAppContainer());
-
-                await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED, FETCH_ALERTS_FOR_QUESTION])
-
-                const actionsMenu = app.find(QueryHeader).find(EntityMenu)
-                click(actionsMenu.childAt(0))
-
-                const alertsMenuItem = actionsMenu.find(Icon).filterWhere(i => i.prop("name") === "alert")
-                click(alertsMenuItem)
-
-                await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-                const alertModal = app.find(QueryHeader).find(".test-modal")
-                expect(alertModal.find(ChannelSetupModal).length).toBe(1)
-            })
-        })
-    })
-
-    describe("with only slack set", () => {
-        const normalFormInput = PulseApi.form_input
-        beforeAll(async () => {
-            const formInput = await PulseApi.form_input()
-            PulseApi.form_input = () => ({
-                channels: {
-                ...formInput.channels,
-                    "slack": {
-                        ...formInput.channels.slack,
-                        "configured": true
-                    }
-                }
-            })
-        })
-        afterAll(() => {
-            PulseApi.form_input = normalFormInput
-        })
-
-        it("should let admins create alerts", async () => {
-            useSharedAdminLogin()
-            const store = await createTestStore()
-            store.pushPath(Urls.question(rawDataQuestion.id()))
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED, FETCH_ALERTS_FOR_QUESTION])
-
-            const actionsMenu = app.find(QueryHeader).find(EntityMenu)
-            click(actionsMenu.childAt(0))
-
-            const alertsMenuItem = actionsMenu.find(Icon).filterWhere(i => i.prop("name") === "alert")
-            click(alertsMenuItem)
-
-            await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-            const alertModal = app.find(QueryHeader).find(".test-modal")
-            expect(alertModal.find(ChannelSetupModal).length).toBe(0)
-            expect(alertModal.find(AlertEducationalScreen).length).toBe(1)
-        })
-
-        it("should say to non-admins that admin must add email credentials", async () => {
-            useSharedNormalLogin()
-            const store = await createTestStore()
-            store.pushPath(Urls.question(rawDataQuestion.id()))
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED, FETCH_ALERTS_FOR_QUESTION])
-
-            const actionsMenu = app.find(QueryHeader).find(EntityMenu)
-            click(actionsMenu.childAt(0))
-
-            const alertsMenuItem = actionsMenu.find(Icon).filterWhere(i => i.prop("name") === "alert")
-            click(alertsMenuItem)
-
-            await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-            const alertModal = app.find(QueryHeader).find(".test-modal")
-            expect(alertModal.find(ChannelSetupModal).length).toBe(1)
-            expect(alertModal.find(ChannelSetupModal).prop("channels")).toEqual(["email"])
+  let rawDataQuestion = null;
+  let timeSeriesQuestion = null;
+  let timeSeriesWithGoalQuestion = null;
+  let timeMultiSeriesWithGoalQuestion = null;
+  let progressBarQuestion = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+
+    const store = await createTestStore();
+
+    // table metadata is needed for `Question.alertType()` calls
+    await store.dispatch(fetchTableMetadata(1));
+    const metadata = getMetadata(store.getState());
+
+    rawDataQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata })
+        .query()
+        .addFilter(["=", ["field-id", 4], 123456])
+        .question()
+        .setDisplayName("Just raw, untamed data"),
+    );
+
+    timeSeriesQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata })
+        .query()
+        .addAggregation(["count"])
+        .addBreakout(["datetime-field", ["field-id", 1], "month"])
+        .question()
+        .setDisplay("line")
+        .setVisualizationSettings({
+          "graph.dimensions": ["CREATED_AT"],
+          "graph.metrics": ["count"],
         })
-    })
-
-    describe("alert creation", () => {
-        const normalFormInput = PulseApi.form_input
-        beforeAll(async () => {
-            // all channels configured
-            const formInput = await PulseApi.form_input()
-            PulseApi.form_input = () => ({
-                channels: {
-                    ...formInput.channels,
-                    "email": {
-                        ...formInput.channels.email,
-                        configured: true
-                    },
-                    "slack": {
-                        ...formInput.channels.slack,
-                        configured: true
-                    }
-                }
-            })
+        .setDisplayName("Time series line"),
+    );
+
+    timeSeriesWithGoalQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata })
+        .query()
+        .addAggregation(["count"])
+        .addBreakout(["datetime-field", ["field-id", 1], "month"])
+        .question()
+        .setDisplay("line")
+        .setVisualizationSettings({
+          "graph.show_goal": true,
+          "graph.goal_value": 10,
+          "graph.dimensions": ["CREATED_AT"],
+          "graph.metrics": ["count"],
         })
-        afterAll(async () => {
-            PulseApi.form_input = normalFormInput
-            await removeAllCreatedAlerts()
-        })
-
-        it("should show you the first time educational screen", async () => {
-            await forBothAdminsAndNormalUsers(async () => {
-                useSharedNormalLogin()
-                const { app, store } = await initQbWithAlertMenuItemClicked(rawDataQuestion, { hasSeenAlertSplash: false })
-
-                await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-                const alertModal = app.find(QueryHeader).find(".test-modal")
-                const educationalScreen = alertModal.find(AlertEducationalScreen)
-
-                clickButton(educationalScreen.find(Button))
-                const creationScreen = alertModal.find(CreateAlertModalContent)
-                expect(creationScreen.length).toBe(1)
-            })
-        });
-
-        it("should support 'rows present' alert for raw data questions", async () => {
-            useSharedNormalLogin()
-            const { app, store } = await initQbWithAlertMenuItemClicked(rawDataQuestion)
-
-            await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-            const alertModal = app.find(QueryHeader).find(".test-modal")
-            const creationScreen = alertModal.find(CreateAlertModalContent)
-            expect(creationScreen.find(RawDataAlertTip).length).toBe(1)
-            expect(creationScreen.find(NormalAlertTip).length).toBe(1)
-            expect(creationScreen.find(AlertSettingToggle).length).toBe(0)
-
-            clickButton(creationScreen.find(".Button.Button--primary"))
-            await store.waitForActions([CREATE_ALERT])
-        })
-
-        it("should support 'rows present' alert for timeseries questions without a goal", async () => {
-            useSharedNormalLogin()
-            const { app, store } = await initQbWithAlertMenuItemClicked(timeSeriesQuestion)
-
-            await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-            const alertModal = app.find(QueryHeader).find(".test-modal")
-            const creationScreen = alertModal.find(CreateAlertModalContent)
-            expect(creationScreen.find(RawDataAlertTip).length).toBe(1)
-            expect(creationScreen.find(AlertSettingToggle).length).toBe(0)
-        })
-
-        it("should work for timeseries questions with a set goal", async () => {
-            useSharedNormalLogin()
-            const { app, store } = await initQbWithAlertMenuItemClicked(timeSeriesWithGoalQuestion)
-
-            await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-            const alertModal = app.find(QueryHeader).find(".test-modal")
-            // why sometimes the educational screen is shown for a second ...?
-            expect(alertModal.find(AlertEducationalScreen).length).toBe(0)
-
-            const creationScreen = alertModal.find(CreateAlertModalContent)
-            expect(creationScreen.find(RawDataAlertTip).length).toBe(0)
-
-            const toggles = creationScreen.find(AlertSettingToggle)
-            expect(toggles.length).toBe(2)
-
-            const aboveGoalToggle = toggles.at(0)
-            expect(aboveGoalToggle.find(Radio).prop("value")).toBe(true)
-            click(aboveGoalToggle.find("li").last())
-            expect(aboveGoalToggle.find(Radio).prop("value")).toBe(false)
-
-            const firstOnlyToggle = toggles.at(1)
-            expect(firstOnlyToggle.find(Radio).prop("value")).toBe(true)
-
-            click(creationScreen.find(".Button.Button--primary"))
-            await store.waitForActions([CREATE_ALERT])
-
-            const alert = Object.values(getQuestionAlerts(store.getState()))[0]
-            expect(alert.alert_above_goal).toBe(false)
-            expect(alert.alert_first_only).toBe(true)
+        .setDisplayName("Time series line with goal"),
+    );
+
+    timeMultiSeriesWithGoalQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata })
+        .query()
+        .addAggregation(["count"])
+        .addAggregation(["sum", ["field-id", 6]])
+        .addBreakout(["datetime-field", ["field-id", 1], "month"])
+        .question()
+        .setDisplay("line")
+        .setVisualizationSettings({
+          "graph.show_goal": true,
+          "graph.goal_value": 10,
+          "graph.dimensions": ["CREATED_AT"],
+          "graph.metrics": ["count", "sum"],
         })
+        .setDisplayName("Time multiseries line with goal"),
+    );
+    progressBarQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata })
+        .query()
+        .addAggregation(["count"])
+        .question()
+        .setDisplay("progress")
+        .setVisualizationSettings({ "progress.goal": 50 })
+        .setDisplayName("Progress bar question"),
+    );
+  });
+
+  afterAll(async () => {
+    await CardApi.delete({ cardId: rawDataQuestion.id() });
+    await CardApi.delete({ cardId: timeSeriesQuestion.id() });
+    await CardApi.delete({ cardId: timeSeriesWithGoalQuestion.id() });
+    await CardApi.delete({ cardId: timeMultiSeriesWithGoalQuestion.id() });
+    await CardApi.delete({ cardId: progressBarQuestion.id() });
+  });
+
+  describe("missing email/slack credentials", () => {
+    it("should prompt you to add email/slack credentials", async () => {
+      await forBothAdminsAndNormalUsers(async () => {
+        MetabaseCookies.getHasSeenAlertSplash = () => false;
+
+        const store = await createTestStore();
+        store.pushPath(Urls.question(rawDataQuestion.id()));
+        const app = mount(store.getAppContainer());
+
+        await store.waitForActions([
+          INITIALIZE_QB,
+          QUERY_COMPLETED,
+          FETCH_ALERTS_FOR_QUESTION,
+        ]);
+
+        const actionsMenu = app.find(QueryHeader).find(EntityMenu);
+        click(actionsMenu.childAt(0));
+
+        const alertsMenuItem = actionsMenu
+          .find(Icon)
+          .filterWhere(i => i.prop("name") === "alert");
+        click(alertsMenuItem);
+
+        await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+        const alertModal = app.find(QueryHeader).find(".test-modal");
+        expect(alertModal.find(ChannelSetupModal).length).toBe(1);
+      });
+    });
+  });
+
+  describe("with only slack set", () => {
+    const normalFormInput = PulseApi.form_input;
+    beforeAll(async () => {
+      const formInput = await PulseApi.form_input();
+      PulseApi.form_input = () => ({
+        channels: {
+          ...formInput.channels,
+          slack: {
+            ...formInput.channels.slack,
+            configured: true,
+          },
+        },
+      });
+    });
+    afterAll(() => {
+      PulseApi.form_input = normalFormInput;
+    });
+
+    it("should let admins create alerts", async () => {
+      useSharedAdminLogin();
+      const store = await createTestStore();
+      store.pushPath(Urls.question(rawDataQuestion.id()));
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([
+        INITIALIZE_QB,
+        QUERY_COMPLETED,
+        FETCH_ALERTS_FOR_QUESTION,
+      ]);
+
+      const actionsMenu = app.find(QueryHeader).find(EntityMenu);
+      click(actionsMenu.childAt(0));
+
+      const alertsMenuItem = actionsMenu
+        .find(Icon)
+        .filterWhere(i => i.prop("name") === "alert");
+      click(alertsMenuItem);
+
+      await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+      const alertModal = app.find(QueryHeader).find(".test-modal");
+      expect(alertModal.find(ChannelSetupModal).length).toBe(0);
+      expect(alertModal.find(AlertEducationalScreen).length).toBe(1);
+    });
+
+    it("should say to non-admins that admin must add email credentials", async () => {
+      useSharedNormalLogin();
+      const store = await createTestStore();
+      store.pushPath(Urls.question(rawDataQuestion.id()));
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([
+        INITIALIZE_QB,
+        QUERY_COMPLETED,
+        FETCH_ALERTS_FOR_QUESTION,
+      ]);
+
+      const actionsMenu = app.find(QueryHeader).find(EntityMenu);
+      click(actionsMenu.childAt(0));
+
+      const alertsMenuItem = actionsMenu
+        .find(Icon)
+        .filterWhere(i => i.prop("name") === "alert");
+      click(alertsMenuItem);
+
+      await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+      const alertModal = app.find(QueryHeader).find(".test-modal");
+      expect(alertModal.find(ChannelSetupModal).length).toBe(1);
+      expect(alertModal.find(ChannelSetupModal).prop("channels")).toEqual([
+        "email",
+      ]);
+    });
+  });
+
+  describe("alert creation", () => {
+    const normalFormInput = PulseApi.form_input;
+    beforeAll(async () => {
+      // all channels configured
+      const formInput = await PulseApi.form_input();
+      PulseApi.form_input = () => ({
+        channels: {
+          ...formInput.channels,
+          email: {
+            ...formInput.channels.email,
+            configured: true,
+          },
+          slack: {
+            ...formInput.channels.slack,
+            configured: true,
+          },
+        },
+      });
+    });
+    afterAll(async () => {
+      PulseApi.form_input = normalFormInput;
+      await removeAllCreatedAlerts();
+    });
+
+    it("should show you the first time educational screen", async () => {
+      await forBothAdminsAndNormalUsers(async () => {
+        useSharedNormalLogin();
+        const { app, store } = await initQbWithAlertMenuItemClicked(
+          rawDataQuestion,
+          { hasSeenAlertSplash: false },
+        );
+
+        await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+        const alertModal = app.find(QueryHeader).find(".test-modal");
+        const educationalScreen = alertModal.find(AlertEducationalScreen);
+
+        clickButton(educationalScreen.find(Button));
+        const creationScreen = alertModal.find(CreateAlertModalContent);
+        expect(creationScreen.length).toBe(1);
+      });
+    });
+
+    it("should support 'rows present' alert for raw data questions", async () => {
+      useSharedNormalLogin();
+      const { app, store } = await initQbWithAlertMenuItemClicked(
+        rawDataQuestion,
+      );
+
+      await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+      const alertModal = app.find(QueryHeader).find(".test-modal");
+      const creationScreen = alertModal.find(CreateAlertModalContent);
+      expect(creationScreen.find(RawDataAlertTip).length).toBe(1);
+      expect(creationScreen.find(NormalAlertTip).length).toBe(1);
+      expect(creationScreen.find(AlertSettingToggle).length).toBe(0);
+
+      clickButton(creationScreen.find(".Button.Button--primary"));
+      await store.waitForActions([CREATE_ALERT]);
+    });
+
+    it("should support 'rows present' alert for timeseries questions without a goal", async () => {
+      useSharedNormalLogin();
+      const { app, store } = await initQbWithAlertMenuItemClicked(
+        timeSeriesQuestion,
+      );
+
+      await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+      const alertModal = app.find(QueryHeader).find(".test-modal");
+      const creationScreen = alertModal.find(CreateAlertModalContent);
+      expect(creationScreen.find(RawDataAlertTip).length).toBe(1);
+      expect(creationScreen.find(AlertSettingToggle).length).toBe(0);
+    });
+
+    it("should work for timeseries questions with a set goal", async () => {
+      useSharedNormalLogin();
+      const { app, store } = await initQbWithAlertMenuItemClicked(
+        timeSeriesWithGoalQuestion,
+      );
+
+      await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+      const alertModal = app.find(QueryHeader).find(".test-modal");
+      // why sometimes the educational screen is shown for a second ...?
+      expect(alertModal.find(AlertEducationalScreen).length).toBe(0);
+
+      const creationScreen = alertModal.find(CreateAlertModalContent);
+      expect(creationScreen.find(RawDataAlertTip).length).toBe(0);
+
+      const toggles = creationScreen.find(AlertSettingToggle);
+      expect(toggles.length).toBe(2);
+
+      const aboveGoalToggle = toggles.at(0);
+      expect(aboveGoalToggle.find(Radio).prop("value")).toBe(true);
+      click(aboveGoalToggle.find("li").last());
+      expect(aboveGoalToggle.find(Radio).prop("value")).toBe(false);
+
+      const firstOnlyToggle = toggles.at(1);
+      expect(firstOnlyToggle.find(Radio).prop("value")).toBe(true);
+
+      click(creationScreen.find(".Button.Button--primary"));
+      await store.waitForActions([CREATE_ALERT]);
+
+      const alert = Object.values(getQuestionAlerts(store.getState()))[0];
+      expect(alert.alert_above_goal).toBe(false);
+      expect(alert.alert_first_only).toBe(true);
+    });
+
+    it("should fall back to raw data alert and show a warning for time-multiseries questions with a set goal", async () => {
+      useSharedNormalLogin();
+      const { app, store } = await initQbWithAlertMenuItemClicked(
+        timeMultiSeriesWithGoalQuestion,
+      );
+
+      await store.waitForActions([FETCH_PULSE_FORM_INPUT]);
+      const alertModal = app.find(QueryHeader).find(".test-modal");
+      const creationScreen = alertModal.find(CreateAlertModalContent);
+      // console.log(creationScreen.debug())
+      expect(creationScreen.find(RawDataAlertTip).length).toBe(1);
+      expect(creationScreen.find(MultiSeriesAlertTip).length).toBe(1);
+      expect(creationScreen.find(AlertSettingToggle).length).toBe(0);
+
+      clickButton(creationScreen.find(".Button.Button--primary"));
+      await store.waitForActions([CREATE_ALERT]);
+    });
+  });
+
+  describe("alert list for a question", () => {
+    beforeAll(async () => {
+      // Both raw data and timeseries questions contain both an alert created by a normal user and by an admin.
+      // The difference is that the admin-created alert in raw data question contains also the normal user
+      // as a recipient.
+      useSharedAdminLogin();
+      const adminUser = await UserApi.current();
+      await AlertApi.create(
+        getDefaultAlert(timeSeriesWithGoalQuestion, adminUser),
+      );
+
+      useSharedNormalLogin();
+      const normalUser = await UserApi.current();
+      await AlertApi.create(
+        getDefaultAlert(timeSeriesWithGoalQuestion, normalUser),
+      );
+      await AlertApi.create(getDefaultAlert(rawDataQuestion, normalUser));
+
+      useSharedAdminLogin();
+      const defaultRawDataAlert = getDefaultAlert(rawDataQuestion, adminUser);
+      const alertWithTwoRecipients = setIn(
+        defaultRawDataAlert,
+        ["channels", 0, "recipients"],
+        [adminUser, normalUser],
+      );
+      await AlertApi.create(alertWithTwoRecipients);
+    });
 
-        it("should fall back to raw data alert and show a warning for time-multiseries questions with a set goal", async () => {
-            useSharedNormalLogin()
-            const { app, store } = await initQbWithAlertMenuItemClicked(timeMultiSeriesWithGoalQuestion)
-
-            await store.waitForActions([FETCH_PULSE_FORM_INPUT])
-            const alertModal = app.find(QueryHeader).find(".test-modal")
-            const creationScreen = alertModal.find(CreateAlertModalContent)
-            // console.log(creationScreen.debug())
-            expect(creationScreen.find(RawDataAlertTip).length).toBe(1)
-            expect(creationScreen.find(MultiSeriesAlertTip).length).toBe(1)
-            expect(creationScreen.find(AlertSettingToggle).length).toBe(0)
-
-            clickButton(creationScreen.find(".Button.Button--primary"))
-            await store.waitForActions([CREATE_ALERT])
-        })
-    })
-
-    describe("alert list for a question", () => {
-        beforeAll(async () => {
-            // Both raw data and timeseries questions contain both an alert created by a normal user and by an admin.
-            // The difference is that the admin-created alert in raw data question contains also the normal user
-            // as a recipient.
-            useSharedAdminLogin()
-            const adminUser = await UserApi.current();
-            await AlertApi.create(getDefaultAlert(timeSeriesWithGoalQuestion, adminUser))
-
-            useSharedNormalLogin()
-            const normalUser = await UserApi.current();
-            await AlertApi.create(getDefaultAlert(timeSeriesWithGoalQuestion, normalUser))
-            await AlertApi.create(getDefaultAlert(rawDataQuestion, normalUser))
-
-            useSharedAdminLogin()
-            const defaultRawDataAlert = getDefaultAlert(rawDataQuestion, adminUser)
-            const alertWithTwoRecipients = setIn(
-                defaultRawDataAlert,
-                ["channels", 0, "recipients"],
-                [adminUser, normalUser]
-            )
-            await AlertApi.create(alertWithTwoRecipients)
-        })
-
-        afterAll(async () => {
-            await removeAllCreatedAlerts()
-        })
-
-        describe("as an admin", () => {
-            it("should let you see all created alerts", async () => {
-                useSharedAdminLogin()
-                const { app } = await initQbWithAlertMenuItemClicked(timeSeriesWithGoalQuestion)
-
-                const alertListPopover = app.find(AlertListPopoverContent)
-
-                const alertListItems = alertListPopover.find(AlertListItem)
-                expect(alertListItems.length).toBe(2)
-                expect(alertListItems.at(1).text()).toMatch(/Robert/)
-            })
-
-            it("should let you edit an alert", async () => {
-                // let's in this case try editing someone else's alert
-                useSharedAdminLogin()
-                const { app, store } = await initQbWithAlertMenuItemClicked(timeSeriesWithGoalQuestion)
-
-                const alertListPopover = app.find(AlertListPopoverContent)
-
-                const alertListItems = alertListPopover.find(AlertListItem)
-                expect(alertListItems.length).toBe(2)
-                expect(alertListItems.at(1).text()).toMatch(/Robert/)
-
-                const othersAlertListItem = alertListItems.at(1)
-
-                click(othersAlertListItem.find("a").filterWhere((item) => /Edit/.test(item.text())))
-
-                const editingScreen = app.find(UpdateAlertModalContent)
-                expect(editingScreen.length).toBe(1)
-
-                await store.waitForActions([FETCH_USERS, FETCH_PULSE_FORM_INPUT])
-
-                const toggles = editingScreen.find(AlertSettingToggle)
-                const aboveGoalToggle = toggles.at(0)
-                expect(aboveGoalToggle.find(Radio).prop("value")).toBe(true)
-                click(aboveGoalToggle.find("li").last())
-                expect(aboveGoalToggle.find(Radio).prop("value")).toBe(false)
-
-                click(editingScreen.find(".Button.Button--primary").last())
-                await store.waitForActions([UPDATE_ALERT])
-
-                const alerts = Object.values(getQuestionAlerts(store.getState()))
-                const othersAlert = alerts.find((alert) => alert.creator_id === 2)
-                expect(othersAlert.alert_above_goal).toBe(false)
-            })
-        })
-
-        describe("as a non-admin / normal user", () => {
-            it("should let you see your own alerts", async () => {
-                useSharedNormalLogin()
-                const { app } = await initQbWithAlertMenuItemClicked(timeSeriesWithGoalQuestion)
-
-                const alertListPopover = app.find(AlertListPopoverContent)
-
-                const alertListItems = alertListPopover.find(AlertListItem)
-                expect(alertListItems.length).toBe(1)
-            })
-
-            it("should let you see also other alerts where you are a recipient", async () => {
-                useSharedNormalLogin()
-                const { app } = await initQbWithAlertMenuItemClicked(rawDataQuestion)
-
-                const alertListPopover = app.find(AlertListPopoverContent)
-
-                const alertListItems = alertListPopover.find(AlertListItem)
-                expect(alertListItems.length).toBe(2)
-                expect(alertListItems.at(1).text()).toMatch(/Bobby/)
-            })
-
-            it("should let you unsubscribe from both your own and others' alerts", async () => {
-                useSharedNormalLogin()
-                const { app, store } = await initQbWithAlertMenuItemClicked(rawDataQuestion)
-
-                const alertListPopover = app.find(AlertListPopoverContent)
-
-                const alertListItems = alertListPopover.find(AlertListItem)
-                expect(alertListItems.length).toBe(2)
-                const ownAlertListItem = alertListItems.at(0)
-                // const otherAlertListItem = alertListItems.at(1)
-
-                // unsubscribe from the alert of some other user
-                click(ownAlertListItem.find("a").filterWhere((item) => /Unsubscribe/.test(item.text())))
-                await store.waitForActions([UNSUBSCRIBE_FROM_ALERT])
-
-            })
-        })
-    })
-})
\ No newline at end of file
+    afterAll(async () => {
+      await removeAllCreatedAlerts();
+    });
+
+    describe("as an admin", () => {
+      it("should let you see all created alerts", async () => {
+        useSharedAdminLogin();
+        const { app } = await initQbWithAlertMenuItemClicked(
+          timeSeriesWithGoalQuestion,
+        );
+
+        const alertListPopover = app.find(AlertListPopoverContent);
+
+        const alertListItems = alertListPopover.find(AlertListItem);
+        expect(alertListItems.length).toBe(2);
+        expect(alertListItems.at(1).text()).toMatch(/Robert/);
+      });
+
+      it("should let you edit an alert", async () => {
+        // let's in this case try editing someone else's alert
+        useSharedAdminLogin();
+        const { app, store } = await initQbWithAlertMenuItemClicked(
+          timeSeriesWithGoalQuestion,
+        );
+
+        const alertListPopover = app.find(AlertListPopoverContent);
+
+        const alertListItems = alertListPopover.find(AlertListItem);
+        expect(alertListItems.length).toBe(2);
+        expect(alertListItems.at(1).text()).toMatch(/Robert/);
+
+        const othersAlertListItem = alertListItems.at(1);
+
+        click(
+          othersAlertListItem
+            .find("a")
+            .filterWhere(item => /Edit/.test(item.text())),
+        );
+
+        const editingScreen = app.find(UpdateAlertModalContent);
+        expect(editingScreen.length).toBe(1);
+
+        await store.waitForActions([FETCH_USERS, FETCH_PULSE_FORM_INPUT]);
+
+        const toggles = editingScreen.find(AlertSettingToggle);
+        const aboveGoalToggle = toggles.at(0);
+        expect(aboveGoalToggle.find(Radio).prop("value")).toBe(true);
+        click(aboveGoalToggle.find("li").last());
+        expect(aboveGoalToggle.find(Radio).prop("value")).toBe(false);
+
+        click(editingScreen.find(".Button.Button--primary").last());
+        await store.waitForActions([UPDATE_ALERT]);
+
+        const alerts = Object.values(getQuestionAlerts(store.getState()));
+        const othersAlert = alerts.find(alert => alert.creator_id === 2);
+        expect(othersAlert.alert_above_goal).toBe(false);
+      });
+    });
+
+    describe("as a non-admin / normal user", () => {
+      it("should let you see your own alerts", async () => {
+        useSharedNormalLogin();
+        const { app } = await initQbWithAlertMenuItemClicked(
+          timeSeriesWithGoalQuestion,
+        );
+
+        const alertListPopover = app.find(AlertListPopoverContent);
+
+        const alertListItems = alertListPopover.find(AlertListItem);
+        expect(alertListItems.length).toBe(1);
+      });
+
+      it("should let you see also other alerts where you are a recipient", async () => {
+        useSharedNormalLogin();
+        const { app } = await initQbWithAlertMenuItemClicked(rawDataQuestion);
+
+        const alertListPopover = app.find(AlertListPopoverContent);
+
+        const alertListItems = alertListPopover.find(AlertListItem);
+        expect(alertListItems.length).toBe(2);
+        expect(alertListItems.at(1).text()).toMatch(/Bobby/);
+      });
+
+      it("should let you unsubscribe from both your own and others' alerts", async () => {
+        useSharedNormalLogin();
+        const { app, store } = await initQbWithAlertMenuItemClicked(
+          rawDataQuestion,
+        );
+
+        const alertListPopover = app.find(AlertListPopoverContent);
+
+        const alertListItems = alertListPopover.find(AlertListItem);
+        expect(alertListItems.length).toBe(2);
+        const ownAlertListItem = alertListItems.at(0);
+        // const otherAlertListItem = alertListItems.at(1)
+
+        // unsubscribe from the alert of some other user
+        click(
+          ownAlertListItem
+            .find("a")
+            .filterWhere(item => /Unsubscribe/.test(item.text())),
+        );
+        await store.waitForActions([UNSUBSCRIBE_FROM_ALERT]);
+      });
+    });
+  });
+});
diff --git a/frontend/test/components/AccordianList.unit.test.js b/frontend/test/components/AccordianList.unit.test.js
index ee159d9e228455d401405de3dbeef0bdfed5cc79..901a13705edb6c17bc681028a893355f66d808df 100644
--- a/frontend/test/components/AccordianList.unit.test.js
+++ b/frontend/test/components/AccordianList.unit.test.js
@@ -5,67 +5,71 @@ import AccordianList from "metabase/components/AccordianList";
 import ListSearchField from "metabase/components/ListSearchField";
 
 const SECTIONS = [
-    {
-        name: "Widgets",
-        items: [{ name: "Foo" }, { name: "Bar" }]
-    },
-    {
-        name: "Doohickeys",
-        items: [{ name: "Baz" }]
-    }
+  {
+    name: "Widgets",
+    items: [{ name: "Foo" }, { name: "Bar" }],
+  },
+  {
+    name: "Doohickeys",
+    items: [{ name: "Baz" }],
+  },
 ];
 
 describe("AccordianList", () => {
-    it("should open the first section by default", () => {
-        const wrapper = mount(<AccordianList sections={SECTIONS} />);
-        expect(wrapper.find(".List-section-header").length).toBe(2);
-        expect(wrapper.find(".List-item").length).toBe(2);
-    });
-    it("should open the second section if initiallyOpenSection=1", () => {
-        const wrapper = mount(
-            <AccordianList sections={SECTIONS} initiallyOpenSection={1} />
-        );
-        expect(wrapper.find(".List-item").length).toBe(1);
-    });
-    it("should not open a section if initiallyOpenSection=null", () => {
-        const wrapper = mount(
-            <AccordianList sections={SECTIONS} initiallyOpenSection={null} />
-        );
-        expect(wrapper.find(".List-item").length).toBe(0);
-    });
-    it("should open all sections if alwaysExpanded is set", () => {
-        const wrapper = mount(
-            <AccordianList sections={SECTIONS} alwaysExpanded />
-        );
-        expect(wrapper.find(".List-item").length).toBe(3);
-    });
-    it("should not show search field by default", () => {
-        const wrapper = mount(<AccordianList sections={SECTIONS} />);
-        expect(wrapper.find(ListSearchField).length).toBe(0);
-    });
-    it("should show search field is searchable is set", () => {
-        const wrapper = mount(<AccordianList sections={SECTIONS} searchable />);
-        expect(wrapper.find(ListSearchField).length).toBe(1);
-    });
-    it("should close the section when header is clicked", () => {
-        const wrapper = mount(<AccordianList sections={SECTIONS} />);
-        expect(wrapper.find(".List-item").length).toBe(2);
-        wrapper.find(".List-section-header").first().simulate('click');
-        expect(wrapper.find(".List-item").length).toBe(0);
-    });
-    it("should switch sections when another section is clicked", () => {
-        const wrapper = mount(<AccordianList sections={SECTIONS} />);
-        expect(wrapper.find(".List-item").length).toBe(2);
-        wrapper.find(".List-section-header").last().simulate('click');
-        expect(wrapper.find(".List-item").length).toBe(1);
-    });
-    it("should filter items when searched", () => {
-        const wrapper = mount(<AccordianList sections={SECTIONS} searchable />);
-        const searchInput = wrapper.find(ListSearchField).find("input");
-        expect(wrapper.find(".List-item").length).toBe(2);
-        searchInput.simulate("change", { target: { value: "Foo" }})
-        expect(wrapper.find(".List-item").length).toBe(1);
-        searchInput.simulate("change", { target: { value: "Something Else" }})
-        expect(wrapper.find(".List-item").length).toBe(0);
-    });
+  it("should open the first section by default", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} />);
+    expect(wrapper.find(".List-section-header").length).toBe(2);
+    expect(wrapper.find(".List-item").length).toBe(2);
+  });
+  it("should open the second section if initiallyOpenSection=1", () => {
+    const wrapper = mount(
+      <AccordianList sections={SECTIONS} initiallyOpenSection={1} />,
+    );
+    expect(wrapper.find(".List-item").length).toBe(1);
+  });
+  it("should not open a section if initiallyOpenSection=null", () => {
+    const wrapper = mount(
+      <AccordianList sections={SECTIONS} initiallyOpenSection={null} />,
+    );
+    expect(wrapper.find(".List-item").length).toBe(0);
+  });
+  it("should open all sections if alwaysExpanded is set", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} alwaysExpanded />);
+    expect(wrapper.find(".List-item").length).toBe(3);
+  });
+  it("should not show search field by default", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} />);
+    expect(wrapper.find(ListSearchField).length).toBe(0);
+  });
+  it("should show search field is searchable is set", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} searchable />);
+    expect(wrapper.find(ListSearchField).length).toBe(1);
+  });
+  it("should close the section when header is clicked", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} />);
+    expect(wrapper.find(".List-item").length).toBe(2);
+    wrapper
+      .find(".List-section-header")
+      .first()
+      .simulate("click");
+    expect(wrapper.find(".List-item").length).toBe(0);
+  });
+  it("should switch sections when another section is clicked", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} />);
+    expect(wrapper.find(".List-item").length).toBe(2);
+    wrapper
+      .find(".List-section-header")
+      .last()
+      .simulate("click");
+    expect(wrapper.find(".List-item").length).toBe(1);
+  });
+  it("should filter items when searched", () => {
+    const wrapper = mount(<AccordianList sections={SECTIONS} searchable />);
+    const searchInput = wrapper.find(ListSearchField).find("input");
+    expect(wrapper.find(".List-item").length).toBe(2);
+    searchInput.simulate("change", { target: { value: "Foo" } });
+    expect(wrapper.find(".List-item").length).toBe(1);
+    searchInput.simulate("change", { target: { value: "Something Else" } });
+    expect(wrapper.find(".List-item").length).toBe(0);
+  });
 });
diff --git a/frontend/test/components/Button.unit.spec.js b/frontend/test/components/Button.unit.spec.js
index 94046caa501a73384759f0936cb27fec4818d892..80b8b057dbf3fc77a99954477950e9251fe3cd83 100644
--- a/frontend/test/components/Button.unit.spec.js
+++ b/frontend/test/components/Button.unit.spec.js
@@ -1,35 +1,27 @@
-import React from 'react';
-import renderer from 'react-test-renderer';
+import React from "react";
+import renderer from "react-test-renderer";
 
-import { render } from 'enzyme';
+import { render } from "enzyme";
 
-import Button from '../../src/metabase/components/Button';
+import Button from "../../src/metabase/components/Button";
 
-describe('Button', () => {
-    it('should render correctly', () => {
-        const tree = renderer.create(
-            <Button>Clickity click</Button>
-        ).toJSON();
+describe("Button", () => {
+  it("should render correctly", () => {
+    const tree = renderer.create(<Button>Clickity click</Button>).toJSON();
 
-        expect(tree).toMatchSnapshot()
-    })
-    it('should render correctly with an icon', () => {
-        const tree = renderer.create(
-            <Button icon='star'>
-                Clickity click
-            </Button>
-        ).toJSON();
+    expect(tree).toMatchSnapshot();
+  });
+  it("should render correctly with an icon", () => {
+    const tree = renderer
+      .create(<Button icon="star">Clickity click</Button>)
+      .toJSON();
 
-        expect(tree).toMatchSnapshot()
-    })
+    expect(tree).toMatchSnapshot();
+  });
 
-    it('should render a primary button given the primary prop', () => {
-        const button = render(
-            <Button primary>
-                Clickity click
-            </Button>
-        )
+  it("should render a primary button given the primary prop", () => {
+    const button = render(<Button primary>Clickity click</Button>);
 
-        expect(button.find('button.Button--primary').length).toEqual(1)
-    })
-})
+    expect(button.find("button.Button--primary").length).toEqual(1);
+  });
+});
diff --git a/frontend/test/components/Calendar.unit.test.js b/frontend/test/components/Calendar.unit.test.js
index 551d5fde3e8682e0ff562de482ade38ea68e91c5..8cca46d8ed034025f56e5a43e0d1ac6ca9f7998d 100644
--- a/frontend/test/components/Calendar.unit.test.js
+++ b/frontend/test/components/Calendar.unit.test.js
@@ -7,29 +7,29 @@ import { mount } from "enzyme";
 import Calendar from "../../src/metabase/components/Calendar";
 
 describe("Calendar", () => {
-    afterEach(() => {
-        mockDate.reset();
-    });
+  afterEach(() => {
+    mockDate.reset();
+  });
 
-    it("should render correctly", () => {
-        // set the system clock to the snapshot's current date
-        mockDate.set('2018-01-12T12:00:00Z', 0);
-        const tree = renderer.create(
-            <Calendar selected={moment("2018-01-01")} onChange={() => {}}/>
-        ).toJSON();
-        expect(tree).toMatchSnapshot()
-    });
+  it("should render correctly", () => {
+    // set the system clock to the snapshot's current date
+    mockDate.set("2018-01-12T12:00:00Z", 0);
+    const tree = renderer
+      .create(<Calendar selected={moment("2018-01-01")} onChange={() => {}} />)
+      .toJSON();
+    expect(tree).toMatchSnapshot();
+  });
 
-    it("should switch months correctly", () => {
-        mockDate.set('2018-01-12T12:00:00Z', 0);
-        const calendar = mount(
-            <Calendar selected={moment("2018-01-01")} onChange={() => {}}/>
-        );
-        expect(calendar.find(".Calendar-header").text()).toEqual("January 2018");
-        calendar.find(".Icon-chevronleft").simulate("click");
-        expect(calendar.find(".Calendar-header").text()).toEqual("December 2017");
-        calendar.find(".Icon-chevronright").simulate("click");
-        calendar.find(".Icon-chevronright").simulate("click");
-        expect(calendar.find(".Calendar-header").text()).toEqual("February 2018");
-    });
+  it("should switch months correctly", () => {
+    mockDate.set("2018-01-12T12:00:00Z", 0);
+    const calendar = mount(
+      <Calendar selected={moment("2018-01-01")} onChange={() => {}} />,
+    );
+    expect(calendar.find(".Calendar-header").text()).toEqual("January 2018");
+    calendar.find(".Icon-chevronleft").simulate("click");
+    expect(calendar.find(".Calendar-header").text()).toEqual("December 2017");
+    calendar.find(".Icon-chevronright").simulate("click");
+    calendar.find(".Icon-chevronright").simulate("click");
+    expect(calendar.find(".Calendar-header").text()).toEqual("February 2018");
+  });
 });
diff --git a/frontend/test/components/EntityMenuItem.unit.test.js b/frontend/test/components/EntityMenuItem.unit.test.js
index 5599c227b74edeb7a28ed67eaa908ca7fe8effb9..e5f9684b98f9830445eacbeef1595ad2e554377b 100644
--- a/frontend/test/components/EntityMenuItem.unit.test.js
+++ b/frontend/test/components/EntityMenuItem.unit.test.js
@@ -1,58 +1,48 @@
-import React from 'react'
-import { shallow, mount } from 'enzyme'
-import { Link } from 'react-router'
-
-import Icon from 'metabase/components/Icon'
-import EntityMenuItem from 'metabase/components/EntityMenuItem'
-
-describe('EntityMenuItem', () => {
-    it('should display the proper title and icon', () => {
-
-        const wrapper = shallow(
-            <EntityMenuItem
-                title="A pencil icon"
-                icon="pencil"
-                action={() => ({})}
-            />
-        )
-
-        const icon = wrapper.find(Icon)
-
-        expect(icon.length).toBe(1)
-        expect(icon.props().name).toEqual('pencil')
-
-    })
-
-    describe('actions and links', () => {
-        describe('actions', () => {
-            it('should call an action function if an action is provided', () => {
-                const spy = jest.fn()
-
-                const wrapper = mount(
-                    <EntityMenuItem
-                        title="A pencil icon"
-                        icon="pencil"
-                        action={spy}
-                    />
-                )
-
-                wrapper.simulate('click')
-                expect(spy).toHaveBeenCalled()
-            })
-        })
-
-        describe('links', () => {
-            it('should be a link if a link is provided', () => {
-                const wrapper = mount(
-                    <EntityMenuItem
-                        title="A pencil icon"
-                        icon="pencil"
-                        link='/derp'
-                    />
-                )
-
-                expect(wrapper.find(Link).length).toBe(1)
-            })
-        })
-    })
-})
+import React from "react";
+import { shallow, mount } from "enzyme";
+import { Link } from "react-router";
+
+import Icon from "metabase/components/Icon";
+import EntityMenuItem from "metabase/components/EntityMenuItem";
+
+describe("EntityMenuItem", () => {
+  it("should display the proper title and icon", () => {
+    const wrapper = shallow(
+      <EntityMenuItem
+        title="A pencil icon"
+        icon="pencil"
+        action={() => ({})}
+      />,
+    );
+
+    const icon = wrapper.find(Icon);
+
+    expect(icon.length).toBe(1);
+    expect(icon.props().name).toEqual("pencil");
+  });
+
+  describe("actions and links", () => {
+    describe("actions", () => {
+      it("should call an action function if an action is provided", () => {
+        const spy = jest.fn();
+
+        const wrapper = mount(
+          <EntityMenuItem title="A pencil icon" icon="pencil" action={spy} />,
+        );
+
+        wrapper.simulate("click");
+        expect(spy).toHaveBeenCalled();
+      });
+    });
+
+    describe("links", () => {
+      it("should be a link if a link is provided", () => {
+        const wrapper = mount(
+          <EntityMenuItem title="A pencil icon" icon="pencil" link="/derp" />,
+        );
+
+        expect(wrapper.find(Link).length).toBe(1);
+      });
+    });
+  });
+});
diff --git a/frontend/test/components/EntityMenuTrigger.unit.test.js b/frontend/test/components/EntityMenuTrigger.unit.test.js
index 16a1bb027078da18ddc9fed2b3788c232b52a5bf..16df1fb1cea1baf51d043a2d01f3a19dba0d8e39 100644
--- a/frontend/test/components/EntityMenuTrigger.unit.test.js
+++ b/frontend/test/components/EntityMenuTrigger.unit.test.js
@@ -1,25 +1,20 @@
-import React from 'react'
-import { shallow } from 'enzyme'
+import React from "react";
+import { shallow } from "enzyme";
 
-import Icon from 'metabase/components/Icon'
-import EntityMenuTrigger from 'metabase/components/EntityMenuTrigger'
+import Icon from "metabase/components/Icon";
+import EntityMenuTrigger from "metabase/components/EntityMenuTrigger";
 
-describe('EntityMenuTrigger', () => {
-    it('should render the desired icon and call its onClick fn', () => {
-        const spy = jest.fn()
-        const wrapper = shallow(
-            <EntityMenuTrigger
-                icon='pencil'
-                onClick={spy}
-            />
-        )
+describe("EntityMenuTrigger", () => {
+  it("should render the desired icon and call its onClick fn", () => {
+    const spy = jest.fn();
+    const wrapper = shallow(<EntityMenuTrigger icon="pencil" onClick={spy} />);
 
-        const icon = wrapper.find(Icon)
+    const icon = wrapper.find(Icon);
 
-        expect(icon.length).toBe(1)
-        expect(icon.props().name).toEqual('pencil')
+    expect(icon.length).toBe(1);
+    expect(icon.props().name).toEqual("pencil");
 
-        wrapper.simulate('click')
-        expect(spy).toHaveBeenCalled()
-    })
-})
+    wrapper.simulate("click");
+    expect(spy).toHaveBeenCalled();
+  });
+});
diff --git a/frontend/test/components/FieldValuesWidget.unit.spec.js b/frontend/test/components/FieldValuesWidget.unit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..d2f54abada422b45d28aefb90c6f9ea331817d3c
--- /dev/null
+++ b/frontend/test/components/FieldValuesWidget.unit.spec.js
@@ -0,0 +1,137 @@
+import React from "react";
+import { mount } from "enzyme";
+
+import {
+  metadata,
+  PRODUCT_CATEGORY_FIELD_ID,
+  ORDERS_PRODUCT_FK_FIELD_ID,
+} from "../__support__/sample_dataset_fixture";
+
+import { FieldValuesWidget } from "../../src/metabase/components/FieldValuesWidget";
+import TokenField from "../../src/metabase/components/TokenField";
+
+const mock = (object, properties) =>
+  Object.assign(Object.create(object), properties);
+
+const mountFieldValuesWidget = props =>
+  mount(
+    <FieldValuesWidget
+      value={[]}
+      onChange={() => {}}
+      fetchFieldValues={() => {}}
+      {...props}
+    />,
+  );
+
+describe("FieldValuesWidget", () => {
+  describe("category field", () => {
+    describe("has_field_values = none", () => {
+      const props = {
+        field: mock(metadata.field(PRODUCT_CATEGORY_FIELD_ID), {
+          has_field_values: "none",
+        }),
+      };
+      it("should not call fetchFieldValues", () => {
+        const fetchFieldValues = jest.fn();
+        mountFieldValuesWidget({ ...props, fetchFieldValues });
+        expect(fetchFieldValues).not.toHaveBeenCalled();
+      });
+      it("should have 'Enter some text' as the placeholder text", () => {
+        const component = mountFieldValuesWidget({ ...props });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Enter some text",
+        );
+      });
+    });
+    describe("has_field_values = list", () => {
+      const props = {
+        field: metadata.field(PRODUCT_CATEGORY_FIELD_ID),
+      };
+      it("should call fetchFieldValues", () => {
+        const fetchFieldValues = jest.fn();
+        mountFieldValuesWidget({ ...props, fetchFieldValues });
+        expect(fetchFieldValues).toHaveBeenCalledWith(
+          PRODUCT_CATEGORY_FIELD_ID,
+        );
+      });
+      it("should have 'Search the list' as the placeholder text", () => {
+        const component = mountFieldValuesWidget({ ...props });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Search the list",
+        );
+      });
+    });
+    describe("has_field_values = search", () => {
+      const props = {
+        field: mock(metadata.field(PRODUCT_CATEGORY_FIELD_ID), {
+          has_field_values: "search",
+        }),
+        searchField: metadata.field(PRODUCT_CATEGORY_FIELD_ID),
+      };
+      it("should not call fetchFieldValues", () => {
+        const fetchFieldValues = jest.fn();
+        mountFieldValuesWidget({ ...props, fetchFieldValues });
+        expect(fetchFieldValues).not.toHaveBeenCalled();
+      });
+      it("should have 'Search by Category' as the placeholder text", () => {
+        const component = mountFieldValuesWidget({ ...props });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Search by Category",
+        );
+      });
+    });
+  });
+  describe("id field", () => {
+    describe("has_field_values = none", () => {
+      it("should have 'Enter an ID' as the placeholder text", () => {
+        const component = mountFieldValuesWidget({
+          field: mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
+            has_field_values: "none",
+          }),
+        });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Enter an ID",
+        );
+      });
+    });
+    describe("has_field_values = list", () => {
+      it("should have 'Search the list' as the placeholder text", () => {
+        const component = mountFieldValuesWidget({
+          field: mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
+            has_field_values: "list",
+            values: [[1234]],
+          }),
+        });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Search the list",
+        );
+      });
+    });
+    describe("has_field_values = search", () => {
+      it("should have 'Search by Category or enter an ID' as the placeholder text", () => {
+        const component = mountFieldValuesWidget({
+          field: mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
+            has_field_values: "search",
+          }),
+          searchField: metadata.field(PRODUCT_CATEGORY_FIELD_ID),
+        });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Search by Category or enter an ID",
+        );
+      });
+      it("should not duplicate 'ID' in placeholder when ID itself is searchable", () => {
+        const field = mock(metadata.field(ORDERS_PRODUCT_FK_FIELD_ID), {
+          base_type: "type/Text",
+          has_field_values: "search",
+        });
+        const component = mountFieldValuesWidget({
+          field: field,
+          searchField: field,
+        });
+        expect(component.find(TokenField).props().placeholder).toEqual(
+          "Search by Product",
+        );
+      });
+    });
+  });
+});
diff --git a/frontend/test/components/Icon.unit.test.js b/frontend/test/components/Icon.unit.test.js
index b4e096f4b913c5e1e0178a089f187e8490ea7fd7..85c5c0635dd619f8547154baf0e294d6b6273e7f 100644
--- a/frontend/test/components/Icon.unit.test.js
+++ b/frontend/test/components/Icon.unit.test.js
@@ -1,35 +1,30 @@
-import {
-    ICON_PATHS,
-    loadIcon,
-    parseViewBox
-} from 'metabase/icon_paths'
+import { ICON_PATHS, loadIcon, parseViewBox } from "metabase/icon_paths";
 
 // find the first icon with a non standard viewBox
 const NON_STANDARD_VIEWBOX_ICON = Object.keys(ICON_PATHS).filter(key => {
-    if(ICON_PATHS[key].attrs && ICON_PATHS[key].attrs.viewBox) {
-        return ICON_PATHS[key]
-    }
-})[0]
+  if (ICON_PATHS[key].attrs && ICON_PATHS[key].attrs.viewBox) {
+    return ICON_PATHS[key];
+  }
+})[0];
 
-describe('Icon', () => {
+describe("Icon", () => {
+  describe("parseViewBox", () => {
+    it("should return the proper values from a viewBox", () => {
+      const value = 32;
+      const viewBox = `0 0 ${value} ${value}`;
 
-    describe('parseViewBox', () => {
-        it('should return the proper values from a viewBox', () => {
-            const value = 32
-            const viewBox = `0 0 ${value} ${value}`
+      expect(parseViewBox(viewBox)).toEqual([value, value]);
+    });
+  });
 
-            expect(parseViewBox(viewBox)).toEqual([value, value])
-        })
-    })
+  describe("loadIcon", () => {
+    it("should properly set a width and height based on the viewbox", () => {
+      const def = loadIcon(NON_STANDARD_VIEWBOX_ICON);
+      const { width, height, viewBox } = def.attrs;
+      const [parsedWidth, parsedHeight] = parseViewBox(viewBox);
 
-    describe('loadIcon', () => {
-       it('should properly set a width and height based on the viewbox', () => {
-           const def = loadIcon(NON_STANDARD_VIEWBOX_ICON)
-           const { width, height, viewBox } = def.attrs
-           const [parsedWidth, parsedHeight] = parseViewBox(viewBox)
-
-           expect(width).toEqual(`${parsedWidth / 2}px`)
-           expect(height).toEqual(`${parsedHeight / 2}px`)
-       })
-    })
-})
+      expect(width).toEqual(`${parsedWidth / 2}px`);
+      expect(height).toEqual(`${parsedHeight / 2}px`);
+    });
+  });
+});
diff --git a/frontend/test/components/LoadingAndErrorWrapper.unit.spec.js b/frontend/test/components/LoadingAndErrorWrapper.unit.spec.js
index 35a916a1b5a02876192ed90aae6a8ba0fa1949ee..f5b389915008266333aed568fc79005f4f13462c 100644
--- a/frontend/test/components/LoadingAndErrorWrapper.unit.spec.js
+++ b/frontend/test/components/LoadingAndErrorWrapper.unit.spec.js
@@ -1,108 +1,88 @@
-import React from 'react'
-import { shallow, mount } from 'enzyme'
-
-import LoadingAndErrorWrapper from 'metabase/components/LoadingAndErrorWrapper'
-
-describe('LoadingAndErrorWrapper', () => {
-
-    describe('Loading', () => {
-        it('should display a loading message if given a true loading prop', () => {
-            const wrapper = shallow(
-                <LoadingAndErrorWrapper loading={true}>
-                </LoadingAndErrorWrapper>
-            )
-
-            expect(wrapper.text()).toMatch(/Loading/)
-        })
-
-        it('should display a given child if loading is false', () => {
-            const Child = () => <div>Hey</div>
-
-            const wrapper = shallow(
-                <LoadingAndErrorWrapper loading={false} error={null}>
-                    { () => <Child /> }
-                </LoadingAndErrorWrapper>
-            )
-            expect(wrapper.find(Child).length).toEqual(1)
-        })
-
-        it('should display a given scene during loading', () => {
-            const Scene = () => <div>Fun load animation</div>
-
-            const wrapper = shallow(
-                <LoadingAndErrorWrapper
-                    loading={true}
-                    error={null}
-                    loadingScenes={[<Scene />]}
-                >
-                </LoadingAndErrorWrapper>
-            )
-
-            expect(wrapper.find(Scene).length).toEqual(1)
-        })
-
-        describe('cycling', () => {
-            it('should cycle through loading messages if provided', () => {
-                jest.useFakeTimers()
-
-                const interval = 6000
-
-                const wrapper = mount(
-                    <LoadingAndErrorWrapper
-                        loading={true}
-                        error={null}
-                        loadingMessages={[
-                            'One',
-                            'Two',
-                            'Three'
-                        ]}
-                        messageInterval={interval}
-                    >
-                    </LoadingAndErrorWrapper>
-
-                )
-
-                const instance = wrapper.instance()
-                const spy = jest.spyOn(instance, 'cycleLoadingMessage')
-
-                expect(wrapper.text()).toMatch(/One/)
-
-                jest.runTimersToTime(interval)
-                expect(spy).toHaveBeenCalled()
-                expect(wrapper.text()).toMatch(/Two/)
-
-                jest.runTimersToTime(interval)
-                expect(spy).toHaveBeenCalled()
-                expect(wrapper.text()).toMatch(/Three/)
-
-                jest.runTimersToTime(interval)
-                expect(spy).toHaveBeenCalled()
-                expect(wrapper.text()).toMatch(/One/)
-            })
-
-        })
-    })
-
-    describe('Errors', () => {
-
-        it('should display an error message if given an error object', () => {
-
-            const error = {
-                type: 500,
-                message: 'Big error here folks'
-            }
-
-            const wrapper = mount(
-                <LoadingAndErrorWrapper
-                    loading={true}
-                    error={error}
-                >
-                </LoadingAndErrorWrapper>
-
-            )
-
-            expect(wrapper.text()).toMatch(error.message)
-        })
-    })
-
-})
+import React from "react";
+import { shallow, mount } from "enzyme";
+
+import LoadingAndErrorWrapper from "metabase/components/LoadingAndErrorWrapper";
+
+describe("LoadingAndErrorWrapper", () => {
+  describe("Loading", () => {
+    it("should display a loading message if given a true loading prop", () => {
+      const wrapper = shallow(<LoadingAndErrorWrapper loading={true} />);
+
+      expect(wrapper.text()).toMatch(/Loading/);
+    });
+
+    it("should display a given child if loading is false", () => {
+      const Child = () => <div>Hey</div>;
+
+      const wrapper = shallow(
+        <LoadingAndErrorWrapper loading={false} error={null}>
+          {() => <Child />}
+        </LoadingAndErrorWrapper>,
+      );
+      expect(wrapper.find(Child).length).toEqual(1);
+    });
+
+    it("should display a given scene during loading", () => {
+      const Scene = () => <div>Fun load animation</div>;
+
+      const wrapper = shallow(
+        <LoadingAndErrorWrapper
+          loading={true}
+          error={null}
+          loadingScenes={[<Scene />]}
+        />,
+      );
+
+      expect(wrapper.find(Scene).length).toEqual(1);
+    });
+
+    describe("cycling", () => {
+      it("should cycle through loading messages if provided", () => {
+        jest.useFakeTimers();
+
+        const interval = 6000;
+
+        const wrapper = mount(
+          <LoadingAndErrorWrapper
+            loading={true}
+            error={null}
+            loadingMessages={["One", "Two", "Three"]}
+            messageInterval={interval}
+          />,
+        );
+
+        const instance = wrapper.instance();
+        const spy = jest.spyOn(instance, "cycleLoadingMessage");
+
+        expect(wrapper.text()).toMatch(/One/);
+
+        jest.runTimersToTime(interval);
+        expect(spy).toHaveBeenCalled();
+        expect(wrapper.text()).toMatch(/Two/);
+
+        jest.runTimersToTime(interval);
+        expect(spy).toHaveBeenCalled();
+        expect(wrapper.text()).toMatch(/Three/);
+
+        jest.runTimersToTime(interval);
+        expect(spy).toHaveBeenCalled();
+        expect(wrapper.text()).toMatch(/One/);
+      });
+    });
+  });
+
+  describe("Errors", () => {
+    it("should display an error message if given an error object", () => {
+      const error = {
+        type: 500,
+        message: "Big error here folks",
+      };
+
+      const wrapper = mount(
+        <LoadingAndErrorWrapper loading={true} error={error} />,
+      );
+
+      expect(wrapper.text()).toMatch(error.message);
+    });
+  });
+});
diff --git a/frontend/test/components/Logs.unit.spec.js b/frontend/test/components/Logs.unit.spec.js
index cdd3b3f42ae6e004ab393264a130321703589b7e..0c1f1becf7e1d9dbb3880a524ae8e3eba847b9b1 100644
--- a/frontend/test/components/Logs.unit.spec.js
+++ b/frontend/test/components/Logs.unit.spec.js
@@ -1,20 +1,19 @@
-import React from 'react'
-import Logs from '../../src/metabase/components/Logs'
-import { mount } from 'enzyme'
+import React from "react";
+import Logs from "../../src/metabase/components/Logs";
+import { mount } from "enzyme";
 
-import { UtilApi } from 'metabase/services'
+import { UtilApi } from "metabase/services";
 
-describe('Logs', () => {
-    describe('log fetching', () => {
+describe("Logs", () => {
+  describe("log fetching", () => {
+    it("should call UtilApi.logs after 1 second", () => {
+      jest.useFakeTimers();
+      const wrapper = mount(<Logs />);
+      const utilSpy = jest.spyOn(UtilApi, "logs");
 
-        it('should call UtilApi.logs after 1 second', () => {
-            jest.useFakeTimers()
-            const wrapper = mount(<Logs />)
-            const utilSpy = jest.spyOn(UtilApi, "logs")
-
-            expect(wrapper.state().logs.length).toEqual(0)
-            jest.runTimersToTime(1001)
-            expect(utilSpy).toHaveBeenCalled()
-        })
-    })
-})
+      expect(wrapper.state().logs.length).toEqual(0);
+      jest.runTimersToTime(1001);
+      expect(utilSpy).toHaveBeenCalled();
+    });
+  });
+});
diff --git a/frontend/test/components/PasswordReveal.unit.spec.js b/frontend/test/components/PasswordReveal.unit.spec.js
index d5ad07180abda4da39dcdd39ff523449534c31d4..abc9b4dcaae25cffc499c32df852c34fb6aad38a 100644
--- a/frontend/test/components/PasswordReveal.unit.spec.js
+++ b/frontend/test/components/PasswordReveal.unit.spec.js
@@ -1,25 +1,25 @@
 import { click } from "__support__/enzyme_utils";
 
-import React from 'react'
-import PasswordReveal from '../../src/metabase/components/PasswordReveal'
-import CopyButton from 'metabase/components/CopyButton'
+import React from "react";
+import PasswordReveal from "../../src/metabase/components/PasswordReveal";
+import CopyButton from "metabase/components/CopyButton";
 
-import { shallow } from 'enzyme'
+import { shallow } from "enzyme";
 
-describe('password reveal', () => {
-    let wrapper
+describe("password reveal", () => {
+  let wrapper;
 
-    beforeEach(() => {
-        wrapper = shallow(<PasswordReveal />)
-    })
+  beforeEach(() => {
+    wrapper = shallow(<PasswordReveal />);
+  });
 
-    it('should toggle the visibility state when hide / show are clicked', () => {
-        expect(wrapper.state().visible).toEqual(false)
-        click(wrapper.find('a'))
-        expect(wrapper.state().visible).toEqual(true)
-    })
+  it("should toggle the visibility state when hide / show are clicked", () => {
+    expect(wrapper.state().visible).toEqual(false);
+    click(wrapper.find("a"));
+    expect(wrapper.state().visible).toEqual(true);
+  });
 
-    it('should render a copy button', () => {
-        expect(wrapper.find(CopyButton).length).toEqual(1)
-    })
-})
+  it("should render a copy button", () => {
+    expect(wrapper.find(CopyButton).length).toEqual(1);
+  });
+});
diff --git a/frontend/test/components/StepIndicators.unit.spec.js b/frontend/test/components/StepIndicators.unit.spec.js
index 1f1048d27731df9cc2c88a1d23d2110813df2a01..d324db18077b393fdeb48e58353b1749e4488647 100644
--- a/frontend/test/components/StepIndicators.unit.spec.js
+++ b/frontend/test/components/StepIndicators.unit.spec.js
@@ -1,37 +1,39 @@
 import { click } from "__support__/enzyme_utils";
 
-import React from 'react'
-import { shallow } from 'enzyme'
+import React from "react";
+import { shallow } from "enzyme";
 
-import { normal } from 'metabase/lib/colors'
+import { normal } from "metabase/lib/colors";
 
-import StepIndicators from '../../src/metabase/components/StepIndicators'
+import StepIndicators from "../../src/metabase/components/StepIndicators";
 
-describe('Step indicators', () => {
-    let steps = [{}, {}, {}]
+describe("Step indicators", () => {
+  let steps = [{}, {}, {}];
 
-    it('should render as many indicators as steps', () => {
-        const wrapper = shallow(<StepIndicators steps={steps} />)
+  it("should render as many indicators as steps", () => {
+    const wrapper = shallow(<StepIndicators steps={steps} />);
 
-        expect(wrapper.find('li').length).toEqual(steps.length)
-    })
+    expect(wrapper.find("li").length).toEqual(steps.length);
+  });
 
-    it('should indicate the current step', () => {
-        const wrapper = shallow(<StepIndicators steps={steps} currentStep={1} />)
+  it("should indicate the current step", () => {
+    const wrapper = shallow(<StepIndicators steps={steps} currentStep={1} />);
 
-        expect(wrapper.find('li').get(0).props.style.backgroundColor).toEqual(normal.blue)
-    })
+    expect(wrapper.find("li").get(0).props.style.backgroundColor).toEqual(
+      normal.blue,
+    );
+  });
 
-    describe('goToStep', () => {
-        it('should call goToStep with the proper number when a step is clicked', () => {
-            const goToStep = jest.fn()
-            const wrapper = shallow(
-                <StepIndicators steps={steps} goToStep={goToStep} currentStep={1} />
-            )
+  describe("goToStep", () => {
+    it("should call goToStep with the proper number when a step is clicked", () => {
+      const goToStep = jest.fn();
+      const wrapper = shallow(
+        <StepIndicators steps={steps} goToStep={goToStep} currentStep={1} />,
+      );
 
-            const targetIndicator = wrapper.find('li').first()
-            click(targetIndicator);
-            expect(goToStep).toHaveBeenCalledWith(1)
-        })
-    })
-})
+      const targetIndicator = wrapper.find("li").first();
+      click(targetIndicator);
+      expect(goToStep).toHaveBeenCalledWith(1);
+    });
+  });
+});
diff --git a/frontend/test/components/TokenField.unit.spec.js b/frontend/test/components/TokenField.unit.spec.js
new file mode 100644
index 0000000000000000000000000000000000000000..e13be0899b66594c3a8317117cd744048ac2cfec
--- /dev/null
+++ b/frontend/test/components/TokenField.unit.spec.js
@@ -0,0 +1,452 @@
+/* eslint-disable react/display-name */
+
+import React from "react";
+import { mount } from "enzyme";
+
+import TokenField from "../../src/metabase/components/TokenField";
+
+import { delay } from "../../src/metabase/lib/promise";
+
+import {
+  KEYCODE_DOWN,
+  KEYCODE_TAB,
+  KEYCODE_ENTER,
+  KEYCODE_COMMA,
+} from "metabase/lib/keyboard";
+
+const DEFAULT_OPTIONS = ["Doohickey", "Gadget", "Gizmo", "Widget"];
+
+const MockValue = ({ value }) => <span>{value}</span>;
+const MockOption = ({ option }) => <span>{option}</span>;
+
+const DEFAULT_TOKEN_FIELD_PROPS = {
+  options: [],
+  value: [],
+  valueKey: option => option,
+  labelKey: option => option,
+  valueRenderer: value => <MockValue value={value} />,
+  optionRenderer: option => <MockOption option={option} />,
+  layoutRenderer: ({ valuesList, optionsList }) => (
+    <div>
+      {valuesList}
+      {optionsList}
+    </div>
+  ),
+};
+
+class TokenFieldWithStateAndDefaults extends React.Component {
+  constructor(props) {
+    super(props);
+    this.state = {
+      value: props.value || [],
+    };
+  }
+  render() {
+    // allow overriding everything except value and onChange which we provide
+    // eslint-disable-next-line no-unused-vars
+    const { value, onChange, ...props } = this.props;
+    return (
+      <TokenField
+        {...DEFAULT_TOKEN_FIELD_PROPS}
+        {...props}
+        value={this.state.value}
+        onChange={value => {
+          this.setState({ value });
+          if (onChange) {
+            onChange(value);
+          }
+        }}
+      />
+    );
+  }
+}
+
+describe("TokenField", () => {
+  let component;
+  const input = () => component.find("input");
+  const value = () => component.state().value;
+  const options = () => component.find(MockOption).map(o => o.text());
+  const values = () => component.find(MockValue).map(v => v.text());
+  const blur = () => input().simulate("blur");
+  const focus = () => input().simulate("focus");
+  const type = str => input().simulate("change", { target: { value: str } });
+  const focusAndType = str => focus() && type(str);
+  const keyDown = keyCode => input().simulate("keydown", { keyCode });
+  const clickOption = (n = 0) =>
+    component
+      .find(MockOption)
+      .at(n)
+      .simulate("click");
+
+  afterEach(() => {
+    component = null;
+  });
+
+  it("should render with no options or values", () => {
+    component = mount(<TokenFieldWithStateAndDefaults />);
+    expect(values()).toEqual([]);
+    expect(options()).toEqual([]);
+  });
+  it("should render with 1 options and 1 values", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults value={["foo"]} options={["bar"]} />,
+    );
+    expect(values()).toEqual(["foo"]);
+    expect(options()).toEqual(["bar"]);
+  });
+  it("shouldn't show previous used option by default", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults value={["foo"]} options={["foo"]} />,
+    );
+    expect(options()).toEqual([]);
+  });
+  it("should show previous used option if removeSelected={false} is provided", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults
+        value={["foo"]}
+        options={["foo"]}
+        removeSelected={false}
+      />,
+    );
+    expect(options()).toEqual(["foo"]);
+  });
+  it("should filter correctly", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults
+        value={["foo"]}
+        options={["bar", "baz"]}
+      />,
+    );
+    focusAndType("nope");
+    expect(options()).toEqual([]);
+    type("bar");
+    expect(options()).toEqual(["bar"]);
+  });
+
+  it("should add freeform value if parseFreeformValue is provided", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults
+        value={[]}
+        options={["bar", "baz"]}
+        parseFreeformValue={value => value}
+      />,
+    );
+    focusAndType("yep");
+    expect(value()).toEqual([]);
+    keyDown(KEYCODE_ENTER);
+    expect(value()).toEqual(["yep"]);
+  });
+
+  it("should add option when clicked", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults value={[]} options={["bar", "baz"]} />,
+    );
+    expect(value()).toEqual([]);
+    clickOption(0);
+    expect(value()).toEqual(["bar"]);
+  });
+
+  it("should hide the added option", async () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults value={[]} options={["bar", "baz"]} />,
+    );
+    expect(options()).toEqual(["bar", "baz"]);
+    clickOption(0);
+    await delay(100);
+    expect(options()).toEqual(["baz"]);
+  });
+
+  it("should add option when filtered and clicked", () => {
+    component = mount(
+      <TokenFieldWithStateAndDefaults value={[]} options={["foo", "bar"]} />,
+    );
+
+    focus();
+    expect(value()).toEqual([]);
+    type("ba");
+    clickOption(0);
+    expect(value()).toEqual(["bar"]);
+  });
+
+  describe("when updateOnInputChange is provided", () => {
+    beforeEach(() => {
+      component = mount(
+        <TokenFieldWithStateAndDefaults
+          options={DEFAULT_OPTIONS}
+          multi
+          // return null for empty string since it's not a valid
+          parseFreeformValue={value => value || null}
+          updateOnInputChange
+        />,
+      );
+    });
+
+    it("should add freeform value immediately if updateOnInputChange is provided", () => {
+      focusAndType("yep");
+      expect(value()).toEqual(["yep"]);
+    });
+
+    it("should only add one option when filtered and clicked", async () => {
+      expect(value()).toEqual([]);
+      focusAndType("Do");
+      expect(value()).toEqual(["Do"]);
+
+      clickOption(0);
+      expect(value()).toEqual(["Doohickey"]);
+      expect(input().props().value).toEqual("");
+    });
+
+    it("should only add one option when filtered and enter is pressed", async () => {
+      expect(value()).toEqual([]);
+      focusAndType("Do");
+      expect(value()).toEqual(["Do"]);
+
+      // press enter
+      keyDown(KEYCODE_ENTER);
+      expect(value()).toEqual(["Doohickey"]);
+      expect(input().props().value).toEqual("");
+    });
+
+    it("shouldn't hide option matching input freeform value", () => {
+      expect(options()).toEqual(DEFAULT_OPTIONS);
+      focusAndType("Doohickey");
+      expect(value()).toEqual(["Doohickey"]);
+      expect(options()).toEqual(["Doohickey"]);
+    });
+
+    it("should commit after typing an option and hitting enter", () => {
+      expect(options()).toEqual(DEFAULT_OPTIONS);
+      focusAndType("Doohickey");
+      expect(value()).toEqual(["Doohickey"]);
+
+      keyDown(KEYCODE_ENTER);
+      expect(values()).toEqual(["Doohickey"]);
+      expect(options()).toEqual(["Gadget", "Gizmo", "Widget"]);
+    });
+
+    it("should not commit empty freeform value", () => {
+      focusAndType("Doohickey");
+      focusAndType("");
+      blur();
+      expect(value()).toEqual([]);
+      expect(values()).toEqual([]);
+    });
+
+    it("should hide the input but not clear the search after accepting an option", () => {
+      focusAndType("G");
+      expect(options()).toEqual(["Gadget", "Gizmo"]);
+      keyDown(KEYCODE_ENTER);
+      expect(options()).toEqual(["Gizmo"]);
+      expect(input().props().value).toEqual("");
+    });
+
+    it("should reset the search when focusing", () => {
+      focusAndType("G");
+      expect(options()).toEqual(["Gadget", "Gizmo"]);
+      keyDown(KEYCODE_ENTER);
+      expect(options()).toEqual(["Gizmo"]);
+      focus();
+      expect(options()).toEqual(["Doohickey", "Gizmo", "Widget"]);
+    });
+
+    it("should reset the search when adding the last option", () => {
+      focusAndType("G");
+      expect(options()).toEqual(["Gadget", "Gizmo"]);
+      keyDown(KEYCODE_ENTER);
+      expect(options()).toEqual(["Gizmo"]);
+      keyDown(KEYCODE_ENTER);
+      expect(options()).toEqual(["Doohickey", "Widget"]);
+    });
+
+    it("should hide the option if typed exactly then press enter", () => {
+      focusAndType("Gadget");
+      expect(options()).toEqual(["Gadget"]);
+      keyDown(KEYCODE_ENTER);
+      expect(values()).toEqual(["Gadget"]);
+      expect(options()).toEqual(["Doohickey", "Gizmo", "Widget"]);
+    });
+
+    it("should hide the option if typed partially then press enter", () => {
+      focusAndType("Gad");
+      expect(options()).toEqual(["Gadget"]);
+      keyDown(KEYCODE_ENTER);
+      expect(values()).toEqual(["Gadget"]);
+      expect(options()).toEqual(["Doohickey", "Gizmo", "Widget"]);
+    });
+
+    it("should hide the option if typed exactly then clicked", () => {
+      focusAndType("Gadget");
+      expect(options()).toEqual(["Gadget"]);
+      clickOption(0);
+      expect(values()).toEqual(["Gadget"]);
+      expect(options()).toEqual(["Doohickey", "Gizmo", "Widget"]);
+    });
+
+    it("should hide the option if typed partially then clicked", () => {
+      focusAndType("Gad");
+      expect(options()).toEqual(["Gadget"]);
+      clickOption(0);
+      expect(values()).toEqual(["Gadget"]);
+      expect(options()).toEqual(["Doohickey", "Gizmo", "Widget"]);
+    });
+  });
+
+  describe("key selection", () => {
+    [KEYCODE_TAB, KEYCODE_ENTER, KEYCODE_COMMA].map(key =>
+      it(`should allow the user to use arrow keys and then ${key} to select a recipient`, () => {
+        const spy = jest.fn();
+
+        component = mount(
+          <TokenField
+            {...DEFAULT_TOKEN_FIELD_PROPS}
+            options={DEFAULT_OPTIONS}
+            onChange={spy}
+          />,
+        );
+
+        // limit our options by typing
+        focusAndType("G");
+
+        // the initially selected option should be the first option
+        expect(component.state().selectedOptionValue).toBe(DEFAULT_OPTIONS[1]);
+
+        input().simulate("keydown", {
+          keyCode: KEYCODE_DOWN,
+          preventDefault: jest.fn(),
+        });
+
+        // the next possible option should be selected now
+        expect(component.state().selectedOptionValue).toBe(DEFAULT_OPTIONS[2]);
+
+        input().simulate("keydown", {
+          keyCode: key,
+          preventDefalut: jest.fn(),
+        });
+
+        expect(spy).toHaveBeenCalledTimes(1);
+        expect(spy).toHaveBeenCalledWith([DEFAULT_OPTIONS[2]]);
+      }),
+    );
+  });
+
+  describe("with multi=true", () => {
+    it("should prevent blurring on tab", () => {
+      const preventDefault = jest.fn();
+      component = mount(
+        <TokenFieldWithStateAndDefaults
+          options={DEFAULT_OPTIONS}
+          // return null for empty string since it's not a valid
+          parseFreeformValue={value => value || null}
+          updateOnInputChange
+          multi
+        />,
+      );
+      focusAndType("asdf");
+      input().simulate("keydown", {
+        keyCode: KEYCODE_TAB,
+        preventDefault: preventDefault,
+      });
+      expect(preventDefault).toHaveBeenCalled();
+    });
+    it('should paste "1,2,3" as multiple values', () => {
+      const preventDefault = jest.fn();
+      component = mount(
+        <TokenFieldWithStateAndDefaults
+          // return null for empty string since it's not a valid
+          parseFreeformValue={value => value || null}
+          updateOnInputChange
+          multi
+        />,
+      );
+      input().simulate("paste", {
+        clipboardData: {
+          getData: () => "1,2,3",
+        },
+        preventDefault,
+      });
+      expect(values()).toEqual(["1", "2", "3"]);
+      // prevent pasting into <input>
+      expect(preventDefault).toHaveBeenCalled();
+    });
+  });
+  describe("with multi=false", () => {
+    it("should not prevent blurring on tab", () => {
+      const preventDefault = jest.fn();
+      component = mount(
+        <TokenFieldWithStateAndDefaults
+          options={DEFAULT_OPTIONS}
+          // return null for empty string since it's not a valid
+          parseFreeformValue={value => value || null}
+          updateOnInputChange
+        />,
+      );
+      focusAndType("asdf");
+      input().simulate("keydown", {
+        keyCode: KEYCODE_TAB,
+        preventDefault: preventDefault,
+      });
+      expect(preventDefault).not.toHaveBeenCalled();
+    });
+    it('should paste "1,2,3" as one value', () => {
+      const preventDefault = jest.fn();
+      component = mount(
+        <TokenFieldWithStateAndDefaults
+          // return null for empty string since it's not a valid
+          parseFreeformValue={value => value || null}
+          updateOnInputChange
+        />,
+      );
+      input().simulate("paste", {
+        clipboardData: {
+          getData: () => "1,2,3",
+        },
+        preventDefault,
+      });
+      expect(values()).toEqual(["1,2,3"]);
+      // prevent pasting into <input>
+      expect(preventDefault).toHaveBeenCalled();
+    });
+  });
+
+  describe("custom layoutRenderer", () => {
+    let layoutRenderer;
+    beforeEach(() => {
+      layoutRenderer = jest
+        .fn()
+        .mockImplementation(({ valuesList, optionsList }) => (
+          <div>
+            {valuesList}
+            {optionsList}
+          </div>
+        ));
+      component = mount(
+        <TokenFieldWithStateAndDefaults
+          options={["hello"]}
+          layoutRenderer={layoutRenderer}
+        />,
+      );
+    });
+    it("should be called with isFiltered=true when filtered", () => {
+      let call = layoutRenderer.mock.calls.pop();
+      expect(call[0].isFiltered).toEqual(false);
+      expect(call[0].isAllSelected).toEqual(false);
+      focus();
+      type("blah");
+      call = layoutRenderer.mock.calls.pop();
+      expect(call[0].optionList).toEqual(undefined);
+      expect(call[0].isFiltered).toEqual(true);
+      expect(call[0].isAllSelected).toEqual(false);
+    });
+    it("should be called with isAllSelected=true when all options are selected", () => {
+      let call = layoutRenderer.mock.calls.pop();
+      expect(call[0].isFiltered).toEqual(false);
+      expect(call[0].isAllSelected).toEqual(false);
+      focus();
+      keyDown(KEYCODE_ENTER);
+      call = layoutRenderer.mock.calls.pop();
+      expect(call[0].optionList).toEqual(undefined);
+      expect(call[0].isFiltered).toEqual(false);
+      expect(call[0].isAllSelected).toEqual(true);
+    });
+  });
+});
diff --git a/frontend/test/containers/SaveQuestionModal.unit.spec.js b/frontend/test/containers/SaveQuestionModal.unit.spec.js
index 86f46ba51a682dcb88870fc3a830703ace08e336..11494be72d51b7744db8c0b1fc5a07f7da153695 100644
--- a/frontend/test/containers/SaveQuestionModal.unit.spec.js
+++ b/frontend/test/containers/SaveQuestionModal.unit.spec.js
@@ -1,86 +1,103 @@
-import React from 'react'
-import { shallow } from 'enzyme'
+import React from "react";
+import { shallow } from "enzyme";
 
-import SaveQuestionModal from '../../src/metabase/containers/SaveQuestionModal';
+import SaveQuestionModal from "../../src/metabase/containers/SaveQuestionModal";
 import Question from "metabase-lib/lib/Question";
+
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    PEOPLE_TABLE_ID,
-    metadata,
-    ORDERS_TOTAL_FIELD_ID
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  PEOPLE_TABLE_ID,
+  metadata,
+  ORDERS_TOTAL_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 const createFnMock = jest.fn(() => Promise.resolve());
 let saveFnMock;
 
-const getSaveQuestionModal = (question, originalQuestion) =>
-    <SaveQuestionModal
-        card={question.card()}
-        originalCard={originalQuestion && originalQuestion.card()}
-        tableMetadata={question.tableMetadata()}
-        createFn={createFnMock}
-        saveFn={saveFnMock}
-        onClose={() => {}}
-    />
+const getSaveQuestionModal = (question, originalQuestion) => (
+  <SaveQuestionModal
+    card={question.card()}
+    originalCard={originalQuestion && originalQuestion.card()}
+    tableMetadata={question.tableMetadata()}
+    createFn={createFnMock}
+    saveFn={saveFnMock}
+    onClose={() => {}}
+  />
+);
+
+describe("SaveQuestionModal", () => {
+  beforeEach(() => {
+    // we need to create a new save mock before each test to ensure that each
+    // test has its own instance
+    saveFnMock = jest.fn(() => Promise.resolve());
+  });
 
-describe('SaveQuestionModal', () => {
-    beforeEach(() => {
-        // we need to create a new save mock before each test to ensure that each
-        // test has its own instance
-        saveFnMock = jest.fn(() => Promise.resolve());
+  it("should call createFn correctly for a new question", async () => {
+    const newQuestion = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
     })
+      .query()
+      .addAggregation(["count"])
+      .question();
 
-    it("should call createFn correctly for a new question", async () => {
-        const newQuestion = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-            .query()
-            .addAggregation(["count"])
-            .question()
+    // Use the count aggregation as an example case (this is equally valid for filters and groupings)
+    const component = shallow(getSaveQuestionModal(newQuestion, null));
+    await component.instance().formSubmitted();
+    expect(createFnMock.mock.calls.length).toBe(1);
+  });
+  it("should call saveFn correctly for a dirty, saved question", async () => {
+    const originalQuestion = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
+    })
+      .query()
+      .addAggregation(["count"])
+      .question();
+    // "Save" the question
+    originalQuestion.card.id = 5;
 
-        // Use the count aggregation as an example case (this is equally valid for filters and groupings)
-        const component = shallow(getSaveQuestionModal(newQuestion, null));
-        await component.instance().formSubmitted();
-        expect(createFnMock.mock.calls.length).toBe(1);
+    const dirtyQuestion = originalQuestion
+      .query()
+      .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
+      .question();
 
-    });
-    it("should call saveFn correctly for a dirty, saved question", async () => {
-        const originalQuestion = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-            .query()
-            .addAggregation(["count"])
-            .question()
-        // "Save" the question
-        originalQuestion.card.id = 5;
+    // Use the count aggregation as an example case (this is equally valid for filters and groupings)
+    const component = shallow(
+      getSaveQuestionModal(dirtyQuestion, originalQuestion),
+    );
+    await component.instance().formSubmitted();
+    expect(saveFnMock.mock.calls.length).toBe(1);
+  });
 
-        const dirtyQuestion = originalQuestion
-            .query()
-            .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
-            .question()
+  it("should preserve the collection_id of a question in overwrite mode", async () => {
+    let originalQuestion = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: PEOPLE_TABLE_ID,
+      metadata,
+    })
+      .query()
+      .addAggregation(["count"])
+      .question();
 
-        // Use the count aggregation as an example case (this is equally valid for filters and groupings)
-        const component = shallow(getSaveQuestionModal(dirtyQuestion, originalQuestion));
-        await component.instance().formSubmitted();
-        expect(saveFnMock.mock.calls.length).toBe(1);
+    // set the collection_id of the original question
+    originalQuestion = originalQuestion.setCard({
+      ...originalQuestion.card(),
+      collection_id: 5,
     });
 
-    it("should preserve the collection_id of a question in overwrite mode", async () => {
-        let originalQuestion = Question.create({databaseId: DATABASE_ID, tableId: PEOPLE_TABLE_ID, metadata})
-            .query()
-            .addAggregation(["count"])
-            .question()
-
-        // set the collection_id of the original question
-        originalQuestion = originalQuestion.setCard({
-            ...originalQuestion.card(),
-            collection_id: 5
-        })
+    let dirtyQuestion = originalQuestion
+      .query()
+      .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
+      .question();
 
-        let dirtyQuestion = originalQuestion
-            .query()
-            .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
-            .question()
-
-        const component = shallow(getSaveQuestionModal(dirtyQuestion, originalQuestion))
-        await component.instance().formSubmitted();
-        expect(saveFnMock.mock.calls[0][0].collection_id).toEqual(5);
-    })
+    const component = shallow(
+      getSaveQuestionModal(dirtyQuestion, originalQuestion),
+    );
+    await component.instance().formSubmitted();
+    expect(saveFnMock.mock.calls[0][0].collection_id).toEqual(5);
+  });
 });
diff --git a/frontend/test/dashboard/DashCard.unit.spec.js b/frontend/test/dashboard/DashCard.unit.spec.js
index b5fdc845ad073ba297a1a1ea98c20fc59962d88d..6aaefd4b297592fba3545b7d0ecfc3bc115b6737 100644
--- a/frontend/test/dashboard/DashCard.unit.spec.js
+++ b/frontend/test/dashboard/DashCard.unit.spec.js
@@ -8,38 +8,38 @@ import DashCard from "metabase/dashboard/components/DashCard";
 jest.mock("metabase/visualizations/components/Visualization.jsx");
 
 const DEFAULT_PROPS = {
-    dashcard: {
-        card: { id: 1 },
-        series: [],
-        parameter_mappings: []
-    },
-    dashcardData: {
-        1: { cols: [], rows: [] }
-    },
-    slowCards: {},
-    parameterValues: {},
-    markNewCardSeen: () => {},
-    fetchCardData: () => {}
+  dashcard: {
+    card: { id: 1 },
+    series: [],
+    parameter_mappings: [],
+  },
+  dashcardData: {
+    1: { cols: [], rows: [] },
+  },
+  slowCards: {},
+  parameterValues: {},
+  markNewCardSeen: () => {},
+  fetchCardData: () => {},
 };
 
 describe("DashCard", () => {
-    it("should render with no special classNames", () => {
-        expect(
-            renderer.create(<DashCard {...DEFAULT_PROPS} />).toJSON()
-        ).toMatchSnapshot();
-    });
-    it("should render slow card with Card--slow className", () => {
-        const props = assocIn(DEFAULT_PROPS, ["slowCards", 1], true);
-        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);
-    });
+  it("should render with no special classNames", () => {
+    expect(
+      renderer.create(<DashCard {...DEFAULT_PROPS} />).toJSON(),
+    ).toMatchSnapshot();
+  });
+  it("should render slow card with Card--slow className", () => {
+    const props = assocIn(DEFAULT_PROPS, ["slowCards", 1], true);
+    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/test/dashboard/dashboard.integ.spec.js b/frontend/test/dashboard/dashboard.integ.spec.js
index d743e95979d5509496f8df78093f91cb379d3fd5..298004dfd7ff3ff206e015b30b3abc7e3f6f9820 100644
--- a/frontend/test/dashboard/dashboard.integ.spec.js
+++ b/frontend/test/dashboard/dashboard.integ.spec.js
@@ -1,11 +1,10 @@
+import "__support__/mocks";
 import {
-    createTestStore,
-    useSharedAdminLogin
+  BROWSER_HISTORY_PUSH,
+  createTestStore,
+  useSharedAdminLogin,
 } from "__support__/integrated_tests";
-import {
-    click, clickButton,
-    setInputValue
-} from "__support__/enzyme_utils"
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 
 import { DashboardApi, PublicApi } from "metabase/services";
 import * as Urls from "metabase/lib/urls";
@@ -13,165 +12,241 @@ import { makeGetMergedParameterFieldValues } from "metabase/selectors/metadata";
 import { ADD_PARAM_VALUES } from "metabase/redux/metadata";
 import { mount } from "enzyme";
 import {
-    fetchDashboard,
-    ADD_PARAMETER,
-    FETCH_DASHBOARD,
-    SAVE_DASHBOARD_AND_CARDS,
-    SET_EDITING_DASHBOARD,
-    SET_EDITING_PARAMETER_ID
+  fetchDashboard,
+  ADD_PARAMETER,
+  FETCH_DASHBOARD,
+  SAVE_DASHBOARD_AND_CARDS,
+  SET_EDITING_DASHBOARD,
+  SET_EDITING_PARAMETER_ID,
+  FETCH_REVISIONS,
 } from "metabase/dashboard/dashboard";
 import EditBar from "metabase/components/EditBar";
 
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 import DashboardHeader from "metabase/dashboard/components/DashboardHeader";
-import { ParameterOptionItem, ParameterOptionsSection } from "metabase/dashboard/components/ParametersPopover";
+import {
+  ParameterOptionItem,
+  ParameterOptionsSection,
+} from "metabase/dashboard/components/ParametersPopover";
 import ParameterValueWidget from "metabase/parameters/components/ParameterValueWidget";
 import { PredefinedRelativeDatePicker } from "metabase/parameters/components/widgets/DateRelativeWidget";
 import HeaderModal from "metabase/components/HeaderModal";
+import { DashboardHistoryModal } from "metabase/dashboard/components/DashboardHistoryModal";
 
 // TODO Atte Keinänen 7/17/17: When we have a nice way to create dashboards in tests, this could use a real saved dashboard
 // instead of mocking the API endpoint
 
 // Mock the dashboard endpoint using a real response of `public/dashboard/:dashId`
 const mockPublicDashboardResponse = {
-    "name": "Dashboard",
-    "description": "For testing parameter values",
-    "id": 40,
-    "parameters": [{"name": "Category", "slug": "category", "id": "598ab323", "type": "category"}],
-    "ordered_cards": [{
-        "sizeX": 6,
-        "series": [],
-        "card": {
-            "id": 25,
-            "name": "Orders over time",
-            "description": null,
-            "display": "line",
-            "dataset_query": {"type": "query"}
+  name: "Dashboard",
+  description: "For testing parameter values",
+  id: 40,
+  parameters: [
+    { name: "Category", slug: "category", id: "598ab323", type: "category" },
+  ],
+  ordered_cards: [
+    {
+      sizeX: 6,
+      series: [],
+      card: {
+        id: 25,
+        name: "Orders over time",
+        description: null,
+        display: "line",
+        dataset_query: { type: "query" },
+      },
+      col: 0,
+      id: 105,
+      parameter_mappings: [
+        {
+          parameter_id: "598ab323",
+          card_id: 25,
+          target: ["dimension", ["fk->", 3, 21]],
         },
-        "col": 0,
-        "id": 105,
-        "parameter_mappings": [{
-            "parameter_id": "598ab323",
-            "card_id": 25,
-            "target": ["dimension", ["fk->", 3, 21]]
-        }],
-        "card_id": 25,
-        "visualization_settings": {},
-        "dashboard_id": 40,
-        "sizeY": 6,
-        "row": 0
-    }],
-    // Parameter values are self-contained in the public dashboard response
-    "param_values": {
-        "21": {
-            "values": ["Doohickey", "Gadget", "Gizmo", "Widget"],
-            "human_readable_values": {},
-            "field_id": 21
-        }
-    }
-}
+      ],
+      card_id: 25,
+      visualization_settings: {},
+      dashboard_id: 40,
+      sizeY: 6,
+      row: 0,
+    },
+  ],
+  // Parameter values are self-contained in the public dashboard response
+  param_values: {
+    "21": {
+      values: ["Doohickey", "Gadget", "Gizmo", "Widget"],
+      human_readable_values: {},
+      field_id: 21,
+    },
+  },
+};
 PublicApi.dashboard = async () => {
-    return mockPublicDashboardResponse;
-}
+  return mockPublicDashboardResponse;
+};
 
 describe("Dashboard", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    })
-
-    describe("redux actions", () => {
-        describe("fetchDashboard(...)", () => {
-            it("should add the parameter values to state tree for public dashboards", async () => {
-                const store = await createTestStore();
-                // using hash as dashboard id should invoke the public API
-                await store.dispatch(fetchDashboard('6e59cc97-3b6a-4bb6-9e7a-5efeee27e40f'));
-                await store.waitForActions(ADD_PARAM_VALUES)
-
-                const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
-                const fieldValues = await getMergedParameterFieldValues(store.getState(), { parameter: { field_ids: [ 21 ] }});
-                expect(fieldValues).toEqual([["Doohickey"], ["Gadget"], ["Gizmo"], ["Widget"]]);
-            })
-        })
-    })
-
-    // Converted from Selenium E2E test
-    describe("dashboard page", () => {
-        let dashboardId = null;
-
-        it("lets you change title and description", async () => {
-            const name = "Customer Feedback Analysis"
-            const description = "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";
-
-            // Create a dashboard programmatically
-            const dashboard = await DashboardApi.create({name, description});
-            dashboardId = dashboard.id;
-
-            const store = await createTestStore();
-            store.pushPath(Urls.dashboard(dashboardId));
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([FETCH_DASHBOARD])
-
-            // Test dashboard renaming
-            click(app.find(".Icon.Icon-pencil"));
-            await store.waitForActions([SET_EDITING_DASHBOARD]);
-
-            const headerInputs = app.find(".Header-title input")
-            setInputValue(headerInputs.first(), "Customer Analysis Paralysis")
-            setInputValue(headerInputs.at(1), "")
-
-            clickButton(app.find(EditBar).find(".Button--primary.Button"));
-            await store.waitForActions([SAVE_DASHBOARD_AND_CARDS, FETCH_DASHBOARD])
-
-            await delay(500)
-
-            expect(app.find(DashboardHeader).text()).toMatch(/Customer Analysis Paralysis/)
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("redux actions", () => {
+    describe("fetchDashboard(...)", () => {
+      it("should add the parameter values to state tree for public dashboards", async () => {
+        const store = await createTestStore();
+        // using hash as dashboard id should invoke the public API
+        await store.dispatch(
+          fetchDashboard("6e59cc97-3b6a-4bb6-9e7a-5efeee27e40f"),
+        );
+        await store.waitForActions(ADD_PARAM_VALUES);
+
+        const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
+        const fieldValues = await getMergedParameterFieldValues(
+          store.getState(),
+          { parameter: { field_ids: [21] } },
+        );
+        expect(fieldValues).toEqual([
+          ["Doohickey"],
+          ["Gadget"],
+          ["Gizmo"],
+          ["Widget"],
+        ]);
+      });
+    });
+  });
+
+  // Converted from Selenium E2E test
+  describe("dashboard page", () => {
+    let dashboardId = null;
+
+    it("lets you change title and description", async () => {
+      const name = "Customer Feedback Analysis";
+      const description =
+        "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";
+
+      // Create a dashboard programmatically
+      const dashboard = await DashboardApi.create({ name, description });
+      dashboardId = dashboard.id;
+
+      const store = await createTestStore();
+      store.pushPath(Urls.dashboard(dashboardId));
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([FETCH_DASHBOARD]);
+
+      // Test dashboard renaming
+      click(app.find(".Icon.Icon-pencil"));
+      await store.waitForActions([SET_EDITING_DASHBOARD]);
+
+      const headerInputs = app.find(".Header-title input");
+      setInputValue(headerInputs.first(), "Customer Analysis Paralysis");
+      setInputValue(headerInputs.at(1), "");
+
+      clickButton(app.find(EditBar).find(".Button--primary.Button"));
+      await store.waitForActions([SAVE_DASHBOARD_AND_CARDS, FETCH_DASHBOARD]);
+
+      await delay(500);
+
+      expect(app.find(DashboardHeader).text()).toMatch(
+        /Customer Analysis Paralysis/,
+      );
+    });
+
+    it("lets you add a filter", async () => {
+      if (!dashboardId)
+        throw new Error(
+          "Test fails because previous tests failed to create a dashboard",
+        );
+
+      const store = await createTestStore();
+      store.pushPath(Urls.dashboard(dashboardId));
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DASHBOARD]);
+
+      // Test parameter filter creation
+      click(app.find(".Icon.Icon-pencil"));
+      await store.waitForActions([SET_EDITING_DASHBOARD]);
+      click(app.find(".Icon.Icon-funneladd"));
+      // Choose Time filter type
+      click(
+        app
+          .find(ParameterOptionsSection)
+          .filterWhere(section => section.text().match(/Time/)),
+      );
+
+      // Choose Relative date filter
+      click(
+        app
+          .find(ParameterOptionItem)
+          .filterWhere(item => item.text().match(/Relative Date/)),
+      );
+
+      await store.waitForActions(ADD_PARAMETER);
+
+      click(app.find(ParameterValueWidget));
+      clickButton(
+        app
+          .find(PredefinedRelativeDatePicker)
+          .find("button[children='Yesterday']"),
+      );
+      expect(app.find(ParameterValueWidget).text()).toEqual("Yesterday");
+
+      clickButton(app.find(HeaderModal).find("button[children='Done']"));
+
+      // Wait until the header modal exit animation is finished
+      await store.waitForActions([SET_EDITING_PARAMETER_ID]);
+    });
+
+    it("lets you open and close the revisions screen", async () => {
+      if (!dashboardId)
+        throw new Error(
+          "Test fails because previous tests failed to create a dashboard",
+        );
+
+      const store = await createTestStore();
+      const dashboardUrl = Urls.dashboard(dashboardId);
+      store.pushPath(dashboardUrl);
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_DASHBOARD]);
+
+      click(app.find(".Icon.Icon-pencil"));
+      await store.waitForActions([SET_EDITING_DASHBOARD]);
+
+      click(app.find(".Icon.Icon-history"));
+
+      await store.waitForActions([FETCH_REVISIONS]);
+      const modal = app.find(DashboardHistoryModal);
+      expect(modal.length).toBe(1);
+      expect(store.getPath()).toBe(`${dashboardUrl}/history`);
+
+      click(modal.find(".Icon.Icon-close"));
+      await store.waitForActions([BROWSER_HISTORY_PUSH]);
+      expect(store.getPath()).toBe(`/dashboard/${dashboardId}`);
+    });
+
+    it("lets you go directly to the revisions screen via url", async () => {
+      const store = await createTestStore();
+      const dashboardUrl = Urls.dashboard(dashboardId);
+      store.pushPath(dashboardUrl + `/history`);
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_REVISIONS]);
+
+      const modal = app.find(DashboardHistoryModal);
+      expect(modal.length).toBe(1);
+      expect(store.getPath()).toBe(`${dashboardUrl}/history`);
+
+      // check that we can normally return to the revisions screen
+      click(modal.find(".Icon.Icon-close"));
+      await store.waitForActions([BROWSER_HISTORY_PUSH]);
+      expect(store.getPath()).toBe(`/dashboard/${dashboardId}`);
+    });
+
+    afterAll(async () => {
+      if (dashboardId) {
+        await DashboardApi.update({
+          id: dashboardId,
+          archived: true,
         });
-
-        it("lets you add a filter", async () => {
-            if (!dashboardId) throw new Error("Test fails because previous tests failed to create a dashboard");
-
-            const store = await createTestStore();
-            store.pushPath(Urls.dashboard(dashboardId));
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_DASHBOARD])
-
-            // Test parameter filter creation
-            click(app.find(".Icon.Icon-pencil"));
-            await store.waitForActions([SET_EDITING_DASHBOARD]);
-            click(app.find(".Icon.Icon-funneladd"));
-            // Choose Time filter type
-            click(
-                app.find(ParameterOptionsSection)
-                    .filterWhere((section) => section.text().match(/Time/))
-            );
-
-            // Choose Relative date filter
-            click(
-                app.find(ParameterOptionItem)
-                    .filterWhere((item) => item.text().match(/Relative Date/))
-            )
-
-            await store.waitForActions(ADD_PARAMETER)
-
-            click(app.find(ParameterValueWidget));
-            clickButton(app.find(PredefinedRelativeDatePicker).find("button[children='Yesterday']"));
-            expect(app.find(ParameterValueWidget).text()).toEqual("Yesterday");
-
-            clickButton(app.find(HeaderModal).find("button[children='Done']"))
-
-            // Wait until the header modal exit animation is finished
-            await store.waitForActions([SET_EDITING_PARAMETER_ID])
-        })
-
-        afterAll(async () => {
-            if (dashboardId) {
-                await DashboardApi.update({
-                    id: dashboardId,
-                    archived: true
-                });
-            }
-        })
-    })
-})
-
+      }
+    });
+  });
+});
diff --git a/frontend/test/dashboard/dashboard.unit.spec.js b/frontend/test/dashboard/dashboard.unit.spec.js
index 9290c95206994a319f13d6066fc1af3963f0e84d..7871e47bc9d041f323adf85b9d37ad144e84f880 100644
--- a/frontend/test/dashboard/dashboard.unit.spec.js
+++ b/frontend/test/dashboard/dashboard.unit.spec.js
@@ -1,32 +1,33 @@
-import { fetchDataOrError } from 'metabase/dashboard/dashboard';
+import { fetchDataOrError } from "metabase/dashboard/dashboard";
 
 describe("Dashboard", () => {
-    describe("fetchDataOrError()", () => {
-        it("should return data on successful fetch", async () => {
-            const data = {
-                series: [1, 2, 3]
-            };
+  describe("fetchDataOrError()", () => {
+    it("should return data on successful fetch", async () => {
+      const data = {
+        series: [1, 2, 3],
+      };
 
-            const successfulFetch = Promise.resolve(data);
+      const successfulFetch = Promise.resolve(data);
 
-            const result = await fetchDataOrError(successfulFetch);
-            expect(result.error).toBeUndefined();
-            expect(result).toEqual(data);
-        });
+      const result = await fetchDataOrError(successfulFetch);
+      expect(result.error).toBeUndefined();
+      expect(result).toEqual(data);
+    });
 
-        it("should return map with error key on failed fetch", async () => {
-            const error = {
-                status: 504,
-                statusText: "GATEWAY_TIMEOUT",
-                data: {
-                    message: "Failed to load resource: the server responded with a status of 504 (GATEWAY_TIMEOUT)"
-                }
-            };
+    it("should return map with error key on failed fetch", async () => {
+      const error = {
+        status: 504,
+        statusText: "GATEWAY_TIMEOUT",
+        data: {
+          message:
+            "Failed to load resource: the server responded with a status of 504 (GATEWAY_TIMEOUT)",
+        },
+      };
 
-            const failedFetch = Promise.reject(error);
+      const failedFetch = Promise.reject(error);
 
-            const result = await fetchDataOrError(failedFetch);
-            expect(result.error).toEqual(error);
-        });
+      const result = await fetchDataOrError(failedFetch);
+      expect(result.error).toEqual(error);
     });
+  });
 });
diff --git a/frontend/test/dashboard/selectors.unit.spec.js b/frontend/test/dashboard/selectors.unit.spec.js
index 4f1b3ba5dc80fcaf1d61553cd31088cce79e0f50..12891474095966ffd4b1623b1324a4b9b8bdd71f 100644
--- a/frontend/test/dashboard/selectors.unit.spec.js
+++ b/frontend/test/dashboard/selectors.unit.spec.js
@@ -1,134 +1,156 @@
+// import { getMetadata } from "metabase/selectors/metadata";
 import { getParameters } from "metabase/dashboard/selectors";
 
 import { chain } from "icepick";
 
 const STATE = {
-    dashboard: {
-        dashboardId: 0,
-        dashboards: {
-            0: {
-                ordered_cards: [0, 1],
-                parameters: []
-            }
-        },
-        dashcards: {
-            0: {
-                card: { id: 0 },
-                parameter_mappings: []
-            },
-            1: {
-                card: { id: 1 },
-                parameter_mappings: []
-            }
-        }
+  dashboard: {
+    dashboardId: 0,
+    dashboards: {
+      0: {
+        ordered_cards: [0, 1],
+        parameters: [],
+      },
+    },
+    dashcards: {
+      0: {
+        card: { id: 0 },
+        parameter_mappings: [],
+      },
+      1: {
+        card: { id: 1 },
+        parameter_mappings: [],
+      },
+    },
+  },
+  metadata: {
+    databases: {},
+    tables: {},
+    fields: {
+      1: { id: 1 },
+      2: { id: 2 },
     },
-    metadata: {
-        databases: {},
-        tables: {},
-        fields: {},
-        metrics: {},
-        segments: {}
-    }
-}
+    metrics: {},
+    segments: {},
+  },
+};
 
 describe("dashboard/selectors", () => {
-    describe("getParameters", () => {
-        it("should work with no parameters", () => {
-            expect(getParameters(STATE)).toEqual([]);
+  describe("getParameters", () => {
+    it("should work with no parameters", () => {
+      expect(getParameters(STATE)).toEqual([]);
+    });
+    it("should not include field id with no mappings", () => {
+      const state = chain(STATE)
+        .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
+        .value();
+      expect(getParameters(state)).toEqual([
+        {
+          id: 1,
+          field_ids: [],
+          field_id: null,
+        },
+      ]);
+    });
+    it("should not include field id with one mapping, no field id", () => {
+      const state = chain(STATE)
+        .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
+        .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
+          card_id: 0,
+          parameter_id: 1,
+          target: ["variable", ["template-tag", "foo"]],
+        })
+        .value();
+      expect(getParameters(state)).toEqual([
+        {
+          id: 1,
+          field_ids: [],
+          field_id: null,
+        },
+      ]);
+    });
+    it("should include field id with one mappings, with field id", () => {
+      const state = chain(STATE)
+        .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
+        .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
+          card_id: 0,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 1]],
         })
-        it("should not include field id with no mappings", () => {
-            const state = chain(STATE)
-                .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
-                .value();
-            expect(getParameters(state)).toEqual([{
-                id: 1,
-                field_ids: []
-            }]);
+        .value();
+      expect(getParameters(state)).toEqual([
+        {
+          id: 1,
+          field_ids: [1],
+          field_id: 1,
+        },
+      ]);
+    });
+    it("should include field id with two mappings, with same field id", () => {
+      const state = chain(STATE)
+        .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
+        .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
+          card_id: 0,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 1]],
         })
-        it("should not include field id with one mapping, no field id", () => {
-            const state = chain(STATE)
-                .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
-                .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
-                    card_id: 0,
-                    parameter_id: 1,
-                    target: ["variable", ["template-tag", "foo"]]
-                })
-                .value();
-            expect(getParameters(state)).toEqual([{
-                id: 1,
-                field_ids: []
-            }]);
+        .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], {
+          card_id: 1,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 1]],
         })
-        it("should include field id with one mappings, with field id", () => {
-            const state = chain(STATE)
-                .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
-                .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
-                    card_id: 0,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 1]]
-                })
-                .value();
-            expect(getParameters(state)).toEqual([{
-                id: 1,
-                field_ids: [1]
-            }]);
+        .value();
+      expect(getParameters(state)).toEqual([
+        {
+          id: 1,
+          field_ids: [1],
+          field_id: 1,
+        },
+      ]);
+    });
+    it("should include field id with two mappings, one with field id, one without", () => {
+      const state = chain(STATE)
+        .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
+        .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
+          card_id: 0,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 1]],
         })
-        it("should include field id with two mappings, with same field id", () => {
-            const state = chain(STATE)
-                .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
-                .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
-                    card_id: 0,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 1]]
-                })
-                .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], {
-                    card_id: 1,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 1]]
-                })
-                .value();
-            expect(getParameters(state)).toEqual([{
-                id: 1,
-                field_ids: [1]
-            }]);
+        .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], {
+          card_id: 1,
+          parameter_id: 1,
+          target: ["variable", ["template-tag", "foo"]],
         })
-        it("should include field id with two mappings, one with field id, one without", () => {
-            const state = chain(STATE)
-                .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
-                .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
-                    card_id: 0,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 1]]
-                })
-                .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], {
-                    card_id: 1,
-                    parameter_id: 1,
-                    target: ["variable", ["template-tag", "foo"]]
-                })
-                .value();
-            expect(getParameters(state)).toEqual([{
-                id: 1,
-                field_ids: [1]
-            }]);
+        .value();
+      expect(getParameters(state)).toEqual([
+        {
+          id: 1,
+          field_ids: [1],
+          field_id: 1,
+        },
+      ]);
+    });
+    it("should include all field ids with two mappings, with different field ids", () => {
+      const state = chain(STATE)
+        .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
+        .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
+          card_id: 0,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 1]],
         })
-        it("should include all field ids with two mappings, with different field ids", () => {
-            const state = chain(STATE)
-                .assocIn(["dashboard", "dashboards", 0, "parameters", 0], { id: 1 })
-                .assocIn(["dashboard", "dashcards", 0, "parameter_mappings", 0], {
-                    card_id: 0,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 1]]
-                })
-                .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], {
-                    card_id: 1,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 2]]
-                })
-                .value();
-            expect(getParameters(state)).toEqual([{
-                id: 1,
-                field_ids: [1, 2]
-            }]);
+        .assocIn(["dashboard", "dashcards", 1, "parameter_mappings", 0], {
+          card_id: 1,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 2]],
         })
-    })
-})
+        .value();
+      expect(getParameters(state)).toEqual([
+        {
+          id: 1,
+          field_ids: [1, 2],
+          field_id: null,
+        },
+      ]);
+    });
+  });
+});
diff --git a/frontend/test/dashboards/dashboards.integ.spec.js b/frontend/test/dashboards/dashboards.integ.spec.js
index 0a4413a55e587dea67c1515cd9b732d97c8feb19..e6ddea94275df6236cefdc129726ba62e5ba760f 100644
--- a/frontend/test/dashboards/dashboards.integ.spec.js
+++ b/frontend/test/dashboards/dashboards.integ.spec.js
@@ -1,15 +1,16 @@
 import {
-    createTestStore,
-    useSharedAdminLogin
+  createTestStore,
+  useSharedAdminLogin,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton,
-    setInputValue
-} from "__support__/enzyme_utils"
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 
 import { mount } from "enzyme";
-import { FETCH_ARCHIVE, FETCH_DASHBOARDS, SET_ARCHIVED, SET_FAVORITED } from "metabase/dashboards/dashboards";
+import {
+  FETCH_ARCHIVE,
+  FETCH_DASHBOARDS,
+  SET_ARCHIVED,
+  SET_FAVORITED,
+} from "metabase/dashboards/dashboards";
 import CreateDashboardModal from "metabase/components/CreateDashboardModal";
 import { FETCH_DASHBOARD } from "metabase/dashboard/dashboard";
 import { DashboardApi } from "metabase/services";
@@ -21,102 +22,133 @@ import ListFilterWidget from "metabase/components/ListFilterWidget";
 import ArchivedItem from "metabase/components/ArchivedItem";
 
 describe("dashboards list", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    })
-
-    afterAll(async () => {
-        const dashboardIds = (await DashboardApi.list())
-            .filter((dash) => !dash.archived)
-            .map((dash) => dash.id)
-
-        await Promise.all(dashboardIds.map((id) => DashboardApi.update({ id, archived: true })))
-    })
-
-    it("should let you create a dashboard when there are no existing dashboards", async () => {
-        const store = await createTestStore();
-        store.pushPath("/dashboards")
-        const app = mount(store.getAppContainer());
-
-        await store.waitForActions([FETCH_DASHBOARDS])
-
-        // // Create a new dashboard in the empty state (EmptyState react component)
-        click(app.find(".Button.Button--primary"))
-        // click(app.find(".Icon.Icon-add"))
-
-        const modal = app.find(CreateDashboardModal)
-
-        setInputValue(modal.find('input[name="name"]'), "Customer Feedback Analysis")
-        setInputValue(modal.find('input[name="description"]'), "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")
-        clickButton(modal.find(".Button--primary"))
-
-        // should navigate to dashboard page
-        await store.waitForActions(FETCH_DASHBOARD)
-        expect(app.find(Dashboard).length).toBe(1)
-    })
-
-    it("should let you create a dashboard when there are existing dashboards", async () => {
-        // Return to the dashboard list and check that we see an expected list item
-        const store = await createTestStore();
-        store.pushPath("/dashboards")
-        const app = mount(store.getAppContainer());
-
-        await store.waitForActions([FETCH_DASHBOARDS])
-        expect(app.find(DashboardListItem).length).toBe(1)
-
-        // Create another one
-        click(app.find(".Icon.Icon-add"))
-        const modal2 = app.find(CreateDashboardModal)
-        setInputValue(modal2.find('input[name="name"]'), "Some Excessively Long Dashboard Title Just For Fun")
-        setInputValue(modal2.find('input[name="description"]'), "")
-        clickButton(modal2.find(".Button--primary"))
-
-        await store.waitForActions(FETCH_DASHBOARD)
-    })
-
-    it("should let you search form both title and description", async () => {
-        const store = await createTestStore();
-        store.pushPath("/dashboards")
-        const app = mount(store.getAppContainer());
-        await store.waitForActions([FETCH_DASHBOARDS])
-
-        setInputValue(app.find(SearchHeader).find("input"), "this should produce no results")
-        expect(app.find(EmptyState).length).toBe(1)
-
-        // Should search from both title and description
-        setInputValue(app.find(SearchHeader).find("input"), "usual response times")
-        expect(app.find(DashboardListItem).text()).toMatch(/Customer Feedback Analysis/)
-    })
-
-    it("should let you favorite and unfavorite dashboards", async () => {
-        const store = await createTestStore();
-        store.pushPath("/dashboards")
-        const app = mount(store.getAppContainer());
-        await store.waitForActions([FETCH_DASHBOARDS])
-
-        click(app.find(DashboardListItem).first().find(".Icon-staroutline"));
-        await store.waitForActions([SET_FAVORITED])
-        click(app.find(ListFilterWidget))
-
-        click(app.find(".TestPopover").find('h4[children="Favorites"]'))
-
-        click(app.find(DashboardListItem).first().find(".Icon-star").first());
-        await store.waitForActions([SET_FAVORITED])
-        expect(app.find(EmptyState).length).toBe(1)
-    })
-
-    it("should let you archive and unarchive dashboards", async () => {
-        const store = await createTestStore();
-        store.pushPath("/dashboards")
-        const app = mount(store.getAppContainer());
-        await store.waitForActions([FETCH_DASHBOARDS])
-
-        click(app.find(DashboardListItem).first().find(".Icon-archive"));
-        await store.waitForActions([SET_ARCHIVED])
-
-        click(app.find(".Icon-viewArchive"))
-        await store.waitForActions([FETCH_ARCHIVE])
-        expect(app.find(ArchivedItem).length).toBeGreaterThan(0)
-    });
-
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  afterAll(async () => {
+    const dashboardIds = (await DashboardApi.list())
+      .filter(dash => !dash.archived)
+      .map(dash => dash.id);
+
+    await Promise.all(
+      dashboardIds.map(id => DashboardApi.update({ id, archived: true })),
+    );
+  });
+
+  it("should let you create a dashboard when there are no existing dashboards", async () => {
+    const store = await createTestStore();
+    store.pushPath("/dashboards");
+    const app = mount(store.getAppContainer());
+
+    await store.waitForActions([FETCH_DASHBOARDS]);
+
+    // // Create a new dashboard in the empty state (EmptyState react component)
+    click(app.find(".Button.Button--primary"));
+    // click(app.find(".Icon.Icon-add"))
+
+    const modal = app.find(CreateDashboardModal);
+
+    setInputValue(
+      modal.find('input[name="name"]'),
+      "Customer Feedback Analysis",
+    );
+    setInputValue(
+      modal.find('input[name="description"]'),
+      "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",
+    );
+    clickButton(modal.find(".Button--primary"));
+
+    // should navigate to dashboard page
+    await store.waitForActions(FETCH_DASHBOARD);
+    expect(app.find(Dashboard).length).toBe(1);
+  });
+
+  it("should let you create a dashboard when there are existing dashboards", async () => {
+    // Return to the dashboard list and check that we see an expected list item
+    const store = await createTestStore();
+    store.pushPath("/dashboards");
+    const app = mount(store.getAppContainer());
+
+    await store.waitForActions([FETCH_DASHBOARDS]);
+    expect(app.find(DashboardListItem).length).toBe(1);
+
+    // Create another one
+    click(app.find(".Icon.Icon-add"));
+    const modal2 = app.find(CreateDashboardModal);
+    setInputValue(
+      modal2.find('input[name="name"]'),
+      "Some Excessively Long Dashboard Title Just For Fun",
+    );
+    setInputValue(modal2.find('input[name="description"]'), "");
+    clickButton(modal2.find(".Button--primary"));
+
+    await store.waitForActions(FETCH_DASHBOARD);
+  });
+
+  it("should let you search form both title and description", async () => {
+    const store = await createTestStore();
+    store.pushPath("/dashboards");
+    const app = mount(store.getAppContainer());
+    await store.waitForActions([FETCH_DASHBOARDS]);
+
+    setInputValue(
+      app.find(SearchHeader).find("input"),
+      "this should produce no results",
+    );
+    expect(app.find(EmptyState).length).toBe(1);
+
+    // Should search from both title and description
+    setInputValue(app.find(SearchHeader).find("input"), "usual response times");
+    expect(app.find(DashboardListItem).text()).toMatch(
+      /Customer Feedback Analysis/,
+    );
+  });
+
+  it("should let you favorite and unfavorite dashboards", async () => {
+    const store = await createTestStore();
+    store.pushPath("/dashboards");
+    const app = mount(store.getAppContainer());
+    await store.waitForActions([FETCH_DASHBOARDS]);
+
+    click(
+      app
+        .find(DashboardListItem)
+        .first()
+        .find(".Icon-staroutline"),
+    );
+    await store.waitForActions([SET_FAVORITED]);
+    click(app.find(ListFilterWidget));
+
+    click(app.find(".TestPopover").find('h4[children="Favorites"]'));
+
+    click(
+      app
+        .find(DashboardListItem)
+        .first()
+        .find(".Icon-star")
+        .first(),
+    );
+    await store.waitForActions([SET_FAVORITED]);
+    expect(app.find(EmptyState).length).toBe(1);
+  });
+
+  it("should let you archive and unarchive dashboards", async () => {
+    const store = await createTestStore();
+    store.pushPath("/dashboards");
+    const app = mount(store.getAppContainer());
+    await store.waitForActions([FETCH_DASHBOARDS]);
+
+    click(
+      app
+        .find(DashboardListItem)
+        .first()
+        .find(".Icon-archive"),
+    );
+    await store.waitForActions([SET_ARCHIVED]);
+
+    click(app.find(".Icon-viewArchive"));
+    await store.waitForActions([FETCH_ARCHIVE]);
+    expect(app.find(ArchivedItem).length).toBeGreaterThan(0);
+  });
 });
diff --git a/frontend/test/hoc/Background.unit.spec.js b/frontend/test/hoc/Background.unit.spec.js
index 7666f3a970bdb746bea4a0197d56ea3f99784313..41f4fe65995dfcf80af7ed692106889c9e5a4ef8 100644
--- a/frontend/test/hoc/Background.unit.spec.js
+++ b/frontend/test/hoc/Background.unit.spec.js
@@ -1,37 +1,36 @@
-import React from 'react'
-import jsdom from 'jsdom'
-import { mount } from 'enzyme'
-import { withBackground } from 'metabase/hoc/Background'
-
-describe('withBackground', () => {
-    let wrapper
-
-    beforeEach(() => {
-        window.document = jsdom.jsdom('')
-        document.body.appendChild(document.createElement('div'))
-        // have an existing class to make sure we don't nuke stuff that might be there already
-        document.body.classList.add('existing-class')
-    })
-
-    afterEach(() => {
-        wrapper.detach()
-        window.document = jsdom.jsdom('')
-    })
-
-    it('should properly apply the provided class to the body', () => {
-        const TestComponent = withBackground('my-bg-class')(() => <div>Yo</div>)
-
-        wrapper = mount(<TestComponent />, { attachTo: document.body.firstChild })
-
-        const classListBefore = Object.values(document.body.classList)
-        expect(classListBefore.includes('my-bg-class')).toEqual(true)
-        expect(classListBefore.includes('existing-class')).toEqual(true)
-
-        wrapper.unmount()
-
-        const classListAfter = Object.values(document.body.classList)
-        expect(classListAfter.includes('my-bg-class')).toEqual(false)
-        expect(classListAfter.includes('existing-class')).toEqual(true)
-
-    })
-})
+import React from "react";
+import jsdom from "jsdom";
+import { mount } from "enzyme";
+import { withBackground } from "metabase/hoc/Background";
+
+describe("withBackground", () => {
+  let wrapper;
+
+  beforeEach(() => {
+    window.document = jsdom.jsdom("");
+    document.body.appendChild(document.createElement("div"));
+    // have an existing class to make sure we don't nuke stuff that might be there already
+    document.body.classList.add("existing-class");
+  });
+
+  afterEach(() => {
+    wrapper.detach();
+    window.document = jsdom.jsdom("");
+  });
+
+  it("should properly apply the provided class to the body", () => {
+    const TestComponent = withBackground("my-bg-class")(() => <div>Yo</div>);
+
+    wrapper = mount(<TestComponent />, { attachTo: document.body.firstChild });
+
+    const classListBefore = Object.values(document.body.classList);
+    expect(classListBefore.includes("my-bg-class")).toEqual(true);
+    expect(classListBefore.includes("existing-class")).toEqual(true);
+
+    wrapper.unmount();
+
+    const classListAfter = Object.values(document.body.classList);
+    expect(classListAfter.includes("my-bg-class")).toEqual(false);
+    expect(classListAfter.includes("existing-class")).toEqual(true);
+  });
+});
diff --git a/frontend/test/home/HomepageApp.integ.spec.js b/frontend/test/home/HomepageApp.integ.spec.js
index a0a382443e9c5ba02de589eb23753b9ddb05d92e..17248a700d6db02682f8d31d0d9ace920b633f32 100644
--- a/frontend/test/home/HomepageApp.integ.spec.js
+++ b/frontend/test/home/HomepageApp.integ.spec.js
@@ -1,18 +1,18 @@
 import {
-    useSharedAdminLogin,
-    createTestStore,
-    createSavedQuestion
+  useSharedAdminLogin,
+  createTestStore,
+  createSavedQuestion,
 } from "__support__/integrated_tests";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import { mount } from "enzyme";
 import {
-    orders_past_300_days_segment,
-    unsavedOrderCountQuestion,
-    vendor_count_metric
+  orders_past_300_days_segment,
+  unsavedOrderCountQuestion,
+  vendor_count_metric,
 } from "__support__/sample_dataset_fixture";
-import { delay } from 'metabase/lib/promise';
+import { delay } from "metabase/lib/promise";
 
 import HomepageApp from "metabase/home/containers/HomepageApp";
 import { FETCH_ACTIVITY } from "metabase/home/actions";
@@ -25,74 +25,86 @@ import Scalar from "metabase/visualizations/visualizations/Scalar";
 import { CardApi, MetricApi, SegmentApi } from "metabase/services";
 
 describe("HomepageApp", () => {
-    let questionId = null;
-    let segmentId = null;
-    let metricId = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin()
-
-        // Create some entities that will show up in the top of activity feed
-        // This test doesn't care if there already are existing items in the feed or not
-        // Delays are required for having separable creation times for each entity
-        questionId = (await createSavedQuestion(unsavedOrderCountQuestion)).id()
-        await delay(100);
-        segmentId = (await SegmentApi.create(orders_past_300_days_segment)).id;
-        await delay(100);
-        metricId = (await MetricApi.create(vendor_count_metric)).id;
-        await delay(100);
-    })
-
-    afterAll(async () => {
-        await MetricApi.delete({ metricId, revision_message: "Let's exterminate this metric" })
-        await SegmentApi.delete({ segmentId, revision_message: "Let's exterminate this segment" })
-        await CardApi.delete({ cardId: questionId })
-    })
-
-    describe("activity feed", async () => {
-        it("shows the expected list of activity", async () => {
-            const store = await createTestStore()
-
-            store.pushPath("/");
-            const homepageApp = mount(store.connectContainer(<HomepageApp />));
-            await store.waitForActions([FETCH_ACTIVITY])
-
-            const activityFeed = homepageApp.find(Activity);
-            const activityItems = activityFeed.find(ActivityItem);
-            const activityStories = activityFeed.find(ActivityStory);
-
-            expect(activityItems.length).toBeGreaterThanOrEqual(3);
-            expect(activityStories.length).toBeGreaterThanOrEqual(3);
-
-            expect(activityItems.at(0).text()).toMatch(/Vendor count/);
-            expect(activityStories.at(0).text()).toMatch(/Tells how many vendors we have/);
-
-            expect(activityItems.at(1).text()).toMatch(/Past 300 days/);
-            expect(activityStories.at(1).text()).toMatch(/Past 300 days created at/);
-
-            // eslint-disable-next-line no-irregular-whitespace
-            expect(activityItems.at(2).text()).toMatch(/You saved a question about Orders/);
-            expect(activityStories.at(2).text()).toMatch(new RegExp(unsavedOrderCountQuestion.displayName()));
-
-
-        });
-        it("shows successfully open QB for a metric when clicking the metric name", async () => {
-            const store = await createTestStore()
-
-            store.pushPath("/");
-
-            // In this test we have to render the whole app in order to get links work properly
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([FETCH_ACTIVITY])
-            const homepageApp = app.find(HomepageApp);
-
-            const activityFeed = homepageApp.find(Activity);
-            const metricLink = activityFeed.find(ActivityItem).find('a[children="Vendor count"]').first();
-            click(metricLink)
-            
-            await store.waitForActions([QUERY_COMPLETED]);
-            expect(app.find(Scalar).text()).toBe("200");
-        })
+  let questionId = null;
+  let segmentId = null;
+  let metricId = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+
+    // Create some entities that will show up in the top of activity feed
+    // This test doesn't care if there already are existing items in the feed or not
+    // Delays are required for having separable creation times for each entity
+    questionId = (await createSavedQuestion(unsavedOrderCountQuestion)).id();
+    await delay(100);
+    segmentId = (await SegmentApi.create(orders_past_300_days_segment)).id;
+    await delay(100);
+    metricId = (await MetricApi.create(vendor_count_metric)).id;
+    await delay(100);
+  });
+
+  afterAll(async () => {
+    await MetricApi.delete({
+      metricId,
+      revision_message: "Let's exterminate this metric",
     });
+    await SegmentApi.delete({
+      segmentId,
+      revision_message: "Let's exterminate this segment",
+    });
+    await CardApi.delete({ cardId: questionId });
+  });
+
+  describe("activity feed", async () => {
+    it("shows the expected list of activity", async () => {
+      const store = await createTestStore();
+
+      store.pushPath("/");
+      const homepageApp = mount(store.connectContainer(<HomepageApp />));
+      await store.waitForActions([FETCH_ACTIVITY]);
+
+      const activityFeed = homepageApp.find(Activity);
+      const activityItems = activityFeed.find(ActivityItem);
+      const activityStories = activityFeed.find(ActivityStory);
+
+      expect(activityItems.length).toBeGreaterThanOrEqual(3);
+      expect(activityStories.length).toBeGreaterThanOrEqual(3);
+
+      expect(activityItems.at(0).text()).toMatch(/Vendor count/);
+      expect(activityStories.at(0).text()).toMatch(
+        /Tells how many vendors we have/,
+      );
+
+      expect(activityItems.at(1).text()).toMatch(/Past 300 days/);
+      expect(activityStories.at(1).text()).toMatch(/Past 300 days created at/);
+
+      expect(activityItems.at(2).text()).toMatch(
+        // eslint-disable-next-line no-irregular-whitespace
+        /You saved a question about Orders/,
+      );
+      expect(activityStories.at(2).text()).toMatch(
+        new RegExp(unsavedOrderCountQuestion.displayName()),
+      );
+    });
+    it("shows successfully open QB for a metric when clicking the metric name", async () => {
+      const store = await createTestStore();
+
+      store.pushPath("/");
 
+      // In this test we have to render the whole app in order to get links work properly
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_ACTIVITY]);
+      const homepageApp = app.find(HomepageApp);
+
+      const activityFeed = homepageApp.find(Activity);
+      const metricLink = activityFeed
+        .find(ActivityItem)
+        .find('a[children="Vendor count"]')
+        .first();
+      click(metricLink);
+
+      await store.waitForActions([QUERY_COMPLETED]);
+      expect(app.find(Scalar).text()).toBe("200");
+    });
+  });
 });
diff --git a/frontend/test/home/NewUserOnboardingModal.unit.spec.js b/frontend/test/home/NewUserOnboardingModal.unit.spec.js
index 2ea520d9b5287e24cca85286c83a4027bc3f888c..9673f3ac46a9cf071d83c3179f63261fd4d7be1b 100644
--- a/frontend/test/home/NewUserOnboardingModal.unit.spec.js
+++ b/frontend/test/home/NewUserOnboardingModal.unit.spec.js
@@ -1,34 +1,30 @@
 import { click } from "__support__/enzyme_utils";
 
-import React from 'react'
-import { shallow } from 'enzyme'
-import NewUserOnboardingModal from '../../src/metabase/home/components/NewUserOnboardingModal'
+import React from "react";
+import { shallow } from "enzyme";
+import NewUserOnboardingModal from "../../src/metabase/home/components/NewUserOnboardingModal";
 
-describe('new user onboarding modal', () => {
-    describe('advance steps', () => {
-        it('should advance through steps properly', () => {
-            const wrapper = shallow(
-                <NewUserOnboardingModal />
-            )
-            const nextButton = wrapper.find('a')
+describe("new user onboarding modal", () => {
+  describe("advance steps", () => {
+    it("should advance through steps properly", () => {
+      const wrapper = shallow(<NewUserOnboardingModal />);
+      const nextButton = wrapper.find("a");
 
-            expect(wrapper.state().step).toEqual(1)
-            click(nextButton)
-            expect(wrapper.state().step).toEqual(2)
-        })
+      expect(wrapper.state().step).toEqual(1);
+      click(nextButton);
+      expect(wrapper.state().step).toEqual(2);
+    });
 
-        it('should close if on the last step', () => {
-            const onClose = jest.fn()
-            const wrapper = shallow(
-                <NewUserOnboardingModal onClose={onClose} />
-            )
-            // go to the last step
-            wrapper.setState({ step: 3 })
+    it("should close if on the last step", () => {
+      const onClose = jest.fn();
+      const wrapper = shallow(<NewUserOnboardingModal onClose={onClose} />);
+      // go to the last step
+      wrapper.setState({ step: 3 });
 
-            const nextButton = wrapper.find('a')
-            expect(nextButton.text()).toEqual('Let\'s go')
-            click(nextButton);
-            expect(onClose.mock.calls.length).toEqual(1)
-        })
-    })
-})
+      const nextButton = wrapper.find("a");
+      expect(nextButton.text()).toEqual("Let's go");
+      click(nextButton);
+      expect(onClose.mock.calls.length).toEqual(1);
+    });
+  });
+});
diff --git a/frontend/test/internal/__snapshots__/components.unit.spec.js.snap b/frontend/test/internal/__snapshots__/components.unit.spec.js.snap
index 50beb91febb7970606d925a2dc38e44e14ec60d1..819163c5566ee713b2f37cb1653b336078907676 100644
--- a/frontend/test/internal/__snapshots__/components.unit.spec.js.snap
+++ b/frontend/test/internal/__snapshots__/components.unit.spec.js.snap
@@ -275,7 +275,7 @@ exports[`EntityMenu should render "Edit menu" correctly 1`] = `
       className="relative"
     >
       <div
-        className="x0 x1 xj xk xl xm x2 xn xo xp xa xq xr"
+        className="x0 x1 xl xm xn xo x2 xp xq xr xa xs xt"
         onClick={[Function]}
       >
         <svg
@@ -307,7 +307,7 @@ exports[`EntityMenu should render "More menu" correctly 1`] = `
       className="relative"
     >
       <div
-        className="x0 x1 xj xk xl xm x2 xn xo xp xa xq xr"
+        className="x0 x1 xl xm xn xo x2 xp xq xr xa xs xt"
         onClick={[Function]}
       >
         <svg
@@ -339,7 +339,7 @@ exports[`EntityMenu should render "Multiple menus" correctly 1`] = `
       className="relative"
     >
       <div
-        className="x0 x1 xj xk xl xm x2 xn xo xp xa xq xr"
+        className="x0 x1 xl xm xn xo x2 xp xq xr xa xs xt"
         onClick={[Function]}
       >
         <svg
@@ -360,7 +360,7 @@ exports[`EntityMenu should render "Multiple menus" correctly 1`] = `
       className="relative"
     >
       <div
-        className="x0 x1 xj xk xl xm x2 xn xo xp xa xq xr"
+        className="x0 x1 xl xm xn xo x2 xp xq xr xa xs xt"
         onClick={[Function]}
       >
         <svg
@@ -382,7 +382,7 @@ exports[`EntityMenu should render "Multiple menus" correctly 1`] = `
       className="relative"
     >
       <div
-        className="x0 x1 xj xk xl xm x2 xn xo xp xa xq xr"
+        className="x0 x1 xl xm xn xo x2 xp xq xr xa xs xt"
         onClick={[Function]}
       >
         <svg
@@ -414,7 +414,7 @@ exports[`EntityMenu should render "Share menu" correctly 1`] = `
       className="relative"
     >
       <div
-        className="x0 x1 xj xk xl xm x2 xn xo xp xa xq xr"
+        className="x0 x1 xl xm xn xo x2 xp xq xr xa xs xt"
         onClick={[Function]}
       >
         <svg
@@ -444,7 +444,7 @@ exports[`Select should render "Default" correctly 1`] = `
   style={undefined}
 >
   <div
-    className="AdminSelect flex align-center  text-grey-3"
+    className="AdminSelect border-med flex align-center  text-grey-3"
   >
     <span
       className="AdminSelect-content mr1"
@@ -479,7 +479,7 @@ exports[`Select should render "With search" correctly 1`] = `
   style={undefined}
 >
   <div
-    className="AdminSelect flex align-center  text-grey-3"
+    className="AdminSelect border-med flex align-center  text-grey-3"
   >
     <span
       className="AdminSelect-content mr1"
@@ -755,3 +755,167 @@ exports[`Toggle should render "on" correctly 1`] = `
   }
 />
 `;
+
+exports[`TokenField should render "" correctly 1`] = `
+<div>
+  <ul
+    className="m1 p0 pb1 bordered rounded flex flex-wrap bg-white scroll-x scroll-y xj xk"
+    onMouseDownCapture={[Function]}
+    style={undefined}
+  >
+    <li
+      className="flex-full mr1 py1 pl1 mt1 bg-white"
+    >
+      <input
+        autoFocus={false}
+        className="full h4 text-bold text-default no-focus borderless"
+        onBlur={[Function]}
+        onChange={[Function]}
+        onFocus={[Function]}
+        onKeyDown={[Function]}
+        onPaste={[Function]}
+        placeholder={undefined}
+        size={10}
+        value=""
+      />
+    </li>
+  </ul>
+  <ul
+    className="ml1 scroll-y scroll-show"
+    onMouseEnter={[Function]}
+    onMouseLeave={[Function]}
+    style={
+      Object {
+        "maxHeight": 300,
+      }
+    }
+  >
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Doohickey
+        </span>
+      </div>
+    </li>
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Gadget
+        </span>
+      </div>
+    </li>
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Gizmo
+        </span>
+      </div>
+    </li>
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Widget
+        </span>
+      </div>
+    </li>
+  </ul>
+</div>
+`;
+
+exports[`TokenField should render "updateOnInputChange" correctly 1`] = `
+<div>
+  <ul
+    className="m1 p0 pb1 bordered rounded flex flex-wrap bg-white scroll-x scroll-y xj xk"
+    onMouseDownCapture={[Function]}
+    style={undefined}
+  >
+    <li
+      className="flex-full mr1 py1 pl1 mt1 bg-white"
+    >
+      <input
+        autoFocus={false}
+        className="full h4 text-bold text-default no-focus borderless"
+        onBlur={[Function]}
+        onChange={[Function]}
+        onFocus={[Function]}
+        onKeyDown={[Function]}
+        onPaste={[Function]}
+        placeholder={undefined}
+        size={10}
+        value=""
+      />
+    </li>
+  </ul>
+  <ul
+    className="ml1 scroll-y scroll-show"
+    onMouseEnter={[Function]}
+    onMouseLeave={[Function]}
+    style={
+      Object {
+        "maxHeight": 300,
+      }
+    }
+  >
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Doohickey
+        </span>
+      </div>
+    </li>
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Gadget
+        </span>
+      </div>
+    </li>
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Gizmo
+        </span>
+      </div>
+    </li>
+    <li>
+      <div
+        className="py1 pl1 pr2 block rounded text-bold text-brand-hover inline-block full cursor-pointer bg-grey-0-hover"
+        onClick={[Function]}
+        onMouseDown={[Function]}
+      >
+        <span>
+          Widget
+        </span>
+      </div>
+    </li>
+  </ul>
+</div>
+`;
diff --git a/frontend/test/internal/components.unit.spec.js b/frontend/test/internal/components.unit.spec.js
index e572657b0c37751b8e633a0041e00f5f936e32d8..00c2db9107a8eac94c9689eb7ad4e0e51996719b 100644
--- a/frontend/test/internal/components.unit.spec.js
+++ b/frontend/test/internal/components.unit.spec.js
@@ -3,10 +3,14 @@ import renderer from "react-test-renderer";
 import components from "metabase/internal/lib/components-node";
 
 // generates a snapshot test for every example in every component's `.info.js`
-components.map(({ component, examples, noSnapshotTest }) =>
-    !noSnapshotTest && describe(component.displayName, () => {
-        Object.entries(examples).map(([exampleName, element]) =>
-            it(`should render "${exampleName}" correctly`, () => {
-                expect(renderer.create(element).toJSON()).toMatchSnapshot();
-            }));
-    }));
+components.map(
+  ({ component, examples, noSnapshotTest }) =>
+    !noSnapshotTest &&
+    describe(component.displayName, () => {
+      Object.entries(examples).map(([exampleName, element]) =>
+        it(`should render "${exampleName}" correctly`, () => {
+          expect(renderer.create(element).toJSON()).toMatchSnapshot();
+        }),
+      );
+    }),
+);
diff --git a/frontend/test/karma.conf.js b/frontend/test/karma.conf.js
index 82c48ff488f0176b01e31550779840a32ec6d83d..39351f61eac327500a14eb74e0081c40665975cc 100644
--- a/frontend/test/karma.conf.js
+++ b/frontend/test/karma.conf.js
@@ -1,46 +1,48 @@
-var webpackConfig = require('../../webpack.config');
-console.dir(webpackConfig.module.rules, { depth: null })
+var webpackConfig = require("../../webpack.config");
+console.dir(webpackConfig.module.rules, { depth: null });
 webpackConfig.module.rules.forEach(function(loader) {
-    loader.use = loader.use.filter((item) => !item.loader.includes("extract-text-webpack-plugin"));
+  loader.use = loader.use.filter(
+    item => !item.loader.includes("extract-text-webpack-plugin"),
+  );
 });
 
 module.exports = function(config) {
-    config.set({
-        basePath: '../',
-        files: [
-            'test/metabase-bootstrap.js',
-            // prevent tests from running twice: https://github.com/nikku/karma-browserify/issues/67#issuecomment-84448491
-            { pattern: 'test/legacy-karma/**/*.spec.js', watched: false, included: true, served: true }
-        ],
-        exclude: [
-        ],
-        preprocessors: {
-            'test/metabase-bootstrap.js': ['webpack'],
-            'test/legacy-karma/**/*.spec.js': ['webpack']
-        },
-        frameworks: [
-            'jasmine'
-        ],
-        reporters: [
-            'progress',
-            'junit'
-        ],
-        webpack: {
-            resolve: webpackConfig.resolve,
-            module: webpackConfig.module,
-            postcss: webpackConfig.postcss
-        },
-        webpackMiddleware: {
-            stats: "errors-only"
-        },
-        junitReporter: {
-            outputDir: (process.env["CIRCLE_TEST_REPORTS"] || "..") + "/test-report-frontend"
-        },
-        port: 9876,
-        colors: true,
-        logLevel: config.LOG_INFO,
-        browsers: ['Chrome'],
-        autoWatch: true,
-        singleRun: false
-    });
+  config.set({
+    basePath: "../",
+    files: [
+      "test/metabase-bootstrap.js",
+      // prevent tests from running twice: https://github.com/nikku/karma-browserify/issues/67#issuecomment-84448491
+      {
+        pattern: "test/legacy-karma/**/*.spec.js",
+        watched: false,
+        included: true,
+        served: true,
+      },
+    ],
+    exclude: [],
+    preprocessors: {
+      "test/metabase-bootstrap.js": ["webpack"],
+      "test/legacy-karma/**/*.spec.js": ["webpack"],
+    },
+    frameworks: ["jasmine"],
+    reporters: ["progress", "junit"],
+    webpack: {
+      resolve: webpackConfig.resolve,
+      module: webpackConfig.module,
+      postcss: webpackConfig.postcss,
+    },
+    webpackMiddleware: {
+      stats: "errors-only",
+    },
+    junitReporter: {
+      outputDir:
+        (process.env["CIRCLE_TEST_REPORTS"] || "..") + "/test-report-frontend",
+    },
+    port: 9876,
+    colors: true,
+    logLevel: config.LOG_INFO,
+    browsers: ["Chrome"],
+    autoWatch: true,
+    singleRun: false,
+  });
 };
diff --git a/frontend/test/legacy-karma/lib/dom.spec.js b/frontend/test/legacy-karma/lib/dom.spec.js
index f66416d3ea7272f4cb9f513a68679dad47d71c42..f8ac79d163c960c5827b26599f2fef283649f8c9 100644
--- a/frontend/test/legacy-karma/lib/dom.spec.js
+++ b/frontend/test/legacy-karma/lib/dom.spec.js
@@ -1,42 +1,42 @@
 // NOTE Atte Keinänen 8/8/17: Uses Karma because selection API isn't available in jsdom which Jest only supports
 // Has its own `legacy-karma` directory as a reminder that would be nice to get completely rid of Karma for good at some point
 
-import { getSelectionPosition, setSelectionPosition } from "metabase/lib/dom"
+import { getSelectionPosition, setSelectionPosition } from "metabase/lib/dom";
 
 describe("getSelectionPosition/setSelectionPosition", () => {
-    let container;
-    beforeEach(() => {
-        container = document.createElement("div");
-        document.body.appendChild(container);
-    })
-    afterEach(() => {
-        document.body.removeChild(container);
-    })
+  let container;
+  beforeEach(() => {
+    container = document.createElement("div");
+    document.body.appendChild(container);
+  });
+  afterEach(() => {
+    document.body.removeChild(container);
+  });
 
-    it("should get/set selection on input correctly", () => {
-        let input = document.createElement("input");
-        container.appendChild(input);
-        input.value = "hello world";
-        setSelectionPosition(input, [3, 6]);
-        const position = getSelectionPosition(input);
-        expect(position).toEqual([3, 6]);
-    });
-    it("should get/set selection on contenteditable correctly", () => {
-        let contenteditable = document.createElement("div");
-        container.appendChild(contenteditable);
-        contenteditable.textContent = "<div>hello world</div>"
-        setSelectionPosition(contenteditable, [3, 6]);
-        const position = getSelectionPosition(contenteditable);
-        expect(position).toEqual([3, 6]);
-    });
-    it("should not mutate the actual selection", () => {
-        let contenteditable = document.createElement("div");
-        container.appendChild(contenteditable);
-        contenteditable.textContent = "<div>hello world</div>"
-        setSelectionPosition(contenteditable, [3, 6]);
-        const position = getSelectionPosition(contenteditable);
-        expect(position).toEqual([3, 6]);
-        const position2 = getSelectionPosition(contenteditable);
-        expect(position2).toEqual([3, 6]);
-    })
-})
+  it("should get/set selection on input correctly", () => {
+    let input = document.createElement("input");
+    container.appendChild(input);
+    input.value = "hello world";
+    setSelectionPosition(input, [3, 6]);
+    const position = getSelectionPosition(input);
+    expect(position).toEqual([3, 6]);
+  });
+  it("should get/set selection on contenteditable correctly", () => {
+    let contenteditable = document.createElement("div");
+    container.appendChild(contenteditable);
+    contenteditable.textContent = "<div>hello world</div>";
+    setSelectionPosition(contenteditable, [3, 6]);
+    const position = getSelectionPosition(contenteditable);
+    expect(position).toEqual([3, 6]);
+  });
+  it("should not mutate the actual selection", () => {
+    let contenteditable = document.createElement("div");
+    container.appendChild(contenteditable);
+    contenteditable.textContent = "<div>hello world</div>";
+    setSelectionPosition(contenteditable, [3, 6]);
+    const position = getSelectionPosition(contenteditable);
+    expect(position).toEqual([3, 6]);
+    const position2 = getSelectionPosition(contenteditable);
+    expect(position2).toEqual([3, 6]);
+  });
+});
diff --git a/frontend/test/legacy-selenium/auth/login.spec.js b/frontend/test/legacy-selenium/auth/login.spec.js
index df4f699274f434175930d3257adf772efaef66b6..c2d77233c96eea6d332c2a606846ed1c97ab3b1f 100644
--- a/frontend/test/legacy-selenium/auth/login.spec.js
+++ b/frontend/test/legacy-selenium/auth/login.spec.js
@@ -4,63 +4,79 @@
 
 import { By } from "selenium-webdriver";
 import {
-    waitForUrl,
-    screenshot,
-    loginMetabase,
-    describeE2E
+  waitForUrl,
+  screenshot,
+  loginMetabase,
+  describeE2E,
 } from "../support/utils";
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
 
 describeE2E("auth/login", () => {
-    let sessionId;
+  let sessionId;
 
-    describe("has no cookie", () => {
-        beforeEach(async () => {
-            await driver.get(`${server.host}/`);
-            await driver.manage().deleteAllCookies();
-        });
+  describe("has no cookie", () => {
+    beforeEach(async () => {
+      await driver.get(`${server.host}/`);
+      await driver.manage().deleteAllCookies();
+    });
 
-        it("should take you to the login page", async () => {
-            await driver.get(`${server.host}/`);
-            await waitForUrl(driver, `${server.host}/auth/login?redirect=%2F`);
-            expect(await driver.isElementPresent(By.css("[name=email]"))).toEqual(true);
-            await screenshot(driver, "screenshots/auth-login.png");
-        });
+    it("should take you to the login page", async () => {
+      await driver.get(`${server.host}/`);
+      await waitForUrl(driver, `${server.host}/auth/login?redirect=%2F`);
+      expect(await driver.isElementPresent(By.css("[name=email]"))).toEqual(
+        true,
+      );
+      await screenshot(driver, "screenshots/auth-login.png");
+    });
 
-        it("should log you in", async () => {
-            await driver.get(`${server.host}/`);
-            await loginMetabase(driver, "bob@metabase.com", "12341234");
-            await waitForUrl(driver, `${server.host}/`);
-            const sessionCookie = await driver.manage().getCookie("metabase.SESSION_ID");
-            sessionId = sessionCookie.value;
-        });
+    it("should log you in", async () => {
+      await driver.get(`${server.host}/`);
+      await loginMetabase(driver, "bob@metabase.com", "12341234");
+      await waitForUrl(driver, `${server.host}/`);
+      const sessionCookie = await driver
+        .manage()
+        .getCookie("metabase.SESSION_ID");
+      sessionId = sessionCookie.value;
+    });
 
-        it("should redirect you after logging in", async () => {
-            await driver.get(`${server.host}/questions`);
-            await waitForUrl(driver, `${server.host}/auth/login?redirect=%2Fquestions`);
-            await loginMetabase(driver, "bob@metabase.com", "12341234");
-            await waitForUrl(driver, `${server.host}/questions`);
-        });
+    it("should redirect you after logging in", async () => {
+      await driver.get(`${server.host}/questions`);
+      await waitForUrl(
+        driver,
+        `${server.host}/auth/login?redirect=%2Fquestions`,
+      );
+      await loginMetabase(driver, "bob@metabase.com", "12341234");
+      await waitForUrl(driver, `${server.host}/questions`);
     });
+  });
 
-    describe("valid session cookie", () => {
-        beforeEach(async () => {
-            await driver.get(`${server.host}/`);
-            await driver.manage().deleteAllCookies();
-            await driver.manage().addCookie("metabase.SESSION_ID", sessionId);
-        });
+  describe("valid session cookie", () => {
+    beforeEach(async () => {
+      await driver.get(`${server.host}/`);
+      await driver.manage().deleteAllCookies();
+      await driver.manage().addCookie("metabase.SESSION_ID", sessionId);
+    });
 
-        it("is logged in", async () => {
-            await driver.get(`${server.host}/`);
-            await waitForUrl(driver, `${server.host}/`);
-            await screenshot(driver, "screenshots/loggedin.png");
-        });
+    it("is logged in", async () => {
+      await driver.get(`${server.host}/`);
+      await waitForUrl(driver, `${server.host}/`);
+      await screenshot(driver, "screenshots/loggedin.png");
+    });
 
-        it("loads the qb", async () => {
-            await driver.get(`${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
-            await waitForUrl(driver, `${server.host}/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`);
-            await screenshot(driver, "screenshots/qb.png");
-        });
+    it("loads the qb", async () => {
+      await driver.get(
+        `${
+          server.host
+        }/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`,
+      );
+      await waitForUrl(
+        driver,
+        `${
+          server.host
+        }/question#eyJuYW1lIjpudWxsLCJkYXRhc2V0X3F1ZXJ5Ijp7ImRhdGFiYXNlIjoxLCJ0eXBlIjoibmF0aXZlIiwibmF0aXZlIjp7InF1ZXJ5Ijoic2VsZWN0ICdvaCBoYWkgZ3Vpc2Ug8J-QsScifSwicGFyYW1ldGVycyI6W119LCJkaXNwbGF5Ijoic2NhbGFyIiwidmlzdWFsaXphdGlvbl9zZXR0aW5ncyI6e319`,
+      );
+      await screenshot(driver, "screenshots/qb.png");
     });
+  });
 });
diff --git a/frontend/test/legacy-selenium/query_builder/tutorial.spec.js b/frontend/test/legacy-selenium/query_builder/tutorial.spec.js
index 18e16b317ee4ccdb3039bd38d822ad64512db3d0..e524a821d721e53e2f5dc5935613205034ea88ed 100644
--- a/frontend/test/legacy-selenium/query_builder/tutorial.spec.js
+++ b/frontend/test/legacy-selenium/query_builder/tutorial.spec.js
@@ -3,94 +3,133 @@
 // lots of direct DOM manipulation. See also "Ability to dismiss popovers, modals etc" in
 // https://github.com/metabase/metabase/issues/5527
 
-
 import {
-    waitForElement,
-    waitForElementRemoved,
-    waitForElementAndClick,
-    waitForElementAndSendKeys,
-    waitForUrl,
-    screenshot,
-    describeE2E,
-    ensureLoggedIn
+  waitForElement,
+  waitForElementRemoved,
+  waitForElementAndClick,
+  waitForElementAndSendKeys,
+  waitForUrl,
+  screenshot,
+  describeE2E,
+  ensureLoggedIn,
 } from "../support/utils";
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
 
 describeE2E("tutorial", () => {
-    beforeAll(async () => {
-        await ensureLoggedIn(server, driver, "bob@metabase.com", "12341234");
-    });
-
-    // TODO Atte Keinänen 6/22/17: Failing test, disabled until converted to use Jest and Enzyme
-    xit("should guide users through query builder tutorial", async () => {
-        await driver.get(`${server.host}/?new`);
-        await waitForUrl(driver, `${server.host}/?new`);
-
-        await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
-        await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
-        await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
-
-        await waitForUrl(driver, `${server.host}/question`);
-        await waitForElement(driver, ".Modal .Button.Button--primary");
-        await screenshot(driver, "screenshots/setup-tutorial-qb.png");
-        await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
-
-        await waitForElement(driver, "#QB-TutorialTableImg");
-        // a .Modal-backdrop element blocks clicks for a while during transition?
-        await waitForElementRemoved(driver, '.Modal-backdrop');
-        await waitForElementAndClick(driver, ".GuiBuilder-data a");
-
-        // select sample dataset db
-        try {
-            // in a try/catch in case the instance only has one db
-            await waitForElementAndClick(driver, "#DatabaseSchemaPicker .List-section:last-child .List-section-header", 1000);
-        } catch (e) {
-        }
-
-        // select orders table
-        await waitForElementAndClick(driver, "#TablePicker .List-item:first-child>a");
-
-        // select filters
-        await waitForElement(driver, "#QB-TutorialFunnelImg");
-        await waitForElementAndClick(driver, ".GuiBuilder-filtered-by .Query-section:not(.disabled) a");
-
-        await waitForElementAndClick(driver, "#FilterPopover .List-item:first-child>a");
-
-        await waitForElementAndClick(driver, "input[data-ui-tag='relative-date-input']");
-        await waitForElementAndSendKeys(driver, "#FilterPopover input.border-purple", '10');
-        await waitForElementAndClick(driver, ".Button[data-ui-tag='add-filter']:not(.disabled)");
-
-        // select aggregations
-        await waitForElement(driver, "#QB-TutorialCalculatorImg");
-        await waitForElementAndClick(driver, "#Query-section-aggregation");
-        await waitForElementAndClick(driver, "#AggregationPopover .List-item:nth-child(2)>a");
-
-        // select breakouts
-        await waitForElement(driver, "#QB-TutorialBananaImg");
-        await waitForElementAndClick(driver, ".Query-section.Query-section-breakout>div");
-
-        await waitForElementAndClick(driver, "#BreakoutPopover .List-item:first-child .Field-extra>a");
-        await waitForElementAndClick(driver, "#TimeGroupingPopover .List-item:nth-child(4)>a");
-
-        // run query
-        await waitForElement(driver, "#QB-TutorialRocketImg");
-        await waitForElementAndClick(driver, ".Button.RunButton");
-
-        // wait for query to complete
-        await waitForElement(driver, "#QB-TutorialChartImg", 20000);
-
-        // switch visualization
-        await waitForElementAndClick(driver, "#VisualizationTrigger");
-        // this step occassionally fails without the timeout
-        // await driver.sleep(500);
-        await waitForElementAndClick(driver, "#VisualizationPopover li:nth-child(4)");
-
-        // end tutorial
-        await waitForElement(driver, "#QB-TutorialBoatImg");
-        await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
-        await waitForElementAndClick(driver, ".PopoverBody .Button.Button--primary");
-
-        await screenshot(driver, "screenshots/setup-tutorial-qb-end.png");
-    });
+  beforeAll(async () => {
+    await ensureLoggedIn(server, driver, "bob@metabase.com", "12341234");
+  });
+
+  // TODO Atte Keinänen 6/22/17: Failing test, disabled until converted to use Jest and Enzyme
+  xit("should guide users through query builder tutorial", async () => {
+    await driver.get(`${server.host}/?new`);
+    await waitForUrl(driver, `${server.host}/?new`);
+
+    await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
+    await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
+    await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
+
+    await waitForUrl(driver, `${server.host}/question`);
+    await waitForElement(driver, ".Modal .Button.Button--primary");
+    await screenshot(driver, "screenshots/setup-tutorial-qb.png");
+    await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
+
+    await waitForElement(driver, "#QB-TutorialTableImg");
+    // a .Modal-backdrop element blocks clicks for a while during transition?
+    await waitForElementRemoved(driver, ".Modal-backdrop");
+    await waitForElementAndClick(driver, ".GuiBuilder-data a");
+
+    // select sample dataset db
+    try {
+      // in a try/catch in case the instance only has one db
+      await waitForElementAndClick(
+        driver,
+        "#DatabaseSchemaPicker .List-section:last-child .List-section-header",
+        1000,
+      );
+    } catch (e) {}
+
+    // select orders table
+    await waitForElementAndClick(
+      driver,
+      "#TablePicker .List-item:first-child>a",
+    );
+
+    // select filters
+    await waitForElement(driver, "#QB-TutorialFunnelImg");
+    await waitForElementAndClick(
+      driver,
+      ".GuiBuilder-filtered-by .Query-section:not(.disabled) a",
+    );
+
+    await waitForElementAndClick(
+      driver,
+      "#FilterPopover .List-item:first-child>a",
+    );
+
+    await waitForElementAndClick(
+      driver,
+      "input[data-ui-tag='relative-date-input']",
+    );
+    await waitForElementAndSendKeys(
+      driver,
+      "#FilterPopover input.border-purple",
+      "10",
+    );
+    await waitForElementAndClick(
+      driver,
+      ".Button[data-ui-tag='add-filter']:not(.disabled)",
+    );
+
+    // select aggregations
+    await waitForElement(driver, "#QB-TutorialCalculatorImg");
+    await waitForElementAndClick(driver, "#Query-section-aggregation");
+    await waitForElementAndClick(
+      driver,
+      "#AggregationPopover .List-item:nth-child(2)>a",
+    );
+
+    // select breakouts
+    await waitForElement(driver, "#QB-TutorialBananaImg");
+    await waitForElementAndClick(
+      driver,
+      ".Query-section.Query-section-breakout>div",
+    );
+
+    await waitForElementAndClick(
+      driver,
+      "#BreakoutPopover .List-item:first-child .Field-extra>a",
+    );
+    await waitForElementAndClick(
+      driver,
+      "#TimeGroupingPopover .List-item:nth-child(4)>a",
+    );
+
+    // run query
+    await waitForElement(driver, "#QB-TutorialRocketImg");
+    await waitForElementAndClick(driver, ".Button.RunButton");
+
+    // wait for query to complete
+    await waitForElement(driver, "#QB-TutorialChartImg", 20000);
+
+    // switch visualization
+    await waitForElementAndClick(driver, "#VisualizationTrigger");
+    // this step occassionally fails without the timeout
+    // await driver.sleep(500);
+    await waitForElementAndClick(
+      driver,
+      "#VisualizationPopover li:nth-child(4)",
+    );
+
+    // end tutorial
+    await waitForElement(driver, "#QB-TutorialBoatImg");
+    await waitForElementAndClick(driver, ".Modal .Button.Button--primary");
+    await waitForElementAndClick(
+      driver,
+      ".PopoverBody .Button.Button--primary",
+    );
+
+    await screenshot(driver, "screenshots/setup-tutorial-qb-end.png");
+  });
 });
diff --git a/frontend/test/lib/browser.unit.spec.js b/frontend/test/lib/browser.unit.spec.js
index 2728ccf04ccfc6305018a59a1f2f275b62c60014..da1b5ca7f67a0cc1b05e2e1235bd28329afab6d3 100644
--- a/frontend/test/lib/browser.unit.spec.js
+++ b/frontend/test/lib/browser.unit.spec.js
@@ -1,61 +1,65 @@
 import { parseHashOptions, stringifyHashOptions } from "metabase/lib/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");
-        })
-    })
-})
+  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/test/lib/card.unit.spec.js b/frontend/test/lib/card.unit.spec.js
index 9d2a955d470c01932b58c234b703eee53f9853c8..58ee91d698e53c35fb838e0ddff13f81cfa99c94 100644
--- a/frontend/test/lib/card.unit.spec.js
+++ b/frontend/test/lib/card.unit.spec.js
@@ -1,140 +1,146 @@
 import {
-    createCard,
-    utf8_to_b64,
-    b64_to_utf8,
-    utf8_to_b64url,
-    b64url_to_utf8,
-    isCardDirty,
-    serializeCardForUrl,
-    deserializeCardFromUrl
-} from '../../src/metabase/lib/card';
+  createCard,
+  utf8_to_b64,
+  b64_to_utf8,
+  utf8_to_b64url,
+  b64url_to_utf8,
+  isCardDirty,
+  serializeCardForUrl,
+  deserializeCardFromUrl,
+} from "../../src/metabase/lib/card";
 
 const CARD_ID = 31;
 
 // TODO Atte Keinänen 8/5/17: Create a reusable version `getCard` for reducing test code duplication
 const getCard = ({
-    newCard = false,
-    hasOriginalCard = false,
-    isNative = false,
-    database = 1,
-    display = "table",
-    queryFields = {},
-    table = undefined,
- }) => {
-    const savedCardFields = {
-        name: "Example Saved Question",
-        description: "For satisfying your craving for information",
-        created_at: "2017-04-20T16:52:55.353Z",
-        id: CARD_ID
-    };
+  newCard = false,
+  hasOriginalCard = false,
+  isNative = false,
+  database = 1,
+  display = "table",
+  queryFields = {},
+  table = undefined,
+}) => {
+  const savedCardFields = {
+    name: "Example Saved Question",
+    description: "For satisfying your craving for information",
+    created_at: "2017-04-20T16:52:55.353Z",
+    id: CARD_ID,
+  };
 
-    return {
-        "name": null,
-        "display": display,
-        "visualization_settings": {},
-        "dataset_query": {
-            "database": database,
-            "type": isNative ? "native" : "query",
-            ...(!isNative ? {
-                query: {
-                    ...(table ? {"source_table": table} : {}),
-                    ...queryFields
-                }
-            } : {}),
-            ...(isNative ? {
-                native: { query: "SELECT * FROM ORDERS"}
-            } : {})
-        },
-        ...(newCard ? {} : savedCardFields),
-        ...(hasOriginalCard ? {"original_card_id": CARD_ID} : {})
-    };
+  return {
+    name: null,
+    display: display,
+    visualization_settings: {},
+    dataset_query: {
+      database: database,
+      type: isNative ? "native" : "query",
+      ...(!isNative
+        ? {
+            query: {
+              ...(table ? { source_table: table } : {}),
+              ...queryFields,
+            },
+          }
+        : {}),
+      ...(isNative
+        ? {
+            native: { query: "SELECT * FROM ORDERS" },
+          }
+        : {}),
+    },
+    ...(newCard ? {} : savedCardFields),
+    ...(hasOriginalCard ? { original_card_id: CARD_ID } : {}),
+  };
 };
 
 describe("lib/card", () => {
-    describe("createCard", () => {
-        it("should return a new card", () => {
-            expect(createCard()).toEqual({
-                name: null,
-                display: "table",
-                visualization_settings: {},
-                dataset_query: {},
-            });
-        });
-
-        it("should set the name if supplied", () => {
-            expect(createCard("something")).toEqual({
-                name: "something",
-                display: "table",
-                visualization_settings: {},
-                dataset_query: {},
-            });
-        });
+  describe("createCard", () => {
+    it("should return a new card", () => {
+      expect(createCard()).toEqual({
+        name: null,
+        display: "table",
+        visualization_settings: {},
+        dataset_query: {},
+      });
     });
 
-    describe('utf8_to_b64', () => {
-        it('should encode with non-URL-safe characters', () => {
-            expect(utf8_to_b64("  ?").indexOf("/")).toEqual(3);
-            expect(utf8_to_b64("  ?")).toEqual("ICA/");
-        });
+    it("should set the name if supplied", () => {
+      expect(createCard("something")).toEqual({
+        name: "something",
+        display: "table",
+        visualization_settings: {},
+        dataset_query: {},
+      });
     });
+  });
 
-    describe('b64_to_utf8', () => {
-        it('should decode corretly', () => {
-            expect(b64_to_utf8("ICA/")).toEqual("  ?");
-        });
+  describe("utf8_to_b64", () => {
+    it("should encode with non-URL-safe characters", () => {
+      expect(utf8_to_b64("  ?").indexOf("/")).toEqual(3);
+      expect(utf8_to_b64("  ?")).toEqual("ICA/");
     });
+  });
 
-    describe('utf8_to_b64url', () => {
-        it('should encode with URL-safe characters', () => {
-            expect(utf8_to_b64url("  ?").indexOf("/")).toEqual(-1);
-            expect(utf8_to_b64url("  ?")).toEqual("ICA_");
-        });
+  describe("b64_to_utf8", () => {
+    it("should decode corretly", () => {
+      expect(b64_to_utf8("ICA/")).toEqual("  ?");
     });
+  });
 
-    describe('b64url_to_utf8', () => {
-        it('should decode corretly', () => {
-            expect(b64url_to_utf8("ICA_")).toEqual("  ?");
-        });
+  describe("utf8_to_b64url", () => {
+    it("should encode with URL-safe characters", () => {
+      expect(utf8_to_b64url("  ?").indexOf("/")).toEqual(-1);
+      expect(utf8_to_b64url("  ?")).toEqual("ICA_");
     });
+  });
 
-    describe("isCardDirty", () => {
-        it("should consider a new card clean if no db table or native query is defined", () => {
-            expect(isCardDirty(
-                getCard({newCard: true}),
-                null
-            )).toBe(false);
-        });
-        it("should consider a new card dirty if a db table is chosen", () => {
-            expect(isCardDirty(
-                getCard({newCard: true, table: 5}),
-                null
-            )).toBe(true);
-        });
-        it("should consider a new card dirty if there is any content on the native query", () => {
-            expect(isCardDirty(
-                getCard({newCard: true, table: 5}),
-                null
-            )).toBe(true);
-        });
-        it("should consider a saved card and a matching original card identical", () => {
-            expect(isCardDirty(
-                getCard({hasOriginalCard: true}),
-                getCard({hasOriginalCard: false})
-            )).toBe(false);
-        });
-        it("should consider a saved card dirty if the current card doesn't match the last saved version", () => {
-            expect(isCardDirty(
-                getCard({hasOriginalCard: true, queryFields: [["field-id", 21]]}),
-                getCard({hasOriginalCard: false})
-            )).toBe(true);
-        });
+  describe("b64url_to_utf8", () => {
+    it("should decode corretly", () => {
+      expect(b64url_to_utf8("ICA_")).toEqual("  ?");
     });
-    describe("serializeCardForUrl", () => {
-        it("should include `original_card_id` property to the serialized URL", () => {
-            const cardAfterSerialization =
-                deserializeCardFromUrl(serializeCardForUrl(getCard({hasOriginalCard: true})));
-            expect(cardAfterSerialization).toHaveProperty("original_card_id", CARD_ID)
+  });
 
-        })
-    })
+  describe("isCardDirty", () => {
+    it("should consider a new card clean if no db table or native query is defined", () => {
+      expect(isCardDirty(getCard({ newCard: true }), null)).toBe(false);
+    });
+    it("should consider a new card dirty if a db table is chosen", () => {
+      expect(isCardDirty(getCard({ newCard: true, table: 5 }), null)).toBe(
+        true,
+      );
+    });
+    it("should consider a new card dirty if there is any content on the native query", () => {
+      expect(isCardDirty(getCard({ newCard: true, table: 5 }), null)).toBe(
+        true,
+      );
+    });
+    it("should consider a saved card and a matching original card identical", () => {
+      expect(
+        isCardDirty(
+          getCard({ hasOriginalCard: true }),
+          getCard({ hasOriginalCard: false }),
+        ),
+      ).toBe(false);
+    });
+    it("should consider a saved card dirty if the current card doesn't match the last saved version", () => {
+      expect(
+        isCardDirty(
+          getCard({ hasOriginalCard: true, queryFields: [["field-id", 21]] }),
+          getCard({ hasOriginalCard: false }),
+        ),
+      ).toBe(true);
+    });
+  });
+  describe("serializeCardForUrl", () => {
+    it("should include `original_card_id` property to the serialized URL", () => {
+      const cardAfterSerialization = deserializeCardFromUrl(
+        serializeCardForUrl(getCard({ hasOriginalCard: true })),
+      );
+      expect(cardAfterSerialization).toHaveProperty(
+        "original_card_id",
+        CARD_ID,
+      );
+    });
+  });
 });
diff --git a/frontend/test/lib/colors.unit.spec.js b/frontend/test/lib/colors.unit.spec.js
index 1d5d641f775be548f95efc1c00bb7222a433eaff..cf317a890df20f4180007295782674a664b0f0e5 100644
--- a/frontend/test/lib/colors.unit.spec.js
+++ b/frontend/test/lib/colors.unit.spec.js
@@ -1,8 +1,8 @@
-import { getRandomColor, normal } from 'metabase/lib/colors'
+import { getRandomColor, normal } from "metabase/lib/colors";
 
-describe('getRandomColor', () => {
-    it('should return a color string from the proper family', () => {
-        const color = getRandomColor(normal)
-        expect(Object.values(normal)).toContain(color)
-    })
-})
+describe("getRandomColor", () => {
+  it("should return a color string from the proper family", () => {
+    const color = getRandomColor(normal);
+    expect(Object.values(normal)).toContain(color);
+  });
+});
diff --git a/frontend/test/lib/dashboard_grid.unit.spec.js b/frontend/test/lib/dashboard_grid.unit.spec.js
index 735bc9412115b4a8d2390bf18d2afbbc62e0f3a0..a7c14237c9c3d009f7cabf7094c46d8493dbfb74 100644
--- a/frontend/test/lib/dashboard_grid.unit.spec.js
+++ b/frontend/test/lib/dashboard_grid.unit.spec.js
@@ -1,34 +1,33 @@
-import { getPositionForNewDashCard } from 'metabase/lib/dashboard_grid';
+import { getPositionForNewDashCard } from "metabase/lib/dashboard_grid";
 
-const getPos = (cards) =>
-    getPositionForNewDashCard(cards, 2, 2, 6);
+const getPos = cards => getPositionForNewDashCard(cards, 2, 2, 6);
 
-describe('dashboard_grid', () => {
-    describe('getPositionForNewDashCard', () => {
-        it('should default size to 2x2 and place first card at 0,0', () => {
-            expect(getPos([])).toEqual(pos(0,0));
-        });
-        it('should place card at correct locations on the first row', () => {
-            expect(getPos([pos(0,0)])).toEqual(pos(2,0));
-            expect(getPos([pos(1,0)])).toEqual(pos(3,0));
-            expect(getPos([pos(0,0), pos(2,0)])).toEqual(pos(4,0));
-            expect(getPos([pos(0,0), pos(4,0)])).toEqual(pos(2,0));
-        });
-        it('should place card at correct locations on the second row', () => {
-            expect(getPos([pos(0,0), pos(2,0), pos(4,0)])).toEqual(pos(0,2));
-            expect(getPos([pos(1,0), pos(4,0)])).toEqual(pos(0,2));
-        });
-        it('should place card correctly with non-default sizes', () => {
-            expect(getPos([pos(1,0,2,4), pos(4,0)])).toEqual(pos(3,2));
-            expect(getPos([pos(0,0,3,1), pos(4,0,2,1)])).toEqual(pos(0,1));
-        });
-        it('should not place card over the right edge of the grid', () => {
-            expect(getPos([pos(0,0,5,1)])).toEqual(pos(0,1));
-        });
+describe("dashboard_grid", () => {
+  describe("getPositionForNewDashCard", () => {
+    it("should default size to 2x2 and place first card at 0,0", () => {
+      expect(getPos([])).toEqual(pos(0, 0));
     });
+    it("should place card at correct locations on the first row", () => {
+      expect(getPos([pos(0, 0)])).toEqual(pos(2, 0));
+      expect(getPos([pos(1, 0)])).toEqual(pos(3, 0));
+      expect(getPos([pos(0, 0), pos(2, 0)])).toEqual(pos(4, 0));
+      expect(getPos([pos(0, 0), pos(4, 0)])).toEqual(pos(2, 0));
+    });
+    it("should place card at correct locations on the second row", () => {
+      expect(getPos([pos(0, 0), pos(2, 0), pos(4, 0)])).toEqual(pos(0, 2));
+      expect(getPos([pos(1, 0), pos(4, 0)])).toEqual(pos(0, 2));
+    });
+    it("should place card correctly with non-default sizes", () => {
+      expect(getPos([pos(1, 0, 2, 4), pos(4, 0)])).toEqual(pos(3, 2));
+      expect(getPos([pos(0, 0, 3, 1), pos(4, 0, 2, 1)])).toEqual(pos(0, 1));
+    });
+    it("should not place card over the right edge of the grid", () => {
+      expect(getPos([pos(0, 0, 5, 1)])).toEqual(pos(0, 1));
+    });
+  });
 });
 
 // shorthand for creating a position object, default to 2x2
 function pos(col, row, sizeX = 2, sizeY = 2) {
-    return { col, row, sizeX, sizeY }
+  return { col, row, sizeX, sizeY };
 }
diff --git a/frontend/test/lib/data_grid.unit.spec.js b/frontend/test/lib/data_grid.unit.spec.js
index fd968a0cbd64e53a57ea0d65643ae990cb3adb52..f23f872a93ff1c72861debc83e1c42cd7104a6d0 100644
--- a/frontend/test/lib/data_grid.unit.spec.js
+++ b/frontend/test/lib/data_grid.unit.spec.js
@@ -3,48 +3,45 @@ import { pivot } from "metabase/lib/data_grid";
 import { TYPE } from "metabase/lib/types";
 
 function makeData(rows) {
-    return {
-        rows: rows,
-        cols: [
-            { name: "D1", display_name: "Dimension 1", base_type: TYPE.Text },
-            { name: "D2", display_name: "Dimension 2", base_type: TYPE.Text },
-            { name: "M",  display_name: "Metric",      base_type: TYPE.Integer }
-        ]
-    };
+  return {
+    rows: rows,
+    cols: [
+      { name: "D1", display_name: "Dimension 1", base_type: TYPE.Text },
+      { name: "D2", display_name: "Dimension 2", base_type: TYPE.Text },
+      { name: "M", display_name: "Metric", base_type: TYPE.Integer },
+    ],
+  };
 }
 
 describe("data_grid", () => {
-    describe("pivot", () => {
+  describe("pivot", () => {
+    it("should pivot values correctly", () => {
+      let data = makeData([
+        ["a", "x", 1],
+        ["a", "y", 2],
+        ["a", "z", 3],
+        ["b", "x", 4],
+        ["b", "y", 5],
+        ["b", "z", 6],
+      ]);
+      let pivotedData = pivot(data);
+      expect(pivotedData.cols.length).toEqual(3);
+      expect(pivotedData.rows.map(row => [...row])).toEqual([
+        ["x", 1, 4],
+        ["y", 2, 5],
+        ["z", 3, 6],
+      ]);
+    });
 
-        it("should pivot values correctly", () => {
-            let data = makeData([
-                ["a", "x", 1],
-                ["a", "y", 2],
-                ["a", "z", 3],
-                ["b", "x", 4],
-                ["b", "y", 5],
-                ["b", "z", 6]
-            ])
-            let pivotedData = pivot(data);
-            expect(pivotedData.cols.length).toEqual(3);
-            expect(pivotedData.rows.map(row => [...row])).toEqual([
-                ["x", 1, 4],
-                ["y", 2, 5],
-                ["z", 3, 6]
-            ]);
-        })
-
-        it("should not return null column names from null values", () => {
-            let data = makeData([
-                [null, null, 1]
-            ]);
-            let pivotedData = pivot(data);
-            expect(pivotedData.rows.length).toEqual(1);
-            expect(pivotedData.cols.length).toEqual(2);
-            expect(pivotedData.cols[0].name).toEqual(jasmine.any(String));
-            expect(pivotedData.cols[0].display_name).toEqual(jasmine.any(String));
-            expect(pivotedData.cols[1].name).toEqual(jasmine.any(String));
-            expect(pivotedData.cols[1].display_name).toEqual(jasmine.any(String));
-        })
-    })
-})
+    it("should not return null column names from null values", () => {
+      let data = makeData([[null, null, 1]]);
+      let pivotedData = pivot(data);
+      expect(pivotedData.rows.length).toEqual(1);
+      expect(pivotedData.cols.length).toEqual(2);
+      expect(pivotedData.cols[0].name).toEqual(jasmine.any(String));
+      expect(pivotedData.cols[0].display_name).toEqual(jasmine.any(String));
+      expect(pivotedData.cols[1].name).toEqual(jasmine.any(String));
+      expect(pivotedData.cols[1].display_name).toEqual(jasmine.any(String));
+    });
+  });
+});
diff --git a/frontend/test/lib/expressions/formatter.unit.spec.js b/frontend/test/lib/expressions/formatter.unit.spec.js
index adea7131fbcd83480b91a42e14a5c64d337a00cc..dd5e9edc2596e2105d84a6d938054925cff766da 100644
--- a/frontend/test/lib/expressions/formatter.unit.spec.js
+++ b/frontend/test/lib/expressions/formatter.unit.spec.js
@@ -1,61 +1,92 @@
 import { format } from "metabase/lib/expressions/formatter";
 
 const mockMetadata = {
-    tableMetadata: {
-        fields: [
-            {id: 1, display_name: "A"},
-            {id: 2, display_name: "B"},
-            {id: 3, display_name: "C"},
-            {id: 10, display_name: "Toucan Sam"},
-            {id: 11, display_name: "count"},
-            {id: 12, display_name: "Count"}
-        ],
-        metrics: [
-            {id: 1, name: "foo bar"},
-        ]
-    }
-}
+  tableMetadata: {
+    fields: [
+      { id: 1, display_name: "A" },
+      { id: 2, display_name: "B" },
+      { id: 3, display_name: "C" },
+      { id: 10, display_name: "Toucan Sam" },
+      { id: 11, display_name: "count" },
+      { id: 12, display_name: "Count" },
+    ],
+    metrics: [{ id: 1, name: "foo bar" }],
+  },
+};
 
 describe("lib/expressions/parser", () => {
-    describe("format", () => {
-        it("can format simple expressions", () => {
-            expect(format(["+", ["field-id", 1], ["field-id", 2]], mockMetadata)).toEqual("A + B");
-        });
-
-        it("can format expressions with parentheses", () => {
-            expect(format(["+", ["/", ["field-id", 1], ["field-id", 2]], ["field-id", 3]], mockMetadata)).toEqual("(A / B) + C");
-            expect(format(["+", ["/", ["field-id", 1], ["*", ["field-id", 2], ["field-id", 2]]], ["field-id", 3]], mockMetadata)).toEqual("(A / (B * B)) + C");
-        });
-
-        it("quotes fields with spaces in them", () => {
-            expect(format(["+", ["/", ["field-id", 1], ["field-id", 10]], ["field-id", 3]], mockMetadata)).toEqual("(A / \"Toucan Sam\") + C");
-        });
-
-        it("quotes fields that conflict with reserved words", () => {
-            expect(format(["+", 1, ["field-id", 11]], mockMetadata)).toEqual('1 + "count"');
-            expect(format(["+", 1, ["field-id", 12]], mockMetadata)).toEqual('1 + "Count"');
-        });
-
-        it("format aggregations", () => {
-            expect(format(["count"], mockMetadata)).toEqual("Count");
-            expect(format(["sum", ["field-id", 1]], mockMetadata)).toEqual("Sum(A)");
-        });
-
-        it("nested aggregation", () => {
-            expect(format(["+", 1, ["count"]], mockMetadata)).toEqual("1 + Count");
-            expect(format(["/", ["sum", ["field-id", 1]], ["count"]], mockMetadata)).toEqual("Sum(A) / Count");
-        });
-
-        it("aggregation with expressions", () => {
-            expect(format(["sum", ["/", ["field-id", 1], ["field-id", 2]]], mockMetadata)).toEqual("Sum(A / B)");
-        });
-
-        it("expression with metric", () => {
-            expect(format(["+", 1, ["METRIC", 1]], mockMetadata)).toEqual("1 + \"foo bar\"");
-        });
-
-        it("expression with custom field", () => {
-            expect(format(["+", 1, ["sum", ["expression", "foo bar"]]], mockMetadata)).toEqual("1 + Sum(\"foo bar\")");
-        });
+  describe("format", () => {
+    it("can format simple expressions", () => {
+      expect(
+        format(["+", ["field-id", 1], ["field-id", 2]], mockMetadata),
+      ).toEqual("A + B");
     });
+
+    it("can format expressions with parentheses", () => {
+      expect(
+        format(
+          ["+", ["/", ["field-id", 1], ["field-id", 2]], ["field-id", 3]],
+          mockMetadata,
+        ),
+      ).toEqual("(A / B) + C");
+      expect(
+        format(
+          [
+            "+",
+            ["/", ["field-id", 1], ["*", ["field-id", 2], ["field-id", 2]]],
+            ["field-id", 3],
+          ],
+          mockMetadata,
+        ),
+      ).toEqual("(A / (B * B)) + C");
+    });
+
+    it("quotes fields with spaces in them", () => {
+      expect(
+        format(
+          ["+", ["/", ["field-id", 1], ["field-id", 10]], ["field-id", 3]],
+          mockMetadata,
+        ),
+      ).toEqual('(A / "Toucan Sam") + C');
+    });
+
+    it("quotes fields that conflict with reserved words", () => {
+      expect(format(["+", 1, ["field-id", 11]], mockMetadata)).toEqual(
+        '1 + "count"',
+      );
+      expect(format(["+", 1, ["field-id", 12]], mockMetadata)).toEqual(
+        '1 + "Count"',
+      );
+    });
+
+    it("format aggregations", () => {
+      expect(format(["count"], mockMetadata)).toEqual("Count");
+      expect(format(["sum", ["field-id", 1]], mockMetadata)).toEqual("Sum(A)");
+    });
+
+    it("nested aggregation", () => {
+      expect(format(["+", 1, ["count"]], mockMetadata)).toEqual("1 + Count");
+      expect(
+        format(["/", ["sum", ["field-id", 1]], ["count"]], mockMetadata),
+      ).toEqual("Sum(A) / Count");
+    });
+
+    it("aggregation with expressions", () => {
+      expect(
+        format(["sum", ["/", ["field-id", 1], ["field-id", 2]]], mockMetadata),
+      ).toEqual("Sum(A / B)");
+    });
+
+    it("expression with metric", () => {
+      expect(format(["+", 1, ["METRIC", 1]], mockMetadata)).toEqual(
+        '1 + "foo bar"',
+      );
+    });
+
+    it("expression with custom field", () => {
+      expect(
+        format(["+", 1, ["sum", ["expression", "foo bar"]]], mockMetadata),
+      ).toEqual('1 + Sum("foo bar")');
+    });
+  });
 });
diff --git a/frontend/test/lib/expressions/parser.unit.spec.js b/frontend/test/lib/expressions/parser.unit.spec.js
index 69b197453d7e32e630c32221fb8b221612fe05c4..5541cb8051d336283fec91fe1153f0924191f0c3 100644
--- a/frontend/test/lib/expressions/parser.unit.spec.js
+++ b/frontend/test/lib/expressions/parser.unit.spec.js
@@ -3,170 +3,225 @@ import _ from "underscore";
 import { TYPE } from "metabase/lib/types";
 
 const mockMetadata = {
-    tableMetadata: {
-        fields: [
-            {id: 1, display_name: "A", base_type: TYPE.Float },
-            {id: 2, display_name: "B", base_type: TYPE.Float},
-            {id: 3, display_name: "C", base_type: TYPE.Float},
-            {id: 10, display_name: "Toucan Sam", base_type: TYPE.Float},
-            {id: 11, display_name: "count", base_type: TYPE.Float}
-        ],
-        metrics: [
-            {id: 1, name: "foo bar"},
-        ],
-        aggregation_options: [
-            { short: "count", fields: [] },
-            { short: "sum", fields: [[]] }
-        ]
-    }
-}
+  tableMetadata: {
+    fields: [
+      { id: 1, display_name: "A", base_type: TYPE.Float },
+      { id: 2, display_name: "B", base_type: TYPE.Float },
+      { id: 3, display_name: "C", base_type: TYPE.Float },
+      { id: 10, display_name: "Toucan Sam", base_type: TYPE.Float },
+      { id: 11, display_name: "count", base_type: TYPE.Float },
+    ],
+    metrics: [{ id: 1, name: "foo bar" }],
+    aggregation_options: [
+      { short: "count", fields: [] },
+      { short: "sum", fields: [[]] },
+    ],
+  },
+};
 
 const expressionOpts = { ...mockMetadata, startRule: "expression" };
 const aggregationOpts = { ...mockMetadata, startRule: "aggregation" };
 
 describe("lib/expressions/parser", () => {
-    describe("compile()", () => {
-        it("should return empty array for null or empty string", () => {
-            expect(compile()).toEqual([]);
-            expect(compile(null)).toEqual([]);
-            expect(compile("")).toEqual([]);
-        });
-
-        it("can parse simple expressions", () => {
-            expect(compile("A", expressionOpts)).toEqual(['field-id', 1]);
-            expect(compile("1", expressionOpts)).toEqual(1);
-            expect(compile("1.1", expressionOpts)).toEqual(1.1);
-        });
-
-        it("can parse single operator math", () => {
-            expect(compile("A-B", expressionOpts)).toEqual(["-", ['field-id', 1], ['field-id', 2]]);
-            expect(compile("A - B", expressionOpts)).toEqual(["-", ['field-id', 1], ['field-id', 2]]);
-            expect(compile("1 - B", expressionOpts)).toEqual(["-", 1, ['field-id', 2]]);
-            expect(compile("1 - 2", expressionOpts)).toEqual(["-", 1, 2]);
-        });
-
-        it("can handle operator precedence", () => {
-            expect(compile("1 + 2 * 3", expressionOpts)).toEqual(["+", 1, ["*", 2, 3]]);
-            expect(compile("1 * 2 + 3", expressionOpts)).toEqual(["+", ["*", 1, 2], 3]);
-        });
-
-        it("can collapse consecutive identical operators", () => {
-            expect(compile("1 + 2 + 3 * 4 * 5", expressionOpts)).toEqual(["+", 1, 2, ["*", 3, 4, 5]]);
-        });
-
-        it("can handle negative number literals", () => {
-            expect(compile("1 + -1", expressionOpts)).toEqual(["+", 1, -1]);
-        });
-
-        // quoted field name w/ a space in it
-        it("can parse a field with quotes and spaces", () => {
-            expect(compile("\"Toucan Sam\" + B", expressionOpts)).toEqual(["+", ['field-id', 10], ['field-id', 2]]);
-        });
-
-        // parentheses / nested parens
-        it("can parse expressions with parentheses", () => {
-            expect(compile("(1 + 2) * 3", expressionOpts)).toEqual(["*", ["+", 1, 2], 3]);
-            expect(compile("1 * (2 + 3)", expressionOpts)).toEqual(["*", 1, ["+", 2, 3]]);
-            expect(compile("\"Toucan Sam\" + (A * (B / C))", expressionOpts)).toEqual(
-                ["+", ['field-id', 10], ["*", [ 'field-id', 1 ], ["/", [ 'field-id', 2 ], [ 'field-id', 3 ]]]]
-            );
-        });
-
-        it("can parse aggregation with no arguments", () => {
-            expect(compile("Count", aggregationOpts)).toEqual(["count"]);
-            expect(compile("Count()", aggregationOpts)).toEqual(["count"]);
-        });
-
-        it("can parse aggregation with argument", () => {
-            expect(compile("Sum(A)", aggregationOpts)).toEqual(["sum", ["field-id", 1]]);
-        });
-
-        it("can handle negative number literals in aggregations", () => {
-            expect(compile("-1 * Count", aggregationOpts)).toEqual(["*", -1, ["count"]]);
-        });
-
-        it("can parse complex aggregation", () => {
-            expect(compile("1 - Sum(A * 2) / Count", aggregationOpts)).toEqual(["-", 1, ["/", ["sum", ["*", ["field-id", 1], 2]], ["count"]]]);
-        });
-
-        it("should throw exception on invalid input", () => {
-            expect(() => compile("1 + ", expressionOpts)).toThrow();
-        });
-
-        it("should treat aggregations as case-insensitive", () => {
-            expect(compile("count", aggregationOpts)).toEqual(["count"]);
-            expect(compile("cOuNt", aggregationOpts)).toEqual(["count"]);
-            expect(compile("average(A)", aggregationOpts)).toEqual(["avg", ["field-id", 1]]);
-        });
-
-        // fks
-        // multiple tables with the same field name resolution
-    });
-
-    describe("suggest()", () => {
-        it("should suggest aggregations and metrics after an operator", () => {
-            expect(cleanSuggestions(suggest("1 + ", aggregationOpts))).toEqual([
-                { type: 'aggregations', text: 'Count ' },
-                { type: 'aggregations', text: 'Sum(' },
-                // NOTE: metrics support currently disabled
-                // { type: 'metrics',     text: '"foo bar"' },
-                { type: 'other',       text: ' (' },
-            ]);
-        })
-        it("should suggest fields after an operator", () => {
-            expect(cleanSuggestions(suggest("1 + ", expressionOpts))).toEqual([
-                // quoted because has a space
-                { type: 'fields',      text: '"Toucan Sam" ' },
-                // quoted because conflicts with aggregation
-                { type: 'fields',      text: '"count" ' },
-                { type: 'fields',      text: 'A ' },
-                { type: 'fields',      text: 'B ' },
-                { type: 'fields',      text: 'C ' },
-                { type: 'other',       text: ' (' },
-            ]);
-        })
-        it("should suggest partial matches in aggregation", () => {
-            expect(cleanSuggestions(suggest("1 + C", aggregationOpts))).toEqual([
-                { type: 'aggregations', text: 'Count ' },
-            ]);
-        })
-        it("should suggest partial matches in expression", () => {
-            expect(cleanSuggestions(suggest("1 + C", expressionOpts))).toEqual([
-                { type: 'fields', text: '"count" ' },
-                { type: 'fields', text: 'C ' },
-            ]);
-        })
-        it("should suggest partial matches after an aggregation", () => {
-            expect(cleanSuggestions(suggest("average(c", expressionOpts))).toEqual([
-                { type: 'fields',      text: '"count" ' },
-                { type: 'fields',      text: 'C ' }
-            ]);
-        })
-    })
-
-    describe("compile() in syntax mode", () => {
-        it ("should parse source without whitespace into a recoverable syntax tree", () => {
-            const source = "1-Sum(A*2+\"Toucan Sam\")/Count()";
-            const tree = parse(source, aggregationOpts);
-            expect(serialize(tree)).toEqual(source)
-        })
-        xit ("should parse source with whitespace into a recoverable syntax tree", () => {
-            // FIXME: not preserving whitespace
-            const source = "1 - Sum(A * 2 + \"Toucan Sam\") / Count";
-            const tree = parse(source, aggregationOpts);
-            expect(serialize(tree)).toEqual(source)
-        })
-    })
+  describe("compile()", () => {
+    it("should return empty array for null or empty string", () => {
+      expect(compile()).toEqual([]);
+      expect(compile(null)).toEqual([]);
+      expect(compile("")).toEqual([]);
+    });
+
+    it("can parse simple expressions", () => {
+      expect(compile("A", expressionOpts)).toEqual(["field-id", 1]);
+      expect(compile("1", expressionOpts)).toEqual(1);
+      expect(compile("1.1", expressionOpts)).toEqual(1.1);
+    });
+
+    it("can parse single operator math", () => {
+      expect(compile("A-B", expressionOpts)).toEqual([
+        "-",
+        ["field-id", 1],
+        ["field-id", 2],
+      ]);
+      expect(compile("A - B", expressionOpts)).toEqual([
+        "-",
+        ["field-id", 1],
+        ["field-id", 2],
+      ]);
+      expect(compile("1 - B", expressionOpts)).toEqual([
+        "-",
+        1,
+        ["field-id", 2],
+      ]);
+      expect(compile("1 - 2", expressionOpts)).toEqual(["-", 1, 2]);
+    });
+
+    it("can handle operator precedence", () => {
+      expect(compile("1 + 2 * 3", expressionOpts)).toEqual([
+        "+",
+        1,
+        ["*", 2, 3],
+      ]);
+      expect(compile("1 * 2 + 3", expressionOpts)).toEqual([
+        "+",
+        ["*", 1, 2],
+        3,
+      ]);
+    });
+
+    it("can collapse consecutive identical operators", () => {
+      expect(compile("1 + 2 + 3 * 4 * 5", expressionOpts)).toEqual([
+        "+",
+        1,
+        2,
+        ["*", 3, 4, 5],
+      ]);
+    });
+
+    it("can handle negative number literals", () => {
+      expect(compile("1 + -1", expressionOpts)).toEqual(["+", 1, -1]);
+    });
+
+    // quoted field name w/ a space in it
+    it("can parse a field with quotes and spaces", () => {
+      expect(compile('"Toucan Sam" + B', expressionOpts)).toEqual([
+        "+",
+        ["field-id", 10],
+        ["field-id", 2],
+      ]);
+    });
+
+    // parentheses / nested parens
+    it("can parse expressions with parentheses", () => {
+      expect(compile("(1 + 2) * 3", expressionOpts)).toEqual([
+        "*",
+        ["+", 1, 2],
+        3,
+      ]);
+      expect(compile("1 * (2 + 3)", expressionOpts)).toEqual([
+        "*",
+        1,
+        ["+", 2, 3],
+      ]);
+      expect(compile('"Toucan Sam" + (A * (B / C))', expressionOpts)).toEqual([
+        "+",
+        ["field-id", 10],
+        ["*", ["field-id", 1], ["/", ["field-id", 2], ["field-id", 3]]],
+      ]);
+    });
+
+    it("can parse aggregation with no arguments", () => {
+      expect(compile("Count", aggregationOpts)).toEqual(["count"]);
+      expect(compile("Count()", aggregationOpts)).toEqual(["count"]);
+    });
+
+    it("can parse aggregation with argument", () => {
+      expect(compile("Sum(A)", aggregationOpts)).toEqual([
+        "sum",
+        ["field-id", 1],
+      ]);
+    });
+
+    it("can handle negative number literals in aggregations", () => {
+      expect(compile("-1 * Count", aggregationOpts)).toEqual([
+        "*",
+        -1,
+        ["count"],
+      ]);
+    });
+
+    it("can parse complex aggregation", () => {
+      expect(compile("1 - Sum(A * 2) / Count", aggregationOpts)).toEqual([
+        "-",
+        1,
+        ["/", ["sum", ["*", ["field-id", 1], 2]], ["count"]],
+      ]);
+    });
+
+    it("should throw exception on invalid input", () => {
+      expect(() => compile("1 + ", expressionOpts)).toThrow();
+    });
+
+    it("should treat aggregations as case-insensitive", () => {
+      expect(compile("count", aggregationOpts)).toEqual(["count"]);
+      expect(compile("cOuNt", aggregationOpts)).toEqual(["count"]);
+      expect(compile("average(A)", aggregationOpts)).toEqual([
+        "avg",
+        ["field-id", 1],
+      ]);
+    });
+
+    // fks
+    // multiple tables with the same field name resolution
+  });
+
+  describe("suggest()", () => {
+    it("should suggest aggregations and metrics after an operator", () => {
+      expect(cleanSuggestions(suggest("1 + ", aggregationOpts))).toEqual([
+        { type: "aggregations", text: "Count " },
+        { type: "aggregations", text: "Sum(" },
+        // NOTE: metrics support currently disabled
+        // { type: 'metrics',     text: '"foo bar"' },
+        { type: "other", text: " (" },
+      ]);
+    });
+    it("should suggest fields after an operator", () => {
+      expect(cleanSuggestions(suggest("1 + ", expressionOpts))).toEqual([
+        // quoted because has a space
+        { type: "fields", text: '"Toucan Sam" ' },
+        // quoted because conflicts with aggregation
+        { type: "fields", text: '"count" ' },
+        { type: "fields", text: "A " },
+        { type: "fields", text: "B " },
+        { type: "fields", text: "C " },
+        { type: "other", text: " (" },
+      ]);
+    });
+    it("should suggest partial matches in aggregation", () => {
+      expect(cleanSuggestions(suggest("1 + C", aggregationOpts))).toEqual([
+        { type: "aggregations", text: "Count " },
+      ]);
+    });
+    it("should suggest partial matches in expression", () => {
+      expect(cleanSuggestions(suggest("1 + C", expressionOpts))).toEqual([
+        { type: "fields", text: '"count" ' },
+        { type: "fields", text: "C " },
+      ]);
+    });
+    it("should suggest partial matches after an aggregation", () => {
+      expect(cleanSuggestions(suggest("average(c", expressionOpts))).toEqual([
+        { type: "fields", text: '"count" ' },
+        { type: "fields", text: "C " },
+      ]);
+    });
+  });
+
+  describe("compile() in syntax mode", () => {
+    it("should parse source without whitespace into a recoverable syntax tree", () => {
+      const source = '1-Sum(A*2+"Toucan Sam")/Count()';
+      const tree = parse(source, aggregationOpts);
+      expect(serialize(tree)).toEqual(source);
+    });
+    xit("should parse source with whitespace into a recoverable syntax tree", () => {
+      // FIXME: not preserving whitespace
+      const source = '1 - Sum(A * 2 + "Toucan Sam") / Count';
+      const tree = parse(source, aggregationOpts);
+      expect(serialize(tree)).toEqual(source);
+    });
+  });
 });
 
 function serialize(tree) {
-    if (tree.type === "token") {
-        return tree.text;
-    } else {
-        return tree.children.map(serialize).join("");
-    }
+  if (tree.type === "token") {
+    return tree.text;
+  } else {
+    return tree.children.map(serialize).join("");
+  }
 }
 
 function cleanSuggestions(suggestions) {
-    return _.chain(suggestions).map(s => _.pick(s, "type", "text")).sortBy("text").sortBy("type").value();
+  return _.chain(suggestions)
+    .map(s => _.pick(s, "type", "text"))
+    .sortBy("text")
+    .sortBy("type")
+    .value();
 }
diff --git a/frontend/test/lib/formatting.unit.spec.js b/frontend/test/lib/formatting.unit.spec.js
index c8344508e9d270b13934d6892a6d6b3cf6470b4b..de2a443a3915d2c0f67fc5993962f1bdd4189311 100644
--- a/frontend/test/lib/formatting.unit.spec.js
+++ b/frontend/test/lib/formatting.unit.spec.js
@@ -1,86 +1,137 @@
-
 import { isElementOfType } from "react-dom/test-utils";
 
-import { formatNumber, formatValue, formatUrl } from 'metabase/lib/formatting';
+import { formatNumber, formatValue, formatUrl } from "metabase/lib/formatting";
 import ExternalLink from "metabase/components/ExternalLink.jsx";
 import { TYPE } from "metabase/lib/types";
 
-describe('formatting', () => {
-    describe('formatNumber', () => {
-        it('should format 0 correctly', () => {
-            expect(formatNumber(0)).toEqual("0");
-        });
-        it('should format 1 and -1 correctly', () => {
-            expect(formatNumber(1)).toEqual("1");
-            expect(formatNumber(-1)).toEqual("-1");
-        });
-        it('should format large positive and negative numbers correctly', () => {
-            expect(formatNumber(10)).toEqual("10");
-            expect(formatNumber(99999999)).toEqual("99,999,999");
-            expect(formatNumber(-10)).toEqual("-10");
-            expect(formatNumber(-99999999)).toEqual("-99,999,999");
-        });
-        it('should format to 2 significant digits', () => {
-            expect(formatNumber(1/3)).toEqual("0.33");
-            expect(formatNumber(-1/3)).toEqual("-0.33");
-            expect(formatNumber(0.0001/3)).toEqual("0.000033");
-        });
-        describe("in compact mode", () => {
-            it("should format 0 as 0", () => {
-                expect(formatNumber(0, { compact: true })).toEqual("0");
-            })
-            it("shouldn't display small numbers as 0", () => {
-                expect(formatNumber(0.1, { compact: true })).toEqual("0.1");
-                expect(formatNumber(-0.1, { compact: true })).toEqual("-0.1");
-                expect(formatNumber(0.01, { compact: true })).toEqual("~ 0");
-                expect(formatNumber(-0.01, { compact: true })).toEqual("~ 0");
-            });
-            it("should format large numbers with metric units", () => {
-                expect(formatNumber(1, { compact: true })).toEqual("1");
-                expect(formatNumber(1000, { compact: true })).toEqual("1.0k");
-                expect(formatNumber(1111, { compact: true })).toEqual("1.1k");
-            })
-        });
+describe("formatting", () => {
+  describe("formatNumber", () => {
+    it("should format 0 correctly", () => {
+      expect(formatNumber(0)).toEqual("0");
+    });
+    it("should format 1 and -1 correctly", () => {
+      expect(formatNumber(1)).toEqual("1");
+      expect(formatNumber(-1)).toEqual("-1");
+    });
+    it("should format large positive and negative numbers correctly", () => {
+      expect(formatNumber(10)).toEqual("10");
+      expect(formatNumber(99999999)).toEqual("99,999,999");
+      expect(formatNumber(-10)).toEqual("-10");
+      expect(formatNumber(-99999999)).toEqual("-99,999,999");
+    });
+    it("should format to 2 significant digits", () => {
+      expect(formatNumber(1 / 3)).toEqual("0.33");
+      expect(formatNumber(-1 / 3)).toEqual("-0.33");
+      expect(formatNumber(0.0001 / 3)).toEqual("0.000033");
     });
+    describe("in compact mode", () => {
+      it("should format 0 as 0", () => {
+        expect(formatNumber(0, { compact: true })).toEqual("0");
+      });
+      it("shouldn't display small numbers as 0", () => {
+        expect(formatNumber(0.1, { compact: true })).toEqual("0.1");
+        expect(formatNumber(-0.1, { compact: true })).toEqual("-0.1");
+        expect(formatNumber(0.01, { compact: true })).toEqual("~ 0");
+        expect(formatNumber(-0.01, { compact: true })).toEqual("~ 0");
+      });
+      it("should format large numbers with metric units", () => {
+        expect(formatNumber(1, { compact: true })).toEqual("1");
+        expect(formatNumber(1000, { compact: true })).toEqual("1.0k");
+        expect(formatNumber(1111, { compact: true })).toEqual("1.1k");
+      });
+    });
+  });
 
-    describe("formatValue", () => {
-        it("should format numbers with null column", () => {
-            expect(formatValue(12345)).toEqual("12345");
-        });
-        it("should format numbers with commas", () => {
-            expect(formatValue(12345, { column: { base_type: TYPE.Number, special_type: TYPE.Number }})).toEqual("12,345");
-        });
-        it("should format zip codes without commas", () => {
-            expect(formatValue(12345, { column: { base_type: TYPE.Number, special_type: TYPE.ZipCode }})).toEqual("12345");
-        });
-        it("should format latitude and longitude columns correctly", () => {
-            expect(formatValue(37.7749, { column: { base_type: TYPE.Number, special_type: TYPE.Latitude }})).toEqual("37.77490000° N");
-            expect(formatValue(-122.4194, { column: { base_type: TYPE.Number, special_type: TYPE.Longitude }})).toEqual("122.41940000° W");
-        });
-        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("formatValue", () => {
+    it("should format numbers with null column", () => {
+      expect(formatValue(12345)).toEqual("12345");
+    });
+    it("should format numbers with commas", () => {
+      expect(
+        formatValue(12345, {
+          column: { base_type: TYPE.Number, special_type: TYPE.Number },
+        }),
+      ).toEqual("12,345");
+    });
+    it("should format zip codes without commas", () => {
+      expect(
+        formatValue(12345, {
+          column: { base_type: TYPE.Number, special_type: TYPE.ZipCode },
+        }),
+      ).toEqual("12345");
     });
+    it("should format latitude and longitude columns correctly", () => {
+      expect(
+        formatValue(37.7749, {
+          column: { base_type: TYPE.Number, special_type: TYPE.Latitude },
+        }),
+      ).toEqual("37.77490000° N");
+      expect(
+        formatValue(-122.4194, {
+          column: { base_type: TYPE.Number, special_type: TYPE.Longitude },
+        }),
+      ).toEqual("122.41940000° W");
+    });
+    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", () => {
-        it("should return a string when not in jsx mode", () => {
-            expect(formatUrl("http://metabase.com/")).toEqual("http://metabase.com/")
-        });
-        it("should return a component for http:, https:, and mailto: links in jsx mode", () => {
-            expect(isElementOfType(formatUrl("http://metabase.com/", { jsx: true }), ExternalLink)).toEqual(true);
-            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");
-        });
-    })
+  describe("formatUrl", () => {
+    it("should return a string when not in jsx mode", () => {
+      expect(formatUrl("http://metabase.com/")).toEqual("http://metabase.com/");
+    });
+    it("should return a component for http:, https:, and mailto: links in jsx mode", () => {
+      expect(
+        isElementOfType(
+          formatUrl("http://metabase.com/", { jsx: true }),
+          ExternalLink,
+        ),
+      ).toEqual(true);
+      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/lib/query.unit.spec.js b/frontend/test/lib/query.unit.spec.js
index fb870b4f9f6bfbf1589b32d8f4f87931bfbe9775..4932a92f294bde12cf9b7c7f0a44660e8adc0886 100644
--- a/frontend/test/lib/query.unit.spec.js
+++ b/frontend/test/lib/query.unit.spec.js
@@ -1,478 +1,477 @@
-import Query, { createQuery, AggregationClause, BreakoutClause } from "metabase/lib/query";
-import {
-    question,
-} from "__support__/sample_dataset_fixture";
+import Query, {
+  createQuery,
+  AggregationClause,
+  BreakoutClause,
+} from "metabase/lib/query";
+import { question } from "__support__/sample_dataset_fixture";
 import Utils from "metabase/lib/utils";
 
 const mockTableMetadata = {
-    display_name: "Order",
-    fields: [
-        { id: 1, display_name: "Total" }
-    ]
-}
-
-describe('Legacy Query library', () => {
-    describe('createQuery', () => {
-        it("should provide a structured query with no args", () => {
-            expect(createQuery()).toEqual({
-                database: null,
-                type: "query",
-                query: {
-                    source_table: null
-                }
-            });
-        });
-
-        it("should be able to create a native type query", () => {
-            expect(createQuery("native")).toEqual({
-                database: null,
-                type: "native",
-                native: {
-                    query: ""
-                }
-            });
-        });
-
-        it("should populate the databaseId if specified", () => {
-            expect(createQuery("query", 123).database).toEqual(123);
-        });
-
-        it("should populate the tableId if specified", () => {
-            expect(createQuery("query", 123, 456).query.source_table).toEqual(456);
-        });
-
-        it("should NOT set the tableId if query type is native", () => {
-            expect(createQuery("native", 123, 456).query).toEqual(undefined);
-        });
-
-        it("should NOT populate the tableId if no database specified", () => {
-            expect(createQuery("query", null, 456).query.source_table).toEqual(null);
-        });
-    });
-
-    describe('cleanQuery', () => {
-        it("should pass for a query created with metabase-lib", () => {
-            const datasetQuery = question.query()
-                .addAggregation(["count"])
-                .datasetQuery()
-
-            // We have to take a copy because the original object isn't extensible
-            const copiedDatasetQuery = Utils.copy(datasetQuery);
-            Query.cleanQuery(copiedDatasetQuery)
-
-            expect(copiedDatasetQuery).toBeDefined()
-        })
-        it('should not remove complete sort clauses', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["rows"],
-                breakout: [],
-                filter: [],
-                order_by: [
-                    [1, "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[1, "ascending"]]);
-        });
-        it('should remove incomplete sort clauses', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["rows"],
-                breakout: [],
-                filter: [],
-                order_by: [
-                    [null, "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual(undefined);
-        });
-
-        it('should not remove sort clauses on aggregations if that aggregation supports it', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [1],
-                filter: [],
-                order_by: [
-                    [["aggregation", 0], "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[["aggregation", 0], "ascending"]]);
-        });
-        it('should remove sort clauses on aggregations if that aggregation doesn\'t support it', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["rows"],
-                breakout: [],
-                filter: [],
-                order_by: [
-                    [["aggregation", 0], "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual(undefined);
-        });
-
-        it('should not remove sort clauses on fields appearing in breakout', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [1],
-                filter: [],
-                order_by: [
-                    [1, "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[1, "ascending"]]);
-        });
-        it('should remove sort clauses on fields not appearing in breakout', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [],
-                filter: [],
-                order_by: [
-                    [1, "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual(undefined);
-        });
-
-        it('should not remove sort clauses with foreign keys on fields appearing in breakout', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["fk->", 1, 2]],
-                filter: [],
-                order_by: [
-                    [["fk->", 1, 2], "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[["fk->", 1, 2], "ascending"]]);
-        });
-
-        it('should not remove sort clauses with datetime-fields on fields appearing in breakout', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["datetime-field", 1, "as", "week"]],
-                filter: [],
-                order_by: [
-                    [["datetime-field", 1, "as", "week"], "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[["datetime-field", 1, "as", "week"], "ascending"]]);
-        });
-
-        it('should replace order_by clauses with the exact matching datetime-fields version in the breakout', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["datetime-field", 1, "as", "week"]],
-                filter: [],
-                order_by: [
-                    [1, "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[["datetime-field", 1, "as", "week"], "ascending"]]);
-        });
-
-        it('should replace order_by clauses with the exact matching fk-> version in the breakout', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["fk->", 1, 2]],
-                filter: [],
-                order_by: [
-                    [2, "ascending"]
-                ]
-            };
-            Query.cleanQuery(query);
-            expect(query.order_by).toEqual([[["fk->", 1, 2], "ascending"]]);
-        });
-    });
-
-    describe('removeBreakout', () => {
-        it('should not mutate the query', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["field-id", 1]],
-                filter: []
-            };
-            Query.removeBreakout(query, 0);
-            expect(query.breakout).toEqual([["field-id", 1]]);
-        });
-        it('should remove the dimension', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["field-id", 1]],
-                filter: []
-            };
-            query = Query.removeBreakout(query, 0);
-            expect(query.breakout).toEqual(undefined);
-        });
-        it('should remove sort clauses for the dimension that was removed', () => {
-            let query = {
-                source_table: 0,
-                aggregation: ["count"],
-                breakout: [["field-id", 1]],
-                filter: [],
-                order_by: [
-                    [1, "ascending"]
-                ]
-            };
-            query = Query.removeBreakout(query, 0);
-            expect(query.order_by).toEqual(undefined);
-        });
-    });
-
-    describe('getFieldTarget', () => {
-        let field2 = {
-            display_name: "field2",
-        }
-        let table2 = {
-            display_name: "table2",
-            fields_lookup: {
-                2: field2
-            }
-        }
-        let field1 = {
-            display_name: "field1",
-            target: {
-                table: table2
-            }
-        }
-        let table1 = {
-            display_name: "table1",
-            fields_lookup: {
-                1: field1
-            }
-        }
-
-        it('should return field object for old-style local field', () => {
-            let target = Query.getFieldTarget(1, table1);
-            expect(target.table).toEqual(table1);
-            expect(target.field).toEqual(field1);
-            expect(target.path).toEqual([]);
-            expect(target.unit).toEqual(undefined);
-        });
-        it('should return field object for new-style local field', () => {
-            let target = Query.getFieldTarget(["field-id", 1], table1);
-            expect(target.table).toEqual(table1);
-            expect(target.field).toEqual(field1);
-            expect(target.path).toEqual([]);
-            expect(target.unit).toEqual(undefined);
-        });
-        it('should return unit object for old-style datetime-field', () => {
-            let target = Query.getFieldTarget(["datetime-field", 1, "as", "day"], table1);
-            expect(target.table).toEqual(table1);
-            expect(target.field).toEqual(field1);
-            expect(target.path).toEqual([]);
-            expect(target.unit).toEqual("day");
-        });
-        it('should return unit object for new-style datetime-field', () => {
-            let target = Query.getFieldTarget(["datetime-field", 1, "as", "day"], table1);
-            expect(target.table).toEqual(table1);
-            expect(target.field).toEqual(field1);
-            expect(target.path).toEqual([]);
-            expect(target.unit).toEqual("day");
-        });
-
-        it('should return field object and table for fk field', () => {
-            let target = Query.getFieldTarget(["fk->", 1, 2], table1);
-            expect(target.table).toEqual(table2);
-            expect(target.field).toEqual(field2);
-            expect(target.path).toEqual([field1]);
-            expect(target.unit).toEqual(undefined);
-        });
-
-        it('should return field object and table and unit for fk + datetime field', () => {
-            let target = Query.getFieldTarget(["datetime-field", ["fk->", 1, 2], "day"], table1);
-            expect(target.table).toEqual(table2);
-            expect(target.field).toEqual(field2);
-            expect(target.path).toEqual([field1]);
-            expect(target.unit).toEqual("day");
-        });
-
-        it('should return field object and table for expression', () => {
-            let target = Query.getFieldTarget(["expression", "foo"], table1);
-            expect(target.table).toEqual(table1);
-            expect(target.field.display_name).toEqual("foo");
-            expect(target.path).toEqual([]);
-            expect(target.unit).toEqual(undefined);
-        });
-    })
-})
+  display_name: "Order",
+  fields: [{ id: 1, display_name: "Total" }],
+};
+
+describe("Legacy Query library", () => {
+  describe("createQuery", () => {
+    it("should provide a structured query with no args", () => {
+      expect(createQuery()).toEqual({
+        database: null,
+        type: "query",
+        query: {
+          source_table: null,
+        },
+      });
+    });
 
-describe("generateQueryDescription", () => {
-    it("should work with multiple aggregations", () => {
-        expect(Query.generateQueryDescription(mockTableMetadata, {
-            source_table: 1,
-            aggregation: [["count"], ["sum", ["field-id", 1]]]
-        })).toEqual("Orders, Count and Sum of Total")
-    })
-    it("should work with named aggregations", () => {
-        expect(Query.generateQueryDescription(mockTableMetadata, {
-            source_table: 1,
-            aggregation: [["named", ["sum", ["field-id", 1]], "Revenue"]]
-        })).toEqual("Orders, Revenue")
-    })
-})
-
-describe('AggregationClause', () => {
-
-    describe('isValid', () => {
-        it("should fail on bad clauses", () => {
-            expect(AggregationClause.isValid(undefined)).toEqual(false);
-            expect(AggregationClause.isValid(null)).toEqual(false);
-            expect(AggregationClause.isValid([])).toEqual(false);
-            expect(AggregationClause.isValid([null])).toEqual(false);
-            expect(AggregationClause.isValid("ab")).toEqual(false);
-            expect(AggregationClause.isValid(["foo", null])).toEqual(false);
-            expect(AggregationClause.isValid(["a", "b", "c"])).toEqual(false);
-        });
-
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.isValid(["METRIC", 123])).toEqual(true);
-            expect(AggregationClause.isValid(["rows"])).toEqual(true);
-            expect(AggregationClause.isValid(["sum", 456])).toEqual(true);
-        });
-    });
-
-    describe('isBareRows', () => {
-        it("should fail on bad clauses", () => {
-            expect(AggregationClause.isBareRows(undefined)).toEqual(false);
-            expect(AggregationClause.isBareRows(null)).toEqual(false);
-            expect(AggregationClause.isBareRows([])).toEqual(false);
-            expect(AggregationClause.isBareRows([null])).toEqual(false);
-            expect(AggregationClause.isBareRows("ab")).toEqual(false);
-            expect(AggregationClause.isBareRows(["foo", null])).toEqual(false);
-            expect(AggregationClause.isBareRows(["a", "b", "c"])).toEqual(false);
-            expect(AggregationClause.isBareRows(["METRIC", 123])).toEqual(false);
-            expect(AggregationClause.isBareRows(["sum", 456])).toEqual(false);
-        });
-
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.isBareRows(["rows"])).toEqual(true);
-        });
-    });
-
-    describe('isStandard', () => {
-        it("should fail on bad clauses", () => {
-            expect(AggregationClause.isStandard(undefined)).toEqual(false);
-            expect(AggregationClause.isStandard(null)).toEqual(false);
-            expect(AggregationClause.isStandard([])).toEqual(false);
-            expect(AggregationClause.isStandard([null])).toEqual(false);
-            expect(AggregationClause.isStandard("ab")).toEqual(false);
-            expect(AggregationClause.isStandard(["foo", null])).toEqual(false);
-            expect(AggregationClause.isStandard(["a", "b", "c"])).toEqual(false);
-            expect(AggregationClause.isStandard(["METRIC", 123])).toEqual(false);
-        });
-
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.isStandard(["rows"])).toEqual(true);
-            expect(AggregationClause.isStandard(["sum", 456])).toEqual(true);
-        });
-    });
-
-    describe('isMetric', () => {
-        it("should fail on bad clauses", () => {
-            expect(AggregationClause.isMetric(undefined)).toEqual(false);
-            expect(AggregationClause.isMetric(null)).toEqual(false);
-            expect(AggregationClause.isMetric([])).toEqual(false);
-            expect(AggregationClause.isMetric([null])).toEqual(false);
-            expect(AggregationClause.isMetric("ab")).toEqual(false);
-            expect(AggregationClause.isMetric(["foo", null])).toEqual(false);
-            expect(AggregationClause.isMetric(["a", "b", "c"])).toEqual(false);
-            expect(AggregationClause.isMetric(["rows"])).toEqual(false);
-            expect(AggregationClause.isMetric(["sum", 456])).toEqual(false);
-        });
-
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.isMetric(["METRIC", 123])).toEqual(true);
-        });
-    });
-
-    describe('getMetric', () => {
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.getMetric(["METRIC", 123])).toEqual(123);
-        });
-
-        it("should be null on non-metric clauses", () => {
-            expect(AggregationClause.getMetric(["sum", 123])).toEqual(null);
-        });
-    });
-
-    describe('getOperator', () => {
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.getOperator(["rows"])).toEqual("rows");
-            expect(AggregationClause.getOperator(["sum", 123])).toEqual("sum");
-        });
-
-        it("should be null on metric clauses", () => {
-            expect(AggregationClause.getOperator(["METRIC", 123])).toEqual(null);
-        });
-    });
-
-    describe('getField', () => {
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.getField(["sum", 123])).toEqual(123);
-        });
-
-        it("should be null on clauses w/out a field", () => {
-            expect(AggregationClause.getField(["rows"])).toEqual(null);
-        });
-
-        it("should be null on metric clauses", () => {
-            expect(AggregationClause.getField(["METRIC", 123])).toEqual(null);
-        });
-    });
-
-    describe('setField', () => {
-        it("should succeed on good clauses", () => {
-            expect(AggregationClause.setField(["avg"], 123)).toEqual(["avg", 123]);
-            expect(AggregationClause.setField(["sum", null], 123)).toEqual(["sum", 123]);
-        });
-
-        it("should return unmodified on metric clauses", () => {
-            expect(AggregationClause.setField(["METRIC", 123], 456)).toEqual(["METRIC", 123]);
-        });
+    it("should be able to create a native type query", () => {
+      expect(createQuery("native")).toEqual({
+        database: null,
+        type: "native",
+        native: {
+          query: "",
+        },
+      });
+    });
+
+    it("should populate the databaseId if specified", () => {
+      expect(createQuery("query", 123).database).toEqual(123);
+    });
+
+    it("should populate the tableId if specified", () => {
+      expect(createQuery("query", 123, 456).query.source_table).toEqual(456);
+    });
+
+    it("should NOT set the tableId if query type is native", () => {
+      expect(createQuery("native", 123, 456).query).toEqual(undefined);
+    });
+
+    it("should NOT populate the tableId if no database specified", () => {
+      expect(createQuery("query", null, 456).query.source_table).toEqual(null);
+    });
+  });
+
+  describe("cleanQuery", () => {
+    it("should pass for a query created with metabase-lib", () => {
+      const datasetQuery = question
+        .query()
+        .addAggregation(["count"])
+        .datasetQuery();
+
+      // We have to take a copy because the original object isn't extensible
+      const copiedDatasetQuery = Utils.copy(datasetQuery);
+      Query.cleanQuery(copiedDatasetQuery);
+
+      expect(copiedDatasetQuery).toBeDefined();
+    });
+    it("should not remove complete sort clauses", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["rows"],
+        breakout: [],
+        filter: [],
+        order_by: [[1, "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([[1, "ascending"]]);
+    });
+    it("should remove incomplete sort clauses", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["rows"],
+        breakout: [],
+        filter: [],
+        order_by: [[null, "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual(undefined);
+    });
+
+    it("should not remove sort clauses on aggregations if that aggregation supports it", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [1],
+        filter: [],
+        order_by: [[["aggregation", 0], "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([[["aggregation", 0], "ascending"]]);
+    });
+    it("should remove sort clauses on aggregations if that aggregation doesn't support it", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["rows"],
+        breakout: [],
+        filter: [],
+        order_by: [[["aggregation", 0], "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual(undefined);
+    });
+
+    it("should not remove sort clauses on fields appearing in breakout", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [1],
+        filter: [],
+        order_by: [[1, "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([[1, "ascending"]]);
+    });
+    it("should remove sort clauses on fields not appearing in breakout", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [],
+        filter: [],
+        order_by: [[1, "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual(undefined);
+    });
+
+    it("should not remove sort clauses with foreign keys on fields appearing in breakout", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["fk->", 1, 2]],
+        filter: [],
+        order_by: [[["fk->", 1, 2], "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([[["fk->", 1, 2], "ascending"]]);
+    });
+
+    it("should not remove sort clauses with datetime-fields on fields appearing in breakout", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["datetime-field", 1, "as", "week"]],
+        filter: [],
+        order_by: [[["datetime-field", 1, "as", "week"], "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([
+        [["datetime-field", 1, "as", "week"], "ascending"],
+      ]);
+    });
+
+    it("should replace order_by clauses with the exact matching datetime-fields version in the breakout", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["datetime-field", 1, "as", "week"]],
+        filter: [],
+        order_by: [[1, "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([
+        [["datetime-field", 1, "as", "week"], "ascending"],
+      ]);
+    });
+
+    it("should replace order_by clauses with the exact matching fk-> version in the breakout", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["fk->", 1, 2]],
+        filter: [],
+        order_by: [[2, "ascending"]],
+      };
+      Query.cleanQuery(query);
+      expect(query.order_by).toEqual([[["fk->", 1, 2], "ascending"]]);
+    });
+  });
+
+  describe("removeBreakout", () => {
+    it("should not mutate the query", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["field-id", 1]],
+        filter: [],
+      };
+      Query.removeBreakout(query, 0);
+      expect(query.breakout).toEqual([["field-id", 1]]);
+    });
+    it("should remove the dimension", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["field-id", 1]],
+        filter: [],
+      };
+      query = Query.removeBreakout(query, 0);
+      expect(query.breakout).toEqual(undefined);
+    });
+    it("should remove sort clauses for the dimension that was removed", () => {
+      let query = {
+        source_table: 0,
+        aggregation: ["count"],
+        breakout: [["field-id", 1]],
+        filter: [],
+        order_by: [[1, "ascending"]],
+      };
+      query = Query.removeBreakout(query, 0);
+      expect(query.order_by).toEqual(undefined);
+    });
+  });
+
+  describe("getFieldTarget", () => {
+    let field2 = {
+      display_name: "field2",
+    };
+    let table2 = {
+      display_name: "table2",
+      fields_lookup: {
+        2: field2,
+      },
+    };
+    let field1 = {
+      display_name: "field1",
+      target: {
+        table: table2,
+      },
+    };
+    let table1 = {
+      display_name: "table1",
+      fields_lookup: {
+        1: field1,
+      },
+    };
+
+    it("should return field object for old-style local field", () => {
+      let target = Query.getFieldTarget(1, table1);
+      expect(target.table).toEqual(table1);
+      expect(target.field).toEqual(field1);
+      expect(target.path).toEqual([]);
+      expect(target.unit).toEqual(undefined);
+    });
+    it("should return field object for new-style local field", () => {
+      let target = Query.getFieldTarget(["field-id", 1], table1);
+      expect(target.table).toEqual(table1);
+      expect(target.field).toEqual(field1);
+      expect(target.path).toEqual([]);
+      expect(target.unit).toEqual(undefined);
+    });
+    it("should return unit object for old-style datetime-field", () => {
+      let target = Query.getFieldTarget(
+        ["datetime-field", 1, "as", "day"],
+        table1,
+      );
+      expect(target.table).toEqual(table1);
+      expect(target.field).toEqual(field1);
+      expect(target.path).toEqual([]);
+      expect(target.unit).toEqual("day");
+    });
+    it("should return unit object for new-style datetime-field", () => {
+      let target = Query.getFieldTarget(
+        ["datetime-field", 1, "as", "day"],
+        table1,
+      );
+      expect(target.table).toEqual(table1);
+      expect(target.field).toEqual(field1);
+      expect(target.path).toEqual([]);
+      expect(target.unit).toEqual("day");
+    });
+
+    it("should return field object and table for fk field", () => {
+      let target = Query.getFieldTarget(["fk->", 1, 2], table1);
+      expect(target.table).toEqual(table2);
+      expect(target.field).toEqual(field2);
+      expect(target.path).toEqual([field1]);
+      expect(target.unit).toEqual(undefined);
     });
+
+    it("should return field object and table and unit for fk + datetime field", () => {
+      let target = Query.getFieldTarget(
+        ["datetime-field", ["fk->", 1, 2], "day"],
+        table1,
+      );
+      expect(target.table).toEqual(table2);
+      expect(target.field).toEqual(field2);
+      expect(target.path).toEqual([field1]);
+      expect(target.unit).toEqual("day");
+    });
+
+    it("should return field object and table for expression", () => {
+      let target = Query.getFieldTarget(["expression", "foo"], table1);
+      expect(target.table).toEqual(table1);
+      expect(target.field.display_name).toEqual("foo");
+      expect(target.path).toEqual([]);
+      expect(target.unit).toEqual(undefined);
+    });
+  });
+});
+
+describe("generateQueryDescription", () => {
+  it("should work with multiple aggregations", () => {
+    expect(
+      Query.generateQueryDescription(mockTableMetadata, {
+        source_table: 1,
+        aggregation: [["count"], ["sum", ["field-id", 1]]],
+      }),
+    ).toEqual("Orders, Count and Sum of Total");
+  });
+  it("should work with named aggregations", () => {
+    expect(
+      Query.generateQueryDescription(mockTableMetadata, {
+        source_table: 1,
+        aggregation: [["named", ["sum", ["field-id", 1]], "Revenue"]],
+      }),
+    ).toEqual("Orders, Revenue");
+  });
 });
 
+describe("AggregationClause", () => {
+  describe("isValid", () => {
+    it("should fail on bad clauses", () => {
+      expect(AggregationClause.isValid(undefined)).toEqual(false);
+      expect(AggregationClause.isValid(null)).toEqual(false);
+      expect(AggregationClause.isValid([])).toEqual(false);
+      expect(AggregationClause.isValid([null])).toEqual(false);
+      expect(AggregationClause.isValid("ab")).toEqual(false);
+      expect(AggregationClause.isValid(["foo", null])).toEqual(false);
+      expect(AggregationClause.isValid(["a", "b", "c"])).toEqual(false);
+    });
+
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.isValid(["METRIC", 123])).toEqual(true);
+      expect(AggregationClause.isValid(["rows"])).toEqual(true);
+      expect(AggregationClause.isValid(["sum", 456])).toEqual(true);
+    });
+  });
+
+  describe("isBareRows", () => {
+    it("should fail on bad clauses", () => {
+      expect(AggregationClause.isBareRows(undefined)).toEqual(false);
+      expect(AggregationClause.isBareRows(null)).toEqual(false);
+      expect(AggregationClause.isBareRows([])).toEqual(false);
+      expect(AggregationClause.isBareRows([null])).toEqual(false);
+      expect(AggregationClause.isBareRows("ab")).toEqual(false);
+      expect(AggregationClause.isBareRows(["foo", null])).toEqual(false);
+      expect(AggregationClause.isBareRows(["a", "b", "c"])).toEqual(false);
+      expect(AggregationClause.isBareRows(["METRIC", 123])).toEqual(false);
+      expect(AggregationClause.isBareRows(["sum", 456])).toEqual(false);
+    });
+
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.isBareRows(["rows"])).toEqual(true);
+    });
+  });
+
+  describe("isStandard", () => {
+    it("should fail on bad clauses", () => {
+      expect(AggregationClause.isStandard(undefined)).toEqual(false);
+      expect(AggregationClause.isStandard(null)).toEqual(false);
+      expect(AggregationClause.isStandard([])).toEqual(false);
+      expect(AggregationClause.isStandard([null])).toEqual(false);
+      expect(AggregationClause.isStandard("ab")).toEqual(false);
+      expect(AggregationClause.isStandard(["foo", null])).toEqual(false);
+      expect(AggregationClause.isStandard(["a", "b", "c"])).toEqual(false);
+      expect(AggregationClause.isStandard(["METRIC", 123])).toEqual(false);
+    });
+
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.isStandard(["rows"])).toEqual(true);
+      expect(AggregationClause.isStandard(["sum", 456])).toEqual(true);
+    });
+  });
+
+  describe("isMetric", () => {
+    it("should fail on bad clauses", () => {
+      expect(AggregationClause.isMetric(undefined)).toEqual(false);
+      expect(AggregationClause.isMetric(null)).toEqual(false);
+      expect(AggregationClause.isMetric([])).toEqual(false);
+      expect(AggregationClause.isMetric([null])).toEqual(false);
+      expect(AggregationClause.isMetric("ab")).toEqual(false);
+      expect(AggregationClause.isMetric(["foo", null])).toEqual(false);
+      expect(AggregationClause.isMetric(["a", "b", "c"])).toEqual(false);
+      expect(AggregationClause.isMetric(["rows"])).toEqual(false);
+      expect(AggregationClause.isMetric(["sum", 456])).toEqual(false);
+    });
+
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.isMetric(["METRIC", 123])).toEqual(true);
+    });
+  });
+
+  describe("getMetric", () => {
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.getMetric(["METRIC", 123])).toEqual(123);
+    });
+
+    it("should be null on non-metric clauses", () => {
+      expect(AggregationClause.getMetric(["sum", 123])).toEqual(null);
+    });
+  });
+
+  describe("getOperator", () => {
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.getOperator(["rows"])).toEqual("rows");
+      expect(AggregationClause.getOperator(["sum", 123])).toEqual("sum");
+    });
 
-describe('BreakoutClause', () => {
+    it("should be null on metric clauses", () => {
+      expect(AggregationClause.getOperator(["METRIC", 123])).toEqual(null);
+    });
+  });
 
-    describe('setBreakout', () => {
-        it("should append if index is greater than current breakouts", () => {
-            expect(BreakoutClause.setBreakout([], 0, 123)).toEqual([123]);
-            expect(BreakoutClause.setBreakout([123], 1, 456)).toEqual([123, 456]);
-            expect(BreakoutClause.setBreakout([123], 5, 456)).toEqual([123, 456]);
-        });
+  describe("getField", () => {
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.getField(["sum", 123])).toEqual(123);
+    });
 
-        it("should replace if index already exists", () => {
-            expect(BreakoutClause.setBreakout([123], 0, 456)).toEqual([456]);
-        });
+    it("should be null on clauses w/out a field", () => {
+      expect(AggregationClause.getField(["rows"])).toEqual(null);
     });
 
-    describe('removeBreakout', () => {
-        it("should remove breakout if index exists", () => {
-            expect(BreakoutClause.removeBreakout([123], 0)).toEqual([]);
-            expect(BreakoutClause.removeBreakout([123, 456], 1)).toEqual([123]);
-        });
+    it("should be null on metric clauses", () => {
+      expect(AggregationClause.getField(["METRIC", 123])).toEqual(null);
+    });
+  });
+
+  describe("setField", () => {
+    it("should succeed on good clauses", () => {
+      expect(AggregationClause.setField(["avg"], 123)).toEqual(["avg", 123]);
+      expect(AggregationClause.setField(["sum", null], 123)).toEqual([
+        "sum",
+        123,
+      ]);
+    });
+
+    it("should return unmodified on metric clauses", () => {
+      expect(AggregationClause.setField(["METRIC", 123], 456)).toEqual([
+        "METRIC",
+        123,
+      ]);
+    });
+  });
+});
+
+describe("BreakoutClause", () => {
+  describe("setBreakout", () => {
+    it("should append if index is greater than current breakouts", () => {
+      expect(BreakoutClause.setBreakout([], 0, 123)).toEqual([123]);
+      expect(BreakoutClause.setBreakout([123], 1, 456)).toEqual([123, 456]);
+      expect(BreakoutClause.setBreakout([123], 5, 456)).toEqual([123, 456]);
+    });
+
+    it("should replace if index already exists", () => {
+      expect(BreakoutClause.setBreakout([123], 0, 456)).toEqual([456]);
+    });
+  });
+
+  describe("removeBreakout", () => {
+    it("should remove breakout if index exists", () => {
+      expect(BreakoutClause.removeBreakout([123], 0)).toEqual([]);
+      expect(BreakoutClause.removeBreakout([123, 456], 1)).toEqual([123]);
+    });
 
-        it("should make no changes if index does not exist", () => {
-            expect(BreakoutClause.removeBreakout([123], 1)).toEqual([123]);
-        });
+    it("should make no changes if index does not exist", () => {
+      expect(BreakoutClause.removeBreakout([123], 1)).toEqual([123]);
     });
+  });
 });
diff --git a/frontend/test/lib/query/query.unit.spec.js b/frontend/test/lib/query/query.unit.spec.js
index 9d24ce6ad1b42ca88ba970694fe430f52199f177..034c024fc8be1681cc5454a41de1b3e4f6e05b65 100644
--- a/frontend/test/lib/query/query.unit.spec.js
+++ b/frontend/test/lib/query/query.unit.spec.js
@@ -1,73 +1,120 @@
 import * as Query from "metabase/lib/query/query";
 
 describe("Query", () => {
-    describe("isBareRowsAggregation", () => {
-        it("should return true for all bare rows variation", () => {
-            expect(Query.isBareRows({})).toBe(true);
-            expect(Query.isBareRows({ "aggregation": null })).toBe(true);  // deprecated
-            expect(Query.isBareRows({ "aggregation": ["rows"] })).toBe(true); // deprecated
-            expect(Query.isBareRows({ "aggregation": [] })).toBe(true); // deprecated
-            expect(Query.isBareRows({ "aggregation": [["rows"]] })).toBe(true); // deprecated
-        })
-        it("should return false for other aggregations", () => {
-            expect(Query.isBareRows({ "aggregation": [["count"]] })).toBe(false);
-            expect(Query.isBareRows({ "aggregation": ["count"] })).toBe(false); // deprecated
-        })
-    })
-    describe("getAggregations", () => {
-        it("should return an empty list for bare rows", () => {
-            expect(Query.getAggregations({})).toEqual([]);
-            expect(Query.getAggregations({ "aggregation": [["rows"]] })).toEqual([]);
-            expect(Query.getAggregations({ "aggregation": ["rows"] })).toEqual([]); // deprecated
-        })
-        it("should return a single aggregation", () => {
-            expect(Query.getAggregations({ "aggregation": [["count"]] })).toEqual([["count"]]);
-            expect(Query.getAggregations({ "aggregation": ["count"] })).toEqual([["count"]]); // deprecated
-        })
-        it("should return multiple aggregations", () => {
-            expect(Query.getAggregations({ "aggregation": [["count"], ["sum", 1]] })).toEqual([["count"], ["sum", 1]]);
-        })
-    })
-    describe("addAggregation", () => {
-        it("should add one aggregation", () => {
-            expect(Query.addAggregation({}, ["count"])).toEqual({ aggregation: [["count"]] });
-        })
-        it("should add an aggregation to an existing one", () => {
-            expect(Query.addAggregation({ aggregation: [["count"]] }, ["sum", 1])).toEqual({ aggregation: [["count"], ["sum", 1]] });
-            // legacy
-            expect(Query.addAggregation({ aggregation: [["count"]] }, ["sum", 1])).toEqual({ aggregation: [["count"], ["sum", 1]] });
-        })
-    })
-    describe("updateAggregation", () => {
-        it("should update the correct aggregation", () => {
-            expect(Query.updateAggregation({ aggregation: [["count"], ["sum", 1]] }, 1, ["sum", 2])).toEqual({ aggregation: [["count"], ["sum", 2]] });
-        })
-    })
-    describe("removeAggregation", () => {
-        it("should remove one of two aggregations", () => {
-            expect(Query.removeAggregation({ aggregation: [["count"], ["sum", 1]] }, 0)).toEqual({ aggregation: [["sum", 1]] });
-        })
-        it("should remove the last aggregations", () => {
-            expect(Query.removeAggregation({ aggregation: [["count"]] }, 0)).toEqual({});
-            expect(Query.removeAggregation({ aggregation: ["count"] }, 0)).toEqual({}); // deprecated
-        })
-    })
-    describe("clearAggregations", () => {
-        it("should remove all aggregations", () => {
-            expect(Query.clearAggregations({ aggregation: [["count"]] })).toEqual({});
-            expect(Query.clearAggregations({ aggregation: [["count"], ["sum", 1]] })).toEqual({});
-            expect(Query.clearAggregations({ aggregation: ["count"] })).toEqual({}); // deprecated
-        })
-    })
+  describe("isBareRowsAggregation", () => {
+    it("should return true for all bare rows variation", () => {
+      expect(Query.isBareRows({})).toBe(true);
+      expect(Query.isBareRows({ aggregation: null })).toBe(true); // deprecated
+      expect(Query.isBareRows({ aggregation: ["rows"] })).toBe(true); // deprecated
+      expect(Query.isBareRows({ aggregation: [] })).toBe(true); // deprecated
+      expect(Query.isBareRows({ aggregation: [["rows"]] })).toBe(true); // deprecated
+    });
+    it("should return false for other aggregations", () => {
+      expect(Query.isBareRows({ aggregation: [["count"]] })).toBe(false);
+      expect(Query.isBareRows({ aggregation: ["count"] })).toBe(false); // deprecated
+    });
+  });
+  describe("getAggregations", () => {
+    it("should return an empty list for bare rows", () => {
+      expect(Query.getAggregations({})).toEqual([]);
+      expect(Query.getAggregations({ aggregation: [["rows"]] })).toEqual([]);
+      expect(Query.getAggregations({ aggregation: ["rows"] })).toEqual([]); // deprecated
+    });
+    it("should return a single aggregation", () => {
+      expect(Query.getAggregations({ aggregation: [["count"]] })).toEqual([
+        ["count"],
+      ]);
+      expect(Query.getAggregations({ aggregation: ["count"] })).toEqual([
+        ["count"],
+      ]); // deprecated
+    });
+    it("should return multiple aggregations", () => {
+      expect(
+        Query.getAggregations({ aggregation: [["count"], ["sum", 1]] }),
+      ).toEqual([["count"], ["sum", 1]]);
+    });
+  });
+  describe("addAggregation", () => {
+    it("should add one aggregation", () => {
+      expect(Query.addAggregation({}, ["count"])).toEqual({
+        aggregation: [["count"]],
+      });
+    });
+    it("should add an aggregation to an existing one", () => {
+      expect(
+        Query.addAggregation({ aggregation: [["count"]] }, ["sum", 1]),
+      ).toEqual({ aggregation: [["count"], ["sum", 1]] });
+      // legacy
+      expect(
+        Query.addAggregation({ aggregation: [["count"]] }, ["sum", 1]),
+      ).toEqual({ aggregation: [["count"], ["sum", 1]] });
+    });
+  });
+  describe("updateAggregation", () => {
+    it("should update the correct aggregation", () => {
+      expect(
+        Query.updateAggregation({ aggregation: [["count"], ["sum", 1]] }, 1, [
+          "sum",
+          2,
+        ]),
+      ).toEqual({ aggregation: [["count"], ["sum", 2]] });
+    });
+  });
+  describe("removeAggregation", () => {
+    it("should remove one of two aggregations", () => {
+      expect(
+        Query.removeAggregation({ aggregation: [["count"], ["sum", 1]] }, 0),
+      ).toEqual({ aggregation: [["sum", 1]] });
+    });
+    it("should remove the last aggregations", () => {
+      expect(Query.removeAggregation({ aggregation: [["count"]] }, 0)).toEqual(
+        {},
+      );
+      expect(Query.removeAggregation({ aggregation: ["count"] }, 0)).toEqual(
+        {},
+      ); // deprecated
+    });
+  });
+  describe("clearAggregations", () => {
+    it("should remove all aggregations", () => {
+      expect(Query.clearAggregations({ aggregation: [["count"]] })).toEqual({});
+      expect(
+        Query.clearAggregations({ aggregation: [["count"], ["sum", 1]] }),
+      ).toEqual({});
+      expect(Query.clearAggregations({ aggregation: ["count"] })).toEqual({}); // deprecated
+    });
+  });
 
-    describe("removeBreakout", () => {
-        it("should remove sort as well", () => {
-            expect(Query.removeBreakout({ breakout: [1], order_by: [[1, "ascending"]] }, 0)).toEqual({});
-            expect(Query.removeBreakout({ breakout: [2,1], order_by: [[1, "ascending"]] }, 0)).toEqual({ breakout: [1], order_by: [[1, "ascending"]] });
-        })
-        it("should not remove aggregation sorts", () => {
-            expect(Query.removeBreakout({ aggregation: [["count"]], breakout: [2,1], order_by: [[["aggregation", 0], "ascending"]] }, 0))
-                               .toEqual({ aggregation: [["count"]], breakout: [1], order_by: [[["aggregation", 0], "ascending"]] });
-        })
-    })
-})
+  describe("removeBreakout", () => {
+    it("should remove sort as well", () => {
+      expect(
+        Query.removeBreakout(
+          { breakout: [1], order_by: [[1, "ascending"]] },
+          0,
+        ),
+      ).toEqual({});
+      expect(
+        Query.removeBreakout(
+          { breakout: [2, 1], order_by: [[1, "ascending"]] },
+          0,
+        ),
+      ).toEqual({ breakout: [1], order_by: [[1, "ascending"]] });
+    });
+    it("should not remove aggregation sorts", () => {
+      expect(
+        Query.removeBreakout(
+          {
+            aggregation: [["count"]],
+            breakout: [2, 1],
+            order_by: [[["aggregation", 0], "ascending"]],
+          },
+          0,
+        ),
+      ).toEqual({
+        aggregation: [["count"]],
+        breakout: [1],
+        order_by: [[["aggregation", 0], "ascending"]],
+      });
+    });
+  });
+});
diff --git a/frontend/test/lib/query_time.unit.spec.js b/frontend/test/lib/query_time.unit.spec.js
index 40fa16132ab9e00f7415aa42f2685b4089557b29..c18490fd059c005fce08b857bf37b3224313c981 100644
--- a/frontend/test/lib/query_time.unit.spec.js
+++ b/frontend/test/lib/query_time.unit.spec.js
@@ -1,154 +1,302 @@
 import moment from "moment";
 
-import { parseFieldBucketing, expandTimeIntervalFilter, computeFilterTimeRange, absolute, generateTimeFilterValuesDescriptions } from 'metabase/lib/query_time';
+import {
+  parseFieldBucketing,
+  expandTimeIntervalFilter,
+  computeFilterTimeRange,
+  absolute,
+  generateTimeFilterValuesDescriptions,
+} from "metabase/lib/query_time";
 
-describe('query_time', () => {
-
-    describe("parseFieldBucketing()", () => {
-        it("supports the standard DatetimeField format", () => {
-            expect(parseFieldBucketing(["datetime-field", ["field-id", 3], "week"])).toBe("week");
-            expect(parseFieldBucketing(["datetime-field", ["field-id", 3], "day"])).toBe("day");
-        })
+describe("query_time", () => {
+  describe("parseFieldBucketing()", () => {
+    it("supports the standard DatetimeField format", () => {
+      expect(
+        parseFieldBucketing(["datetime-field", ["field-id", 3], "week"]),
+      ).toBe("week");
+      expect(
+        parseFieldBucketing(["datetime-field", ["field-id", 3], "day"]),
+      ).toBe("day");
+    });
 
-        it("supports the legacy DatetimeField format", () => {
-            expect(parseFieldBucketing(["datetime-field", ["field-id", 3], "as", "week"])).toBe("week");
-            expect(parseFieldBucketing(["datetime-field", ["field-id", 3], "day"])).toBe("day");
-        })
-        it("returns the default unit for FK reference", () => {
-            pending();
-        })
-        it("returns the default unit for local field reference", () => {
-            pending();
-        })
-        it("returns the default unit for other field types", () => {
-            pending();
-        })
-    })
+    it("supports the legacy DatetimeField format", () => {
+      expect(
+        parseFieldBucketing(["datetime-field", ["field-id", 3], "as", "week"]),
+      ).toBe("week");
+      expect(
+        parseFieldBucketing(["datetime-field", ["field-id", 3], "day"]),
+      ).toBe("day");
+    });
+    it("returns the default unit for FK reference", () => {
+      pending();
+    });
+    it("returns the default unit for local field reference", () => {
+      pending();
+    });
+    it("returns the default unit for other field types", () => {
+      pending();
+    });
+  });
 
-    describe('expandTimeIntervalFilter', () => {
-        it('translate ["current" "month"] correctly', () => {
-            expect(
-                expandTimeIntervalFilter(["time-interval", 100, "current", "month"])
-            ).toEqual(
-                ["=", ["datetime-field", 100, "as", "month"], ["relative-datetime", "current"]]
-            );
-        });
-        it('translate [-30, "day"] correctly', () => {
-            expect(
-                expandTimeIntervalFilter(["time-interval", 100, -30, "day"])
-            ).toEqual(
-                ["BETWEEN", ["datetime-field", 100, "as", "day"], ["relative-datetime", -31, "day"], ["relative-datetime", -1, "day"]]
-            );
-        });
+  describe("expandTimeIntervalFilter", () => {
+    it('translate ["current" "month"] correctly', () => {
+      expect(
+        expandTimeIntervalFilter(["time-interval", 100, "current", "month"]),
+      ).toEqual([
+        "=",
+        ["datetime-field", 100, "as", "month"],
+        ["relative-datetime", "current"],
+      ]);
+    });
+    it('translate [-30, "day"] correctly', () => {
+      expect(
+        expandTimeIntervalFilter(["time-interval", 100, -30, "day"]),
+      ).toEqual([
+        "BETWEEN",
+        ["datetime-field", 100, "as", "day"],
+        ["relative-datetime", -31, "day"],
+        ["relative-datetime", -1, "day"],
+      ]);
     });
+  });
 
-    describe('absolute', () => {
-        it ('should pass through absolute dates', () => {
-            expect(
-                absolute("2009-08-07T06:05:04Z").format("YYYY-MM-DD HH:mm:ss")
-            ).toBe(
-                moment("2009-08-07 06:05:04Z").format("YYYY-MM-DD HH:mm:ss")
-            );
-        });
+  describe("absolute", () => {
+    it("should pass through absolute dates", () => {
+      expect(
+        absolute("2009-08-07T06:05:04Z").format("YYYY-MM-DD HH:mm:ss"),
+      ).toBe(moment("2009-08-07 06:05:04Z").format("YYYY-MM-DD HH:mm:ss"));
+    });
 
-        it ('should convert relative-datetime "current"', () => {
-            expect(
-                absolute(["relative-datetime", "current"]).format("YYYY-MM-DD HH")
-            ).toBe(
-                moment().format("YYYY-MM-DD HH")
-            );
-        });
+    it('should convert relative-datetime "current"', () => {
+      expect(
+        absolute(["relative-datetime", "current"]).format("YYYY-MM-DD HH"),
+      ).toBe(moment().format("YYYY-MM-DD HH"));
+    });
 
-        it ('should convert relative-datetime -1 "month"', () => {
-            expect(
-                absolute(["relative-datetime", -1, "month"]).format("YYYY-MM-DD HH")
-            ).toBe(
-                moment().subtract(1, "month").format("YYYY-MM-DD HH")
-            );
-        });
+    it('should convert relative-datetime -1 "month"', () => {
+      expect(
+        absolute(["relative-datetime", -1, "month"]).format("YYYY-MM-DD HH"),
+      ).toBe(
+        moment()
+          .subtract(1, "month")
+          .format("YYYY-MM-DD HH"),
+      );
     });
+  });
 
-    describe("generateTimeFilterValuesDescriptions", () => {
-        it ("should format simple operator values correctly", () => {
-            expect(generateTimeFilterValuesDescriptions(["<", null, "2016-01-01"])).toEqual(["January 1, 2016"])
-        })
-        it ("should format 'time-interval' correctly", () => {
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, -30, "day"])).toEqual(["Past 30 Days"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, 1, "month"])).toEqual(["Next 1 Month"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, 2, "month"])).toEqual(["Next 2 Months"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, 0, "month"])).toEqual(["This Month"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, -1, "month"])).toEqual(["Past 1 Month"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, -2, "month"])).toEqual(["Past 2 Months"])
-        });
-        it ("should format 'time-interval' short names correctly", () => {
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, -1, "day"])).toEqual(["Yesterday"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, 0, "day"])).toEqual(["Today"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, "current", "day"])).toEqual(["Today"])
-            expect(generateTimeFilterValuesDescriptions(["time-interval", null, 1, "day"])).toEqual(["Tomorrow"])
-        });
-        it ("should format legacy 'TIME_INTERVAL' correctly", () => {
-            expect(generateTimeFilterValuesDescriptions(["TIME_INTERVAL", null, -30, "day"])).toEqual(["Past 30 Days"])
-        });
+  describe("generateTimeFilterValuesDescriptions", () => {
+    it("should format simple operator values correctly", () => {
+      expect(
+        generateTimeFilterValuesDescriptions(["<", null, "2016-01-01"]),
+      ).toEqual(["January 1, 2016"]);
+    });
+    it("should format 'time-interval' correctly", () => {
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          -30,
+          "day",
+        ]),
+      ).toEqual(["Past 30 Days"]);
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          1,
+          "month",
+        ]),
+      ).toEqual(["Next 1 Month"]);
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          2,
+          "month",
+        ]),
+      ).toEqual(["Next 2 Months"]);
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          0,
+          "month",
+        ]),
+      ).toEqual(["This Month"]);
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          -1,
+          "month",
+        ]),
+      ).toEqual(["Past 1 Month"]);
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          -2,
+          "month",
+        ]),
+      ).toEqual(["Past 2 Months"]);
+    });
+    it("should format 'time-interval' short names correctly", () => {
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          -1,
+          "day",
+        ]),
+      ).toEqual(["Yesterday"]);
+      expect(
+        generateTimeFilterValuesDescriptions(["time-interval", null, 0, "day"]),
+      ).toEqual(["Today"]);
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "time-interval",
+          null,
+          "current",
+          "day",
+        ]),
+      ).toEqual(["Today"]);
+      expect(
+        generateTimeFilterValuesDescriptions(["time-interval", null, 1, "day"]),
+      ).toEqual(["Tomorrow"]);
     });
+    it("should format legacy 'TIME_INTERVAL' correctly", () => {
+      expect(
+        generateTimeFilterValuesDescriptions([
+          "TIME_INTERVAL",
+          null,
+          -30,
+          "day",
+        ]),
+      ).toEqual(["Past 30 Days"]);
+    });
+  });
 
-    describe('computeFilterTimeRange', () => {
-        describe('absolute dates', () => {
-            it ('should handle "="', () => {
-                let [start, end] = computeFilterTimeRange(["=", 1, "2009-08-07"]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual("2009-08-07 00:00:00");
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual("2009-08-07 23:59:59");
-            });
-            it ('should handle "<"', () => {
-                let [start, end] = computeFilterTimeRange(["<", 1, "2009-08-07"]);
-                expect(start.year()).toBeLessThan(-10000);
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual("2009-08-07 00:00:00");
-            });
-            it ('should handle ">"', () => {
-                let [start, end] = computeFilterTimeRange([">", 1, "2009-08-07"]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual("2009-08-07 23:59:59");
-                expect(end.year()).toBeGreaterThan(10000);
-            });
-            it ('should handle "BETWEEN"', () => {
-                let [start, end] = computeFilterTimeRange(["BETWEEN", 1, "2009-08-07", "2009-08-09"]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual("2009-08-07 00:00:00");
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual("2009-08-09 23:59:59");
-            });
-        })
+  describe("computeFilterTimeRange", () => {
+    describe("absolute dates", () => {
+      it('should handle "="', () => {
+        let [start, end] = computeFilterTimeRange(["=", 1, "2009-08-07"]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          "2009-08-07 00:00:00",
+        );
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          "2009-08-07 23:59:59",
+        );
+      });
+      it('should handle "<"', () => {
+        let [start, end] = computeFilterTimeRange(["<", 1, "2009-08-07"]);
+        expect(start.year()).toBeLessThan(-10000);
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          "2009-08-07 00:00:00",
+        );
+      });
+      it('should handle ">"', () => {
+        let [start, end] = computeFilterTimeRange([">", 1, "2009-08-07"]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          "2009-08-07 23:59:59",
+        );
+        expect(end.year()).toBeGreaterThan(10000);
+      });
+      it('should handle "BETWEEN"', () => {
+        let [start, end] = computeFilterTimeRange([
+          "BETWEEN",
+          1,
+          "2009-08-07",
+          "2009-08-09",
+        ]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          "2009-08-07 00:00:00",
+        );
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          "2009-08-09 23:59:59",
+        );
+      });
+    });
 
-        describe('relative dates', () => {
-            it ('should handle "="', () => {
-                let [start, end] = computeFilterTimeRange(["=", 1, ["relative-datetime", "current"]]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().format("YYYY-MM-DD 00:00:00"));
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().format("YYYY-MM-DD 23:59:59"));
-            });
-            it ('should handle "<"', () => {
-                let [start, end] = computeFilterTimeRange(["<", 1, ["relative-datetime", "current"]]);
-                expect(start.year()).toBeLessThan(-10000);
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().format("YYYY-MM-DD 00:00:00"));
-            });
-            it ('should handle ">"', () => {
-                let [start, end] = computeFilterTimeRange([">", 1, ["relative-datetime", "current"]]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().format("YYYY-MM-DD 23:59:59"));
-                expect(end.year()).toBeGreaterThan(10000);
-            });
-            it ('should handle "BETWEEN"', () => {
-                let [start, end] = computeFilterTimeRange(["BETWEEN", 1, ["relative-datetime", -1, "day"], ["relative-datetime", 1, "day"]]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(1, "day").format("YYYY-MM-DD 00:00:00"));
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().add(1, "day").format("YYYY-MM-DD 23:59:59"));
-            });
-        });
+    describe("relative dates", () => {
+      it('should handle "="', () => {
+        let [start, end] = computeFilterTimeRange([
+          "=",
+          1,
+          ["relative-datetime", "current"],
+        ]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment().format("YYYY-MM-DD 00:00:00"),
+        );
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment().format("YYYY-MM-DD 23:59:59"),
+        );
+      });
+      it('should handle "<"', () => {
+        let [start, end] = computeFilterTimeRange([
+          "<",
+          1,
+          ["relative-datetime", "current"],
+        ]);
+        expect(start.year()).toBeLessThan(-10000);
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment().format("YYYY-MM-DD 00:00:00"),
+        );
+      });
+      it('should handle ">"', () => {
+        let [start, end] = computeFilterTimeRange([
+          ">",
+          1,
+          ["relative-datetime", "current"],
+        ]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment().format("YYYY-MM-DD 23:59:59"),
+        );
+        expect(end.year()).toBeGreaterThan(10000);
+      });
+      it('should handle "BETWEEN"', () => {
+        let [start, end] = computeFilterTimeRange([
+          "BETWEEN",
+          1,
+          ["relative-datetime", -1, "day"],
+          ["relative-datetime", 1, "day"],
+        ]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment()
+            .subtract(1, "day")
+            .format("YYYY-MM-DD 00:00:00"),
+        );
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment()
+            .add(1, "day")
+            .format("YYYY-MM-DD 23:59:59"),
+        );
+      });
+    });
 
-        describe('time-interval', () => {
-            it ('should handle "Past x days"', () => {
-                let [start, end] = computeFilterTimeRange(["time-interval", 1, -7, "day"]);
-                expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(8, "day").format("YYYY-MM-DD 00:00:00"));
-                expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(1, "day").format("YYYY-MM-DD 23:59:59"));
-            });
-            // it ('should handle "last week"', () => {
-            //     let [start, end] = computeFilterTimeRange(["time-interval", 1, "last", "week"]);
-            //     expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(1, "week").startOf("week").format("YYYY-MM-DD 00:00:00"));
-            //     expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(1, "week").endOf("week")..format("YYYY-MM-DD 23:59:59"));
-            // });
-        });
+    describe("time-interval", () => {
+      it('should handle "Past x days"', () => {
+        let [start, end] = computeFilterTimeRange([
+          "time-interval",
+          1,
+          -7,
+          "day",
+        ]);
+        expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment()
+            .subtract(8, "day")
+            .format("YYYY-MM-DD 00:00:00"),
+        );
+        expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(
+          moment()
+            .subtract(1, "day")
+            .format("YYYY-MM-DD 23:59:59"),
+        );
+      });
+      // it ('should handle "last week"', () => {
+      //     let [start, end] = computeFilterTimeRange(["time-interval", 1, "last", "week"]);
+      //     expect(start.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(1, "week").startOf("week").format("YYYY-MM-DD 00:00:00"));
+      //     expect(end.format("YYYY-MM-DD HH:mm:ss")).toEqual(moment().subtract(1, "week").endOf("week")..format("YYYY-MM-DD 23:59:59"));
+      // });
     });
+  });
 });
diff --git a/frontend/test/lib/redux.unit.spec.js b/frontend/test/lib/redux.unit.spec.js
index df8cb0cfc85e5d287c573f089af7d564ec19b4bf..e67857d51e5e308013bff10aa5fdf70bf09485ef 100644
--- a/frontend/test/lib/redux.unit.spec.js
+++ b/frontend/test/lib/redux.unit.spec.js
@@ -1,120 +1,146 @@
-import { 
-    fetchData, 
-    updateData 
-} from 'metabase/lib/redux';
+import { fetchData, updateData } from "metabase/lib/redux";
 
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 
 describe("Metadata", () => {
-    const getDefaultArgs = ({
-        existingData = 'data',
-        newData = 'new data',
-        requestState = null,
-        requestStateLoading = { state: 'LOADING' },
-        requestStateLoaded = { state: 'LOADED' },
-        requestStateError = { error: new Error('error') },
-        statePath = ['test', 'path'],
-        statePathFetch = statePath.concat('fetch'),
-        statePathUpdate = statePath.concat('update'),
-        requestStatePath = statePath,
-        existingStatePath = statePath,
-        getState = () => ({
-            requests: { states: { test: { path: { fetch: requestState, update: requestState } } } },
-            test: { path: existingData }
-        }),
-        dispatch = jasmine.createSpy('dispatch'),
-        getData = () => Promise.resolve(newData),
-        putData = () => Promise.resolve(newData)
-    } = {}) => ({dispatch, getState, requestStatePath, existingStatePath, getData, putData,
-        // passthrough args constants
-        existingData,
-        newData,
-        requestState,
-        requestStateLoading,
-        requestStateLoaded,
-        requestStateError,
-        statePath,
-        statePathFetch,
-        statePathUpdate
-    });
+  const getDefaultArgs = ({
+    existingData = "data",
+    newData = "new data",
+    requestState = null,
+    requestStateLoading = { state: "LOADING" },
+    requestStateLoaded = { state: "LOADED" },
+    requestStateError = { error: new Error("error") },
+    statePath = ["test", "path"],
+    statePathFetch = statePath.concat("fetch"),
+    statePathUpdate = statePath.concat("update"),
+    requestStatePath = statePath,
+    existingStatePath = statePath,
+    getState = () => ({
+      requests: {
+        states: {
+          test: { path: { fetch: requestState, update: requestState } },
+        },
+      },
+      test: { path: existingData },
+    }),
+    dispatch = jasmine.createSpy("dispatch"),
+    getData = () => Promise.resolve(newData),
+    putData = () => Promise.resolve(newData),
+  } = {}) => ({
+    dispatch,
+    getState,
+    requestStatePath,
+    existingStatePath,
+    getData,
+    putData,
+    // passthrough args constants
+    existingData,
+    newData,
+    requestState,
+    requestStateLoading,
+    requestStateLoaded,
+    requestStateError,
+    statePath,
+    statePathFetch,
+    statePathUpdate,
+  });
 
-    const args = getDefaultArgs({});
+  const args = getDefaultArgs({});
 
-    describe("fetchData()", () => {
-        it("should return new data if request hasn't been made", async () => {
-            const argsDefault = getDefaultArgs({});
-            const data = await fetchData(argsDefault);
-            await delay(10);
-            expect(argsDefault.dispatch.calls.count()).toEqual(2);
-            expect(data).toEqual(args.newData);
-        });
+  describe("fetchData()", () => {
+    it("should return new data if request hasn't been made", async () => {
+      const argsDefault = getDefaultArgs({});
+      const data = await fetchData(argsDefault);
+      await delay(10);
+      expect(argsDefault.dispatch.calls.count()).toEqual(2);
+      expect(data).toEqual(args.newData);
+    });
 
-        it("should return existing data if request has been made", async () => {
-            const argsLoading = getDefaultArgs({requestState: args.requestStateLoading});
-            const dataLoading = await fetchData(argsLoading);
-            expect(argsLoading.dispatch.calls.count()).toEqual(0);
-            expect(dataLoading).toEqual(args.existingData);
+    it("should return existing data if request has been made", async () => {
+      const argsLoading = getDefaultArgs({
+        requestState: args.requestStateLoading,
+      });
+      const dataLoading = await fetchData(argsLoading);
+      expect(argsLoading.dispatch.calls.count()).toEqual(0);
+      expect(dataLoading).toEqual(args.existingData);
 
-            const argsLoaded = getDefaultArgs({requestState: args.requestStateLoaded});
-            const dataLoaded = await fetchData(argsLoaded);
-            expect(argsLoaded.dispatch.calls.count()).toEqual(0);
-            expect(dataLoaded).toEqual(args.existingData);
-        });
+      const argsLoaded = getDefaultArgs({
+        requestState: args.requestStateLoaded,
+      });
+      const dataLoaded = await fetchData(argsLoaded);
+      expect(argsLoaded.dispatch.calls.count()).toEqual(0);
+      expect(dataLoaded).toEqual(args.existingData);
+    });
 
-        it("should return new data if previous request ended in error", async () => {
-            const argsError = getDefaultArgs({requestState: args.requestStateError});
-            const dataError = await fetchData(argsError);
-            await delay(10);
-            expect(argsError.dispatch.calls.count()).toEqual(2);
-            expect(dataError).toEqual(args.newData);
-        });
+    it("should return new data if previous request ended in error", async () => {
+      const argsError = getDefaultArgs({
+        requestState: args.requestStateError,
+      });
+      const dataError = await fetchData(argsError);
+      await delay(10);
+      expect(argsError.dispatch.calls.count()).toEqual(2);
+      expect(dataError).toEqual(args.newData);
+    });
 
-        // FIXME: this seems to make jasmine ignore the rest of the tests
-        // is an exception bubbling up from fetchData? why?
-        // how else to test return value in the catch case?
-        it("should return existing data if request fails", async () => {
-            const argsFail = getDefaultArgs({getData: () => Promise.reject('error')});
+    // FIXME: this seems to make jasmine ignore the rest of the tests
+    // is an exception bubbling up from fetchData? why?
+    // how else to test return value in the catch case?
+    it("should return existing data if request fails", async () => {
+      const argsFail = getDefaultArgs({
+        getData: () => Promise.reject("error"),
+      });
 
-            try{
-                const dataFail = await fetchData(argsFail).catch((error) => console.log(error));
-                expect(argsFail.dispatch.calls.count()).toEqual(2);
-                expect(dataFail).toEqual(args.existingData);
-            }
-            catch(error) {
-                return;
-            }
-        });
+      try {
+        const dataFail = await fetchData(argsFail).catch(error =>
+          console.log(error),
+        );
+        expect(argsFail.dispatch.calls.count()).toEqual(2);
+        expect(dataFail).toEqual(args.existingData);
+      } catch (error) {
+        return;
+      }
     });
+  });
 
-    describe("updateData()", () => {
-        it("should return new data regardless of previous request state", async () => {
-            const argsDefault = getDefaultArgs({});
-            const data = await updateData(argsDefault);
-            expect(argsDefault.dispatch.calls.count()).toEqual(2);
-            expect(data).toEqual(args.newData);
+  describe("updateData()", () => {
+    it("should return new data regardless of previous request state", async () => {
+      const argsDefault = getDefaultArgs({});
+      const data = await updateData(argsDefault);
+      expect(argsDefault.dispatch.calls.count()).toEqual(2);
+      expect(data).toEqual(args.newData);
 
-            const argsLoading = getDefaultArgs({requestState: args.requestStateLoading});
-            const dataLoading = await updateData(argsLoading);
-            expect(argsLoading.dispatch.calls.count()).toEqual(2);
-            expect(dataLoading).toEqual(args.newData);
+      const argsLoading = getDefaultArgs({
+        requestState: args.requestStateLoading,
+      });
+      const dataLoading = await updateData(argsLoading);
+      expect(argsLoading.dispatch.calls.count()).toEqual(2);
+      expect(dataLoading).toEqual(args.newData);
 
-            const argsLoaded = getDefaultArgs({requestState: args.requestStateLoaded});
-            const dataLoaded = await updateData(argsLoaded);
-            expect(argsLoaded.dispatch.calls.count()).toEqual(2);
-            expect(dataLoaded).toEqual(args.newData);
+      const argsLoaded = getDefaultArgs({
+        requestState: args.requestStateLoaded,
+      });
+      const dataLoaded = await updateData(argsLoaded);
+      expect(argsLoaded.dispatch.calls.count()).toEqual(2);
+      expect(dataLoaded).toEqual(args.newData);
 
-            const argsError = getDefaultArgs({requestState: args.requestStateError});
-            const dataError = await updateData(argsError);
-            expect(argsError.dispatch.calls.count()).toEqual(2);
-            expect(dataError).toEqual(args.newData);
-        });
+      const argsError = getDefaultArgs({
+        requestState: args.requestStateError,
+      });
+      const dataError = await updateData(argsError);
+      expect(argsError.dispatch.calls.count()).toEqual(2);
+      expect(dataError).toEqual(args.newData);
+    });
 
-        it("should return existing data if request fails", async () => {
-            const argsFail = getDefaultArgs({putData: () => {throw new Error('test')}});
-            const data = await updateData(argsFail);
-            await delay(10)
-            expect(argsFail.dispatch.calls.count()).toEqual(2);
-            expect(data).toEqual(args.existingData);
-        });
+    it("should return existing data if request fails", async () => {
+      const argsFail = getDefaultArgs({
+        putData: () => {
+          throw new Error("test");
+        },
+      });
+      const data = await updateData(argsFail);
+      await delay(10);
+      expect(argsFail.dispatch.calls.count()).toEqual(2);
+      expect(data).toEqual(args.existingData);
     });
+  });
 });
diff --git a/frontend/test/lib/request.integ.spec.js b/frontend/test/lib/request.integ.spec.js
index e69db8ea530a57c36be3c7ad6aa3da3ad42adce6..7ebfbcf1d70f070bef06604524f2f5ede5249f19 100644
--- a/frontend/test/lib/request.integ.spec.js
+++ b/frontend/test/lib/request.integ.spec.js
@@ -1,41 +1,46 @@
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 import { BackgroundJobRequest } from "metabase/lib/request";
 
-
 describe("request API", () => {
-    xdescribe("RestfulRequest", () => {
-        // NOTE Atte Keinänen 9/26/17: RestfulRequest doesn't really need unit tests because the xrays integration tests
-        // basically already verify that the basic functionality works as expected, as in
-        // * Are actions executed in an expected order and do they lead to expected state changes
-        // * Does the promise returned by  `trigger(params)` resolve after the request has completed (either success or failure)
-        // * Does `reset()` reset the request state properly
-    })
+  xdescribe("RestfulRequest", () => {
+    // NOTE Atte Keinänen 9/26/17: RestfulRequest doesn't really need unit tests because the xrays integration tests
+    // basically already verify that the basic functionality works as expected, as in
+    // * Are actions executed in an expected order and do they lead to expected state changes
+    // * Does the promise returned by  `trigger(params)` resolve after the request has completed (either success or failure)
+    // * Does `reset()` reset the request state properly
+  });
 
-    // We don't simulate localStorage in React container tests so stuff regarding reusing existing job IDs is tested here
-    // NOTE: Writing some of these tests might become obsolete if we come up with a more backend-leaning caching strategy
-    xdescribe("BackgroundJobRequest", () => {
-        const createTestRequest = () =>
-            new BackgroundJobRequest({
-                creationEndpoint: async () => { await delay(500); return { "job-id": 57}; },
-                // how should we manipulate what statusEndpoint retuns? maybe just a simple variable? it's a little awkward though :/
-                statusEndpoint: async () => { await delay(500); return { status: "done", result: {} }; },
-                actionPrefix: 'test'
-            })
+  // We don't simulate localStorage in React container tests so stuff regarding reusing existing job IDs is tested here
+  // NOTE: Writing some of these tests might become obsolete if we come up with a more backend-leaning caching strategy
+  xdescribe("BackgroundJobRequest", () => {
+    const createTestRequest = () =>
+      new BackgroundJobRequest({
+        creationEndpoint: async () => {
+          await delay(500);
+          return { "job-id": 57 };
+        },
+        // how should we manipulate what statusEndpoint retuns? maybe just a simple variable? it's a little awkward though :/
+        statusEndpoint: async () => {
+          await delay(500);
+          return { status: "done", result: {} };
+        },
+        actionPrefix: "test",
+      });
 
-        describe("trigger(params)", () => {
-            it("should create a new job when calling `trigger(params)` for a first time", () => {
-                const testRequest = createTestRequest()
-                testRequest.trigger({ id: "1" })
-            })
+    describe("trigger(params)", () => {
+      it("should create a new job when calling `trigger(params)` for a first time", () => {
+        const testRequest = createTestRequest();
+        testRequest.trigger({ id: "1" });
+      });
 
-            it("should restore results of an existing job when calling `trigger(params)` another time", () => {
-                const testRequest = createTestRequest()
-                testRequest.trigger({})
-            })
+      it("should restore results of an existing job when calling `trigger(params)` another time", () => {
+        const testRequest = createTestRequest();
+        testRequest.trigger({});
+      });
 
-            it("should create a new job ", () => {
-                // const testrequest = createTestRequest()
-            })
-        })
-    })
-})
\ No newline at end of file
+      it("should create a new job ", () => {
+        // const testrequest = createTestRequest()
+      });
+    });
+  });
+});
diff --git a/frontend/test/lib/schema_metadata.unit.spec.js b/frontend/test/lib/schema_metadata.unit.spec.js
index ef9739014896dddf07da79b77584344b98b76260..bd0aa13183ae37752ebfa669ec9871c9535d29a4 100644
--- a/frontend/test/lib/schema_metadata.unit.spec.js
+++ b/frontend/test/lib/schema_metadata.unit.spec.js
@@ -1,70 +1,91 @@
 import {
-    getFieldType,
-    DATE_TIME,
-    STRING,
-    STRING_LIKE,
-    NUMBER,
-    BOOLEAN,
-    LOCATION,
-    COORDINATE,
-    foreignKeyCountsByOriginTable
-} from 'metabase/lib/schema_metadata';
+  getFieldType,
+  DATE_TIME,
+  STRING,
+  STRING_LIKE,
+  NUMBER,
+  BOOLEAN,
+  LOCATION,
+  COORDINATE,
+  foreignKeyCountsByOriginTable,
+} from "metabase/lib/schema_metadata";
 
 import { TYPE } from "metabase/lib/types";
 
-describe('schema_metadata', () => {
-    describe('getFieldType', () => {
-        it('should know a date', () => {
-            expect(getFieldType({ base_type: TYPE.Date })).toEqual(DATE_TIME)
-            expect(getFieldType({ base_type: TYPE.DateTime })).toEqual(DATE_TIME)
-            expect(getFieldType({ base_type: TYPE.Time })).toEqual(DATE_TIME)
-            expect(getFieldType({ special_type: TYPE.UNIXTimestampSeconds })).toEqual(DATE_TIME)
-            expect(getFieldType({ special_type: TYPE.UNIXTimestampMilliseconds })).toEqual(DATE_TIME)
-        });
-        it('should know a number', () => {
-            expect(getFieldType({ base_type: TYPE.BigInteger })).toEqual(NUMBER)
-            expect(getFieldType({ base_type: TYPE.Integer })).toEqual(NUMBER)
-            expect(getFieldType({ base_type: TYPE.Float })).toEqual(NUMBER)
-            expect(getFieldType({ base_type: TYPE.Decimal })).toEqual(NUMBER)
-        });
-        it('should know a string', () => {
-            expect(getFieldType({ base_type: TYPE.Text })).toEqual(STRING)
-        });
-        it('should know things that are types of strings', () => {
-            expect(getFieldType({ base_type: TYPE.Text, special_type: TYPE.Name })).toEqual(STRING)
-            expect(getFieldType({ base_type: TYPE.Text, special_type: TYPE.Description })).toEqual(STRING)
-            expect(getFieldType({ base_type: TYPE.Text, special_type: TYPE.UUID })).toEqual(STRING)
-            expect(getFieldType({ base_type: TYPE.Text, special_type: TYPE.URL })).toEqual(STRING)
-        });
-        it('should know a bool', () => {
-            expect(getFieldType({ base_type: TYPE.Boolean })).toEqual(BOOLEAN)
-        });
-        it('should know a location', () => {
-            expect(getFieldType({ special_type: TYPE.City })).toEqual(LOCATION)
-            expect(getFieldType({ special_type: TYPE.Country })).toEqual(LOCATION)
-        });
-        it('should know a coordinate', () => {
-            expect(getFieldType({ special_type: TYPE.Latitude })).toEqual(COORDINATE)
-            expect(getFieldType({ special_type: TYPE.Longitude })).toEqual(COORDINATE)
-        });
-        it('should know something that is string-like', () => {
-            expect(getFieldType({ base_type: TYPE.TextLike })).toEqual(STRING_LIKE);
-            expect(getFieldType({ base_type: TYPE.IPAddress })).toEqual(STRING_LIKE);
-        });
-        it('should know what it doesn\'t know', () => {
-            expect(getFieldType({ base_type: 'DERP DERP DERP' })).toEqual(undefined)
-        });
+describe("schema_metadata", () => {
+  describe("getFieldType", () => {
+    it("should know a date", () => {
+      expect(getFieldType({ base_type: TYPE.Date })).toEqual(DATE_TIME);
+      expect(getFieldType({ base_type: TYPE.DateTime })).toEqual(DATE_TIME);
+      expect(getFieldType({ base_type: TYPE.Time })).toEqual(DATE_TIME);
+      expect(getFieldType({ special_type: TYPE.UNIXTimestampSeconds })).toEqual(
+        DATE_TIME,
+      );
+      expect(
+        getFieldType({ special_type: TYPE.UNIXTimestampMilliseconds }),
+      ).toEqual(DATE_TIME);
     });
+    it("should know a number", () => {
+      expect(getFieldType({ base_type: TYPE.BigInteger })).toEqual(NUMBER);
+      expect(getFieldType({ base_type: TYPE.Integer })).toEqual(NUMBER);
+      expect(getFieldType({ base_type: TYPE.Float })).toEqual(NUMBER);
+      expect(getFieldType({ base_type: TYPE.Decimal })).toEqual(NUMBER);
+    });
+    it("should know a string", () => {
+      expect(getFieldType({ base_type: TYPE.Text })).toEqual(STRING);
+    });
+    it("should know things that are types of strings", () => {
+      expect(
+        getFieldType({ base_type: TYPE.Text, special_type: TYPE.Name }),
+      ).toEqual(STRING);
+      expect(
+        getFieldType({ base_type: TYPE.Text, special_type: TYPE.Description }),
+      ).toEqual(STRING);
+      expect(
+        getFieldType({ base_type: TYPE.Text, special_type: TYPE.UUID }),
+      ).toEqual(STRING);
+      expect(
+        getFieldType({ base_type: TYPE.Text, special_type: TYPE.URL }),
+      ).toEqual(STRING);
+    });
+    it("should know a bool", () => {
+      expect(getFieldType({ base_type: TYPE.Boolean })).toEqual(BOOLEAN);
+    });
+    it("should know a location", () => {
+      expect(getFieldType({ special_type: TYPE.City })).toEqual(LOCATION);
+      expect(getFieldType({ special_type: TYPE.Country })).toEqual(LOCATION);
+    });
+    it("should know a coordinate", () => {
+      expect(getFieldType({ special_type: TYPE.Latitude })).toEqual(COORDINATE);
+      expect(getFieldType({ special_type: TYPE.Longitude })).toEqual(
+        COORDINATE,
+      );
+    });
+    it("should know something that is string-like", () => {
+      expect(getFieldType({ base_type: TYPE.TextLike })).toEqual(STRING_LIKE);
+      expect(getFieldType({ base_type: TYPE.IPAddress })).toEqual(STRING_LIKE);
+    });
+    it("should know what it doesn't know", () => {
+      expect(getFieldType({ base_type: "DERP DERP DERP" })).toEqual(undefined);
+    });
+  });
 
-    describe('foreignKeyCountsByOriginTable', () => {
-        it('should work with null input', () => {
-            expect(foreignKeyCountsByOriginTable(null)).toEqual(null)
-        });
-        it('should require an array as input', () => {
-            expect(foreignKeyCountsByOriginTable({})).toEqual(null)
-        });
-        it('should count occurrences by origin.table.id', () => {
-            expect(foreignKeyCountsByOriginTable([{ origin: {table: {id: 123}} }, { origin: {table: {id: 123}} }, { origin: {table: {id: 123}} }, { origin: {table: {id: 456}} }])).toEqual({123: 3, 456: 1})
-        });
+  describe("foreignKeyCountsByOriginTable", () => {
+    it("should work with null input", () => {
+      expect(foreignKeyCountsByOriginTable(null)).toEqual(null);
+    });
+    it("should require an array as input", () => {
+      expect(foreignKeyCountsByOriginTable({})).toEqual(null);
+    });
+    it("should count occurrences by origin.table.id", () => {
+      expect(
+        foreignKeyCountsByOriginTable([
+          { origin: { table: { id: 123 } } },
+          { origin: { table: { id: 123 } } },
+          { origin: { table: { id: 123 } } },
+          { origin: { table: { id: 456 } } },
+        ]),
+      ).toEqual({ 123: 3, 456: 1 });
     });
+  });
 });
diff --git a/frontend/test/lib/time.unit.spec.js b/frontend/test/lib/time.unit.spec.js
index f4db03f022a49e061fd60ae5cd6d245cdec09963..dd093e18ba428b72ca82f3a46c17fe9fb942487e 100644
--- a/frontend/test/lib/time.unit.spec.js
+++ b/frontend/test/lib/time.unit.spec.js
@@ -1,38 +1,44 @@
-import { parseTimestamp } from 'metabase/lib/time';
-import moment from 'moment';
-
-describe('time', () => {
-    describe('parseTimestamp', () => {
-        const NY15_TOKYO = moment(1420038000000); // 2014-12-31 15:00 UTC
-        const NY15_UTC = moment(1420070400000); // 2015-01-01 00:00 UTC
-        const NY15_LA = moment(1420099200000); // 2015-01-01 00:00 UTC
-
-
-        const TEST_CASES = [
-            ['2015-01-01T00:00:00.000Z',      0,     NY15_UTC],
-            ['2015-01-01T00:00:00.000+00:00', 0,     NY15_UTC],
-            ['2015-01-01T00:00:00.000+0000',  0,     NY15_UTC],
-            ['2015-01-01T00:00:00Z',          0,     NY15_UTC],
-
-            ['2015-01-01T00:00:00.000+09:00', 540, NY15_TOKYO],
-            ['2015-01-01T00:00:00.000+0900',  540, NY15_TOKYO],
-            ['2015-01-01T00:00:00+09:00',     540, NY15_TOKYO],
-            ['2015-01-01T00:00:00+0900',      540, NY15_TOKYO],
-
-            ['2015-01-01T00:00:00.000-08:00', -480,   NY15_LA],
-            ['2015-01-01T00:00:00.000-0800',  -480,   NY15_LA],
-            ['2015-01-01T00:00:00-08:00',     -480,   NY15_LA],
-            ['2015-01-01T00:00:00-0800',      -480,   NY15_LA]]
-
-        TEST_CASES.map(([str, expectedOffset, expectedMoment]) => {
-            it(str + ' should be parsed as  moment reprsenting' + expectedMoment + "with the offset " + expectedOffset, () => {
-                let result = parseTimestamp(str);
-
-                expect(moment.isMoment(result)).toBe(true);
-                expect(result.utcOffset()).toBe(expectedOffset);
-                expect(result.unix()).toEqual(expectedMoment.unix());
-            });
-        });
-
+import { parseTimestamp } from "metabase/lib/time";
+import moment from "moment";
+
+describe("time", () => {
+  describe("parseTimestamp", () => {
+    const NY15_TOKYO = moment(1420038000000); // 2014-12-31 15:00 UTC
+    const NY15_UTC = moment(1420070400000); // 2015-01-01 00:00 UTC
+    const NY15_LA = moment(1420099200000); // 2015-01-01 00:00 UTC
+
+    const TEST_CASES = [
+      ["2015-01-01T00:00:00.000Z", 0, NY15_UTC],
+      ["2015-01-01T00:00:00.000+00:00", 0, NY15_UTC],
+      ["2015-01-01T00:00:00.000+0000", 0, NY15_UTC],
+      ["2015-01-01T00:00:00Z", 0, NY15_UTC],
+
+      ["2015-01-01T00:00:00.000+09:00", 540, NY15_TOKYO],
+      ["2015-01-01T00:00:00.000+0900", 540, NY15_TOKYO],
+      ["2015-01-01T00:00:00+09:00", 540, NY15_TOKYO],
+      ["2015-01-01T00:00:00+0900", 540, NY15_TOKYO],
+
+      ["2015-01-01T00:00:00.000-08:00", -480, NY15_LA],
+      ["2015-01-01T00:00:00.000-0800", -480, NY15_LA],
+      ["2015-01-01T00:00:00-08:00", -480, NY15_LA],
+      ["2015-01-01T00:00:00-0800", -480, NY15_LA],
+    ];
+
+    TEST_CASES.map(([str, expectedOffset, expectedMoment]) => {
+      it(
+        str +
+          " should be parsed as  moment reprsenting" +
+          expectedMoment +
+          "with the offset " +
+          expectedOffset,
+        () => {
+          let result = parseTimestamp(str);
+
+          expect(moment.isMoment(result)).toBe(true);
+          expect(result.utcOffset()).toBe(expectedOffset);
+          expect(result.unix()).toEqual(expectedMoment.unix());
+        },
+      );
     });
+  });
 });
diff --git a/frontend/test/lib/urls.unit.spec.js b/frontend/test/lib/urls.unit.spec.js
index 6b1431d4f4f57cb63683287c9962f59b3c30a265..69c66785144660c3579188f7551cdfd454c7f12b 100644
--- a/frontend/test/lib/urls.unit.spec.js
+++ b/frontend/test/lib/urls.unit.spec.js
@@ -1,16 +1,23 @@
 import { question } from "metabase/lib/urls";
 
 describe("urls", () => {
-    describe("question", () => {
-        describe("with a query", () => {
-            it("returns the correct url", () => {
-                expect(question(null, "", {foo: 'bar'})).toEqual('/question?foo=bar');
-                expect(question(null, "", {foo: 'bar+bar'})).toEqual('/question?foo=bar%2Bbar');
-                expect(question(null, "", {foo: ['bar', 'baz']})).toEqual('/question?foo=bar&foo=baz');
-                expect(question(null, "", {foo: ['bar', 'baz+bay']})).toEqual('/question?foo=bar&foo=baz%2Bbay');
-                expect(question(null, "", {foo: ['bar', 'baz&bay']})).toEqual('/question?foo=bar&foo=baz%26bay');
-            });
-        });
+  describe("question", () => {
+    describe("with a query", () => {
+      it("returns the correct url", () => {
+        expect(question(null, "", { foo: "bar" })).toEqual("/question?foo=bar");
+        expect(question(null, "", { foo: "bar+bar" })).toEqual(
+          "/question?foo=bar%2Bbar",
+        );
+        expect(question(null, "", { foo: ["bar", "baz"] })).toEqual(
+          "/question?foo=bar&foo=baz",
+        );
+        expect(question(null, "", { foo: ["bar", "baz+bay"] })).toEqual(
+          "/question?foo=bar&foo=baz%2Bbay",
+        );
+        expect(question(null, "", { foo: ["bar", "baz&bay"] })).toEqual(
+          "/question?foo=bar&foo=baz%26bay",
+        );
+      });
     });
+  });
 });
-
diff --git a/frontend/test/lib/utils.unit.spec.js b/frontend/test/lib/utils.unit.spec.js
index 6290639d136c00807faaf92f0fc6899559449cfe..d8db00751bcb4b30b474bf37e35a31156ef25bb7 100644
--- a/frontend/test/lib/utils.unit.spec.js
+++ b/frontend/test/lib/utils.unit.spec.js
@@ -1,95 +1,87 @@
-import MetabaseUtils from 'metabase/lib/utils';
+import MetabaseUtils from "metabase/lib/utils";
 
+describe("utils", () => {
+  describe("generatePassword", () => {
+    it("defaults to length 14 passwords", () => {
+      expect(MetabaseUtils.generatePassword().length).toBe(14);
+    });
 
-describe('utils', () => {
-    describe('generatePassword', () => {
-        it('defaults to length 14 passwords', () => {
-            expect(
-                MetabaseUtils.generatePassword().length
-            ).toBe(
-                14
-            );
-        });
-
-        it('creates passwords for the length we specify', () => {
-            expect(
-                MetabaseUtils.generatePassword(25).length
-            ).toBe(
-                25
-            );
-        });
+    it("creates passwords for the length we specify", () => {
+      expect(MetabaseUtils.generatePassword(25).length).toBe(25);
+    });
 
-        it('can enforce ', () => {
-            expect(
-                (MetabaseUtils.generatePassword(14, {digit: 2}).match(/([\d])/g).length >= 2)
-            ).toBe(
-                true
-            );
-        });
+    it("can enforce ", () => {
+      expect(
+        MetabaseUtils.generatePassword(14, { digit: 2 }).match(/([\d])/g)
+          .length >= 2,
+      ).toBe(true);
+    });
 
-        it('can enforce digit requirements', () => {
-            expect(
-                (MetabaseUtils.generatePassword(14, {digit: 2}).match(/([\d])/g).length >= 2)
-            ).toBe(
-                true
-            );
-        });
+    it("can enforce digit requirements", () => {
+      expect(
+        MetabaseUtils.generatePassword(14, { digit: 2 }).match(/([\d])/g)
+          .length >= 2,
+      ).toBe(true);
+    });
 
-        it('can enforce uppercase requirements', () => {
-            expect(
-                (MetabaseUtils.generatePassword(14, {uppercase: 2}).match(/([A-Z])/g).length >= 2)
-            ).toBe(
-                true
-            );
-        });
+    it("can enforce uppercase requirements", () => {
+      expect(
+        MetabaseUtils.generatePassword(14, { uppercase: 2 }).match(/([A-Z])/g)
+          .length >= 2,
+      ).toBe(true);
+    });
 
-        it('can enforce special character requirements', () => {
-            expect(
-                (MetabaseUtils.generatePassword(14, {special: 2}).match(/([!@#\$%\^\&*\)\(+=._-{}])/g).length >= 2)
-            ).toBe(
-                true
-            );
-        });
+    it("can enforce special character requirements", () => {
+      expect(
+        MetabaseUtils.generatePassword(14, { special: 2 }).match(
+          /([!@#\$%\^\&*\)\(+=._-{}])/g,
+        ).length >= 2,
+      ).toBe(true);
     });
+  });
 
-    describe("compareVersions", () => {
-        it ("should compare versions correctly", () => {
-            let expected = [
-                "0.0.9",
-                "0.0.10-snapshot",
-                "0.0.10-alpha1",
-                "0.0.10-rc1",
-                "0.0.10-rc2",
-                "0.0.10-rc10",
-                "0.0.10",
-                "0.1.0",
-                "0.2.0",
-                "0.10.0",
-                "1.1.0"
-            ];
-            let shuffled = expected.slice();
-            shuffle(shuffled);
-            shuffled.sort(MetabaseUtils.compareVersions);
-            expect(shuffled).toEqual(expected);
-        });
+  describe("compareVersions", () => {
+    it("should compare versions correctly", () => {
+      let expected = [
+        "0.0.9",
+        "0.0.10-snapshot",
+        "0.0.10-alpha1",
+        "0.0.10-rc1",
+        "0.0.10-rc2",
+        "0.0.10-rc10",
+        "0.0.10",
+        "0.1.0",
+        "0.2.0",
+        "0.10.0",
+        "1.1.0",
+      ];
+      let shuffled = expected.slice();
+      shuffle(shuffled);
+      shuffled.sort(MetabaseUtils.compareVersions);
+      expect(shuffled).toEqual(expected);
     });
+  });
 
-    describe("isEmpty", () => {
-        it("should not allow all-blank strings", () => {
-            expect(MetabaseUtils.isEmpty(" ")).toEqual(true);
-        });
+  describe("isEmpty", () => {
+    it("should not allow all-blank strings", () => {
+      expect(MetabaseUtils.isEmpty(" ")).toEqual(true);
     });
+  });
 
-    describe("isJWT", () => {
-        it("should allow for JWT tokens with dashes", () => {
-            expect(MetabaseUtils.isJWT("eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJhbXMiOnsicGFyYW0xIjoidGVzdCIsInBhcmFtMiI6ImFiIiwicGFyYW0zIjoiMjAwMC0wMC0wMFQwMDowMDowMCswMDowMCIsInBhcmFtNCI6Iu-8iO-8iSJ9LCJyZXNvdXJjZSI6eyJkYXNoYm9hcmQiOjB9fQ.wsNWliHJNwJBv_hx0sPo1EGY0nATdgEa31TM1AYotIA")).toEqual(true);
-        });
+  describe("isJWT", () => {
+    it("should allow for JWT tokens with dashes", () => {
+      expect(
+        MetabaseUtils.isJWT(
+          "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJwYXJhbXMiOnsicGFyYW0xIjoidGVzdCIsInBhcmFtMiI6ImFiIiwicGFyYW0zIjoiMjAwMC0wMC0wMFQwMDowMDowMCswMDowMCIsInBhcmFtNCI6Iu-8iO-8iSJ9LCJyZXNvdXJjZSI6eyJkYXNoYm9hcmQiOjB9fQ.wsNWliHJNwJBv_hx0sPo1EGY0nATdgEa31TM1AYotIA",
+        ),
+      ).toEqual(true);
     });
+  });
 });
 
 function shuffle(a) {
-    for (let i = a.length; i; i--) {
-        let j = Math.floor(Math.random() * i);
-        [a[i - 1], a[j]] = [a[j], a[i - 1]];
-    }
+  for (let i = a.length; i; i--) {
+    let j = Math.floor(Math.random() * i);
+    [a[i - 1], a[j]] = [a[j], a[i - 1]];
+  }
 }
diff --git a/frontend/test/meta/Card.unit.spec.js b/frontend/test/meta/Card.unit.spec.js
index dbfe563262da3b6d20a90afd19417ecbaab0ea61..3cafc87fae5ae0cce7f9de8229dc2c065c81b75b 100644
--- a/frontend/test/meta/Card.unit.spec.js
+++ b/frontend/test/meta/Card.unit.spec.js
@@ -3,225 +3,235 @@ import * as Card from "metabase/meta/Card";
 import { assocIn, dissoc } from "icepick";
 
 describe("metabase/meta/Card", () => {
-    describe("questionUrlWithParameters", () => {
-        const metadata = {
-            fields: {
-                2: {
-                    base_type: "type/Integer"
-                }
-            }
-        }
+  describe("questionUrlWithParameters", () => {
+    const metadata = {
+      fields: {
+        2: {
+          base_type: "type/Integer",
+        },
+      },
+    };
 
-        const parameters = [
-            {
-                id: 1,
-                slug: "param_string",
-                type: "category"
-            },
-            {
-                id: 2,
-                slug: "param_number",
-                type: "category"
-            },
-            {
-                id: 3,
-                slug: "param_date",
-                type: "date/month"
-            },
-            {
-                id: 4,
-                slug: "param_fk",
-                type: "date/month"
-            }
-        ];
+    const parameters = [
+      {
+        id: 1,
+        slug: "param_string",
+        type: "category",
+      },
+      {
+        id: 2,
+        slug: "param_number",
+        type: "category",
+      },
+      {
+        id: 3,
+        slug: "param_date",
+        type: "date/month",
+      },
+      {
+        id: 4,
+        slug: "param_fk",
+        type: "date/month",
+      },
+    ];
 
-        describe("with SQL card", () => {
-            const card = {
-                id: 1,
-                dataset_query: {
-                    type: "native",
-                    native: {
-                        template_tags: {
-                            baz: { name: "baz", type: "text" }
-                        }
-                    }
-                }
-            };
-            const parameterMappings = [
-                {
-                    card_id: 1,
-                    parameter_id: 1,
-                    target: ["variable", ["template-tag", "baz"]]
-                }
-            ];
-            it("should return question URL with no parameters", () => {
-                const url = Card.questionUrlWithParameters(card, metadata, []);
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: dissoc(card, "id")
-                });
-            });
-            it("should return question URL with query string parameter", () => {
-                const url = Card.questionUrlWithParameters(
-                    card,
-                    metadata,
-                    parameters,
-                    { "1": "bar" },
-                    parameterMappings
-                );
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: { baz: "bar" },
-                    card: dissoc(card, "id")
-                });
-            });
+    describe("with SQL card", () => {
+      const card = {
+        id: 1,
+        dataset_query: {
+          type: "native",
+          native: {
+            template_tags: {
+              baz: { name: "baz", type: "text" },
+            },
+          },
+        },
+      };
+      const parameterMappings = [
+        {
+          card_id: 1,
+          parameter_id: 1,
+          target: ["variable", ["template-tag", "baz"]],
+        },
+      ];
+      it("should return question URL with no parameters", () => {
+        const url = Card.questionUrlWithParameters(card, metadata, []);
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: dissoc(card, "id"),
+        });
+      });
+      it("should return question URL with query string parameter", () => {
+        const url = Card.questionUrlWithParameters(
+          card,
+          metadata,
+          parameters,
+          { "1": "bar" },
+          parameterMappings,
+        );
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: { baz: "bar" },
+          card: dissoc(card, "id"),
         });
-        describe("with structured card", () => {
-            const card = {
-                id: 1,
-                dataset_query: {
-                    type: "query",
-                    query: {
-                        source_table: 1
-                    }
-                }
-            };
-            const parameterMappings = [
-                {
-                    card_id: 1,
-                    parameter_id: 1,
-                    target: ["dimension", ["field-id", 1]]
-                },
-                {
-                    card_id: 1,
-                    parameter_id: 2,
-                    target: ["dimension", ["field-id", 2]]
-                },
-                {
-                    card_id: 1,
-                    parameter_id: 3,
-                    target: ["dimension", ["field-id", 3]]
-                },
-                {
-                    card_id: 1,
-                    parameter_id: 4,
-                    target: ["dimension", ["fk->", 4, 5]]
-                },
-            ];
-            it("should return question URL with no parameters", () => {
-                const url = Card.questionUrlWithParameters(card, metadata, []);
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: dissoc(card, "id")
-                });
-            });
-            it("should return question URL with string MBQL filter added", () => {
-                const url = Card.questionUrlWithParameters(
-                    card,
-                    metadata,
-                    parameters,
-                    { "1": "bar" },
-                    parameterMappings
-                );
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: assocIn(
-                        dissoc(card, "id"),
-                        ["dataset_query", "query", "filter"],
-                        ["AND", ["=", ["field-id", 1], "bar"]]
-                    )
-                });
-            });
-            it("should return question URL even if only original_card_id is present", () => {
-                const cardWithOnlyOriginalCardId = { ...card, id: undefined, original_card_id: card.id };
+      });
+    });
+    describe("with structured card", () => {
+      const card = {
+        id: 1,
+        dataset_query: {
+          type: "query",
+          query: {
+            source_table: 1,
+          },
+        },
+      };
+      const parameterMappings = [
+        {
+          card_id: 1,
+          parameter_id: 1,
+          target: ["dimension", ["field-id", 1]],
+        },
+        {
+          card_id: 1,
+          parameter_id: 2,
+          target: ["dimension", ["field-id", 2]],
+        },
+        {
+          card_id: 1,
+          parameter_id: 3,
+          target: ["dimension", ["field-id", 3]],
+        },
+        {
+          card_id: 1,
+          parameter_id: 4,
+          target: ["dimension", ["fk->", 4, 5]],
+        },
+      ];
+      it("should return question URL with no parameters", () => {
+        const url = Card.questionUrlWithParameters(card, metadata, []);
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: dissoc(card, "id"),
+        });
+      });
+      it("should return question URL with string MBQL filter added", () => {
+        const url = Card.questionUrlWithParameters(
+          card,
+          metadata,
+          parameters,
+          { "1": "bar" },
+          parameterMappings,
+        );
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: assocIn(
+            dissoc(card, "id"),
+            ["dataset_query", "query", "filter"],
+            ["AND", ["=", ["field-id", 1], "bar"]],
+          ),
+        });
+      });
+      it("should return question URL even if only original_card_id is present", () => {
+        const cardWithOnlyOriginalCardId = {
+          ...card,
+          id: undefined,
+          original_card_id: card.id,
+        };
 
-                const url = Card.questionUrlWithParameters(
-                    cardWithOnlyOriginalCardId,
-                    metadata,
-                    parameters,
-                    { "1": "bar" },
-                    parameterMappings
-                );
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: assocIn(
-                        cardWithOnlyOriginalCardId,
-                        ["dataset_query", "query", "filter"],
-                        ["AND", ["=", ["field-id", 1], "bar"]]
-                    )
-                });
-            });
-            it("should return question URL with number MBQL filter added", () => {
-                const url = Card.questionUrlWithParameters(
-                    card,
-                    metadata,
-                    parameters,
-                    { "2": "123" },
-                    parameterMappings
-                );
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: assocIn(
-                        dissoc(card, "id"),
-                        ["dataset_query", "query", "filter"],
-                        ["AND", ["=", ["field-id", 2], 123]]
-                    )
-                });
-            });
-            it("should return question URL with date MBQL filter added", () => {
-                const url = Card.questionUrlWithParameters(
-                    card,
-                    metadata,
-                    parameters,
-                    { "3": "2017-05" },
-                    parameterMappings
-                );
+        const url = Card.questionUrlWithParameters(
+          cardWithOnlyOriginalCardId,
+          metadata,
+          parameters,
+          { "1": "bar" },
+          parameterMappings,
+        );
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: assocIn(
+            cardWithOnlyOriginalCardId,
+            ["dataset_query", "query", "filter"],
+            ["AND", ["=", ["field-id", 1], "bar"]],
+          ),
+        });
+      });
+      it("should return question URL with number MBQL filter added", () => {
+        const url = Card.questionUrlWithParameters(
+          card,
+          metadata,
+          parameters,
+          { "2": "123" },
+          parameterMappings,
+        );
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: assocIn(
+            dissoc(card, "id"),
+            ["dataset_query", "query", "filter"],
+            ["AND", ["=", ["field-id", 2], 123]],
+          ),
+        });
+      });
+      it("should return question URL with date MBQL filter added", () => {
+        const url = Card.questionUrlWithParameters(
+          card,
+          metadata,
+          parameters,
+          { "3": "2017-05" },
+          parameterMappings,
+        );
 
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: assocIn(
-                        dissoc(card, "id"),
-                        ["dataset_query", "query", "filter"],
-                        ["AND", ["=", ["datetime-field", ["field-id", 3], "month"], "2017-05-01"]]
-                    )
-                });
-            });
-            it("should return question URL with date MBQL filter on a FK added", () => {
-                const url = Card.questionUrlWithParameters(
-                    card,
-                    metadata,
-                    parameters,
-                    { "4": "2017-05" },
-                    parameterMappings
-                );
-                expect(parseUrl(url)).toEqual({
-                    pathname: "/question",
-                    query: {},
-                    card: assocIn(
-                        dissoc(card, "id"),
-                        ["dataset_query", "query", "filter"],
-                        ["AND", ["=", ["datetime-field", ["fk->", 4, 5], "month"], "2017-05-01"]]
-                    )
-                });
-            });
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: assocIn(
+            dissoc(card, "id"),
+            ["dataset_query", "query", "filter"],
+            [
+              "AND",
+              ["=", ["datetime-field", ["field-id", 3], "month"], "2017-05-01"],
+            ],
+          ),
+        });
+      });
+      it("should return question URL with date MBQL filter on a FK added", () => {
+        const url = Card.questionUrlWithParameters(
+          card,
+          metadata,
+          parameters,
+          { "4": "2017-05" },
+          parameterMappings,
+        );
+        expect(parseUrl(url)).toEqual({
+          pathname: "/question",
+          query: {},
+          card: assocIn(
+            dissoc(card, "id"),
+            ["dataset_query", "query", "filter"],
+            [
+              "AND",
+              ["=", ["datetime-field", ["fk->", 4, 5], "month"], "2017-05-01"],
+            ],
+          ),
         });
+      });
     });
+  });
 });
 
 import { parse } from "url";
 import { deserializeCardFromUrl } from "metabase/lib/card";
 
 function parseUrl(url) {
-    const parsed = parse(url, true);
-    return {
-        card: parsed.hash && deserializeCardFromUrl(parsed.hash),
-        query: parsed.query,
-        pathname: parsed.pathname
-    };
+  const parsed = parse(url, true);
+  return {
+    card: parsed.hash && deserializeCardFromUrl(parsed.hash),
+    query: parsed.query,
+    pathname: parsed.pathname,
+  };
 }
diff --git a/frontend/test/meta/Parameter.unit.spec.js b/frontend/test/meta/Parameter.unit.spec.js
index 6fc5ba45a2df5e97bf040cc2d31f05f5e584ae2a..5c37fef098cd1426af2a91c0944161b8ee31b784 100644
--- a/frontend/test/meta/Parameter.unit.spec.js
+++ b/frontend/test/meta/Parameter.unit.spec.js
@@ -1,53 +1,113 @@
-import { dateParameterValueToMBQL, stringParameterValueToMBQL } from "metabase/meta/Parameter";
+import {
+  dateParameterValueToMBQL,
+  stringParameterValueToMBQL,
+} from "metabase/meta/Parameter";
 
 describe("metabase/meta/Parameter", () => {
-    describe("dateParameterValueToMBQL", () => {
-        it ("should parse past30days", () => {
-            expect(dateParameterValueToMBQL("past30days", null)).toEqual(["time-interval", null, -30, "day"])
-        })
-        it ("should parse past30days~", () => {
-            expect(dateParameterValueToMBQL("past30days~", null)).toEqual(["time-interval", null, -30, "day", { "include-current": true }])
-        })
-        it ("should parse next2years", () => {
-            expect(dateParameterValueToMBQL("next2years", null)).toEqual(["time-interval", null, 2, "year"])
-        })
-        it ("should parse next2years~", () => {
-            expect(dateParameterValueToMBQL("next2years~", null)).toEqual(["time-interval", null, 2, "year", { "include-current": true }])
-        })
-        it ("should parse thisday", () => {
-            expect(dateParameterValueToMBQL("thisday", null)).toEqual(["time-interval", null, "current", "day"])
-        })
-        it ("should parse ~2017-05-01", () => {
-            expect(dateParameterValueToMBQL("~2017-05-01", null)).toEqual(["<", null, "2017-05-01"])
-        })
-        it ("should parse 2017-05-01~", () => {
-            expect(dateParameterValueToMBQL("2017-05-01~", null)).toEqual([">", null, "2017-05-01"])
-        })
-        it ("should parse 2017-05", () => {
-            expect(dateParameterValueToMBQL("2017-05", null)).toEqual(["=", ["datetime-field", null, "month"], "2017-05-01"])
-        })
-        it ("should parse Q1-2017", () => {
-            expect(dateParameterValueToMBQL("Q1-2017", null)).toEqual(["=", ["datetime-field", null, "quarter"], "2017-01-01"])
-        })
-        it ("should parse 2017-05-01", () => {
-            expect(dateParameterValueToMBQL("2017-05-01", null)).toEqual(["=", null, "2017-05-01"])
-        })
-        it ("should parse 2017-05-01~2017-05-02", () => {
-            expect(dateParameterValueToMBQL("2017-05-01~2017-05-02", null)).toEqual(["BETWEEN", null, "2017-05-01", "2017-05-02"])
-        })
-    })
+  describe("dateParameterValueToMBQL", () => {
+    it("should parse past30days", () => {
+      expect(dateParameterValueToMBQL("past30days", null)).toEqual([
+        "time-interval",
+        null,
+        -30,
+        "day",
+      ]);
+    });
+    it("should parse past30days~", () => {
+      expect(dateParameterValueToMBQL("past30days~", null)).toEqual([
+        "time-interval",
+        null,
+        -30,
+        "day",
+        { "include-current": true },
+      ]);
+    });
+    it("should parse next2years", () => {
+      expect(dateParameterValueToMBQL("next2years", null)).toEqual([
+        "time-interval",
+        null,
+        2,
+        "year",
+      ]);
+    });
+    it("should parse next2years~", () => {
+      expect(dateParameterValueToMBQL("next2years~", null)).toEqual([
+        "time-interval",
+        null,
+        2,
+        "year",
+        { "include-current": true },
+      ]);
+    });
+    it("should parse thisday", () => {
+      expect(dateParameterValueToMBQL("thisday", null)).toEqual([
+        "time-interval",
+        null,
+        "current",
+        "day",
+      ]);
+    });
+    it("should parse ~2017-05-01", () => {
+      expect(dateParameterValueToMBQL("~2017-05-01", null)).toEqual([
+        "<",
+        null,
+        "2017-05-01",
+      ]);
+    });
+    it("should parse 2017-05-01~", () => {
+      expect(dateParameterValueToMBQL("2017-05-01~", null)).toEqual([
+        ">",
+        null,
+        "2017-05-01",
+      ]);
+    });
+    it("should parse 2017-05", () => {
+      expect(dateParameterValueToMBQL("2017-05", null)).toEqual([
+        "=",
+        ["datetime-field", null, "month"],
+        "2017-05-01",
+      ]);
+    });
+    it("should parse Q1-2017", () => {
+      expect(dateParameterValueToMBQL("Q1-2017", null)).toEqual([
+        "=",
+        ["datetime-field", null, "quarter"],
+        "2017-01-01",
+      ]);
+    });
+    it("should parse 2017-05-01", () => {
+      expect(dateParameterValueToMBQL("2017-05-01", null)).toEqual([
+        "=",
+        null,
+        "2017-05-01",
+      ]);
+    });
+    it("should parse 2017-05-01~2017-05-02", () => {
+      expect(dateParameterValueToMBQL("2017-05-01~2017-05-02", null)).toEqual([
+        "BETWEEN",
+        null,
+        "2017-05-01",
+        "2017-05-02",
+      ]);
+    });
+  });
 
-    describe("stringParameterValueToMBQL", () => {
-        describe("when given an array parameter value", () => {
-            it ("should flatten the array parameter values", () => {
-                expect(stringParameterValueToMBQL(["1", "2"], null)).toEqual(["=", null, "1", "2"])
-            })
-        })
+  describe("stringParameterValueToMBQL", () => {
+    describe("when given an array parameter value", () => {
+      it("should flatten the array parameter values", () => {
+        expect(stringParameterValueToMBQL(["1", "2"], null)).toEqual([
+          "=",
+          null,
+          "1",
+          "2",
+        ]);
+      });
+    });
 
-        describe("when given a string parameter value", () => {
-            it ("should return the correct MBQL", () => {
-                expect(stringParameterValueToMBQL("1", null)).toEqual(["=", null, "1"])
-            })
-        })
-    })
-})
+    describe("when given a string parameter value", () => {
+      it("should return the correct MBQL", () => {
+        expect(stringParameterValueToMBQL("1", null)).toEqual(["=", null, "1"]);
+      });
+    });
+  });
+});
diff --git a/frontend/test/metabase-bootstrap.js b/frontend/test/metabase-bootstrap.js
index 1791bf1e7034a63e62cbc412f4154b5c2199e09b..fad39e0091cb16aba9fcc8ca56097b3224319073 100644
--- a/frontend/test/metabase-bootstrap.js
+++ b/frontend/test/metabase-bootstrap.js
@@ -1,61 +1,59 @@
-import 'babel-polyfill';
-import 'number-to-locale-string';
+import "babel-polyfill";
+import "number-to-locale-string";
 import "metabase/css/index.css";
 
 window.MetabaseBootstrap = {
-    timezones: [
-        "GMT",
-        "UTC",
-        "US\/Alaska",
-        "US\/Arizona",
-        "US\/Central",
-        "US\/Eastern",
-        "US\/Hawaii",
-        "US\/Mountain",
-        "US\/Pacific",
-        "America\/Costa_Rica"
-    ],
-    available_locales: [
-        ["en", "English"]
-    ],
-    types: {
-        "type/Address":                   ["type/*"],
-        "type/Array":                     ["type/Collection"],
-        "type/AvatarURL":                 ["type/URL"],
-        "type/BigInteger":                ["type/Integer"],
-        "type/Boolean":                   ["type/*"],
-        "type/Category":                  ["type/Special"],
-        "type/City":                      ["type/Category", "type/Address", "type/Text"],
-        "type/Collection":                ["type/*"],
-        "type/Coordinate":                ["type/Float"],
-        "type/Country":                   ["type/Category", "type/Address", "type/Text"],
-        "type/Date":                      ["type/DateTime"],
-        "type/DateTime":                  ["type/*"],
-        "type/Decimal":                   ["type/Float"],
-        "type/Description":               ["type/Text"],
-        "type/Dictionary":                ["type/Collection"],
-        "type/Email":                     ["type/Text"],
-        "type/FK":                        ["type/Special"],
-        "type/Float":                     ["type/Number"],
-        "type/IPAddress":                 ["type/TextLike"],
-        "type/ImageURL":                  ["type/URL"],
-        "type/Integer":                   ["type/Number"],
-        "type/Latitude":                  ["type/Coordinate"],
-        "type/Longitude":                 ["type/Coordinate"],
-        "type/Name":                      ["type/Category", "type/Text"],
-        "type/Number":                    ["type/*"],
-        "type/PK":                        ["type/Special"],
-        "type/SerializedJSON":            ["type/Text", "type/Collection"],
-        "type/Special":                   ["type/*"],
-        "type/State":                     ["type/Category", "type/Address", "type/Text"],
-        "type/Text":                      ["type/*"],
-        "type/TextLike":                  ["type/*"],
-        "type/Time":                      ["type/DateTime"],
-        "type/UNIXTimestamp":             ["type/Integer", "type/DateTime"],
-        "type/UNIXTimestampMilliseconds": ["type/UNIXTimestamp"],
-        "type/UNIXTimestampSeconds":      ["type/UNIXTimestamp"],
-        "type/URL":                       ["type/Text"],
-        "type/UUID":                      ["type/Text"],
-        "type/ZipCode":                   ["type/Integer", "type/Address"]
-    }
+  timezones: [
+    "GMT",
+    "UTC",
+    "US/Alaska",
+    "US/Arizona",
+    "US/Central",
+    "US/Eastern",
+    "US/Hawaii",
+    "US/Mountain",
+    "US/Pacific",
+    "America/Costa_Rica",
+  ],
+  available_locales: [["en", "English"]],
+  types: {
+    "type/Address": ["type/*"],
+    "type/Array": ["type/Collection"],
+    "type/AvatarURL": ["type/URL"],
+    "type/BigInteger": ["type/Integer"],
+    "type/Boolean": ["type/*"],
+    "type/Category": ["type/Special"],
+    "type/City": ["type/Category", "type/Address", "type/Text"],
+    "type/Collection": ["type/*"],
+    "type/Coordinate": ["type/Float"],
+    "type/Country": ["type/Category", "type/Address", "type/Text"],
+    "type/Date": ["type/DateTime"],
+    "type/DateTime": ["type/*"],
+    "type/Decimal": ["type/Float"],
+    "type/Description": ["type/Text"],
+    "type/Dictionary": ["type/Collection"],
+    "type/Email": ["type/Text"],
+    "type/FK": ["type/Special"],
+    "type/Float": ["type/Number"],
+    "type/IPAddress": ["type/TextLike"],
+    "type/ImageURL": ["type/URL"],
+    "type/Integer": ["type/Number"],
+    "type/Latitude": ["type/Coordinate"],
+    "type/Longitude": ["type/Coordinate"],
+    "type/Name": ["type/Category", "type/Text"],
+    "type/Number": ["type/*"],
+    "type/PK": ["type/Special"],
+    "type/SerializedJSON": ["type/Text", "type/Collection"],
+    "type/Special": ["type/*"],
+    "type/State": ["type/Category", "type/Address", "type/Text"],
+    "type/Text": ["type/*"],
+    "type/TextLike": ["type/*"],
+    "type/Time": ["type/DateTime"],
+    "type/UNIXTimestamp": ["type/Integer", "type/DateTime"],
+    "type/UNIXTimestampMilliseconds": ["type/UNIXTimestamp"],
+    "type/UNIXTimestampSeconds": ["type/UNIXTimestamp"],
+    "type/URL": ["type/Text"],
+    "type/UUID": ["type/Text"],
+    "type/ZipCode": ["type/Integer", "type/Address"],
+  },
 };
diff --git a/frontend/test/metabase-lib/Action.unit.spec.js b/frontend/test/metabase-lib/Action.unit.spec.js
index 82436d9eb53963bd37a48e3ca872923f38a8de04..a41f1546d05fd7a6baf3731aa3d18234a038e473 100644
--- a/frontend/test/metabase-lib/Action.unit.spec.js
+++ b/frontend/test/metabase-lib/Action.unit.spec.js
@@ -1,9 +1,9 @@
 import Action from "metabase-lib/lib/Action";
 
 describe("Action", () => {
-    describe("perform", () => {
-        it("should perform the action", () => {
-            new Action().perform();
-        });
+  describe("perform", () => {
+    it("should perform the action", () => {
+      new Action().perform();
     });
+  });
 });
diff --git a/frontend/test/metabase-lib/Dimension.integ.spec.js b/frontend/test/metabase-lib/Dimension.integ.spec.js
index f247a6182b2f49714003826ee792743fda07062c..179556af33aeed5bb8387ca74704d4c1ad2d6a42 100644
--- a/frontend/test/metabase-lib/Dimension.integ.spec.js
+++ b/frontend/test/metabase-lib/Dimension.integ.spec.js
@@ -1,382 +1,372 @@
-import { createTestStore, useSharedAdminLogin } from "__support__/integrated_tests";
+import {
+  createTestStore,
+  useSharedAdminLogin,
+} from "__support__/integrated_tests";
 
 import {
-    ORDERS_TOTAL_FIELD_ID,
-    PRODUCT_CATEGORY_FIELD_ID,
-    ORDERS_CREATED_DATE_FIELD_ID,
-    ORDERS_PRODUCT_FK_FIELD_ID,
-    PRODUCT_TILE_FIELD_ID
+  ORDERS_TOTAL_FIELD_ID,
+  PRODUCT_CATEGORY_FIELD_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
+  ORDERS_PRODUCT_FK_FIELD_ID,
+  PRODUCT_TILE_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 import {
-    fetchDatabaseMetadata,
-    fetchTableMetadata
+  fetchDatabaseMetadata,
+  fetchTableMetadata,
 } from "metabase/redux/metadata";
 import { getMetadata } from "metabase/selectors/metadata";
 import Dimension from "metabase-lib/lib/Dimension";
 
 describe("Dimension classes", () => {
-    let metadata = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin();
-        const store = await createTestStore();
-        await store.dispatch(fetchDatabaseMetadata(1));
-        await store.dispatch(fetchTableMetadata(1));
-        await store.dispatch(fetchTableMetadata(2));
-        await store.dispatch(fetchTableMetadata(3));
-        metadata = getMetadata(store.getState());
-    });
+  let metadata = null;
 
-    describe("Dimension", () => {
-        describe("STATIC METHODS", () => {
-            describe("parseMBQL(mbql metadata)", () => {
-                it("parses and format MBQL correctly", () => {
-                    expect(Dimension.parseMBQL(1, metadata).mbql()).toEqual([
-                        "field-id",
-                        1
-                    ]);
-                    expect(
-                        Dimension.parseMBQL(["field-id", 1], metadata).mbql()
-                    ).toEqual(["field-id", 1]);
-                    expect(
-                        Dimension.parseMBQL(["fk->", 1, 2], metadata).mbql()
-                    ).toEqual(["fk->", 1, 2]);
-                    expect(
-                        Dimension.parseMBQL(
-                            ["datetime-field", 1, "month"],
-                            metadata
-                        ).mbql()
-                    ).toEqual(["datetime-field", ["field-id", 1], "month"]);
-                    expect(
-                        Dimension.parseMBQL(
-                            ["datetime-field", ["field-id", 1], "month"],
-                            metadata
-                        ).mbql()
-                    ).toEqual(["datetime-field", ["field-id", 1], "month"]);
-                    expect(
-                        Dimension.parseMBQL(
-                            ["datetime-field", ["fk->", 1, 2], "month"],
-                            metadata
-                        ).mbql()
-                    ).toEqual(["datetime-field", ["fk->", 1, 2], "month"]);
-                });
-            });
+  beforeAll(async () => {
+    useSharedAdminLogin();
+    const store = await createTestStore();
+    await store.dispatch(fetchDatabaseMetadata(1));
+    await store.dispatch(fetchTableMetadata(1));
+    await store.dispatch(fetchTableMetadata(2));
+    await store.dispatch(fetchTableMetadata(3));
+    metadata = getMetadata(store.getState());
+  });
 
-            describe("isEqual(other)", () => {
-                it("returns true for equivalent field-ids", () => {
-                    const d1 = Dimension.parseMBQL(1, metadata);
-                    const d2 = Dimension.parseMBQL(["field-id", 1], metadata);
-                    expect(d1.isEqual(d2)).toEqual(true);
-                    expect(d1.isEqual(["field-id", 1])).toEqual(true);
-                    expect(d1.isEqual(1)).toEqual(true);
-                });
-                it("returns false for different type clauses", () => {
-                    const d1 = Dimension.parseMBQL(["fk->", 1, 2], metadata);
-                    const d2 = Dimension.parseMBQL(["field-id", 1], metadata);
-                    expect(d1.isEqual(d2)).toEqual(false);
-                });
-                it("returns false for same type clauses with different arguments", () => {
-                    const d1 = Dimension.parseMBQL(["fk->", 1, 2], metadata);
-                    const d2 = Dimension.parseMBQL(["fk->", 1, 3], metadata);
-                    expect(d1.isEqual(d2)).toEqual(false);
-                });
-            });
+  describe("Dimension", () => {
+    describe("STATIC METHODS", () => {
+      describe("parseMBQL(mbql metadata)", () => {
+        it("parses and format MBQL correctly", () => {
+          expect(Dimension.parseMBQL(1, metadata).mbql()).toEqual([
+            "field-id",
+            1,
+          ]);
+          expect(Dimension.parseMBQL(["field-id", 1], metadata).mbql()).toEqual(
+            ["field-id", 1],
+          );
+          expect(Dimension.parseMBQL(["fk->", 1, 2], metadata).mbql()).toEqual([
+            "fk->",
+            1,
+            2,
+          ]);
+          expect(
+            Dimension.parseMBQL(
+              ["datetime-field", 1, "month"],
+              metadata,
+            ).mbql(),
+          ).toEqual(["datetime-field", ["field-id", 1], "month"]);
+          expect(
+            Dimension.parseMBQL(
+              ["datetime-field", ["field-id", 1], "month"],
+              metadata,
+            ).mbql(),
+          ).toEqual(["datetime-field", ["field-id", 1], "month"]);
+          expect(
+            Dimension.parseMBQL(
+              ["datetime-field", ["fk->", 1, 2], "month"],
+              metadata,
+            ).mbql(),
+          ).toEqual(["datetime-field", ["fk->", 1, 2], "month"]);
         });
+      });
 
-        describe("INSTANCE METHODS", () => {
-            describe("dimensions()", () => {
-                it("returns `dimension_options` of the underlying field if available", () => {
-                    pending();
-                });
-                it("returns sub-dimensions for matching dimension if no `dimension_options`", () => {
-                    // just a single scenario should be sufficient here as we will test
-                    // `static dimensions()` individually for each dimension
-                    pending();
-                });
-            });
-
-            describe("isSameBaseDimension(other)", () => {
-                it("returns true if the base dimensions are same", () => {
-                    pending();
-                });
-                it("returns false if the base dimensions don't match", () => {
-                    pending();
-                });
-            });
+      describe("isEqual(other)", () => {
+        it("returns true for equivalent field-ids", () => {
+          const d1 = Dimension.parseMBQL(1, metadata);
+          const d2 = Dimension.parseMBQL(["field-id", 1], metadata);
+          expect(d1.isEqual(d2)).toEqual(true);
+          expect(d1.isEqual(["field-id", 1])).toEqual(true);
+          expect(d1.isEqual(1)).toEqual(true);
+        });
+        it("returns false for different type clauses", () => {
+          const d1 = Dimension.parseMBQL(["fk->", 1, 2], metadata);
+          const d2 = Dimension.parseMBQL(["field-id", 1], metadata);
+          expect(d1.isEqual(d2)).toEqual(false);
+        });
+        it("returns false for same type clauses with different arguments", () => {
+          const d1 = Dimension.parseMBQL(["fk->", 1, 2], metadata);
+          const d2 = Dimension.parseMBQL(["fk->", 1, 3], metadata);
+          expect(d1.isEqual(d2)).toEqual(false);
         });
+      });
     });
 
-    describe("FieldIDDimension", () => {
-        let dimension = null;
-        let categoryDimension = null;
-        beforeAll(() => {
-            dimension = Dimension.parseMBQL(
-                ["field-id", ORDERS_TOTAL_FIELD_ID],
-                metadata
-            );
-            categoryDimension = Dimension.parseMBQL(
-                ["field-id", PRODUCT_CATEGORY_FIELD_ID],
-                metadata
-            );
+    describe("INSTANCE METHODS", () => {
+      describe("dimensions()", () => {
+        it("returns `dimension_options` of the underlying field if available", () => {
+          pending();
         });
+        it("returns sub-dimensions for matching dimension if no `dimension_options`", () => {
+          // just a single scenario should be sufficient here as we will test
+          // `static dimensions()` individually for each dimension
+          pending();
+        });
+      });
 
-        describe("INSTANCE METHODS", () => {
-            describe("mbql()", () => {
-                it('returns a "field-id" clause', () => {
-                    expect(dimension.mbql()).toEqual([
-                        "field-id",
-                        ORDERS_TOTAL_FIELD_ID
-                    ]);
-                });
-            });
-            describe("displayName()", () => {
-                it("returns the field name", () => {
-                    expect(dimension.displayName()).toEqual("Total");
-                });
-            });
-            describe("subDisplayName()", () => {
-                it("returns 'Default' for numeric fields", () => {
-                    expect(dimension.subDisplayName()).toEqual("Default");
-                });
-                it("returns 'Default' for non-numeric fields", () => {
-                    expect(
-                        Dimension.parseMBQL(
-                            ["field-id", PRODUCT_CATEGORY_FIELD_ID],
-                            metadata
-                        ).subDisplayName()
-                    ).toEqual("Default");
-                });
-            });
-            describe("subTriggerDisplayName()", () => {
-                it("returns 'Unbinned' if the dimension is a binnable number", () => {
-                    expect(dimension.subTriggerDisplayName()).toBe("Unbinned");
-                });
-                it("does not have a value if the dimension is a category", () => {
-                    expect(
-                        categoryDimension.subTriggerDisplayName()
-                    ).toBeFalsy();
-                });
-            });
+      describe("isSameBaseDimension(other)", () => {
+        it("returns true if the base dimensions are same", () => {
+          pending();
+        });
+        it("returns false if the base dimensions don't match", () => {
+          pending();
         });
+      });
     });
+  });
 
-    describe("FKDimension", () => {
-        let dimension = null;
-        beforeAll(() => {
-            dimension = Dimension.parseMBQL(
-                ["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_TILE_FIELD_ID],
-                metadata
-            );
-        });
+  describe("FieldIDDimension", () => {
+    let dimension = null;
+    let categoryDimension = null;
+    beforeAll(() => {
+      dimension = Dimension.parseMBQL(
+        ["field-id", ORDERS_TOTAL_FIELD_ID],
+        metadata,
+      );
+      categoryDimension = Dimension.parseMBQL(
+        ["field-id", PRODUCT_CATEGORY_FIELD_ID],
+        metadata,
+      );
+    });
 
-        describe("STATIC METHODS", () => {
-            describe("dimensions(parentDimension)", () => {
-                it("should return array of FK dimensions for foreign key field dimension", () => {
-                    pending();
-                    // Something like this:
-                    // fieldsInProductsTable = metadata.tables[1].fields.length;
-                    // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
-                });
-                it("should return empty array for non-FK field dimension", () => {
-                    pending();
-                });
-            });
+    describe("INSTANCE METHODS", () => {
+      describe("mbql()", () => {
+        it('returns a "field-id" clause', () => {
+          expect(dimension.mbql()).toEqual(["field-id", ORDERS_TOTAL_FIELD_ID]);
         });
-
-        describe("INSTANCE METHODS", () => {
-            describe("mbql()", () => {
-                it('returns a "fk->" clause', () => {
-                    expect(dimension.mbql()).toEqual([
-                        "fk->",
-                        ORDERS_PRODUCT_FK_FIELD_ID,
-                        PRODUCT_TILE_FIELD_ID
-                    ]);
-                });
-            });
-            describe("displayName()", () => {
-                it("returns the field name", () => {
-                    expect(dimension.displayName()).toEqual("Title");
-                });
-            });
-            describe("subDisplayName()", () => {
-                it("returns the field name", () => {
-                    expect(dimension.subDisplayName()).toEqual("Title");
-                });
-            });
-            describe("subTriggerDisplayName()", () => {
-                it("does not have a value", () => {
-                    expect(dimension.subTriggerDisplayName()).toBeFalsy();
-                });
-            });
+      });
+      describe("displayName()", () => {
+        it("returns the field name", () => {
+          expect(dimension.displayName()).toEqual("Total");
         });
+      });
+      describe("subDisplayName()", () => {
+        it("returns 'Default' for numeric fields", () => {
+          expect(dimension.subDisplayName()).toEqual("Default");
+        });
+        it("returns 'Default' for non-numeric fields", () => {
+          expect(
+            Dimension.parseMBQL(
+              ["field-id", PRODUCT_CATEGORY_FIELD_ID],
+              metadata,
+            ).subDisplayName(),
+          ).toEqual("Default");
+        });
+      });
+      describe("subTriggerDisplayName()", () => {
+        it("returns 'Unbinned' if the dimension is a binnable number", () => {
+          expect(dimension.subTriggerDisplayName()).toBe("Unbinned");
+        });
+        it("does not have a value if the dimension is a category", () => {
+          expect(categoryDimension.subTriggerDisplayName()).toBeFalsy();
+        });
+      });
     });
+  });
 
-    describe("DatetimeFieldDimension", () => {
-        let dimension = null;
-        beforeAll(() => {
-            dimension = Dimension.parseMBQL(
-                ["datetime-field", ORDERS_CREATED_DATE_FIELD_ID, "month"],
-                metadata
-            );
-        });
+  describe("FKDimension", () => {
+    let dimension = null;
+    beforeAll(() => {
+      dimension = Dimension.parseMBQL(
+        ["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_TILE_FIELD_ID],
+        metadata,
+      );
+    });
 
-        describe("STATIC METHODS", () => {
-            describe("dimensions(parentDimension)", () => {
-                it("should return an array with dimensions for each datetime unit", () => {
-                    pending();
-                    // Something like this:
-                    // fieldsInProductsTable = metadata.tables[1].fields.length;
-                    // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
-                });
-                it("should return empty array for non-date field dimension", () => {
-                    pending();
-                });
-            });
-            describe("defaultDimension(parentDimension)", () => {
-                it("should return dimension with 'day' datetime unit", () => {
-                    pending();
-                });
-                it("should return null for non-date field dimension", () => {
-                    pending();
-                });
-            });
+    describe("STATIC METHODS", () => {
+      describe("dimensions(parentDimension)", () => {
+        it("should return array of FK dimensions for foreign key field dimension", () => {
+          pending();
+          // Something like this:
+          // fieldsInProductsTable = metadata.tables[1].fields.length;
+          // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
         });
+        it("should return empty array for non-FK field dimension", () => {
+          pending();
+        });
+      });
+    });
 
-        describe("INSTANCE METHODS", () => {
-            describe("mbql()", () => {
-                it('returns a "datetime-field" clause', () => {
-                    expect(dimension.mbql()).toEqual([
-                        "datetime-field",
-                        ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
-                        "month"
-                    ]);
-                });
-            });
-            describe("displayName()", () => {
-                it("returns the field name", () => {
-                    expect(dimension.displayName()).toEqual("Created At");
-                });
-            });
-            describe("subDisplayName()", () => {
-                it("returns 'Month'", () => {
-                    expect(dimension.subDisplayName()).toEqual("Month");
-                });
-            });
-            describe("subTriggerDisplayName()", () => {
-                it("returns 'by month'", () => {
-                    expect(dimension.subTriggerDisplayName()).toEqual(
-                        "by month"
-                    );
-                });
-            });
+    describe("INSTANCE METHODS", () => {
+      describe("mbql()", () => {
+        it('returns a "fk->" clause', () => {
+          expect(dimension.mbql()).toEqual([
+            "fk->",
+            ORDERS_PRODUCT_FK_FIELD_ID,
+            PRODUCT_TILE_FIELD_ID,
+          ]);
+        });
+      });
+      describe("displayName()", () => {
+        it("returns the field name", () => {
+          expect(dimension.displayName()).toEqual("Title");
+        });
+      });
+      describe("subDisplayName()", () => {
+        it("returns the field name", () => {
+          expect(dimension.subDisplayName()).toEqual("Title");
+        });
+      });
+      describe("subTriggerDisplayName()", () => {
+        it("does not have a value", () => {
+          expect(dimension.subTriggerDisplayName()).toBeFalsy();
         });
+      });
     });
+  });
 
-    describe("BinningStrategyDimension", () => {
-        let dimension = null;
-        beforeAll(() => {
-            dimension = Dimension.parseMBQL(
-                ["field-id", ORDERS_TOTAL_FIELD_ID],
-                metadata
-            ).dimensions()[1];
+  describe("DatetimeFieldDimension", () => {
+    let dimension = null;
+    beforeAll(() => {
+      dimension = Dimension.parseMBQL(
+        ["datetime-field", ORDERS_CREATED_DATE_FIELD_ID, "month"],
+        metadata,
+      );
+    });
+
+    describe("STATIC METHODS", () => {
+      describe("dimensions(parentDimension)", () => {
+        it("should return an array with dimensions for each datetime unit", () => {
+          pending();
+          // Something like this:
+          // fieldsInProductsTable = metadata.tables[1].fields.length;
+          // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
+        });
+        it("should return empty array for non-date field dimension", () => {
+          pending();
         });
+      });
+      describe("defaultDimension(parentDimension)", () => {
+        it("should return dimension with 'day' datetime unit", () => {
+          pending();
+        });
+        it("should return null for non-date field dimension", () => {
+          pending();
+        });
+      });
+    });
 
-        describe("STATIC METHODS", () => {
-            describe("dimensions(parentDimension)", () => {
-                it("should return an array of dimensions based on default binning", () => {
-                    pending();
-                });
-                it("should return empty array for non-number field dimension", () => {
-                    pending();
-                });
-            });
+    describe("INSTANCE METHODS", () => {
+      describe("mbql()", () => {
+        it('returns a "datetime-field" clause', () => {
+          expect(dimension.mbql()).toEqual([
+            "datetime-field",
+            ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+            "month",
+          ]);
         });
+      });
+      describe("displayName()", () => {
+        it("returns the field name", () => {
+          expect(dimension.displayName()).toEqual("Created At");
+        });
+      });
+      describe("subDisplayName()", () => {
+        it("returns 'Month'", () => {
+          expect(dimension.subDisplayName()).toEqual("Month");
+        });
+      });
+      describe("subTriggerDisplayName()", () => {
+        it("returns 'by month'", () => {
+          expect(dimension.subTriggerDisplayName()).toEqual("by month");
+        });
+      });
+    });
+  });
 
-        describe("INSTANCE METHODS", () => {
-            describe("mbql()", () => {
-                it('returns a "binning-strategy" clause', () => {
-                    expect(dimension.mbql()).toEqual([
-                        "binning-strategy",
-                        ["field-id", ORDERS_TOTAL_FIELD_ID],
-                        "num-bins",
-                        10
-                    ]);
-                });
-            });
-            describe("displayName()", () => {
-                it("returns the field name", () => {
-                    expect(dimension.displayName()).toEqual("Total");
-                });
-            });
-            describe("subDisplayName()", () => {
-                it("returns '10 bins'", () => {
-                    expect(dimension.subDisplayName()).toEqual("10 bins");
-                });
-            });
+  describe("BinningStrategyDimension", () => {
+    let dimension = null;
+    beforeAll(() => {
+      dimension = Dimension.parseMBQL(
+        ["field-id", ORDERS_TOTAL_FIELD_ID],
+        metadata,
+      ).dimensions()[1];
+    });
 
-            describe("subTriggerDisplayName()", () => {
-                it("returns '10 bins'", () => {
-                    expect(dimension.subTriggerDisplayName()).toEqual(
-                        "10 bins"
-                    );
-                });
-            });
+    describe("STATIC METHODS", () => {
+      describe("dimensions(parentDimension)", () => {
+        it("should return an array of dimensions based on default binning", () => {
+          pending();
         });
+        it("should return empty array for non-number field dimension", () => {
+          pending();
+        });
+      });
     });
 
-    describe("ExpressionDimension", () => {
-        let dimension = null;
-        beforeAll(() => {
-            dimension = Dimension.parseMBQL(
-                ["expression", "Hello World"],
-                metadata
-            );
+    describe("INSTANCE METHODS", () => {
+      describe("mbql()", () => {
+        it('returns a "binning-strategy" clause', () => {
+          expect(dimension.mbql()).toEqual([
+            "binning-strategy",
+            ["field-id", ORDERS_TOTAL_FIELD_ID],
+            "num-bins",
+            10,
+          ]);
+        });
+      });
+      describe("displayName()", () => {
+        it("returns the field name", () => {
+          expect(dimension.displayName()).toEqual("Total");
+        });
+      });
+      describe("subDisplayName()", () => {
+        it("returns '10 bins'", () => {
+          expect(dimension.subDisplayName()).toEqual("10 bins");
         });
+      });
 
-        describe("STATIC METHODS", () => {
-            describe("dimensions(parentDimension)", () => {
-                it("should return array of FK dimensions for foreign key field dimension", () => {
-                    pending();
-                    // Something like this:
-                    // fieldsInProductsTable = metadata.tables[1].fields.length;
-                    // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
-                });
-                it("should return empty array for non-FK field dimension", () => {
-                    pending();
-                });
-            });
+      describe("subTriggerDisplayName()", () => {
+        it("returns '10 bins'", () => {
+          expect(dimension.subTriggerDisplayName()).toEqual("10 bins");
         });
+      });
+    });
+  });
+
+  describe("ExpressionDimension", () => {
+    let dimension = null;
+    beforeAll(() => {
+      dimension = Dimension.parseMBQL(["expression", "Hello World"], metadata);
+    });
 
-        describe("INSTANCE METHODS", () => {
-            describe("mbql()", () => {
-                it('returns an "expression" clause', () => {
-                    expect(dimension.mbql()).toEqual([
-                        "expression",
-                        "Hello World"
-                    ]);
-                });
-            });
-            describe("displayName()", () => {
-                it("returns the expression name", () => {
-                    expect(dimension.displayName()).toEqual("Hello World");
-                });
-            });
+    describe("STATIC METHODS", () => {
+      describe("dimensions(parentDimension)", () => {
+        it("should return array of FK dimensions for foreign key field dimension", () => {
+          pending();
+          // Something like this:
+          // fieldsInProductsTable = metadata.tables[1].fields.length;
+          // expect(FKDimension.dimensions(fkFieldIdDimension).length).toEqual(fieldsInProductsTable);
         });
+        it("should return empty array for non-FK field dimension", () => {
+          pending();
+        });
+      });
     });
 
-    describe("AggregationDimension", () => {
-        let dimension = null;
-        beforeAll(() => {
-            dimension = Dimension.parseMBQL(["aggregation", 1], metadata);
+    describe("INSTANCE METHODS", () => {
+      describe("mbql()", () => {
+        it('returns an "expression" clause', () => {
+          expect(dimension.mbql()).toEqual(["expression", "Hello World"]);
+        });
+      });
+      describe("displayName()", () => {
+        it("returns the expression name", () => {
+          expect(dimension.displayName()).toEqual("Hello World");
         });
+      });
+    });
+  });
+
+  describe("AggregationDimension", () => {
+    let dimension = null;
+    beforeAll(() => {
+      dimension = Dimension.parseMBQL(["aggregation", 1], metadata);
+    });
 
-        describe("INSTANCE METHODS", () => {
-            describe("mbql()", () => {
-                it('returns an "aggregation" clause', () => {
-                    expect(dimension.mbql()).toEqual(["aggregation", 1]);
-                });
-            });
+    describe("INSTANCE METHODS", () => {
+      describe("mbql()", () => {
+        it('returns an "aggregation" clause', () => {
+          expect(dimension.mbql()).toEqual(["aggregation", 1]);
         });
+      });
     });
+  });
 });
diff --git a/frontend/test/metabase-lib/Mode.unit.spec.js b/frontend/test/metabase-lib/Mode.unit.spec.js
index 5cc71c8f8fb3d3ae1a4e71c97041c638cbe5f5b7..bfb760ead2e2b60f5414cb26f0920a146791e941 100644
--- a/frontend/test/metabase-lib/Mode.unit.spec.js
+++ b/frontend/test/metabase-lib/Mode.unit.spec.js
@@ -1,106 +1,102 @@
 import {
-    metadata,
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    orders_raw_card
+  metadata,
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  orders_raw_card,
 } from "__support__/sample_dataset_fixture";
 
 import Question from "metabase-lib/lib/Question";
 
 describe("Mode", () => {
-    const rawDataQuestionMode = new Question(metadata, orders_raw_card).mode();
-    const timeBreakoutQuestionMode = Question.create({
-        databaseId: DATABASE_ID,
-        tableId: ORDERS_TABLE_ID,
-        metadata
-    })
-        .query()
-        .addAggregation(["count"])
-        .addBreakout(["datetime-field", ["field-id", 1], "day"])
-        .question()
-        .setDisplay("table")
-        .mode();
+  const rawDataQuestionMode = new Question(metadata, orders_raw_card).mode();
+  const timeBreakoutQuestionMode = Question.create({
+    databaseId: DATABASE_ID,
+    tableId: ORDERS_TABLE_ID,
+    metadata,
+  })
+    .query()
+    .addAggregation(["count"])
+    .addBreakout(["datetime-field", ["field-id", 1], "day"])
+    .question()
+    .setDisplay("table")
+    .mode();
 
-    describe("forQuestion(question)", () => {
-        describe("with structured query question", () => {
-            // testbed for generative testing? see http://leebyron.com/testcheck-js
+  describe("forQuestion(question)", () => {
+    describe("with structured query question", () => {
+      // testbed for generative testing? see http://leebyron.com/testcheck-js
 
-            it("returns `segment` mode with raw data", () => {});
+      it("returns `segment` mode with raw data", () => {});
 
-            it("returns `metric` mode with >= 1 aggregations", () => {});
+      it("returns `metric` mode with >= 1 aggregations", () => {});
 
-            it("returns `timeseries` mode with >=1 aggregations and date breakout", () => {});
-            it("returns `timeseries` mode with >=1 aggregations and date + category breakout", () => {});
+      it("returns `timeseries` mode with >=1 aggregations and date breakout", () => {});
+      it("returns `timeseries` mode with >=1 aggregations and date + category breakout", () => {});
 
-            it("returns `geo` mode with >=1 aggregations and an address breakout", () => {});
+      it("returns `geo` mode with >=1 aggregations and an address breakout", () => {});
 
-            it("returns `pivot` mode with >=1 aggregations and 1-2 category breakouts", () => {});
+      it("returns `pivot` mode with >=1 aggregations and 1-2 category breakouts", () => {});
 
-            it("returns `default` mode with >=0 aggregations and >=3 breakouts", () => {});
-            it("returns `default` mode with >=1 aggregations and >=1 breakouts when first neither date or category", () => {});
-        });
-        describe("with native query question", () => {
-            it("returns `NativeMode` for empty query", () => {});
-            it("returns `NativeMode` for query with query text", () => {});
-        });
-        describe("with oddly constructed query", () => {
-            it("should throw an error", () => {
-                // this is not the actual behavior atm (it returns DefaultMode)
-            });
-        });
+      it("returns `default` mode with >=0 aggregations and >=3 breakouts", () => {});
+      it("returns `default` mode with >=1 aggregations and >=1 breakouts when first neither date or category", () => {});
     });
-
-    describe("name()", () => {
-        it("returns the correct name of current mode", () => {});
+    describe("with native query question", () => {
+      it("returns `NativeMode` for empty query", () => {});
+      it("returns `NativeMode` for query with query text", () => {});
     });
+    describe("with oddly constructed query", () => {
+      it("should throw an error", () => {
+        // this is not the actual behavior atm (it returns DefaultMode)
+      });
+    });
+  });
 
-    describe("actions()", () => {
-        describe("for a new question with Orders table and Raw data aggregation", () => {
-            pending();
-            it("returns a correct number of mode actions", () => {
-                expect(rawDataQuestionMode.actions().length).toBe(3);
-            });
-            it("returns a defined metric as mode action 1", () => {
-                expect(rawDataQuestionMode.actions()[0].name).toBe(
-                    "common-metric"
-                );
-                // TODO: Sameer 6/16/17
-                // This is wack and not really testable. We shouldn't be passing around react components in this imo
-                // expect(question.actions()[1].title.props.children).toBe("Total Order Value");
-            });
-            it("returns a count timeseries as mode action 2", () => {
-                expect(rawDataQuestionMode.actions()[1].name).toBe(
-                    "count-by-time"
-                );
-                expect(rawDataQuestionMode.actions()[1].icon).toBe("line");
-                // TODO: Sameer 6/16/17
-                // This is wack and not really testable. We shouldn't be passing around react components in this imo
-                // expect(question.actions()[2].title.props.children).toBe("Count of rows by time");
-            });
-            it("returns summarize as mode action 3", () => {
-                expect(rawDataQuestionMode.actions()[2].name).toBe("summarize");
-                expect(rawDataQuestionMode.actions()[2].icon).toBe("sum");
-                expect(rawDataQuestionMode.actions()[2].title).toBe(
-                    "Summarize this segment"
-                );
-            });
-        });
+  describe("name()", () => {
+    it("returns the correct name of current mode", () => {});
+  });
 
-        describe("for a question with an aggregation and a time breakout", () => {
-            it("has pivot as mode actions 1 and 2", () => {
-                expect(timeBreakoutQuestionMode.actions().length).toBe(3);
-                expect(timeBreakoutQuestionMode.actions()[0].name).toBe(
-                    "pivot-by-category"
-                );
-                expect(timeBreakoutQuestionMode.actions()[1].name).toBe(
-                    "pivot-by-location"
-                );
-            });
-        });
+  describe("actions()", () => {
+    describe("for a new question with Orders table and Raw data aggregation", () => {
+      pending();
+      it("returns a correct number of mode actions", () => {
+        expect(rawDataQuestionMode.actions().length).toBe(3);
+      });
+      it("returns a defined metric as mode action 1", () => {
+        expect(rawDataQuestionMode.actions()[0].name).toBe("common-metric");
+        // TODO: Sameer 6/16/17
+        // This is wack and not really testable. We shouldn't be passing around react components in this imo
+        // expect(question.actions()[1].title.props.children).toBe("Total Order Value");
+      });
+      it("returns a count timeseries as mode action 2", () => {
+        expect(rawDataQuestionMode.actions()[1].name).toBe("count-by-time");
+        expect(rawDataQuestionMode.actions()[1].icon).toBe("line");
+        // TODO: Sameer 6/16/17
+        // This is wack and not really testable. We shouldn't be passing around react components in this imo
+        // expect(question.actions()[2].title.props.children).toBe("Count of rows by time");
+      });
+      it("returns summarize as mode action 3", () => {
+        expect(rawDataQuestionMode.actions()[2].name).toBe("summarize");
+        expect(rawDataQuestionMode.actions()[2].icon).toBe("sum");
+        expect(rawDataQuestionMode.actions()[2].title).toBe(
+          "Summarize this segment",
+        );
+      });
     });
 
-    describe("actionsForClick()", () => {
-        // this is action-specific so just rudimentary tests here showing that the actionsForClick logic works
-        // Action-specific tests would optimally be in their respective test files
+    describe("for a question with an aggregation and a time breakout", () => {
+      it("has pivot as mode actions 1 and 2", () => {
+        expect(timeBreakoutQuestionMode.actions().length).toBe(3);
+        expect(timeBreakoutQuestionMode.actions()[0].name).toBe(
+          "pivot-by-category",
+        );
+        expect(timeBreakoutQuestionMode.actions()[1].name).toBe(
+          "pivot-by-location",
+        );
+      });
     });
+  });
+
+  describe("actionsForClick()", () => {
+    // this is action-specific so just rudimentary tests here showing that the actionsForClick logic works
+    // Action-specific tests would optimally be in their respective test files
+  });
 });
diff --git a/frontend/test/metabase-lib/Question.integ.spec.js b/frontend/test/metabase-lib/Question.integ.spec.js
index 25c1ba3b87db905285de618d3f54b1f1efdb46be..4bbb57452fd063a1f105d9cfee2b9c327d2ef7fc 100644
--- a/frontend/test/metabase-lib/Question.integ.spec.js
+++ b/frontend/test/metabase-lib/Question.integ.spec.js
@@ -1,7 +1,7 @@
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
 import Question from "metabase-lib/lib/Question";
 import { useSharedAdminLogin } from "__support__/integrated_tests";
@@ -11,75 +11,75 @@ import { NATIVE_QUERY_TEMPLATE } from "metabase-lib/lib/queries/NativeQuery";
 // and check that the result is correct
 
 describe("Question", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    });
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-    describe("with SQL questions", () => {
-        it("should return correct result with a static template tag parameter", async () => {
-            const templateTagName = "orderid";
-            const templateTagId = "f1cb12ed3-8727-41b6-bbb4-b7ba31884c30";
-            const question = Question.create({
-                databaseId: DATABASE_ID,
-                tableId: ORDERS_TABLE_ID,
-                metadata
-            }).setDatasetQuery({
-                ...NATIVE_QUERY_TEMPLATE,
-                database: DATABASE_ID,
-                native: {
-                    query: `SELECT SUBTOTAL FROM ORDERS WHERE id = {{${templateTagName}}}`,
-                    template_tags: {
-                        [templateTagName]: {
-                            id: templateTagId,
-                            name: templateTagName,
-                            display_name: "Order ID",
-                            type: "number"
-                        }
-                    }
-                }
-            });
+  describe("with SQL questions", () => {
+    it("should return correct result with a static template tag parameter", async () => {
+      const templateTagName = "orderid";
+      const templateTagId = "f1cb12ed3-8727-41b6-bbb4-b7ba31884c30";
+      const question = Question.create({
+        databaseId: DATABASE_ID,
+        tableId: ORDERS_TABLE_ID,
+        metadata,
+      }).setDatasetQuery({
+        ...NATIVE_QUERY_TEMPLATE,
+        database: DATABASE_ID,
+        native: {
+          query: `SELECT SUBTOTAL FROM ORDERS WHERE id = {{${templateTagName}}}`,
+          template_tags: {
+            [templateTagName]: {
+              id: templateTagId,
+              name: templateTagName,
+              display_name: "Order ID",
+              type: "number",
+            },
+          },
+        },
+      });
 
-            // Without a template tag the query should fail
-            const results1 = await question.apiGetResults({ ignoreCache: true });
-            expect(results1[0].status).toBe("failed");
+      // Without a template tag the query should fail
+      const results1 = await question.apiGetResults({ ignoreCache: true });
+      expect(results1[0].status).toBe("failed");
 
-            question._parameterValues = { [templateTagId]: "5" };
-            const results2 = await question.apiGetResults({ ignoreCache: true });
-            expect(results2[0]).toBeDefined();
-            expect(results2[0].data.rows[0][0]).toEqual(116.35497575401975);
-        });
+      question._parameterValues = { [templateTagId]: "5" };
+      const results2 = await question.apiGetResults({ ignoreCache: true });
+      expect(results2[0]).toBeDefined();
+      expect(results2[0].data.rows[0][0]).toEqual(116.35497575401975);
+    });
 
-        it("should return correct result with an optional template tag clause", async () => {
-            const templateTagName = "orderid";
-            const templateTagId = "f1cb12ed3-8727-41b6-bbb4-b7ba31884c30";
-            const question = Question.create({
-                databaseId: DATABASE_ID,
-                tableId: ORDERS_TABLE_ID,
-                metadata
-            }).setDatasetQuery({
-                ...NATIVE_QUERY_TEMPLATE,
-                database: DATABASE_ID,
-                native: {
-                    query: `SELECT SUBTOTAL FROM ORDERS [[WHERE id = {{${templateTagName}}}]]`,
-                    template_tags: {
-                        [templateTagName]: {
-                            id: templateTagId,
-                            name: templateTagName,
-                            display_name: "Order ID",
-                            type: "number"
-                        }
-                    }
-                }
-            });
+    it("should return correct result with an optional template tag clause", async () => {
+      const templateTagName = "orderid";
+      const templateTagId = "f1cb12ed3-8727-41b6-bbb4-b7ba31884c30";
+      const question = Question.create({
+        databaseId: DATABASE_ID,
+        tableId: ORDERS_TABLE_ID,
+        metadata,
+      }).setDatasetQuery({
+        ...NATIVE_QUERY_TEMPLATE,
+        database: DATABASE_ID,
+        native: {
+          query: `SELECT SUBTOTAL FROM ORDERS [[WHERE id = {{${templateTagName}}}]]`,
+          template_tags: {
+            [templateTagName]: {
+              id: templateTagId,
+              name: templateTagName,
+              display_name: "Order ID",
+              type: "number",
+            },
+          },
+        },
+      });
 
-            const results1 = await question.apiGetResults({ ignoreCache: true });
-            expect(results1[0]).toBeDefined();
-            expect(results1[0].data.rows.length).toEqual(10000);
+      const results1 = await question.apiGetResults({ ignoreCache: true });
+      expect(results1[0]).toBeDefined();
+      expect(results1[0].data.rows.length).toEqual(10000);
 
-            question._parameterValues = { [templateTagId]: "5" };
-            const results2 = await question.apiGetResults({ ignoreCache: true });
-            expect(results2[0]).toBeDefined();
-            expect(results2[0].data.rows[0][0]).toEqual(116.35497575401975);
-        });
+      question._parameterValues = { [templateTagId]: "5" };
+      const results2 = await question.apiGetResults({ ignoreCache: true });
+      expect(results2[0]).toBeDefined();
+      expect(results2[0].data.rows[0][0]).toEqual(116.35497575401975);
     });
+  });
 });
diff --git a/frontend/test/metabase-lib/Question.unit.spec.js b/frontend/test/metabase-lib/Question.unit.spec.js
index 7cf9fe8e939530401082f7620fdc3444d2c9ab43..1e8c1221a47ee110a962e4aa2e02911bb2ae6c45 100644
--- a/frontend/test/metabase-lib/Question.unit.spec.js
+++ b/frontend/test/metabase-lib/Question.unit.spec.js
@@ -1,17 +1,17 @@
 import {
-    metadata,
-    ORDERS_PK_FIELD_ID,
-    PRODUCT_CATEGORY_FIELD_ID,
-    ORDERS_CREATED_DATE_FIELD_ID,
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    ORDERS_PRODUCT_FK_FIELD_ID,
-    card,
-    orders_raw_card,
-    orders_count_card,
-    orders_count_by_id_card,
-    native_orders_count_card,
-    invalid_orders_count_card
+  metadata,
+  ORDERS_PK_FIELD_ID,
+  PRODUCT_CATEGORY_FIELD_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  ORDERS_PRODUCT_FK_FIELD_ID,
+  card,
+  orders_raw_card,
+  orders_count_card,
+  orders_count_by_id_card,
+  native_orders_count_card,
+  invalid_orders_count_card,
 } from "__support__/sample_dataset_fixture";
 
 import Question from "metabase-lib/lib/Question";
@@ -19,558 +19,521 @@ import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 
 describe("Question", () => {
-    describe("CREATED WITH", () => {
-        describe("new Question(metadata, alreadyDefinedCard)", () => {
-            const question = new Question(metadata, orders_raw_card);
-            it("isn't empty", () => {
-                expect(question.isEmpty()).toBe(false);
-            });
-            it("has an id", () => {
-                expect(question.id()).toBe(orders_raw_card.id);
-            });
-            it("has a name", () => {
-                expect(question.displayName()).toBe(orders_raw_card.name);
-            });
-            it("is runnable", () => {
-                expect(question.canRun()).toBe(true);
-            });
-            it("has correct display settings", () => {
-                expect(question.display()).toBe("table");
-            });
-            it("has correct mode", () => {
-                expect(question.mode().name()).toBe("segment");
-            });
-        });
+  describe("CREATED WITH", () => {
+    describe("new Question(metadata, alreadyDefinedCard)", () => {
+      const question = new Question(metadata, orders_raw_card);
+      it("isn't empty", () => {
+        expect(question.isEmpty()).toBe(false);
+      });
+      it("has an id", () => {
+        expect(question.id()).toBe(orders_raw_card.id);
+      });
+      it("has a name", () => {
+        expect(question.displayName()).toBe(orders_raw_card.name);
+      });
+      it("is runnable", () => {
+        expect(question.canRun()).toBe(true);
+      });
+      it("has correct display settings", () => {
+        expect(question.display()).toBe("table");
+      });
+      it("has correct mode", () => {
+        expect(question.mode().name()).toBe("segment");
+      });
+    });
 
-        describe("Question.create(...)", () => {
-            const question = Question.create({
-                metadata,
-                databaseId: DATABASE_ID,
-                tableId: ORDERS_TABLE_ID
-            });
-
-            it("contains an empty structured query", () => {
-                expect(question.query().constructor).toBe(StructuredQuery);
-                expect(question.query().constructor).toBe(StructuredQuery);
-            });
-
-            it("defaults to table display", () => {
-                expect(question.display()).toEqual("table");
-            });
-        });
+    describe("Question.create(...)", () => {
+      const question = Question.create({
+        metadata,
+        databaseId: DATABASE_ID,
+        tableId: ORDERS_TABLE_ID,
+      });
+
+      it("contains an empty structured query", () => {
+        expect(question.query().constructor).toBe(StructuredQuery);
+        expect(question.query().constructor).toBe(StructuredQuery);
+      });
+
+      it("defaults to table display", () => {
+        expect(question.display()).toEqual("table");
+      });
+    });
+  });
+
+  describe("STATUS METHODS", () => {
+    describe("canRun()", () => {
+      it("You should be able to run a newly created query", () => {
+        const question = new Question(metadata, orders_raw_card);
+        expect(question.canRun()).toBe(true);
+      });
+    });
+    describe("canWrite()", () => {
+      it("You should be able to write to a question you have permissions to", () => {
+        const question = new Question(metadata, orders_raw_card);
+        expect(question.canWrite()).toBe(true);
+      });
+      it("You should not be able to write to a question you dont  have permissions to", () => {
+        const question = new Question(metadata, orders_count_by_id_card);
+        expect(question.canWrite()).toBe(false);
+      });
+    });
+    describe("isSaved()", () => {
+      it("A newly created query doesn't have an id and shouldn't be marked as isSaved()", () => {
+        const question = new Question(metadata, card);
+        expect(question.isSaved()).toBe(false);
+      });
+      it("A saved question does have an id and should be marked as isSaved()", () => {
+        const question = new Question(metadata, orders_raw_card);
+        expect(question.isSaved()).toBe(true);
+      });
+    });
+  });
+
+  describe("CARD METHODS", () => {
+    describe("card()", () => {
+      it("A question wraps a query/card and you can see the underlying card with card()", () => {
+        const question = new Question(metadata, orders_raw_card);
+        expect(question.card()).toEqual(orders_raw_card);
+      });
     });
 
-    describe("STATUS METHODS", () => {
-        describe("canRun()", () => {
-            it("You should be able to run a newly created query", () => {
-                const question = new Question(metadata, orders_raw_card);
-                expect(question.canRun()).toBe(true);
-            });
-        });
-        describe("canWrite()", () => {
-            it("You should be able to write to a question you have permissions to", () => {
-                const question = new Question(metadata, orders_raw_card);
-                expect(question.canWrite()).toBe(true);
-            });
-            it("You should not be able to write to a question you dont  have permissions to", () => {
-                const question = new Question(
-                    metadata,
-                    orders_count_by_id_card
-                );
-                expect(question.canWrite()).toBe(false);
-            });
+    describe("setCard(card)", () => {
+      it("changes the underlying card", () => {
+        const question = new Question(metadata, orders_raw_card);
+        expect(question.card()).toEqual(orders_raw_card);
+        const newQustion = question.setCard(orders_count_by_id_card);
+        expect(question.card()).toEqual(orders_raw_card);
+        expect(newQustion.card()).toEqual(orders_count_by_id_card);
+      });
+    });
+  });
+
+  describe("At the heart of a question is an MBQL query.", () => {
+    describe("query()", () => {
+      it("returns a correct class instance for structured query", () => {
+        const question = new Question(metadata, orders_raw_card);
+        // This is a bit wack, and the repetitive naming is pretty confusing.
+        const query = question.query();
+        expect(query instanceof StructuredQuery).toBe(true);
+      });
+      it("returns a correct class instance for native query", () => {
+        const question = new Question(metadata, native_orders_count_card);
+        const query = question.query();
+        expect(query instanceof NativeQuery).toBe(true);
+      });
+      it("throws an error for invalid queries", () => {
+        const question = new Question(metadata, invalid_orders_count_card);
+        expect(question.query).toThrow();
+      });
+    });
+    describe("setQuery(query)", () => {
+      it("updates the dataset_query of card", () => {
+        const question = new Question(metadata, orders_raw_card);
+        const rawQuery = new Question(
+          metadata,
+          native_orders_count_card,
+        ).query();
+
+        const newRawQuestion = question.setQuery(rawQuery);
+
+        expect(newRawQuestion.query() instanceof NativeQuery).toBe(true);
+      });
+    });
+    describe("setDatasetQuery(datasetQuery)", () => {
+      it("updates the dataset_query of card", () => {
+        const question = new Question(metadata, orders_raw_card);
+        const rawQuestion = question.setDatasetQuery(
+          native_orders_count_card.dataset_query,
+        );
+
+        expect(rawQuestion.query() instanceof NativeQuery).toBe(true);
+      });
+    });
+  });
+
+  describe("RESETTING METHODS", () => {
+    describe("withoutNameAndId()", () => {
+      it("unsets the name and id", () => {
+        const question = new Question(metadata, orders_raw_card);
+        const newQuestion = question.withoutNameAndId();
+
+        expect(newQuestion.id()).toBeUndefined();
+        expect(newQuestion.displayName()).toBeUndefined();
+      });
+      it("retains the dataset query", () => {
+        const question = new Question(metadata, orders_raw_card);
+
+        expect(question.id()).toBeDefined();
+        expect(question.displayName()).toBeDefined();
+      });
+    });
+  });
+
+  describe("VISUALIZATION METHODS", () => {
+    describe("display()", () => {
+      it("returns the card's visualization type", () => {
+        const question = new Question(metadata, orders_raw_card);
+        // this forces a table view
+        const tableQuestion = question.toUnderlyingData();
+        // Not sure I'm a huge fan of magic strings here.
+        expect(tableQuestion.display()).toBe("table");
+      });
+    });
+    describe("setDisplay(display)", () => {
+      it("sets the card's visualization type", () => {
+        const question = new Question(metadata, orders_raw_card);
+        // Not sure I'm a huge fan of magic strings here.
+        const scalarQuestion = question.setDisplay("scalar");
+        expect(scalarQuestion.display()).toBe("scalar");
+      });
+    });
+  });
+
+  // TODO: These are mode-dependent and should probably be tied to modes
+  // At the same time, the choice that which actions are visible depend on the question's properties
+  // as actions are filtered using those
+  describe("METHODS FOR DRILL-THROUGH / ACTION WIDGET", () => {
+    const rawDataQuestion = new Question(metadata, orders_raw_card);
+    const timeBreakoutQuestion = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
+    })
+      .query()
+      .addAggregation(["count"])
+      .addBreakout(["datetime-field", ["field-id", 1], "day"])
+      .question()
+      .setDisplay("table");
+
+    describe("mode()", () => {
+      describe("for a new question with Orders table and Raw data aggregation", () => {
+        it("returns the correct mode", () => {
+          expect(rawDataQuestion.mode().name()).toBe("segment");
         });
-        describe("isSaved()", () => {
-            it("A newly created query doesn't have an id and shouldn't be marked as isSaved()", () => {
-                const question = new Question(metadata, card);
-                expect(question.isSaved()).toBe(false);
-            });
-            it("A saved question does have an id and should be marked as isSaved()", () => {
-                const question = new Question(metadata, orders_raw_card);
-                expect(question.isSaved()).toBe(true);
-            });
+      });
+      describe("for a question with an aggregation and a time breakout", () => {
+        it("returns the correct mode", () => {
+          expect(timeBreakoutQuestion.mode().name()).toBe("timeseries");
         });
+      });
     });
 
-    describe("CARD METHODS", () => {
-        describe("card()", () => {
-            it("A question wraps a query/card and you can see the underlying card with card()", () => {
-                const question = new Question(metadata, orders_raw_card);
-                expect(question.card()).toEqual(orders_raw_card);
-            });
-        });
-
-        describe("setCard(card)", () => {
-            it("changes the underlying card", () => {
-                const question = new Question(metadata, orders_raw_card);
-                expect(question.card()).toEqual(orders_raw_card);
-                const newQustion = question.setCard(orders_count_by_id_card);
-                expect(question.card()).toEqual(orders_raw_card);
-                expect(newQustion.card()).toEqual(orders_count_by_id_card);
-            });
-        });
+    describe("summarize(...)", async () => {
+      const question = new Question(metadata, orders_raw_card);
+      it("returns the correct query for a summarization of a raw data table", () => {
+        const summarizedQuestion = question.summarize(["count"]);
+        expect(summarizedQuestion.canRun()).toBe(true);
+        // if I actually call the .query() method below, this blows up garbage collection =/
+        expect(summarizedQuestion._card.dataset_query).toEqual(
+          orders_count_card.dataset_query,
+        );
+      });
     });
 
-    describe("At the heart of a question is an MBQL query.", () => {
-        describe("query()", () => {
-            it("returns a correct class instance for structured query", () => {
-                const question = new Question(metadata, orders_raw_card);
-                // This is a bit wack, and the repetitive naming is pretty confusing.
-                const query = question.query();
-                expect(query instanceof StructuredQuery).toBe(true);
-            });
-            it("returns a correct class instance for native query", () => {
-                const question = new Question(
-                    metadata,
-                    native_orders_count_card
-                );
-                const query = question.query();
-                expect(query instanceof NativeQuery).toBe(true);
-            });
-            it("throws an error for invalid queries", () => {
-                const question = new Question(
-                    metadata,
-                    invalid_orders_count_card
-                );
-                expect(question.query).toThrow();
-            });
+    describe("breakout(...)", async () => {
+      it("works with a datetime field reference", () => {
+        const ordersCountQuestion = new Question(metadata, orders_count_card);
+        const brokenOutCard = ordersCountQuestion.breakout([
+          "field-id",
+          ORDERS_CREATED_DATE_FIELD_ID,
+        ]);
+        expect(brokenOutCard.canRun()).toBe(true);
+
+        expect(brokenOutCard._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            aggregation: [["count"]],
+            breakout: [["field-id", ORDERS_CREATED_DATE_FIELD_ID]],
+          },
         });
-        describe("setQuery(query)", () => {
-            it("updates the dataset_query of card", () => {
-                const question = new Question(metadata, orders_raw_card);
-                const rawQuery = new Question(
-                    metadata,
-                    native_orders_count_card
-                ).query();
-
-                const newRawQuestion = question.setQuery(rawQuery);
-
-                expect(newRawQuestion.query() instanceof NativeQuery).toBe(
-                    true
-                );
-            });
+
+        // Make sure we haven't mutated the underlying query
+        expect(orders_count_card.dataset_query.query).toEqual({
+          source_table: ORDERS_TABLE_ID,
+          aggregation: [["count"]],
         });
-        describe("setDatasetQuery(datasetQuery)", () => {
-            it("updates the dataset_query of card", () => {
-                const question = new Question(metadata, orders_raw_card);
-                const rawQuestion = question.setDatasetQuery(
-                    native_orders_count_card.dataset_query
-                );
-
-                expect(rawQuestion.query() instanceof NativeQuery).toBe(true);
-            });
+      });
+      it("works with a primary key field reference", () => {
+        const ordersCountQuestion = new Question(metadata, orders_count_card);
+        const brokenOutCard = ordersCountQuestion.breakout([
+          "field-id",
+          ORDERS_PK_FIELD_ID,
+        ]);
+        expect(brokenOutCard.canRun()).toBe(true);
+        // This breaks because we're apparently modifying OrdersCountDataCard
+        expect(brokenOutCard._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            aggregation: [["count"]],
+            breakout: [["field-id", ORDERS_PK_FIELD_ID]],
+          },
         });
-    });
 
-    describe("RESETTING METHODS", () => {
-        describe("withoutNameAndId()", () => {
-            it("unsets the name and id", () => {
-                const question = new Question(metadata, orders_raw_card);
-                const newQuestion = question.withoutNameAndId();
-
-                expect(newQuestion.id()).toBeUndefined();
-                expect(newQuestion.displayName()).toBeUndefined();
-            });
-            it("retains the dataset query", () => {
-                const question = new Question(metadata, orders_raw_card);
-
-                expect(question.id()).toBeDefined();
-                expect(question.displayName()).toBeDefined();
-            });
+        // Make sure we haven't mutated the underlying query
+        expect(orders_count_card.dataset_query.query).toEqual({
+          source_table: ORDERS_TABLE_ID,
+          aggregation: [["count"]],
         });
+      });
     });
 
-    describe("VISUALIZATION METHODS", () => {
-        describe("display()", () => {
-            it("returns the card's visualization type", () => {
-                const question = new Question(metadata, orders_raw_card);
-                // this forces a table view
-                const tableQuestion = question.toUnderlyingData();
-                // Not sure I'm a huge fan of magic strings here.
-                expect(tableQuestion.display()).toBe("table");
-            });
+    describe("pivot(...)", async () => {
+      const ordersCountQuestion = new Question(metadata, orders_count_card);
+      it("works with a datetime dimension ", () => {
+        const pivotedCard = ordersCountQuestion.pivot([
+          "field-id",
+          ORDERS_CREATED_DATE_FIELD_ID,
+        ]);
+        expect(pivotedCard.canRun()).toBe(true);
+
+        // if I actually call the .query() method below, this blows up garbage collection =/
+        expect(pivotedCard._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            aggregation: [["count"]],
+            breakout: ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+          },
+        });
+        // Make sure we haven't mutated the underlying query
+        expect(orders_count_card.dataset_query.query).toEqual({
+          source_table: ORDERS_TABLE_ID,
+          aggregation: [["count"]],
+        });
+      });
+      it("works with PK dimension", () => {
+        const pivotedCard = ordersCountQuestion.pivot([
+          "field-id",
+          ORDERS_PK_FIELD_ID,
+        ]);
+        expect(pivotedCard.canRun()).toBe(true);
+
+        // if I actually call the .query() method below, this blows up garbage collection =/
+        expect(pivotedCard._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            aggregation: [["count"]],
+            breakout: ["field-id", ORDERS_PK_FIELD_ID],
+          },
         });
-        describe("setDisplay(display)", () => {
-            it("sets the card's visualization type", () => {
-                const question = new Question(metadata, orders_raw_card);
-                // Not sure I'm a huge fan of magic strings here.
-                const scalarQuestion = question.setDisplay("scalar");
-                expect(scalarQuestion.display()).toBe("scalar");
-            });
+        // Make sure we haven't mutated the underlying query
+        expect(orders_count_card.dataset_query.query).toEqual({
+          source_table: ORDERS_TABLE_ID,
+          aggregation: [["count"]],
         });
+      });
     });
 
-    // TODO: These are mode-dependent and should probably be tied to modes
-    // At the same time, the choice that which actions are visible depend on the question's properties
-    // as actions are filtered using those
-    describe("METHODS FOR DRILL-THROUGH / ACTION WIDGET", () => {
-        const rawDataQuestion = new Question(metadata, orders_raw_card);
-        const timeBreakoutQuestion = Question.create({
-            databaseId: DATABASE_ID,
-            tableId: ORDERS_TABLE_ID,
-            metadata
-        })
-            .query()
-            .addAggregation(["count"])
-            .addBreakout(["datetime-field", ["field-id", 1], "day"])
-            .question()
-            .setDisplay("table");
-
-        describe("mode()", () => {
-            describe("for a new question with Orders table and Raw data aggregation", () => {
-                it("returns the correct mode", () => {
-                    expect(rawDataQuestion.mode().name()).toBe("segment");
-                });
-            });
-            describe("for a question with an aggregation and a time breakout", () => {
-                it("returns the correct mode", () => {
-                    expect(timeBreakoutQuestion.mode().name()).toBe(
-                        "timeseries"
-                    );
-                });
-            });
+    describe("filter(...)", async () => {
+      const questionForFiltering = new Question(metadata, orders_raw_card);
+
+      it("works with an id filter", () => {
+        const filteringQuestion = questionForFiltering.filter(
+          "=",
+          { id: ORDERS_PK_FIELD_ID },
+          1,
+        );
+
+        expect(filteringQuestion._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 1],
+          },
         });
-
-        describe("summarize(...)", async () => {
-            const question = new Question(metadata, orders_raw_card);
-            it("returns the correct query for a summarization of a raw data table", () => {
-                const summarizedQuestion = question.summarize(["count"]);
-                expect(summarizedQuestion.canRun()).toBe(true);
-                // if I actually call the .query() method below, this blows up garbage collection =/
-                expect(summarizedQuestion._card.dataset_query).toEqual(
-                    orders_count_card.dataset_query
-                );
-            });
+      });
+      it("works with a categorical value filter", () => {
+        const filteringQuestion = questionForFiltering.filter(
+          "=",
+          {
+            id: PRODUCT_CATEGORY_FIELD_ID,
+            fk_field_id: ORDERS_PRODUCT_FK_FIELD_ID,
+          },
+          "Doohickey",
+        );
+
+        expect(filteringQuestion._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            filter: [
+              "=",
+              ["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_CATEGORY_FIELD_ID],
+              "Doohickey",
+            ],
+          },
         });
-
-        describe("breakout(...)", async () => {
-            it("works with a datetime field reference", () => {
-                const ordersCountQuestion = new Question(
-                    metadata,
-                    orders_count_card
-                );
-                const brokenOutCard = ordersCountQuestion.breakout([
-                    "field-id",
-                    ORDERS_CREATED_DATE_FIELD_ID
-                ]);
-                expect(brokenOutCard.canRun()).toBe(true);
-
-                expect(brokenOutCard._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        aggregation: [["count"]],
-                        breakout: [["field-id", ORDERS_CREATED_DATE_FIELD_ID]]
-                    }
-                });
-
-                // Make sure we haven't mutated the underlying query
-                expect(orders_count_card.dataset_query.query).toEqual({
-                    source_table: ORDERS_TABLE_ID,
-                    aggregation: [["count"]]
-                });
-            });
-            it("works with a primary key field reference", () => {
-                const ordersCountQuestion = new Question(
-                    metadata,
-                    orders_count_card
-                );
-                const brokenOutCard = ordersCountQuestion.breakout([
-                    "field-id",
-                    ORDERS_PK_FIELD_ID
-                ]);
-                expect(brokenOutCard.canRun()).toBe(true);
-                // This breaks because we're apparently modifying OrdersCountDataCard
-                expect(brokenOutCard._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        aggregation: [["count"]],
-                        breakout: [["field-id", ORDERS_PK_FIELD_ID]]
-                    }
-                });
-
-                // Make sure we haven't mutated the underlying query
-                expect(orders_count_card.dataset_query.query).toEqual({
-                    source_table: ORDERS_TABLE_ID,
-                    aggregation: [["count"]]
-                });
-            });
+      });
+
+      it("works with a time filter", () => {
+        const filteringQuestion = questionForFiltering.filter(
+          "=",
+          { id: ORDERS_CREATED_DATE_FIELD_ID },
+          "12/12/2012",
+        );
+
+        expect(filteringQuestion._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            filter: [
+              "=",
+              ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+              "12/12/2012",
+            ],
+          },
         });
+      });
+    });
 
-        describe("pivot(...)", async () => {
-            const ordersCountQuestion = new Question(
-                metadata,
-                orders_count_card
-            );
-            it("works with a datetime dimension ", () => {
-                const pivotedCard = ordersCountQuestion.pivot([
-                    "field-id",
-                    ORDERS_CREATED_DATE_FIELD_ID
-                ]);
-                expect(pivotedCard.canRun()).toBe(true);
-
-                // if I actually call the .query() method below, this blows up garbage collection =/
-                expect(pivotedCard._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        aggregation: [["count"]],
-                        breakout: ["field-id", ORDERS_CREATED_DATE_FIELD_ID]
-                    }
-                });
-                // Make sure we haven't mutated the underlying query
-                expect(orders_count_card.dataset_query.query).toEqual({
-                    source_table: ORDERS_TABLE_ID,
-                    aggregation: [["count"]]
-                });
-            });
-            it("works with PK dimension", () => {
-                const pivotedCard = ordersCountQuestion.pivot([
-                    "field-id",
-                    ORDERS_PK_FIELD_ID
-                ]);
-                expect(pivotedCard.canRun()).toBe(true);
-
-                // if I actually call the .query() method below, this blows up garbage collection =/
-                expect(pivotedCard._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        aggregation: [["count"]],
-                        breakout: ["field-id", ORDERS_PK_FIELD_ID]
-                    }
-                });
-                // Make sure we haven't mutated the underlying query
-                expect(orders_count_card.dataset_query.query).toEqual({
-                    source_table: ORDERS_TABLE_ID,
-                    aggregation: [["count"]]
-                });
-            });
+    describe("drillUnderlyingRecords(...)", async () => {
+      const ordersCountQuestion = new Question(
+        metadata,
+        orders_count_by_id_card,
+      );
+
+      // ???
+      it("applies a filter to a given filterspec", () => {
+        const dimensions = [
+          { value: 1, column: metadata.fields[ORDERS_PK_FIELD_ID] },
+        ];
+
+        const drilledQuestion = ordersCountQuestion.drillUnderlyingRecords(
+          dimensions,
+        );
+        expect(drilledQuestion.canRun()).toBe(true);
+
+        expect(drilledQuestion._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 1],
+          },
         });
+      });
+    });
 
-        describe("filter(...)", async () => {
-            const questionForFiltering = new Question(
-                metadata,
-                orders_raw_card
-            );
-
-            it("works with an id filter", () => {
-                const filteringQuestion = questionForFiltering.filter(
-                    "=",
-                    { id: ORDERS_PK_FIELD_ID },
-                    1
-                );
-
-                expect(filteringQuestion._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 1]
-                    }
-                });
-            });
-            it("works with a categorical value filter", () => {
-                const filteringQuestion = questionForFiltering.filter(
-                    "=",
-                    {
-                        id: PRODUCT_CATEGORY_FIELD_ID,
-                        fk_field_id: ORDERS_PRODUCT_FK_FIELD_ID
-                    },
-                    "Doohickey"
-                );
-
-                expect(filteringQuestion._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        filter: [
-                            "=",
-                            [
-                                "fk->",
-                                ORDERS_PRODUCT_FK_FIELD_ID,
-                                PRODUCT_CATEGORY_FIELD_ID
-                            ],
-                            "Doohickey"
-                        ]
-                    }
-                });
-            });
-
-            it("works with a time filter", () => {
-                const filteringQuestion = questionForFiltering.filter(
-                    "=",
-                    { id: ORDERS_CREATED_DATE_FIELD_ID },
-                    "12/12/2012"
-                );
-
-                expect(filteringQuestion._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        filter: [
-                            "=",
-                            ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
-                            "12/12/2012"
-                        ]
-                    }
-                });
-            });
-        });
+    describe("toUnderlyingRecords(...)", async () => {
+      const question = new Question(metadata, orders_raw_card);
+      const ordersCountQuestion = new Question(metadata, orders_count_card);
 
-        describe("drillUnderlyingRecords(...)", async () => {
-            const ordersCountQuestion = new Question(
-                metadata,
-                orders_count_by_id_card
-            );
-
-            // ???
-            it("applies a filter to a given filterspec", () => {
-                const dimensions = [
-                    { value: 1, column: metadata.fields[ORDERS_PK_FIELD_ID] }
-                ];
-
-                const drilledQuestion = ordersCountQuestion.drillUnderlyingRecords(
-                    dimensions
-                );
-                expect(drilledQuestion.canRun()).toBe(true);
-
-                expect(drilledQuestion._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 1]
-                    }
-                });
-            });
-        });
+      it("returns underlying records correctly for a raw data query", () => {
+        const underlyingRecordsQuestion = question.toUnderlyingRecords();
 
-        describe("toUnderlyingRecords(...)", async () => {
-            const question = new Question(metadata, orders_raw_card);
-            const ordersCountQuestion = new Question(
-                metadata,
-                orders_count_card
-            );
-
-            it("returns underlying records correctly for a raw data query", () => {
-                const underlyingRecordsQuestion = question.toUnderlyingRecords();
-
-                expect(underlyingRecordsQuestion.canRun()).toBe(true);
-                // if I actually call the .query() method below, this blows up garbage collection =/
-                expect(underlyingRecordsQuestion._card.dataset_query).toEqual(
-                    orders_raw_card.dataset_query
-                );
-
-                // Make sure we haven't mutated the underlying query
-                expect(orders_raw_card.dataset_query.query).toEqual({
-                    source_table: ORDERS_TABLE_ID
-                });
-            });
-            it("returns underlying records correctly for a broken out query", () => {
-                const underlyingRecordsQuestion = ordersCountQuestion.toUnderlyingRecords();
-
-                expect(underlyingRecordsQuestion.canRun()).toBe(true);
-                // if I actually call the .query() method below, this blows up garbage collection =/
-                expect(underlyingRecordsQuestion._card.dataset_query).toEqual(
-                    orders_raw_card.dataset_query
-                );
-
-                // Make sure we haven't mutated the underlying query
-                expect(orders_raw_card.dataset_query.query).toEqual({
-                    source_table: ORDERS_TABLE_ID
-                });
-            });
-        });
+        expect(underlyingRecordsQuestion.canRun()).toBe(true);
+        // if I actually call the .query() method below, this blows up garbage collection =/
+        expect(underlyingRecordsQuestion._card.dataset_query).toEqual(
+          orders_raw_card.dataset_query,
+        );
 
-        describe("toUnderlyingData()", async () => {
-            const ordersCountQuestion = new Question(
-                metadata,
-                orders_count_card
-            );
-
-            it("returns underlying data correctly for table query", () => {
-                const underlyingDataQuestion = ordersCountQuestion
-                    .setDisplay("table")
-                    .toUnderlyingData();
-
-                expect(underlyingDataQuestion.display()).toBe("table");
-            });
-            it("returns underlying data correctly for line chart", () => {
-                const underlyingDataQuestion = ordersCountQuestion
-                    .setDisplay("line")
-                    .toUnderlyingData();
-
-                expect(underlyingDataQuestion.display()).toBe("table");
-            });
+        // Make sure we haven't mutated the underlying query
+        expect(orders_raw_card.dataset_query.query).toEqual({
+          source_table: ORDERS_TABLE_ID,
         });
-
-        describe("drillPK(...)", async () => {
-            const question = new Question(metadata, orders_raw_card);
-            it("returns the correct query for a PK detail drill-through", () => {
-                const drilledQuestion = question.drillPK(
-                    metadata.fields[ORDERS_PK_FIELD_ID],
-                    1
-                );
-
-                expect(drilledQuestion.canRun()).toBe(true);
-
-                // if I actually call the .query() method below, this blows up garbage collection =/
-                expect(drilledQuestion._card.dataset_query).toEqual({
-                    type: "query",
-                    database: DATABASE_ID,
-                    query: {
-                        source_table: ORDERS_TABLE_ID,
-                        filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 1]
-                    }
-                });
-            });
+      });
+      it("returns underlying records correctly for a broken out query", () => {
+        const underlyingRecordsQuestion = ordersCountQuestion.toUnderlyingRecords();
+
+        expect(underlyingRecordsQuestion.canRun()).toBe(true);
+        // if I actually call the .query() method below, this blows up garbage collection =/
+        expect(underlyingRecordsQuestion._card.dataset_query).toEqual(
+          orders_raw_card.dataset_query,
+        );
+
+        // Make sure we haven't mutated the underlying query
+        expect(orders_raw_card.dataset_query.query).toEqual({
+          source_table: ORDERS_TABLE_ID,
         });
+      });
     });
 
-    describe("QUESTION EXECUTION", () => {
-        describe("getResults()", () => {
-            it("executes correctly a native query with field filter parameters", () => {
-                pending();
-                // test also here a combo of parameter with a value + parameter without a value + parameter with a default value
-            });
-        });
-    });
+    describe("toUnderlyingData()", async () => {
+      const ordersCountQuestion = new Question(metadata, orders_count_card);
 
-    describe("COMPARISON TO OTHER QUESTIONS", () => {
-        describe("isDirtyComparedTo(question)", () => {
-            it("New questions are automatically dirty", () => {
-                const question = new Question(metadata, orders_raw_card);
-                const newQuestion = question.withoutNameAndId();
-                expect(newQuestion.isDirtyComparedTo(question)).toBe(true);
-            });
-            it("Changing vis settings makes something dirty", () => {
-                const question = new Question(metadata, orders_count_card);
-                const underlyingDataQuestion = question.toUnderlyingRecords();
-                expect(underlyingDataQuestion.isDirtyComparedTo(question)).toBe(
-                    true
-                );
-            });
-        });
+      it("returns underlying data correctly for table query", () => {
+        const underlyingDataQuestion = ordersCountQuestion
+          .setDisplay("table")
+          .toUnderlyingData();
+
+        expect(underlyingDataQuestion.display()).toBe("table");
+      });
+      it("returns underlying data correctly for line chart", () => {
+        const underlyingDataQuestion = ordersCountQuestion
+          .setDisplay("line")
+          .toUnderlyingData();
+
+        expect(underlyingDataQuestion.display()).toBe("table");
+      });
     });
 
-    describe("URLs", () => {
-        // Covered a lot in query_builder/actions.spec.js, just very basic cases here
-        // (currently getUrl has logic that is strongly tied to the logic query builder Redux actions)
-        describe("getUrl(originalQuestion?)", () => {
-            it("returns a question with hash for an unsaved question", () => {
-                const question = new Question(metadata, orders_raw_card);
-                expect(question.getUrl()).toBe(
-                    "/question#eyJuYW1lIjoiUmF3IG9yZGVycyBkYXRhIiwiZGF0YXNldF9xdWVyeSI6eyJ0eXBlIjoicXVlcnkiLCJkYXRhYmFzZSI6MSwicXVlcnkiOnsic291cmNlX3RhYmxlIjoxfX0sImRpc3BsYXkiOiJ0YWJsZSIsInZpc3VhbGl6YXRpb25fc2V0dGluZ3MiOnt9fQ=="
-                );
-            });
+    describe("drillPK(...)", async () => {
+      const question = new Question(metadata, orders_raw_card);
+      it("returns the correct query for a PK detail drill-through", () => {
+        const drilledQuestion = question.drillPK(
+          metadata.fields[ORDERS_PK_FIELD_ID],
+          1,
+        );
+
+        expect(drilledQuestion.canRun()).toBe(true);
+
+        // if I actually call the .query() method below, this blows up garbage collection =/
+        expect(drilledQuestion._card.dataset_query).toEqual({
+          type: "query",
+          database: DATABASE_ID,
+          query: {
+            source_table: ORDERS_TABLE_ID,
+            filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 1],
+          },
         });
+      });
+    });
+  });
+
+  describe("QUESTION EXECUTION", () => {
+    describe("getResults()", () => {
+      it("executes correctly a native query with field filter parameters", () => {
+        pending();
+        // test also here a combo of parameter with a value + parameter without a value + parameter with a default value
+      });
+    });
+  });
+
+  describe("COMPARISON TO OTHER QUESTIONS", () => {
+    describe("isDirtyComparedTo(question)", () => {
+      it("New questions are automatically dirty", () => {
+        const question = new Question(metadata, orders_raw_card);
+        const newQuestion = question.withoutNameAndId();
+        expect(newQuestion.isDirtyComparedTo(question)).toBe(true);
+      });
+      it("Changing vis settings makes something dirty", () => {
+        const question = new Question(metadata, orders_count_card);
+        const underlyingDataQuestion = question.toUnderlyingRecords();
+        expect(underlyingDataQuestion.isDirtyComparedTo(question)).toBe(true);
+      });
+    });
+  });
+
+  describe("URLs", () => {
+    // Covered a lot in query_builder/actions.spec.js, just very basic cases here
+    // (currently getUrl has logic that is strongly tied to the logic query builder Redux actions)
+    describe("getUrl(originalQuestion?)", () => {
+      it("returns a question with hash for an unsaved question", () => {
+        const question = new Question(metadata, orders_raw_card);
+        expect(question.getUrl()).toBe(
+          "/question#eyJuYW1lIjoiUmF3IG9yZGVycyBkYXRhIiwiZGF0YXNldF9xdWVyeSI6eyJ0eXBlIjoicXVlcnkiLCJkYXRhYmFzZSI6MSwicXVlcnkiOnsic291cmNlX3RhYmxlIjoxfX0sImRpc3BsYXkiOiJ0YWJsZSIsInZpc3VhbGl6YXRpb25fc2V0dGluZ3MiOnt9fQ==",
+        );
+      });
     });
+  });
 });
diff --git a/frontend/test/metabase-lib/metadata/Table.unit.spec.js b/frontend/test/metabase-lib/metadata/Table.unit.spec.js
index 59cb5c3b81f7ad139b453d978ca9d3ee323904e5..cd9596d6bd08bda5e5ecb554d12a1ebcd9526f13 100644
--- a/frontend/test/metabase-lib/metadata/Table.unit.spec.js
+++ b/frontend/test/metabase-lib/metadata/Table.unit.spec.js
@@ -1,38 +1,35 @@
 import Table from "metabase-lib/lib/metadata/Table";
 import Database from "metabase-lib/lib/metadata/Database";
 
-import {
-    state,
-    ORDERS_TABLE_ID
-} from "__support__/sample_dataset_fixture";
+import { state, ORDERS_TABLE_ID } from "__support__/sample_dataset_fixture";
 
 import { getMetadata } from "metabase/selectors/metadata";
 
 describe("Table", () => {
-    let metadata, table;
-    beforeEach(() => {
-        metadata = getMetadata(state);
-        table = metadata.tables[ORDERS_TABLE_ID];
-    });
+  let metadata, table;
+  beforeEach(() => {
+    metadata = getMetadata(state);
+    table = metadata.tables[ORDERS_TABLE_ID];
+  });
 
-    it("should be a table", () => {
-        expect(table).toBeInstanceOf(Table);
-    });
+  it("should be a table", () => {
+    expect(table).toBeInstanceOf(Table);
+  });
 
-    it("should have a database", () => {
-        expect(table.db).toBeInstanceOf(Database);
-    });
+  it("should have a database", () => {
+    expect(table.db).toBeInstanceOf(Database);
+  });
 
-    describe("dimensions", () => {
-        it("returns dimension fields", () => {
-            pending();
-            // expect(table.dimensions().length)
-        });
+  describe("dimensions", () => {
+    it("returns dimension fields", () => {
+      pending();
+      // expect(table.dimensions().length)
     });
+  });
 
-    describe("date fields", () => {
-        it("should return date fields", () => {
-            expect(table.dateFields().length).toEqual(1);
-        });
+  describe("date fields", () => {
+    it("should return date fields", () => {
+      expect(table.dateFields().length).toEqual(1);
     });
+  });
 });
diff --git a/frontend/test/metabase-lib/queries/NativeQuery.unit.spec.js b/frontend/test/metabase-lib/queries/NativeQuery.unit.spec.js
index ac68c970470455a69444543c6e46fca71e31e96e..e6c89894268cc9481a8d2cfa358100f5793054ee 100644
--- a/frontend/test/metabase-lib/queries/NativeQuery.unit.spec.js
+++ b/frontend/test/metabase-lib/queries/NativeQuery.unit.spec.js
@@ -2,181 +2,179 @@
 import "metabase-lib/lib/Question";
 
 import {
-    question,
-    DATABASE_ID,
-    MONGO_DATABASE_ID
+  question,
+  DATABASE_ID,
+  MONGO_DATABASE_ID,
 } from "__support__/sample_dataset_fixture";
 
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 
 function makeDatasetQuery(queryText, templateTags, databaseId) {
-    return {
-        type: "native",
-        database: databaseId,
-        native: {
-            query: queryText,
-            template_tags: templateTags
-        }
-    };
+  return {
+    type: "native",
+    database: databaseId,
+    native: {
+      query: queryText,
+      template_tags: templateTags,
+    },
+  };
 }
 
 function makeQuery(query, templateTags) {
-    return new NativeQuery(
-        question,
-        makeDatasetQuery(query, templateTags, DATABASE_ID)
-    );
+  return new NativeQuery(
+    question,
+    makeDatasetQuery(query, templateTags, DATABASE_ID),
+  );
 }
 
 function makeMongoQuery(query, templateTags) {
-    return new NativeQuery(
-        question,
-        makeDatasetQuery(query, templateTags, MONGO_DATABASE_ID)
-    );
+  return new NativeQuery(
+    question,
+    makeDatasetQuery(query, templateTags, MONGO_DATABASE_ID),
+  );
 }
 
 const query: NativeQuery = makeQuery("");
 
 describe("NativeQuery", () => {
-    describe("You can access the metadata for the database a query has been written against", () => {
-        describe("tables()", () => {
-            it("Tables should return multiple tables", () => {
-                expect(Array.isArray(query.tables())).toBe(true);
-            });
-            it("Tables should return a table map that includes fields", () => {
-                expect(Array.isArray(query.tables()[0].fields)).toBe(true);
-            });
-        });
-        describe("databaseId()", () => {
-            it("returns the Database ID of the wrapped query ", () => {
-                expect(query.databaseId()).toBe(DATABASE_ID);
-            });
-        });
-        describe("database()", () => {
-            it("returns a dictionary with the underlying database of the wrapped query", () => {
-                expect(query.database().id).toBe(DATABASE_ID);
-            });
-        });
+  describe("You can access the metadata for the database a query has been written against", () => {
+    describe("tables()", () => {
+      it("Tables should return multiple tables", () => {
+        expect(Array.isArray(query.tables())).toBe(true);
+      });
+      it("Tables should return a table map that includes fields", () => {
+        expect(Array.isArray(query.tables()[0].fields)).toBe(true);
+      });
+    });
+    describe("databaseId()", () => {
+      it("returns the Database ID of the wrapped query ", () => {
+        expect(query.databaseId()).toBe(DATABASE_ID);
+      });
+    });
+    describe("database()", () => {
+      it("returns a dictionary with the underlying database of the wrapped query", () => {
+        expect(query.database().id).toBe(DATABASE_ID);
+      });
+    });
 
-        describe("engine() tells you what the engine of the database you are querying is", () => {
-            it("identifies the correct engine in H2 queries", () => {
-                // This is a magic constant and we should probably pull this up into an enum
-                expect(query.engine()).toBe("h2");
-            });
-            it("identifies the correct engine for Mongo queries", () => {
-                expect(makeMongoQuery("").engine()).toBe("mongo");
-            });
-        });
-        describe("supportsNativeParameters()", () => {
-            it("Verify that H2 queries support Parameters", () => {
-                expect(query.supportsNativeParameters()).toBe(true);
-            });
-            it("Verify that MongoDB queries do not support Parameters", () => {
-                expect(makeMongoQuery("").supportsNativeParameters()).toBe(
-                    false
-                );
-            });
-        });
-        describe("aceMode()", () => {
-            it("Mongo gets JSON mode ", () => {
-                expect(makeMongoQuery("").aceMode()).toBe("ace/mode/json");
-            });
-            it("H2 gets generic SQL mode in the editor", () => {
-                expect(query.aceMode()).toBe("ace/mode/sql");
-            });
-        });
+    describe("engine() tells you what the engine of the database you are querying is", () => {
+      it("identifies the correct engine in H2 queries", () => {
+        // This is a magic constant and we should probably pull this up into an enum
+        expect(query.engine()).toBe("h2");
+      });
+      it("identifies the correct engine for Mongo queries", () => {
+        expect(makeMongoQuery("").engine()).toBe("mongo");
+      });
+    });
+    describe("supportsNativeParameters()", () => {
+      it("Verify that H2 queries support Parameters", () => {
+        expect(query.supportsNativeParameters()).toBe(true);
+      });
+      it("Verify that MongoDB queries do not support Parameters", () => {
+        expect(makeMongoQuery("").supportsNativeParameters()).toBe(false);
+      });
     });
+    describe("aceMode()", () => {
+      it("Mongo gets JSON mode ", () => {
+        expect(makeMongoQuery("").aceMode()).toBe("ace/mode/json");
+      });
+      it("H2 gets generic SQL mode in the editor", () => {
+        expect(query.aceMode()).toBe("ace/mode/sql");
+      });
+    });
+  });
 
-    describe("Queries have some helpful status checks", () => {
-        describe("isEmpty()", () => {
-            it("Verify that an empty query isEmpty()", () => {
-                expect(query.isEmpty()).toBe(true);
-            });
-            it("Verify that a simple query is not isEmpty()", () => {
-                expect(
-                    query.updateQueryText("SELECT * FROM ORDERS").isEmpty()
-                ).toBe(false);
-            });
-        });
+  describe("Queries have some helpful status checks", () => {
+    describe("isEmpty()", () => {
+      it("Verify that an empty query isEmpty()", () => {
+        expect(query.isEmpty()).toBe(true);
+      });
+      it("Verify that a simple query is not isEmpty()", () => {
+        expect(query.updateQueryText("SELECT * FROM ORDERS").isEmpty()).toBe(
+          false,
+        );
+      });
     });
+  });
 
-    describe("Mongo native queries need to pick a collection the native query is hitting", () => {
-        // should we somehow simulate a mongo query here?
-        // NOTE: Would be nice to have QB UI tests for mongo-specific interactions as well
-        describe("requiresTable()", () => {
-            it("Native H2 Queries should not require table selection", () => {
-                expect(query.requiresTable()).toBe(false);
-            });
-            it("Native Mongo Queries should require table selection", () => {
-                expect(makeMongoQuery("").requiresTable()).toBe(true);
-            });
-        });
+  describe("Mongo native queries need to pick a collection the native query is hitting", () => {
+    // should we somehow simulate a mongo query here?
+    // NOTE: Would be nice to have QB UI tests for mongo-specific interactions as well
+    describe("requiresTable()", () => {
+      it("Native H2 Queries should not require table selection", () => {
+        expect(query.requiresTable()).toBe(false);
+      });
+      it("Native Mongo Queries should require table selection", () => {
+        expect(makeMongoQuery("").requiresTable()).toBe(true);
+      });
+    });
 
-        describe("updateCollection(newCollection) selects or updates a target table for you mongo native query", () => {
-            it("allows you to update mongo collections", () => {
-                const fakeCollectionID = 9999;
-                const fakeMongoQuery = makeMongoQuery("").updateCollection(
-                    fakeCollectionID
-                );
-                expect(fakeMongoQuery.collection()).toBe(fakeCollectionID);
-            });
-            it("sure would be nice to have some error checking on this", () => {
-                pending();
-            });
-        });
-        describe("table()", () => {
-            it("returns null for a non-mongo query", () => {
-                expect(query.table()).toBe(null);
-                expect(
-                    query.updateQueryText("SELECT * FROM ORDERS").table()
-                ).toBe(null);
-            });
-        });
+    describe("updateCollection(newCollection) selects or updates a target table for you mongo native query", () => {
+      it("allows you to update mongo collections", () => {
+        const fakeCollectionID = 9999;
+        const fakeMongoQuery = makeMongoQuery("").updateCollection(
+          fakeCollectionID,
+        );
+        expect(fakeMongoQuery.collection()).toBe(fakeCollectionID);
+      });
+      it("sure would be nice to have some error checking on this", () => {
+        pending();
+      });
+    });
+    describe("table()", () => {
+      it("returns null for a non-mongo query", () => {
+        expect(query.table()).toBe(null);
+        expect(query.updateQueryText("SELECT * FROM ORDERS").table()).toBe(
+          null,
+        );
+      });
     });
-    describe("Acessing the underlying native query", () => {
-        describe("You can access the actual native query via queryText()", () => {
-            expect(makeQuery("SELECT * FROM ORDERS").queryText()).toEqual(
-                "SELECT * FROM ORDERS"
-            );
-        });
-        describe("You can update query text the same way as well via updateQueryText(newQueryText)", () => {
-            const newQuery = makeQuery("SELECT 1");
-            expect(newQuery.queryText()).toEqual("SELECT 1");
-            const newerQuery = newQuery.updateQueryText("SELECT 2");
-            expect(newerQuery.queryText()).toEqual("SELECT 2");
-        });
-        describe("lineCount() lets you know how long your query is", () => {
-            expect(makeQuery("SELECT 1").lineCount()).toBe(1);
-            expect(makeQuery("SELECT \n 1").lineCount()).toBe(2);
-        });
+  });
+  describe("Acessing the underlying native query", () => {
+    describe("You can access the actual native query via queryText()", () => {
+      expect(makeQuery("SELECT * FROM ORDERS").queryText()).toEqual(
+        "SELECT * FROM ORDERS",
+      );
     });
-    describe("Native Queries support Templates and Parameters", () => {
-        describe("You can get the number of parameters via templateTags()", () => {
-            it("Non templated queries don't have parameters", () => {
-                const newQuery = makeQuery().updateQueryText("SELECT 1");
-                expect(newQuery.templateTags().length).toBe(0);
-            });
+    describe("You can update query text the same way as well via updateQueryText(newQueryText)", () => {
+      const newQuery = makeQuery("SELECT 1");
+      expect(newQuery.queryText()).toEqual("SELECT 1");
+      const newerQuery = newQuery.updateQueryText("SELECT 2");
+      expect(newerQuery.queryText()).toEqual("SELECT 2");
+    });
+    describe("lineCount() lets you know how long your query is", () => {
+      expect(makeQuery("SELECT 1").lineCount()).toBe(1);
+      expect(makeQuery("SELECT \n 1").lineCount()).toBe(2);
+    });
+  });
+  describe("Native Queries support Templates and Parameters", () => {
+    describe("You can get the number of parameters via templateTags()", () => {
+      it("Non templated queries don't have parameters", () => {
+        const newQuery = makeQuery().updateQueryText("SELECT 1");
+        expect(newQuery.templateTags().length).toBe(0);
+      });
 
-            it("Templated queries do have parameters", () => {
-                const newQuery = makeQuery().updateQueryText(
-                    "SELECT * from ORDERS where total < {{max_price}}"
-                );
-                expect(newQuery.templateTags().length).toBe(1);
-            });
-        });
-        describe("You can get a pre-structured map keyed by name via templateTagsMap()", () => {
-            it("Non templated queries don't have parameters", () => {
-                const newQuery = makeQuery().updateQueryText("SELECT 1");
-                expect(newQuery.templateTagsMap()).toEqual({});
-            });
+      it("Templated queries do have parameters", () => {
+        const newQuery = makeQuery().updateQueryText(
+          "SELECT * from ORDERS where total < {{max_price}}",
+        );
+        expect(newQuery.templateTags().length).toBe(1);
+      });
+    });
+    describe("You can get a pre-structured map keyed by name via templateTagsMap()", () => {
+      it("Non templated queries don't have parameters", () => {
+        const newQuery = makeQuery().updateQueryText("SELECT 1");
+        expect(newQuery.templateTagsMap()).toEqual({});
+      });
 
-            it("Templated queries do have parameters", () => {
-                const newQuery = makeQuery().updateQueryText(
-                    "SELECT * from ORDERS where total < {{max_price}}"
-                );
-                const tagMaps = newQuery.templateTagsMap();
-                expect(tagMaps["max_price"].name).toEqual("max_price");
-                expect(tagMaps["max_price"].display_name).toEqual("Max price");
-            });
-        });
+      it("Templated queries do have parameters", () => {
+        const newQuery = makeQuery().updateQueryText(
+          "SELECT * from ORDERS where total < {{max_price}}",
+        );
+        const tagMaps = newQuery.templateTagsMap();
+        expect(tagMaps["max_price"].name).toEqual("max_price");
+        expect(tagMaps["max_price"].display_name).toEqual("Max price");
+      });
     });
+  });
 });
diff --git a/frontend/test/metabase-lib/queries/StructuredQuery.unit.spec.js b/frontend/test/metabase-lib/queries/StructuredQuery.unit.spec.js
index 7c9f3c74aac09790fa763054b315062a18b0c456..f44f1434b1e19463a35d2f76c88ad9b1eb5483f8 100644
--- a/frontend/test/metabase-lib/queries/StructuredQuery.unit.spec.js
+++ b/frontend/test/metabase-lib/queries/StructuredQuery.unit.spec.js
@@ -2,608 +2,580 @@
 import "metabase-lib/lib/Question";
 
 import {
-    metadata,
-    question,
-    DATABASE_ID,
-    ANOTHER_DATABASE_ID,
-    ORDERS_TABLE_ID,
-    PRODUCT_TABLE_ID,
-    ORDERS_TOTAL_FIELD_ID,
-    MAIN_METRIC_ID,
-    ORDERS_PRODUCT_FK_FIELD_ID,
-    PRODUCT_TILE_FIELD_ID
+  metadata,
+  question,
+  DATABASE_ID,
+  ANOTHER_DATABASE_ID,
+  ORDERS_TABLE_ID,
+  PRODUCT_TABLE_ID,
+  ORDERS_TOTAL_FIELD_ID,
+  MAIN_METRIC_ID,
+  ORDERS_PRODUCT_FK_FIELD_ID,
+  PRODUCT_TILE_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
 function makeDatasetQuery(query) {
-    return {
-        type: "query",
-        database: DATABASE_ID,
-        query: {
-            source_table: ORDERS_TABLE_ID,
-            ...query
-        }
-    };
+  return {
+    type: "query",
+    database: DATABASE_ID,
+    query: {
+      source_table: ORDERS_TABLE_ID,
+      ...query,
+    },
+  };
 }
 
 function makeQuery(query) {
-    return new StructuredQuery(question, makeDatasetQuery(query));
+  return new StructuredQuery(question, makeDatasetQuery(query));
 }
 
 function makeQueryWithAggregation(agg) {
-    return makeQuery({ aggregation: [agg] });
+  return makeQuery({ aggregation: [agg] });
 }
 
 const query = makeQuery({});
 
 describe("StructuredQuery behavioral tests", () => {
-    it("is able to filter by field which is already used for the query breakout", () => {
-        const breakoutDimensionOptions = query.breakoutOptions().dimensions;
-        const breakoutDimension = breakoutDimensionOptions.find(
-            d => d.field().id === ORDERS_TOTAL_FIELD_ID
-        );
+  it("is able to filter by field which is already used for the query breakout", () => {
+    const breakoutDimensionOptions = query.breakoutOptions().dimensions;
+    const breakoutDimension = breakoutDimensionOptions.find(
+      d => d.field().id === ORDERS_TOTAL_FIELD_ID,
+    );
 
-        expect(breakoutDimension).toBeDefined();
+    expect(breakoutDimension).toBeDefined();
 
-        const queryWithBreakout = query.addBreakout(breakoutDimension.mbql());
+    const queryWithBreakout = query.addBreakout(breakoutDimension.mbql());
 
-        const filterDimensionOptions = queryWithBreakout.filterFieldOptions().dimensions;
-        const filterDimension = filterDimensionOptions.find(
-            d => d.field().id === ORDERS_TOTAL_FIELD_ID
-        );
+    const filterDimensionOptions = queryWithBreakout.filterFieldOptions()
+      .dimensions;
+    const filterDimension = filterDimensionOptions.find(
+      d => d.field().id === ORDERS_TOTAL_FIELD_ID,
+    );
 
-        expect(filterDimension).toBeDefined();
-    });
+    expect(filterDimension).toBeDefined();
+  });
 });
 
 describe("StructuredQuery unit tests", () => {
-    describe("DB METADATA METHODS", () => {
-        describe("tables", () => {
-            it("Tables should return multiple tables", () => {
-                expect(Array.isArray(query.tables())).toBe(true);
-            });
-            it("Tables should return a table map that includes fields", () => {
-                expect(Array.isArray(query.tables()[0].fields)).toBe(true);
-            });
-        });
-        describe("table", () => {
-            it("Return the table wrapper object for the query", () => {
-                expect(query.table()).toBe(metadata.tables[ORDERS_TABLE_ID]);
-            });
-        });
-        describe("databaseId", () => {
-            it("returns the Database ID of the wrapped query ", () => {
-                expect(query.databaseId()).toBe(DATABASE_ID);
-            });
-        });
-        describe("database", () => {
-            it("returns a dictionary with the underlying database of the wrapped query", () => {
-                expect(query.database().id).toBe(DATABASE_ID);
-            });
-        });
-        describe("engine", () => {
-            it("identifies the engine of a query", () => {
-                // This is a magic constant and we should probably pull this up into an enum
-                expect(query.engine()).toBe("h2");
-            });
-        });
+  describe("DB METADATA METHODS", () => {
+    describe("tables", () => {
+      it("Tables should return multiple tables", () => {
+        expect(Array.isArray(query.tables())).toBe(true);
+      });
+      it("Tables should return a table map that includes fields", () => {
+        expect(Array.isArray(query.tables()[0].fields)).toBe(true);
+      });
     });
-
-    describe("SIMPLE QUERY MANIPULATION METHODS", () => {
-        describe("reset", () => {
-            it("Expect a reset query to not have a selected database", () => {
-                expect(query.reset().database()).toBe(null);
-            });
-            it("Expect a reset query to not be runnable", () => {
-                expect(query.reset().canRun()).toBe(false);
-            });
-        });
-        describe("query", () => {
-            it("returns the wrapper for the query dictionary", () => {
-                expect(query.query().source_table).toBe(ORDERS_TABLE_ID);
-            });
-        });
-        describe("setDatabase", () => {
-            it("allows you to set a new database", () => {
-                expect(
-                    query
-                        .setDatabase(metadata.databases[ANOTHER_DATABASE_ID])
-                        .database().id
-                ).toBe(ANOTHER_DATABASE_ID);
-            });
-        });
-        describe("setTable", () => {
-            it("allows you to set a new table", () => {
-                expect(
-                    query.setTable(metadata.tables[PRODUCT_TABLE_ID]).tableId()
-                ).toBe(PRODUCT_TABLE_ID);
-            });
-
-            it("retains the correct database id when setting a new table", () => {
-                expect(
-                    query
-                        .setTable(metadata.tables[PRODUCT_TABLE_ID])
-                        .table().database.id
-                ).toBe(DATABASE_ID);
-            });
-        });
-        describe("tableId", () => {
-            it("Return the right table id", () => {
-                expect(query.tableId()).toBe(ORDERS_TABLE_ID);
-            });
-        });
+    describe("table", () => {
+      it("Return the table wrapper object for the query", () => {
+        expect(query.table()).toBe(metadata.tables[ORDERS_TABLE_ID]);
+      });
     });
-
-    describe("QUERY STATUS METHODS", () => {
-        describe("canRun", () => {
-            it("runs a valid query", () => {
-                expect(query.canRun()).toBe(true);
-            });
-        });
-        describe("isEditable", () => {
-            it("A valid query should be editable", () => {
-                expect(query.isEditable()).toBe(true);
-            });
-        });
-        describe("isEmpty", () => {
-            it("tells that a non-empty query is not empty", () => {
-                expect(query.isEmpty()).toBe(false);
-            });
-        });
+    describe("databaseId", () => {
+      it("returns the Database ID of the wrapped query ", () => {
+        expect(query.databaseId()).toBe(DATABASE_ID);
+      });
     });
+    describe("database", () => {
+      it("returns a dictionary with the underlying database of the wrapped query", () => {
+        expect(query.database().id).toBe(DATABASE_ID);
+      });
+    });
+    describe("engine", () => {
+      it("identifies the engine of a query", () => {
+        // This is a magic constant and we should probably pull this up into an enum
+        expect(query.engine()).toBe("h2");
+      });
+    });
+  });
+
+  describe("SIMPLE QUERY MANIPULATION METHODS", () => {
+    describe("reset", () => {
+      it("Expect a reset query to not have a selected database", () => {
+        expect(query.reset().database()).toBe(null);
+      });
+      it("Expect a reset query to not be runnable", () => {
+        expect(query.reset().canRun()).toBe(false);
+      });
+    });
+    describe("query", () => {
+      it("returns the wrapper for the query dictionary", () => {
+        expect(query.query().source_table).toBe(ORDERS_TABLE_ID);
+      });
+    });
+    describe("setDatabase", () => {
+      it("allows you to set a new database", () => {
+        expect(
+          query.setDatabase(metadata.databases[ANOTHER_DATABASE_ID]).database()
+            .id,
+        ).toBe(ANOTHER_DATABASE_ID);
+      });
+    });
+    describe("setTable", () => {
+      it("allows you to set a new table", () => {
+        expect(
+          query.setTable(metadata.tables[PRODUCT_TABLE_ID]).tableId(),
+        ).toBe(PRODUCT_TABLE_ID);
+      });
+
+      it("retains the correct database id when setting a new table", () => {
+        expect(
+          query.setTable(metadata.tables[PRODUCT_TABLE_ID]).table().database.id,
+        ).toBe(DATABASE_ID);
+      });
+    });
+    describe("tableId", () => {
+      it("Return the right table id", () => {
+        expect(query.tableId()).toBe(ORDERS_TABLE_ID);
+      });
+    });
+  });
 
-    describe("AGGREGATION METHODS", () => {
-        describe("aggregations", () => {
-            it("should return an empty list for an empty query", () => {
-                expect(query.aggregations().length).toBe(0);
-            });
-            it("should return a list of one item after adding an aggregation", () => {
-                expect(
-                    query.addAggregation(["count"]).aggregations().length
-                ).toBe(1);
-            });
-            it("should return an actual count aggregation after trying to add it", () => {
-                expect(
-                    query.addAggregation(["count"]).aggregations()[0]
-                ).toEqual(["count"]);
-            });
-        });
-        describe("aggregationsWrapped", () => {
-            it("should return an empty list for an empty query", () => {
-                expect(query.aggregationsWrapped().length).toBe(0);
-            });
-            it("should return a list with Aggregation after adding an aggregation", () => {
-                expect(
-                    query
-                        .addAggregation(["count"])
-                        .aggregationsWrapped()[0]
-                        .isValid()
-                ).toBe(true);
-            });
-        });
-
-        describe("aggregationOptions", () => {
-            // TODO Atte Keinänen 6/14/17: Add the mock metadata for aggregation options
-            // (currently the fixture doesn't include them)
-            it("should return a non-empty list of options", () => {
-                pending();
-                expect(query.aggregationOptions().length).toBeGreaterThan(0);
-            });
-            it("should contain the count aggregation", () => {
-                pending();
-            });
-        });
-        describe("aggregationOptionsWithoutRaw", () => {
-            // Also waiting for the mock metadata
-            pending();
-        });
-
-        describe("aggregationFieldOptions()", () => {
-            it("includes expressions to the results without a field filter", () => {
-                pending();
-            });
-            it("includes expressions to the results with a field filter", () => {
-                pending();
-            });
-        });
-
-        describe("canRemoveAggregation", () => {
-            it("returns false if there are no aggregations", () => {
-                expect(query.canRemoveAggregation()).toBe(false);
-            });
-            it("returns false for a single aggregation", () => {
-                expect(
-                    query.addAggregation(["count"]).canRemoveAggregation()
-                ).toBe(false);
-            });
-            it("returns true for two aggregations", () => {
-                expect(
-                    query
-                        .addAggregation(["count"])
-                        .addAggregation([
-                            "sum",
-                            ["field-id", ORDERS_TOTAL_FIELD_ID]
-                        ])
-                        .canRemoveAggregation()
-                ).toBe(true);
-            });
-        });
-
-        describe("isBareRows", () => {
-            it("is true for an empty query", () => {
-                expect(query.isBareRows()).toBe(true);
-            });
-            it("is false for a count aggregation", () => {
-                expect(query.addAggregation(["count"]).isBareRows()).toBe(
-                    false
-                );
-            });
-        });
-
-        describe("aggregationName", () => {
-            it("returns a saved metric's name", () => {
-                expect(
-                    makeQueryWithAggregation([
-                        "METRIC",
-                        MAIN_METRIC_ID
-                    ]).aggregationName()
-                ).toBe("Total Order Value");
-            });
-            it("returns a standard aggregation name", () => {
-                expect(
-                    makeQueryWithAggregation(["count"]).aggregationName()
-                ).toBe("Count of rows");
-            });
-            it("returns a standard aggregation name with field", () => {
-                expect(
-                    makeQueryWithAggregation([
-                        "sum",
-                        ["field-id", ORDERS_TOTAL_FIELD_ID]
-                    ]).aggregationName()
-                ).toBe("Sum of Total");
-            });
-            it("returns a standard aggregation name with fk field", () => {
-                expect(
-                    makeQueryWithAggregation([
-                        "sum",
-                        [
-                            "fk->",
-                            ORDERS_PRODUCT_FK_FIELD_ID,
-                            PRODUCT_TILE_FIELD_ID
-                        ]
-                    ]).aggregationName()
-                ).toBe("Sum of Title");
-            });
-            it("returns a custom expression description", () => {
-                expect(
-                    makeQueryWithAggregation([
-                        "+",
-                        1,
-                        ["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]]
-                    ]).aggregationName()
-                ).toBe("1 + Sum(Total)");
-            });
-            it("returns a named expression name", () => {
-                expect(
-                    makeQueryWithAggregation([
-                        "named",
-                        ["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]],
-                        "Named"
-                    ]).aggregationName()
-                ).toBe("Named");
-            });
-        });
+  describe("QUERY STATUS METHODS", () => {
+    describe("canRun", () => {
+      it("runs a valid query", () => {
+        expect(query.canRun()).toBe(true);
+      });
+    });
+    describe("isEditable", () => {
+      it("A valid query should be editable", () => {
+        expect(query.isEditable()).toBe(true);
+      });
+    });
+    describe("isEmpty", () => {
+      it("tells that a non-empty query is not empty", () => {
+        expect(query.isEmpty()).toBe(false);
+      });
+    });
+  });
+
+  describe("AGGREGATION METHODS", () => {
+    describe("aggregations", () => {
+      it("should return an empty list for an empty query", () => {
+        expect(query.aggregations().length).toBe(0);
+      });
+      it("should return a list of one item after adding an aggregation", () => {
+        expect(query.addAggregation(["count"]).aggregations().length).toBe(1);
+      });
+      it("should return an actual count aggregation after trying to add it", () => {
+        expect(query.addAggregation(["count"]).aggregations()[0]).toEqual([
+          "count",
+        ]);
+      });
+    });
+    describe("aggregationsWrapped", () => {
+      it("should return an empty list for an empty query", () => {
+        expect(query.aggregationsWrapped().length).toBe(0);
+      });
+      it("should return a list with Aggregation after adding an aggregation", () => {
+        expect(
+          query
+            .addAggregation(["count"])
+            .aggregationsWrapped()[0]
+            .isValid(),
+        ).toBe(true);
+      });
+    });
 
-        describe("addAggregation", () => {
-            it("adds an aggregation", () => {
-                expect(query.addAggregation(["count"]).query()).toEqual({
-                    source_table: ORDERS_TABLE_ID,
-                    aggregation: [["count"]]
-                });
-            });
-        });
+    describe("aggregationOptions", () => {
+      // TODO Atte Keinänen 6/14/17: Add the mock metadata for aggregation options
+      // (currently the fixture doesn't include them)
+      it("should return a non-empty list of options", () => {
+        pending();
+        expect(query.aggregationOptions().length).toBeGreaterThan(0);
+      });
+      it("should contain the count aggregation", () => {
+        pending();
+      });
+    });
+    describe("aggregationOptionsWithoutRaw", () => {
+      // Also waiting for the mock metadata
+      pending();
+    });
 
-        describe("removeAggregation", () => {
-            it("removes the correct aggregation", () => {
-                pending();
-            });
-            it("removes all breakouts when removing the last aggregation", () => {
-                pending();
-            });
-        });
+    describe("aggregationFieldOptions()", () => {
+      it("includes expressions to the results without a field filter", () => {
+        pending();
+      });
+      it("includes expressions to the results with a field filter", () => {
+        pending();
+      });
+    });
 
-        describe("updateAggregation", () => {
-            it("updates the correct aggregation", () => {
-                pending();
-            });
-            it('removes all breakouts and aggregations when setting an aggregation to "rows"', () => {
-                pending();
-            });
-        });
+    describe("canRemoveAggregation", () => {
+      it("returns false if there are no aggregations", () => {
+        expect(query.canRemoveAggregation()).toBe(false);
+      });
+      it("returns false for a single aggregation", () => {
+        expect(query.addAggregation(["count"]).canRemoveAggregation()).toBe(
+          false,
+        );
+      });
+      it("returns true for two aggregations", () => {
+        expect(
+          query
+            .addAggregation(["count"])
+            .addAggregation(["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]])
+            .canRemoveAggregation(),
+        ).toBe(true);
+      });
+    });
 
-        describe("clearAggregations", () => {
-            it("clears all aggreagtions and breakouts", () => {
-                pending();
-            });
-        });
+    describe("isBareRows", () => {
+      it("is true for an empty query", () => {
+        expect(query.isBareRows()).toBe(true);
+      });
+      it("is false for a count aggregation", () => {
+        expect(query.addAggregation(["count"]).isBareRows()).toBe(false);
+      });
     });
 
-    // BREAKOUTS:
-    describe("BREAKOUT METHODS", () => {
-        describe("breakouts", () => {
-            pending();
-        });
-        describe("breakoutOptions", () => {
-            it("returns the correct count of dimensions", () => {
-                expect(query.breakoutOptions().dimensions.length).toBe(5);
-            });
-
-            it("excludes the already used breakouts", () => {
-                const queryWithBreakout = query.addBreakout([
-                    "field-id",
-                    ORDERS_TOTAL_FIELD_ID
-                ]);
-                expect(
-                    queryWithBreakout.breakoutOptions().dimensions.length
-                ).toBe(4);
-            });
-
-            it("includes an explicitly provided breakout although it has already been used", () => {
-                const breakout = ["field-id", ORDERS_TOTAL_FIELD_ID];
-                const queryWithBreakout = query.addBreakout(breakout);
-                expect(
-                    queryWithBreakout.breakoutOptions().dimensions.length
-                ).toBe(4);
-                expect(
-                    queryWithBreakout.breakoutOptions(
-                        breakout
-                    ).dimensions.length
-                ).toBe(5);
-            });
-        });
-        describe("canAddBreakout", () => {
-            pending();
-        });
-        describe("hasValidBreakout", () => {
-            pending();
-        });
+    describe("aggregationName", () => {
+      it("returns a saved metric's name", () => {
+        expect(
+          makeQueryWithAggregation([
+            "METRIC",
+            MAIN_METRIC_ID,
+          ]).aggregationName(),
+        ).toBe("Total Order Value");
+      });
+      it("returns a standard aggregation name", () => {
+        expect(makeQueryWithAggregation(["count"]).aggregationName()).toBe(
+          "Count of rows",
+        );
+      });
+      it("returns a standard aggregation name with field", () => {
+        expect(
+          makeQueryWithAggregation([
+            "sum",
+            ["field-id", ORDERS_TOTAL_FIELD_ID],
+          ]).aggregationName(),
+        ).toBe("Sum of Total");
+      });
+      it("returns a standard aggregation name with fk field", () => {
+        expect(
+          makeQueryWithAggregation([
+            "sum",
+            ["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_TILE_FIELD_ID],
+          ]).aggregationName(),
+        ).toBe("Sum of Title");
+      });
+      it("returns a custom expression description", () => {
+        expect(
+          makeQueryWithAggregation([
+            "+",
+            1,
+            ["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]],
+          ]).aggregationName(),
+        ).toBe("1 + Sum(Total)");
+      });
+      it("returns a named expression name", () => {
+        expect(
+          makeQueryWithAggregation([
+            "named",
+            ["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]],
+            "Named",
+          ]).aggregationName(),
+        ).toBe("Named");
+      });
+    });
 
-        describe("addBreakout", () => {
-            pending();
+    describe("addAggregation", () => {
+      it("adds an aggregation", () => {
+        expect(query.addAggregation(["count"]).query()).toEqual({
+          source_table: ORDERS_TABLE_ID,
+          aggregation: [["count"]],
         });
+      });
+    });
 
-        describe("removeBreakout", () => {
-            pending();
-        });
+    describe("removeAggregation", () => {
+      it("removes the correct aggregation", () => {
+        pending();
+      });
+      it("removes all breakouts when removing the last aggregation", () => {
+        pending();
+      });
+    });
 
-        describe("updateBreakout", () => {
-            pending();
-        });
+    describe("updateAggregation", () => {
+      it("updates the correct aggregation", () => {
+        pending();
+      });
+      it('removes all breakouts and aggregations when setting an aggregation to "rows"', () => {
+        pending();
+      });
+    });
 
-        describe("clearBreakouts", () => {
-            pending();
-        });
+    describe("clearAggregations", () => {
+      it("clears all aggreagtions and breakouts", () => {
+        pending();
+      });
     });
+  });
 
-    // FILTERS:
-    describe("FILTER METHODS", () => {
-        describe("filters", () => {
-            pending();
-        });
+  // BREAKOUTS:
+  describe("BREAKOUT METHODS", () => {
+    describe("breakouts", () => {
+      pending();
+    });
+    describe("breakoutOptions", () => {
+      it("returns the correct count of dimensions", () => {
+        expect(query.breakoutOptions().dimensions.length).toBe(7);
+      });
+
+      it("excludes the already used breakouts", () => {
+        const queryWithBreakout = query.addBreakout([
+          "field-id",
+          ORDERS_TOTAL_FIELD_ID,
+        ]);
+        expect(queryWithBreakout.breakoutOptions().dimensions.length).toBe(6);
+      });
+
+      it("includes an explicitly provided breakout although it has already been used", () => {
+        const breakout = ["field-id", ORDERS_TOTAL_FIELD_ID];
+        const queryWithBreakout = query.addBreakout(breakout);
+        expect(queryWithBreakout.breakoutOptions().dimensions.length).toBe(6);
+        expect(
+          queryWithBreakout.breakoutOptions(breakout).dimensions.length,
+        ).toBe(7);
+      });
+    });
+    describe("canAddBreakout", () => {
+      pending();
+    });
+    describe("hasValidBreakout", () => {
+      pending();
+    });
 
-        describe("filterFieldOptions", () => {
-            pending();
-        });
-        describe("filterSegmentOptions", () => {
-            pending();
-        });
+    describe("addBreakout", () => {
+      pending();
+    });
 
-        describe("canAddFilter", () => {
-            pending();
-        });
+    describe("removeBreakout", () => {
+      pending();
+    });
 
-        describe("addFilter", () => {
-            it("adds an filter", () => {
-                pending();
-            });
-        });
-        describe("removeFilter", () => {
-            it("removes the correct filter", () => {
-                pending();
-            });
-        });
-        describe("updateFilter", () => {
-            it("updates the correct filter", () => {
-                pending();
-            });
-        });
-        describe("clearFilters", () => {
-            it("clears all filters", () => {
-                pending();
-            });
-        });
+    describe("updateBreakout", () => {
+      pending();
     });
 
-    describe("SORT METHODS", () => {
-        describe("sorts", () => {
-            it("return an empty array", () => {
-                expect(query.sorts()).toEqual([]);
-            });
-            it("return an array with the sort clause", () => {
-                expect(
-                    makeQuery({
-                        order_by: [
-                            ["field-id", ORDERS_TOTAL_FIELD_ID],
-                            "ascending"
-                        ]
-                    }).sorts()
-                ).toEqual([["field-id", ORDERS_TOTAL_FIELD_ID], "ascending"]);
-            });
-        });
+    describe("clearBreakouts", () => {
+      pending();
+    });
+  });
 
-        describe("sortOptions", () => {
-            it("returns the correct count of dimensions", () => {
-                expect(query.sortOptions().dimensions.length).toBe(5);
-            });
-
-            it("excludes the already used sorts", () => {
-                const queryWithBreakout = query.addSort([
-                    ["field-id", ORDERS_TOTAL_FIELD_ID],
-                    "ascending"
-                ]);
-                expect(queryWithBreakout.sortOptions().dimensions.length).toBe(
-                    4
-                );
-            });
-
-            it("includes an explicitly provided sort although it has already been used", () => {
-                const sort = [["field-id", ORDERS_TOTAL_FIELD_ID], "ascending"];
-                const queryWithBreakout = query.addSort(sort);
-                expect(queryWithBreakout.sortOptions().dimensions.length).toBe(
-                    4
-                );
-                expect(
-                    queryWithBreakout.sortOptions(sort).dimensions.length
-                ).toBe(5);
-            });
-        });
+  // FILTERS:
+  describe("FILTER METHODS", () => {
+    describe("filters", () => {
+      pending();
+    });
 
-        describe("canAddSort", () => {
-            pending();
-        });
+    describe("filterFieldOptions", () => {
+      pending();
+    });
+    describe("filterSegmentOptions", () => {
+      pending();
+    });
 
-        describe("addSort", () => {
-            it("adds a sort", () => {
-                pending();
-            });
-        });
-        describe("updateSort", () => {
-            it("", () => {
-                pending();
-            });
-        });
-        describe("removeSort", () => {
-            it("removes the correct sort", () => {
-                pending();
-            });
-        });
-        describe("clearSort", () => {
-            it("clears all sorts", () => {
-                pending();
-            });
-        });
-        describe("replaceSort", () => {
-            it("replaces sorts with a new sort", () => {
-                pending();
-            });
-        });
+    describe("canAddFilter", () => {
+      pending();
     });
-    // LIMIT
 
-    describe("LIMIT METHODS", () => {
-        describe("limit", () => {
-            it("returns null if there is no limit", () => {
-                pending();
-            });
-            it("returns the limit if one has been set", () => {
-                pending();
-            });
-        });
+    describe("addFilter", () => {
+      it("adds an filter", () => {
+        pending();
+      });
+    });
+    describe("removeFilter", () => {
+      it("removes the correct filter", () => {
+        pending();
+      });
+    });
+    describe("updateFilter", () => {
+      it("updates the correct filter", () => {
+        pending();
+      });
+    });
+    describe("clearFilters", () => {
+      it("clears all filters", () => {
+        pending();
+      });
+    });
+  });
+
+  describe("SORT METHODS", () => {
+    describe("sorts", () => {
+      it("return an empty array", () => {
+        expect(query.sorts()).toEqual([]);
+      });
+      it("return an array with the sort clause", () => {
+        expect(
+          makeQuery({
+            order_by: [["field-id", ORDERS_TOTAL_FIELD_ID], "ascending"],
+          }).sorts(),
+        ).toEqual([["field-id", ORDERS_TOTAL_FIELD_ID], "ascending"]);
+      });
+    });
 
-        describe("updateLimit", () => {
-            it("updates the limit", () => {
-                pending();
-            });
-        });
-        describe("clearLimit", () => {
-            it("clears the limit", () => {
-                pending();
-            });
-        });
+    describe("sortOptions", () => {
+      it("returns the correct count of dimensions", () => {
+        expect(query.sortOptions().dimensions.length).toBe(7);
+      });
+
+      it("excludes the already used sorts", () => {
+        const queryWithBreakout = query.addSort([
+          ["field-id", ORDERS_TOTAL_FIELD_ID],
+          "ascending",
+        ]);
+        expect(queryWithBreakout.sortOptions().dimensions.length).toBe(6);
+      });
+
+      it("includes an explicitly provided sort although it has already been used", () => {
+        const sort = [["field-id", ORDERS_TOTAL_FIELD_ID], "ascending"];
+        const queryWithBreakout = query.addSort(sort);
+        expect(queryWithBreakout.sortOptions().dimensions.length).toBe(6);
+        expect(queryWithBreakout.sortOptions(sort).dimensions.length).toBe(7);
+      });
     });
 
-    describe("EXPRESSION METHODS", () => {
-        describe("expressions", () => {
-            it("returns an empty map", () => {
-                pending();
-            });
-            it("returns a map with the expressions", () => {
-                pending();
-            });
-        });
-        describe("updateExpression", () => {
-            it("updates the correct expression", () => {
-                pending();
-            });
-        });
-        describe("removeExpression", () => {
-            it("removes the correct expression", () => {
-                pending();
-            });
-        });
+    describe("canAddSort", () => {
+      pending();
     });
 
-    describe("DIMENSION METHODS", () => {
-        describe("fieldOptions", () => {
-            it("includes the correct number of dimensions", () => {
-                // Should just include the non-fk keys from the current table
-                expect(query.fieldOptions().dimensions.length).toBe(5);
-            });
-            it("does not include foreign key fields in the dimensions list", () => {
-                const dimensions = query.fieldOptions().dimensions;
-                const fkDimensions = dimensions.filter(
-                    dim => dim.field() && dim.field().isFK()
-                );
-                expect(fkDimensions.length).toBe(0);
-            });
-
-            it("returns correct count of foreign keys", () => {
-                expect(query.fieldOptions().fks.length).toBe(2);
-            });
-            it("returns a correct count of fields", () => {
-                expect(query.fieldOptions().count).toBe(26);
-            });
-        });
-        describe("dimensions", () => {
-            pending();
-        });
-        describe("tableDimensions", () => {
-            pending();
-        });
-        describe("expressionDimensions", () => {
-            pending();
-        });
-        describe("aggregationDimensions", () => {
-            pending();
-        });
-        describe("metricDimensions", () => {
-            pending();
-        });
+    describe("addSort", () => {
+      it("adds a sort", () => {
+        pending();
+      });
+    });
+    describe("updateSort", () => {
+      it("", () => {
+        pending();
+      });
+    });
+    describe("removeSort", () => {
+      it("removes the correct sort", () => {
+        pending();
+      });
+    });
+    describe("clearSort", () => {
+      it("clears all sorts", () => {
+        pending();
+      });
+    });
+    describe("replaceSort", () => {
+      it("replaces sorts with a new sort", () => {
+        pending();
+      });
+    });
+  });
+  // LIMIT
+
+  describe("LIMIT METHODS", () => {
+    describe("limit", () => {
+      it("returns null if there is no limit", () => {
+        pending();
+      });
+      it("returns the limit if one has been set", () => {
+        pending();
+      });
     });
 
-    describe("FIELD REFERENCE METHODS", () => {
-        describe("fieldReferenceForColumn", () => {
-            pending();
-        });
+    describe("updateLimit", () => {
+      it("updates the limit", () => {
+        pending();
+      });
+    });
+    describe("clearLimit", () => {
+      it("clears the limit", () => {
+        pending();
+      });
+    });
+  });
+
+  describe("EXPRESSION METHODS", () => {
+    describe("expressions", () => {
+      it("returns an empty map", () => {
+        pending();
+      });
+      it("returns a map with the expressions", () => {
+        pending();
+      });
+    });
+    describe("updateExpression", () => {
+      it("updates the correct expression", () => {
+        pending();
+      });
+    });
+    describe("removeExpression", () => {
+      it("removes the correct expression", () => {
+        pending();
+      });
+    });
+  });
+
+  describe("DIMENSION METHODS", () => {
+    describe("fieldOptions", () => {
+      it("includes the correct number of dimensions", () => {
+        // Should just include the non-fk keys from the current table
+        expect(query.fieldOptions().dimensions.length).toBe(7);
+      });
+      xit("does not include foreign key fields in the dimensions list", () => {
+        const dimensions = query.fieldOptions().dimensions;
+        const fkDimensions = dimensions.filter(
+          dim => dim.field() && dim.field().isFK(),
+        );
+        expect(fkDimensions.length).toBe(0);
+      });
+
+      it("returns correct count of foreign keys", () => {
+        expect(query.fieldOptions().fks.length).toBe(2);
+      });
+      it("returns a correct count of fields", () => {
+        expect(query.fieldOptions().count).toBe(28);
+      });
+    });
+    describe("dimensions", () => {
+      pending();
+    });
+    describe("tableDimensions", () => {
+      pending();
+    });
+    describe("expressionDimensions", () => {
+      pending();
+    });
+    describe("aggregationDimensions", () => {
+      pending();
+    });
+    describe("metricDimensions", () => {
+      pending();
+    });
+  });
 
-        describe("parseFieldReference", () => {
-            pending();
-        });
+  describe("FIELD REFERENCE METHODS", () => {
+    describe("fieldReferenceForColumn", () => {
+      pending();
     });
 
-    describe("DATASET QUERY METHODS", () => {
-        describe("setDatasetQuery", () => {
-            it("replaces the previous dataset query with the provided one", () => {
-                const newDatasetQuery = makeDatasetQuery({
-                    source_table: ORDERS_TABLE_ID,
-                    aggregation: [["count"]]
-                });
+    describe("parseFieldReference", () => {
+      pending();
+    });
+  });
 
-                expect(
-                    query.setDatasetQuery(newDatasetQuery).datasetQuery()
-                ).toBe(newDatasetQuery);
-            });
+  describe("DATASET QUERY METHODS", () => {
+    describe("setDatasetQuery", () => {
+      it("replaces the previous dataset query with the provided one", () => {
+        const newDatasetQuery = makeDatasetQuery({
+          source_table: ORDERS_TABLE_ID,
+          aggregation: [["count"]],
         });
+
+        expect(query.setDatasetQuery(newDatasetQuery).datasetQuery()).toBe(
+          newDatasetQuery,
+        );
+      });
     });
+  });
 });
diff --git a/frontend/test/modes/TimeseriesFilterWidget.unit.spec.jsx b/frontend/test/modes/TimeseriesFilterWidget.unit.spec.jsx
index 71d5e5a98ab8e5880681f689bc53fb3917dec73e..2e6740586bfdbb0584c9cba6fb59c57934d76ffa 100644
--- a/frontend/test/modes/TimeseriesFilterWidget.unit.spec.jsx
+++ b/frontend/test/modes/TimeseriesFilterWidget.unit.spec.jsx
@@ -5,60 +5,60 @@ import { mount } from "enzyme";
 
 import Question from "metabase-lib/lib/Question";
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
 
 const getTimeseriesFilterWidget = question => (
-    <TimeseriesFilterWidget
-        card={question.card()}
-        tableMetadata={question.tableMetadata()}
-        datasetQuery={question.query().datasetQuery()}
-        setDatasetQuery={() => {}}
-    />
+  <TimeseriesFilterWidget
+    card={question.card()}
+    tableMetadata={question.tableMetadata()}
+    datasetQuery={question.query().datasetQuery()}
+    setDatasetQuery={() => {}}
+  />
 );
 
 describe("TimeseriesFilterWidget", () => {
-    const questionWithoutFilter = Question.create({
-        databaseId: DATABASE_ID,
-        tableId: ORDERS_TABLE_ID,
-        metadata
-    })
-        .query()
-        .addAggregation(["count"])
-        .addBreakout(["datetime-field", ["field-id", 1], "day"])
-        .question();
+  const questionWithoutFilter = Question.create({
+    databaseId: DATABASE_ID,
+    tableId: ORDERS_TABLE_ID,
+    metadata,
+  })
+    .query()
+    .addAggregation(["count"])
+    .addBreakout(["datetime-field", ["field-id", 1], "day"])
+    .question();
 
-    it("should display 'All Time' text if no filter is selected", () => {
-        const widget = mount(getTimeseriesFilterWidget(questionWithoutFilter));
-        expect(widget.find(".AdminSelect-content").text()).toBe("All Time");
-    });
-    it("should display 'Past 30 Days' text if that filter is selected", () => {
-        const questionWithFilter = questionWithoutFilter
-            .query()
-            .addFilter(["time-interval", ["field-id", 1], -30, "day"])
-            .question();
+  it("should display 'All Time' text if no filter is selected", () => {
+    const widget = mount(getTimeseriesFilterWidget(questionWithoutFilter));
+    expect(widget.find(".AdminSelect-content").text()).toBe("All Time");
+  });
+  it("should display 'Past 30 Days' text if that filter is selected", () => {
+    const questionWithFilter = questionWithoutFilter
+      .query()
+      .addFilter(["time-interval", ["field-id", 1], -30, "day"])
+      .question();
 
-        const widget = mount(getTimeseriesFilterWidget(questionWithFilter));
-        expect(widget.find(".AdminSelect-content").text()).toBe("Past 30 Days");
-    });
-    it("should display 'Is Empty' text if that filter is selected", () => {
-        const questionWithFilter = questionWithoutFilter
-            .query()
-            .addFilter(["IS_NULL", ["field-id", 1]])
-            .question();
+    const widget = mount(getTimeseriesFilterWidget(questionWithFilter));
+    expect(widget.find(".AdminSelect-content").text()).toBe("Past 30 Days");
+  });
+  it("should display 'Is Empty' text if that filter is selected", () => {
+    const questionWithFilter = questionWithoutFilter
+      .query()
+      .addFilter(["IS_NULL", ["field-id", 1]])
+      .question();
 
-        const widget = mount(getTimeseriesFilterWidget(questionWithFilter));
-        expect(widget.find(".AdminSelect-content").text()).toBe("Is Empty");
-    });
-    it("should display 'Not Empty' text if that filter is selected", () => {
-        const questionWithFilter = questionWithoutFilter
-            .query()
-            .addFilter(["NOT_NULL", ["field-id", 1]])
-            .question();
+    const widget = mount(getTimeseriesFilterWidget(questionWithFilter));
+    expect(widget.find(".AdminSelect-content").text()).toBe("Is Empty");
+  });
+  it("should display 'Not Empty' text if that filter is selected", () => {
+    const questionWithFilter = questionWithoutFilter
+      .query()
+      .addFilter(["NOT_NULL", ["field-id", 1]])
+      .question();
 
-        const widget = mount(getTimeseriesFilterWidget(questionWithFilter));
-        expect(widget.find(".AdminSelect-content").text()).toBe("Not Empty");
-    });
+    const widget = mount(getTimeseriesFilterWidget(questionWithFilter));
+    expect(widget.find(".AdminSelect-content").text()).toBe("Not Empty");
+  });
 });
diff --git a/frontend/test/modes/TimeseriesMode.unit.spec.js b/frontend/test/modes/TimeseriesMode.unit.spec.js
index dc63b10e9deeff438d03290d91406417d3e22f1a..16247cca68a4955086c2170394545abf168328e1 100644
--- a/frontend/test/modes/TimeseriesMode.unit.spec.js
+++ b/frontend/test/modes/TimeseriesMode.unit.spec.js
@@ -3,19 +3,17 @@ import "metabase-lib/lib/Question";
 
 import React from "react";
 import { TimeseriesModeFooter } from "metabase/qb/components/modes/TimeseriesMode";
-import TimeseriesGroupingWidget
-    from "metabase/qb/components/TimeseriesGroupingWidget";
-import TimeseriesFilterWidget
-    from "metabase/qb/components/TimeseriesFilterWidget";
+import TimeseriesGroupingWidget from "metabase/qb/components/TimeseriesGroupingWidget";
+import TimeseriesFilterWidget from "metabase/qb/components/TimeseriesFilterWidget";
 import { shallow } from "enzyme";
 
 describe("TimeSeriesModeFooter", () => {
-    it("should always render TimeseriesFilterWidget", () => {
-        const wrapper = shallow(<TimeseriesModeFooter />);
-        expect(wrapper.find(TimeseriesFilterWidget).length).toEqual(1);
-    });
-    it("should always render TimeseriesGroupingWidget", () => {
-        const wrapper = shallow(<TimeseriesModeFooter />);
-        expect(wrapper.find(TimeseriesGroupingWidget).length).toEqual(1);
-    });
+  it("should always render TimeseriesFilterWidget", () => {
+    const wrapper = shallow(<TimeseriesModeFooter />);
+    expect(wrapper.find(TimeseriesFilterWidget).length).toEqual(1);
+  });
+  it("should always render TimeseriesGroupingWidget", () => {
+    const wrapper = shallow(<TimeseriesModeFooter />);
+    expect(wrapper.find(TimeseriesGroupingWidget).length).toEqual(1);
+  });
 });
diff --git a/frontend/test/modes/actions/CommonMetricsAction.integ.spec.js b/frontend/test/modes/actions/CommonMetricsAction.integ.spec.js
index 0b33bf60198c53e4c646d7b7ee9408ac1c99b273..4f3bcfcdd9ea737a26eff4ca9174d41b187cee04 100644
--- a/frontend/test/modes/actions/CommonMetricsAction.integ.spec.js
+++ b/frontend/test/modes/actions/CommonMetricsAction.integ.spec.js
@@ -1,8 +1,8 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 describe("CommonMetricsAction", () => {
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/actions/CommonMetricsAction.unit.spec.js b/frontend/test/modes/actions/CommonMetricsAction.unit.spec.js
index ca4e80a25f8c78d55ae48099551db9a71ab45740..5b1043cf783358689c687148a128bb370b57e830 100644
--- a/frontend/test/modes/actions/CommonMetricsAction.unit.spec.js
+++ b/frontend/test/modes/actions/CommonMetricsAction.unit.spec.js
@@ -1,9 +1,9 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import {
-    makeQuestion,
-    ORDERS_TABLE_ID,
-    MAIN_METRIC_ID
+  makeQuestion,
+  ORDERS_TABLE_ID,
+  MAIN_METRIC_ID,
 } from "__support__/sample_dataset_fixture";
 
 import CommonMetricsAction from "metabase/qb/components/actions/CommonMetricsAction";
@@ -11,55 +11,51 @@ import CommonMetricsAction from "metabase/qb/components/actions/CommonMetricsAct
 import { assocIn } from "icepick";
 
 const question0Metrics = makeQuestion((card, state) => ({
-    card,
-    state: assocIn(
-        state,
-        ["metadata", "tables", ORDERS_TABLE_ID, "metrics"],
-        []
-    )
+  card,
+  state: assocIn(state, ["metadata", "tables", ORDERS_TABLE_ID, "metrics"], []),
 }));
 const question1Metrics = makeQuestion();
 const question6Metrics = makeQuestion((card, state) => ({
-    card,
-    state: assocIn(
-        state,
-        ["metadata", "tables", ORDERS_TABLE_ID, "metrics"],
-        [
-            MAIN_METRIC_ID,
-            MAIN_METRIC_ID,
-            MAIN_METRIC_ID,
-            MAIN_METRIC_ID,
-            MAIN_METRIC_ID,
-            MAIN_METRIC_ID
-        ]
-    )
+  card,
+  state: assocIn(
+    state,
+    ["metadata", "tables", ORDERS_TABLE_ID, "metrics"],
+    [
+      MAIN_METRIC_ID,
+      MAIN_METRIC_ID,
+      MAIN_METRIC_ID,
+      MAIN_METRIC_ID,
+      MAIN_METRIC_ID,
+      MAIN_METRIC_ID,
+    ],
+  ),
 }));
 
 describe("CommonMetricsAction", () => {
-    it("should not be valid if the table has no metrics", () => {
-        expect(
-            CommonMetricsAction({
-                question: question0Metrics
-            })
-        ).toHaveLength(0);
+  it("should not be valid if the table has no metrics", () => {
+    expect(
+      CommonMetricsAction({
+        question: question0Metrics,
+      }),
+    ).toHaveLength(0);
+  });
+  it("should return a scalar card for the metric", () => {
+    const actions = CommonMetricsAction({
+      question: question1Metrics,
     });
-    it("should return a scalar card for the metric", () => {
-        const actions = CommonMetricsAction({
-            question: question1Metrics
-        });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: ORDERS_TABLE_ID,
-            aggregation: [["METRIC", MAIN_METRIC_ID]]
-        });
-        expect(newCard.display).toEqual("scalar");
-    });
-    it("should only return up to 5 actions", () => {
-        expect(
-            CommonMetricsAction({
-                question: question6Metrics
-            })
-        ).toHaveLength(5);
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: ORDERS_TABLE_ID,
+      aggregation: [["METRIC", MAIN_METRIC_ID]],
     });
+    expect(newCard.display).toEqual("scalar");
+  });
+  it("should only return up to 5 actions", () => {
+    expect(
+      CommonMetricsAction({
+        question: question6Metrics,
+      }),
+    ).toHaveLength(5);
+  });
 });
diff --git a/frontend/test/modes/actions/CompoundQueryAction.unit.spec.js b/frontend/test/modes/actions/CompoundQueryAction.unit.spec.js
index 36a6365c66e6b98e5f01325acdf9a1a732173488..d40c05073bee90f2f393d511d2671f1bd6bbc105 100644
--- a/frontend/test/modes/actions/CompoundQueryAction.unit.spec.js
+++ b/frontend/test/modes/actions/CompoundQueryAction.unit.spec.js
@@ -5,39 +5,36 @@ import CompoundQueryAction from "../../../src/metabase/qb/components/actions/Com
 import Question from "metabase-lib/lib/Question";
 
 import {
-    native_orders_count_card,
-    orders_count_card,
-    unsaved_native_orders_count_card,
-    metadata
+  native_orders_count_card,
+  orders_count_card,
+  unsaved_native_orders_count_card,
+  metadata,
 } from "__support__/sample_dataset_fixture";
 
 describe("CompoundQueryAction", () => {
-    it("should not suggest a compount query for an unsaved native query", () => {
-        const question = new Question(
-            metadata,
-            unsaved_native_orders_count_card
-        );
-        expect(CompoundQueryAction({ question })).toHaveLength(0);
-    });
-    it("should suggest a compound query for a mbql query", () => {
-        const question = new Question(metadata, orders_count_card);
+  it("should not suggest a compount query for an unsaved native query", () => {
+    const question = new Question(metadata, unsaved_native_orders_count_card);
+    expect(CompoundQueryAction({ question })).toHaveLength(0);
+  });
+  it("should suggest a compound query for a mbql query", () => {
+    const question = new Question(metadata, orders_count_card);
 
-        const actions = CompoundQueryAction({ question });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: "card__2"
-        });
+    const actions = CompoundQueryAction({ question });
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: "card__2",
     });
+  });
 
-    it("should return a nested query for a saved native card", () => {
-        const question = new Question(metadata, native_orders_count_card);
+  it("should return a nested query for a saved native card", () => {
+    const question = new Question(metadata, native_orders_count_card);
 
-        const actions = CompoundQueryAction({ question });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: "card__3"
-        });
+    const actions = CompoundQueryAction({ question });
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: "card__3",
     });
+  });
 });
diff --git a/frontend/test/modes/actions/CountByTimeAction.integ.spec.js b/frontend/test/modes/actions/CountByTimeAction.integ.spec.js
index fac800acfafe0625219920eec551357f88b65b2b..2df1d22643b7501aecbeabfd9cd00440c68f845d 100644
--- a/frontend/test/modes/actions/CountByTimeAction.integ.spec.js
+++ b/frontend/test/modes/actions/CountByTimeAction.integ.spec.js
@@ -1,7 +1,7 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("CountByTimeAction", () => {
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/actions/CountByTimeAction.unit.spec.js b/frontend/test/modes/actions/CountByTimeAction.unit.spec.js
index 9dccfeecb4685007e783074da8c8befc1d936b4a..0311738206551a1f61c2be3cda32f143d3e02f8d 100644
--- a/frontend/test/modes/actions/CountByTimeAction.unit.spec.js
+++ b/frontend/test/modes/actions/CountByTimeAction.unit.spec.js
@@ -1,36 +1,34 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import {
-    question,
-    questionNoFields,
-    ORDERS_TABLE_ID,
-    ORDERS_CREATED_DATE_FIELD_ID
+  question,
+  questionNoFields,
+  ORDERS_TABLE_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 import CountByTimeAction from "metabase/qb/components/actions/CountByTimeAction";
 
 describe("CountByTimeAction", () => {
-    it("should not be valid if the table has no metrics", () => {
-        expect(CountByTimeAction({ question: questionNoFields })).toHaveLength(
-            0
-        );
-    });
-    it("should return a scalar card for the metric", () => {
-        const actions = CountByTimeAction({ question: question });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: ORDERS_TABLE_ID,
-            aggregation: [["count"]],
-            breakout: [
-                [
-                    "datetime-field",
-                    ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
-                    "as",
-                    "day"
-                ]
-            ]
-        });
-        expect(newCard.display).toEqual("bar");
+  it("should not be valid if the table has no metrics", () => {
+    expect(CountByTimeAction({ question: questionNoFields })).toHaveLength(0);
+  });
+  it("should return a scalar card for the metric", () => {
+    const actions = CountByTimeAction({ question: question });
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: ORDERS_TABLE_ID,
+      aggregation: [["count"]],
+      breakout: [
+        [
+          "datetime-field",
+          ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+          "as",
+          "day",
+        ],
+      ],
     });
+    expect(newCard.display).toEqual("bar");
+  });
 });
diff --git a/frontend/test/modes/actions/SummarizeBySegmentMetricAction.unit.spec.js b/frontend/test/modes/actions/SummarizeBySegmentMetricAction.unit.spec.js
index 53862b48e502563977a7af0cd07f1cf006f0eb55..f6ac66dad84693641ba5311d5e53108fb9c1b1d0 100644
--- a/frontend/test/modes/actions/SummarizeBySegmentMetricAction.unit.spec.js
+++ b/frontend/test/modes/actions/SummarizeBySegmentMetricAction.unit.spec.js
@@ -1,82 +1,80 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 import Question from "metabase-lib/lib/Question";
 import SummarizeBySegmentMetricAction from "metabase/qb/components/actions/SummarizeBySegmentMetricAction";
 import { mount } from "enzyme";
 
 const question = Question.create({
-    databaseId: DATABASE_ID,
-    tableId: ORDERS_TABLE_ID,
-    metadata
+  databaseId: DATABASE_ID,
+  tableId: ORDERS_TABLE_ID,
+  metadata,
 });
 
 describe("SummarizeBySegmentMetricAction", () => {
-    describe("aggregation options", () => {
-        it("should show only a subset of all query aggregations", () => {
-            const hasAggregationOption = (popover, optionName) =>
-                popover.find(
-                    `.List-item-title[children="${optionName}"]`
-                ).length === 1;
+  describe("aggregation options", () => {
+    it("should show only a subset of all query aggregations", () => {
+      const hasAggregationOption = (popover, optionName) =>
+        popover.find(`.List-item-title[children="${optionName}"]`).length === 1;
 
-            const action = SummarizeBySegmentMetricAction({ question })[0];
-            const popover = mount(
-                action.popover({
-                    onClose: () => {},
-                    onChangeCardAndRun: () => {}
-                })
-            );
+      const action = SummarizeBySegmentMetricAction({ question })[0];
+      const popover = mount(
+        action.popover({
+          onClose: () => {},
+          onChangeCardAndRun: () => {},
+        }),
+      );
 
-            expect(hasAggregationOption(popover, "Count of rows")).toBe(true);
-            expect(hasAggregationOption(popover, "Average of ...")).toBe(true);
-            expect(hasAggregationOption(popover, "Raw data")).toBe(false);
-            expect(
-                hasAggregationOption(popover, "Cumulative count of rows")
-            ).toBe(false);
-            expect(popover.find(".List-section-title").length).toBe(0);
-        });
+      expect(hasAggregationOption(popover, "Count of rows")).toBe(true);
+      expect(hasAggregationOption(popover, "Average of ...")).toBe(true);
+      expect(hasAggregationOption(popover, "Raw data")).toBe(false);
+      expect(hasAggregationOption(popover, "Cumulative count of rows")).toBe(
+        false,
+      );
+      expect(popover.find(".List-section-title").length).toBe(0);
     });
+  });
 
-    describe("onChangeCardAndRun", async () => {
-        it("should be called for 'Count of rows' choice", async () => {
-            const action = SummarizeBySegmentMetricAction({ question })[0];
-
-            await new Promise((resolve, reject) => {
-                const popover = action.popover({
-                    onClose: () => {},
-                    onChangeCardAndRun: async card => {
-                        expect(card).toBeDefined();
-                        resolve();
-                    }
-                });
+  describe("onChangeCardAndRun", async () => {
+    it("should be called for 'Count of rows' choice", async () => {
+      const action = SummarizeBySegmentMetricAction({ question })[0];
 
-                const component = mount(popover);
-                click(component.find('.List-item-title[children="Count of rows"]'));
-            });
+      await new Promise((resolve, reject) => {
+        const popover = action.popover({
+          onClose: () => {},
+          onChangeCardAndRun: async card => {
+            expect(card).toBeDefined();
+            resolve();
+          },
         });
 
-        it("should be called for 'Sum of ...' => 'Subtotal' choice", async () => {
-            const action = SummarizeBySegmentMetricAction({ question })[0];
-
-            await new Promise((resolve, reject) => {
-                const popover = action.popover({
-                    onClose: () => {},
-                    onChangeCardAndRun: async card => {
-                        expect(card).toBeDefined();
-                        resolve();
-                    }
-                });
+        const component = mount(popover);
+        click(component.find('.List-item-title[children="Count of rows"]'));
+      });
+    });
 
-                const component = mount(popover);
-                click(component.find('.List-item-title[children="Sum of ..."]'));
+    it("should be called for 'Sum of ...' => 'Subtotal' choice", async () => {
+      const action = SummarizeBySegmentMetricAction({ question })[0];
 
-                click(component.find('.List-item-title[children="Subtotal"]'));
-            });
+      await new Promise((resolve, reject) => {
+        const popover = action.popover({
+          onClose: () => {},
+          onChangeCardAndRun: async card => {
+            expect(card).toBeDefined();
+            resolve();
+          },
         });
+
+        const component = mount(popover);
+        click(component.find('.List-item-title[children="Sum of ..."]'));
+
+        click(component.find('.List-item-title[children="Subtotal"]'));
+      });
     });
+  });
 });
diff --git a/frontend/test/modes/drills/CountByColumnDrill.integ.spec.js b/frontend/test/modes/drills/CountByColumnDrill.integ.spec.js
index 9a383964327cf0708629c052179cd36321386da1..f62da056533cbb95318d21d61df939a84a049a8e 100644
--- a/frontend/test/modes/drills/CountByColumnDrill.integ.spec.js
+++ b/frontend/test/modes/drills/CountByColumnDrill.integ.spec.js
@@ -1,7 +1,7 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("CountByColumnDrill", () => {
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/drills/CountByColumnDrill.unit.spec.js b/frontend/test/modes/drills/CountByColumnDrill.unit.spec.js
index ef516462dca79b75b56f6bb5fe26fe4efc00aa82..fae1a486928ab8e440f16924e3dad171969331c6 100644
--- a/frontend/test/modes/drills/CountByColumnDrill.unit.spec.js
+++ b/frontend/test/modes/drills/CountByColumnDrill.unit.spec.js
@@ -3,36 +3,36 @@
 import CountByColumnDrill from "metabase/qb/components/drill/CountByColumnDrill";
 
 import {
-    productQuestion,
-    clickedCategoryHeader,
-    PRODUCT_TABLE_ID,
-    PRODUCT_CATEGORY_FIELD_ID
+  productQuestion,
+  clickedCategoryHeader,
+  PRODUCT_TABLE_ID,
+  PRODUCT_CATEGORY_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 describe("CountByColumnDrill", () => {
-    it("should not be valid for top level actions", () => {
-        expect(CountByColumnDrill({ productQuestion })).toHaveLength(0);
+  it("should not be valid for top level actions", () => {
+    expect(CountByColumnDrill({ productQuestion })).toHaveLength(0);
+  });
+  it("should be valid for click on numeric column header", () => {
+    expect(
+      CountByColumnDrill({
+        question: productQuestion,
+        clicked: clickedCategoryHeader,
+      }),
+    ).toHaveLength(1);
+  });
+  it("should be return correct new card", () => {
+    const actions = CountByColumnDrill({
+      question: productQuestion,
+      clicked: clickedCategoryHeader,
     });
-    it("should be valid for click on numeric column header", () => {
-        expect(
-            CountByColumnDrill({
-                question: productQuestion,
-                clicked: clickedCategoryHeader
-            })
-        ).toHaveLength(1);
-    });
-    it("should be return correct new card", () => {
-        const actions = CountByColumnDrill({
-            question: productQuestion,
-            clicked: clickedCategoryHeader
-        });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: PRODUCT_TABLE_ID,
-            aggregation: [["count"]],
-            breakout: [["field-id", PRODUCT_CATEGORY_FIELD_ID]]
-        });
-        expect(newCard.display).toEqual("bar");
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: PRODUCT_TABLE_ID,
+      aggregation: [["count"]],
+      breakout: [["field-id", PRODUCT_CATEGORY_FIELD_ID]],
     });
+    expect(newCard.display).toEqual("bar");
+  });
 });
diff --git a/frontend/test/modes/drills/ObjectDetailDrill.unit.spec.js b/frontend/test/modes/drills/ObjectDetailDrill.unit.spec.js
index 96f650ed85daf3ab98532aa648f466ed6f73e9a6..4cc26e3d8a1cbc69c4b313ab20f8dc17a9efa603 100644
--- a/frontend/test/modes/drills/ObjectDetailDrill.unit.spec.js
+++ b/frontend/test/modes/drills/ObjectDetailDrill.unit.spec.js
@@ -3,47 +3,47 @@
 import ObjectDetailDrill from "metabase/qb/components/drill/ObjectDetailDrill";
 
 import {
-    question,
-    clickedFloatValue,
-    clickedPKValue,
-    clickedFKValue,
-    ORDERS_TABLE_ID,
-    PRODUCT_TABLE_ID,
-    ORDERS_PK_FIELD_ID,
-    PRODUCT_PK_FIELD_ID
+  question,
+  clickedFloatValue,
+  clickedPKValue,
+  clickedFKValue,
+  ORDERS_TABLE_ID,
+  PRODUCT_TABLE_ID,
+  ORDERS_PK_FIELD_ID,
+  PRODUCT_PK_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 describe("ObjectDetailDrill", () => {
-    it("should not be valid non-PK cells", () => {
-        expect(
-            ObjectDetailDrill({
-                question,
-                clicked: clickedFloatValue
-            })
-        ).toHaveLength(0);
+  it("should not be valid non-PK cells", () => {
+    expect(
+      ObjectDetailDrill({
+        question,
+        clicked: clickedFloatValue,
+      }),
+    ).toHaveLength(0);
+  });
+  it("should be return correct new card for PKs", () => {
+    const actions = ObjectDetailDrill({
+      question,
+      clicked: clickedPKValue,
     });
-    it("should be return correct new card for PKs", () => {
-        const actions = ObjectDetailDrill({
-            question,
-            clicked: clickedPKValue
-        });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: ORDERS_TABLE_ID,
-            filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 42]
-        });
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: ORDERS_TABLE_ID,
+      filter: ["=", ["field-id", ORDERS_PK_FIELD_ID], 42],
     });
-    it("should be return correct new card for FKs", () => {
-        const actions = ObjectDetailDrill({
-            question,
-            clicked: clickedFKValue
-        });
-        expect(actions).toHaveLength(1);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: PRODUCT_TABLE_ID,
-            filter: ["=", ["field-id", PRODUCT_PK_FIELD_ID], 43]
-        });
+  });
+  it("should be return correct new card for FKs", () => {
+    const actions = ObjectDetailDrill({
+      question,
+      clicked: clickedFKValue,
     });
+    expect(actions).toHaveLength(1);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: PRODUCT_TABLE_ID,
+      filter: ["=", ["field-id", PRODUCT_PK_FIELD_ID], 43],
+    });
+  });
 });
diff --git a/frontend/test/modes/drills/PivotByCategoryDrill.integ.spec.js b/frontend/test/modes/drills/PivotByCategoryDrill.integ.spec.js
index a50344065156df2ebf882dedc453aa33d6909b6f..42733a87f6ba83aae1d479deed2ce7fbf9def5c8 100644
--- a/frontend/test/modes/drills/PivotByCategoryDrill.integ.spec.js
+++ b/frontend/test/modes/drills/PivotByCategoryDrill.integ.spec.js
@@ -1,32 +1,32 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
 import Question from "metabase-lib/lib/Question";
 import { useSharedAdminLogin } from "__support__/integrated_tests";
 
 describe("PivotByCategoryDrill", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    });
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-    it("should return a result for Order count pivoted by Subtotal", async () => {
-        // NOTE: Using the fixture metadata for now because trying to load the metadata involves a lot of Redux magic
-        const question = Question.create({
-            databaseId: DATABASE_ID,
-            tableId: ORDERS_TABLE_ID,
-            metadata
-        })
-            .query()
-            .addAggregation(["count"])
-            .question();
+  it("should return a result for Order count pivoted by Subtotal", async () => {
+    // NOTE: Using the fixture metadata for now because trying to load the metadata involves a lot of Redux magic
+    const question = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
+    })
+      .query()
+      .addAggregation(["count"])
+      .question();
 
-        const pivotedQuestion = question.pivot([["field-id", 4]]);
+    const pivotedQuestion = question.pivot([["field-id", 4]]);
 
-        const results = await pivotedQuestion.apiGetResults();
-        expect(results[0]).toBeDefined();
-    });
+    const results = await pivotedQuestion.apiGetResults();
+    expect(results[0]).toBeDefined();
+  });
 });
diff --git a/frontend/test/modes/drills/QuickFilterDrill.integ.spec.js b/frontend/test/modes/drills/QuickFilterDrill.integ.spec.js
index dc90db52efacbe609a440e23d10f93b2a2389392..fcdf18a95dbc4c204ce576d173dbbc476929945f 100644
--- a/frontend/test/modes/drills/QuickFilterDrill.integ.spec.js
+++ b/frontend/test/modes/drills/QuickFilterDrill.integ.spec.js
@@ -1,11 +1,11 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("QuickFilterDrill", () => {
-    it("should return correct operators for various inputs", () => {
-        // This could also be in a separate unit test file
-        pending();
-    });
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should return correct operators for various inputs", () => {
+    // This could also be in a separate unit test file
+    pending();
+  });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/drills/SortAction.integ.spec.js b/frontend/test/modes/drills/SortAction.integ.spec.js
index 1b1348c78b0bd3274f4210f5c881967adc8823d6..b308aa926b2ccbba7f591cfc0d34bac673c57684 100644
--- a/frontend/test/modes/drills/SortAction.integ.spec.js
+++ b/frontend/test/modes/drills/SortAction.integ.spec.js
@@ -1,10 +1,10 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("SortAction drill", () => {
-    it("should return correct sort options for various inputs", () => {
-        pending();
-    });
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should return correct sort options for various inputs", () => {
+    pending();
+  });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/drills/SummarizeColumnByTimeDrill.integ.spec.js b/frontend/test/modes/drills/SummarizeColumnByTimeDrill.integ.spec.js
index ee73a9f211377c8bfe7546ed0677e37908ee0de8..709e2faf8ebd017cecdb44411b6d84da0e9d3369 100644
--- a/frontend/test/modes/drills/SummarizeColumnByTimeDrill.integ.spec.js
+++ b/frontend/test/modes/drills/SummarizeColumnByTimeDrill.integ.spec.js
@@ -1,7 +1,7 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("SummarizeColumnByTimeDrill", () => {
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/drills/SummarizeColumnByTimeDrill.unit.spec.js b/frontend/test/modes/drills/SummarizeColumnByTimeDrill.unit.spec.js
index 5d8b579228e665e2616478834cd73717285303f7..e34dc114f51dd52cbf1da64dbc054063a34d4d45 100644
--- a/frontend/test/modes/drills/SummarizeColumnByTimeDrill.unit.spec.js
+++ b/frontend/test/modes/drills/SummarizeColumnByTimeDrill.unit.spec.js
@@ -1,47 +1,47 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import {
-    question,
-    questionNoFields,
-    clickedFloatHeader,
-    ORDERS_TABLE_ID,
-    ORDERS_TOTAL_FIELD_ID,
-    ORDERS_CREATED_DATE_FIELD_ID
+  question,
+  questionNoFields,
+  clickedFloatHeader,
+  ORDERS_TABLE_ID,
+  ORDERS_TOTAL_FIELD_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 import SummarizeColumnByTimeDrill from "metabase/qb/components/drill/SummarizeColumnByTimeDrill";
 
 describe("SummarizeColumnByTimeDrill", () => {
-    it("should not be valid for top level actions", () => {
-        expect(SummarizeColumnByTimeDrill({ question })).toHaveLength(0);
+  it("should not be valid for top level actions", () => {
+    expect(SummarizeColumnByTimeDrill({ question })).toHaveLength(0);
+  });
+  it("should not be valid if there is no time field", () => {
+    expect(
+      SummarizeColumnByTimeDrill({
+        question: questionNoFields,
+        clicked: clickedFloatHeader,
+      }),
+    ).toHaveLength(0);
+  });
+  it("should be return correct new card", () => {
+    const actions = SummarizeColumnByTimeDrill({
+      question: question,
+      clicked: clickedFloatHeader,
     });
-    it("should not be valid if there is no time field", () => {
-        expect(
-            SummarizeColumnByTimeDrill({
-                question: questionNoFields,
-                clicked: clickedFloatHeader
-            })
-        ).toHaveLength(0);
-    });
-    it("should be return correct new card", () => {
-        const actions = SummarizeColumnByTimeDrill({
-            question: question,
-            clicked: clickedFloatHeader
-        });
-        expect(actions).toHaveLength(2);
-        const newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: ORDERS_TABLE_ID,
-            aggregation: [["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]]],
-            breakout: [
-                [
-                    "datetime-field",
-                    ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
-                    "as",
-                    "day"
-                ]
-            ]
-        });
-        expect(newCard.display).toEqual("line");
+    expect(actions).toHaveLength(2);
+    const newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: ORDERS_TABLE_ID,
+      aggregation: [["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]]],
+      breakout: [
+        [
+          "datetime-field",
+          ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+          "as",
+          "day",
+        ],
+      ],
     });
+    expect(newCard.display).toEqual("line");
+  });
 });
diff --git a/frontend/test/modes/drills/SummarizeColumnDrill.integ.spec.js b/frontend/test/modes/drills/SummarizeColumnDrill.integ.spec.js
index 8a9d70a55ec5f86ad9541d4a32836166c9c5e7f5..a83655df7261d33b0a6d578a9a7565b5a9e879ba 100644
--- a/frontend/test/modes/drills/SummarizeColumnDrill.integ.spec.js
+++ b/frontend/test/modes/drills/SummarizeColumnDrill.integ.spec.js
@@ -1,7 +1,7 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("SummarizeColumnDrill", () => {
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
 });
diff --git a/frontend/test/modes/drills/SummarizeColumnDrill.unit.spec.js b/frontend/test/modes/drills/SummarizeColumnDrill.unit.spec.js
index 5560b08901c3bcc95a731b0a6120eac3444f0e9e..42979354ae554bbf1c6f10b580e9be277327cfb2 100644
--- a/frontend/test/modes/drills/SummarizeColumnDrill.unit.spec.js
+++ b/frontend/test/modes/drills/SummarizeColumnDrill.unit.spec.js
@@ -3,27 +3,27 @@
 import SummarizeColumnDrill from "metabase/qb/components/drill/SummarizeColumnDrill";
 
 import {
-    question,
-    clickedFloatHeader,
-    ORDERS_TABLE_ID,
-    ORDERS_TOTAL_FIELD_ID
+  question,
+  clickedFloatHeader,
+  ORDERS_TABLE_ID,
+  ORDERS_TOTAL_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 describe("SummarizeColumnDrill", () => {
-    it("should not be valid for top level actions", () => {
-        expect(SummarizeColumnDrill({ question })).toHaveLength(0);
+  it("should not be valid for top level actions", () => {
+    expect(SummarizeColumnDrill({ question })).toHaveLength(0);
+  });
+  it("should be valid for click on numeric column header", () => {
+    const actions = SummarizeColumnDrill({
+      question,
+      clicked: clickedFloatHeader,
     });
-    it("should be valid for click on numeric column header", () => {
-        const actions = SummarizeColumnDrill({
-            question,
-            clicked: clickedFloatHeader
-        });
-        expect(actions.length).toEqual(5);
-        let newCard = actions[0].question().card();
-        expect(newCard.dataset_query.query).toEqual({
-            source_table: ORDERS_TABLE_ID,
-            aggregation: [["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]]]
-        });
-        expect(newCard.display).toEqual("scalar");
+    expect(actions.length).toEqual(5);
+    let newCard = actions[0].question().card();
+    expect(newCard.dataset_query.query).toEqual({
+      source_table: ORDERS_TABLE_ID,
+      aggregation: [["sum", ["field-id", ORDERS_TOTAL_FIELD_ID]]],
     });
+    expect(newCard.display).toEqual("scalar");
+  });
 });
diff --git a/frontend/test/modes/drills/TimeseriesPivotDrill.integ.spec.js b/frontend/test/modes/drills/TimeseriesPivotDrill.integ.spec.js
index 374ff7274f87011938759e3a9f9ed3e37c1e7b10..ed4b01e88ac9b368a5a8a23be6507fc249697ef1 100644
--- a/frontend/test/modes/drills/TimeseriesPivotDrill.integ.spec.js
+++ b/frontend/test/modes/drills/TimeseriesPivotDrill.integ.spec.js
@@ -1,12 +1,12 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 describe("TimeseriesPivotDrill", () => {
-    it("should produce correct query results for various inputs", () => {
-        // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
-        pending();
-    });
-    it("should accept the dimension value as a year string as in table visualization", () => {
-        // Intented to test that "Zoom In" for a table cell when you have broken out by year works correctly
-        // Could also be part of more comprehensive QB integrated test where the table cell is actually clicked
-        pending();
-    });
+  it("should produce correct query results for various inputs", () => {
+    // Would be nice to run an integration test here to make sure that the resulting MBQL is valid and runnable
+    pending();
+  });
+  it("should accept the dimension value as a year string as in table visualization", () => {
+    // Intented to test that "Zoom In" for a table cell when you have broken out by year works correctly
+    // Could also be part of more comprehensive QB integrated test where the table cell is actually clicked
+    pending();
+  });
 });
diff --git a/frontend/test/modes/lib/drilldown.unit.spec.js b/frontend/test/modes/lib/drilldown.unit.spec.js
index 836950d7f3825545b347c82845a4f46168dec98c..7089a162a0509781498178500ca7ff0dcb847580 100644
--- a/frontend/test/modes/lib/drilldown.unit.spec.js
+++ b/frontend/test/modes/lib/drilldown.unit.spec.js
@@ -1,208 +1,204 @@
 /* eslint-disable flowtype/require-valid-file-annotation */
 
 import {
-    metadata,
-    ORDERS_CREATED_DATE_FIELD_ID,
-    ORDERS_TOTAL_FIELD_ID,
-    PEOPLE_LATITUDE_FIELD_ID,
-    PEOPLE_LONGITUDE_FIELD_ID,
-    PEOPLE_STATE_FIELD_ID
+  metadata,
+  ORDERS_CREATED_DATE_FIELD_ID,
+  ORDERS_TOTAL_FIELD_ID,
+  PEOPLE_LATITUDE_FIELD_ID,
+  PEOPLE_LONGITUDE_FIELD_ID,
+  PEOPLE_STATE_FIELD_ID,
 } from "__support__/sample_dataset_fixture";
 
 import { drillDownForDimensions } from "../../../src/metabase/qb/lib/drilldown";
 
 const col = (fieldId, extra = {}) => ({
-    ...metadata.fields[fieldId],
-    ...extra
+  ...metadata.fields[fieldId],
+  ...extra,
 });
 
 describe("drilldown", () => {
-    describe("drillDownForDimensions", () => {
-        it("should return null if there are no dimensions", () => {
-            const drillDown = drillDownForDimensions([], metadata);
-            expect(drillDown).toEqual(null);
-        });
-
-        // DATE/TIME:
-        it("should return breakout by quarter for breakout by year", () => {
-            const drillDown = drillDownForDimensions(
-                [
-                    {
-                        column: col(ORDERS_CREATED_DATE_FIELD_ID, {
-                            unit: "year"
-                        })
-                    }
-                ],
-                metadata
-            );
-            expect(drillDown).toEqual({
-                breakouts: [
-                    [
-                        "datetime-field",
-                        ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
-                        "quarter"
-                    ]
-                ]
-            });
-        });
-        it("should return breakout by minute for breakout by hour", () => {
-            const drillDown = drillDownForDimensions(
-                [
-                    {
-                        column: col(ORDERS_CREATED_DATE_FIELD_ID, {
-                            unit: "hour"
-                        })
-                    }
-                ],
-                metadata
-            );
-            expect(drillDown).toEqual({
-                breakouts: [
-                    [
-                        "datetime-field",
-                        ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
-                        "minute"
-                    ]
-                ]
-            });
-        });
-        it("should return null for breakout by minute", () => {
-            const drillDown = drillDownForDimensions(
-                [
-                    {
-                        column: col(ORDERS_CREATED_DATE_FIELD_ID, {
-                            unit: "minute"
-                        })
-                    }
-                ],
-                metadata
-            );
-            expect(drillDown).toEqual(null);
-        });
+  describe("drillDownForDimensions", () => {
+    it("should return null if there are no dimensions", () => {
+      const drillDown = drillDownForDimensions([], metadata);
+      expect(drillDown).toEqual(null);
+    });
 
-        // NUMERIC:
-        it("should reset breakout to default binning for num-bins strategy", () => {
-            const drillDown = drillDownForDimensions(
-                [
-                    {
-                        column: col(ORDERS_TOTAL_FIELD_ID, {
-                            binning_info: {
-                                binning_strategy: "num-bins",
-                                num_bins: 10
-                            }
-                        })
-                    }
-                ],
-                metadata
-            );
-            expect(drillDown).toEqual({
-                breakouts: [
-                    [
-                        "binning-strategy",
-                        ["field-id", ORDERS_TOTAL_FIELD_ID],
-                        "default"
-                    ]
-                ]
-            });
-        });
+    // DATE/TIME:
+    it("should return breakout by quarter for breakout by year", () => {
+      const drillDown = drillDownForDimensions(
+        [
+          {
+            column: col(ORDERS_CREATED_DATE_FIELD_ID, {
+              unit: "year",
+            }),
+          },
+        ],
+        metadata,
+      );
+      expect(drillDown).toEqual({
+        breakouts: [
+          [
+            "datetime-field",
+            ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+            "quarter",
+          ],
+        ],
+      });
+    });
+    it("should return breakout by minute for breakout by hour", () => {
+      const drillDown = drillDownForDimensions(
+        [
+          {
+            column: col(ORDERS_CREATED_DATE_FIELD_ID, {
+              unit: "hour",
+            }),
+          },
+        ],
+        metadata,
+      );
+      expect(drillDown).toEqual({
+        breakouts: [
+          [
+            "datetime-field",
+            ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+            "minute",
+          ],
+        ],
+      });
+    });
+    it("should return null for breakout by minute", () => {
+      const drillDown = drillDownForDimensions(
+        [
+          {
+            column: col(ORDERS_CREATED_DATE_FIELD_ID, {
+              unit: "minute",
+            }),
+          },
+        ],
+        metadata,
+      );
+      expect(drillDown).toEqual(null);
+    });
 
-        it("should return breakout with bin-width of 1 for bin-width of 10", () => {
-            const drillDown = drillDownForDimensions(
-                [
-                    {
-                        column: col(ORDERS_TOTAL_FIELD_ID, {
-                            binning_info: {
-                                binning_strategy: "bin-width",
-                                bin_width: 10
-                            }
-                        })
-                    }
-                ],
-                metadata
-            );
-            expect(drillDown).toEqual({
-                breakouts: [
-                    [
-                        "binning-strategy",
-                        ["field-id", ORDERS_TOTAL_FIELD_ID],
-                        "bin-width",
-                        1
-                    ]
-                ]
-            });
-        });
+    // NUMERIC:
+    it("should reset breakout to default binning for num-bins strategy", () => {
+      const drillDown = drillDownForDimensions(
+        [
+          {
+            column: col(ORDERS_TOTAL_FIELD_ID, {
+              binning_info: {
+                binning_strategy: "num-bins",
+                num_bins: 10,
+              },
+            }),
+          },
+        ],
+        metadata,
+      );
+      expect(drillDown).toEqual({
+        breakouts: [
+          ["binning-strategy", ["field-id", ORDERS_TOTAL_FIELD_ID], "default"],
+        ],
+      });
+    });
 
-        // GEO:
-        it("should return breakout by lat/lon for breakout by state", () => {
-            const drillDown = drillDownForDimensions(
-                [{ column: col(PEOPLE_STATE_FIELD_ID) }],
-                metadata
-            );
-            expect(drillDown).toEqual({
-                breakouts: [
-                    [
-                        "binning-strategy",
-                        ["field-id", PEOPLE_LATITUDE_FIELD_ID],
-                        "bin-width",
-                        1
-                    ],
-                    [
-                        "binning-strategy",
-                        ["field-id", PEOPLE_LONGITUDE_FIELD_ID],
-                        "bin-width",
-                        1
-                    ]
-                ]
-            });
-        });
-        it("should return breakout with 10 degree bin-width for lat/lon breakout with 30 degree bin-width", () => {
-            const drillDown = drillDownForDimensions(
-                [
-                    {
-                        column: col(PEOPLE_LATITUDE_FIELD_ID, {
-                            binning_info: {
-                                binning_strategy: "bin-width",
-                                bin_width: 30
-                            }
-                        })
-                    },
-                    {
-                        column: col(PEOPLE_LONGITUDE_FIELD_ID, {
-                            binning_info: {
-                                binning_strategy: "bin-width",
-                                bin_width: 30
-                            }
-                        })
-                    }
-                ],
-                metadata
-            );
-            expect(drillDown).toEqual({
-                breakouts: [
-                    [
-                        "binning-strategy",
-                        ["field-id", PEOPLE_LATITUDE_FIELD_ID],
-                        "bin-width",
-                        10
-                    ],
-                    [
-                        "binning-strategy",
-                        ["field-id", PEOPLE_LONGITUDE_FIELD_ID],
-                        "bin-width",
-                        10
-                    ]
-                ]
-            });
-        });
+    it("should return breakout with bin-width of 1 for bin-width of 10", () => {
+      const drillDown = drillDownForDimensions(
+        [
+          {
+            column: col(ORDERS_TOTAL_FIELD_ID, {
+              binning_info: {
+                binning_strategy: "bin-width",
+                bin_width: 10,
+              },
+            }),
+          },
+        ],
+        metadata,
+      );
+      expect(drillDown).toEqual({
+        breakouts: [
+          [
+            "binning-strategy",
+            ["field-id", ORDERS_TOTAL_FIELD_ID],
+            "bin-width",
+            1,
+          ],
+        ],
+      });
+    });
 
-        // it("should return breakout by state for breakout by country", () => {
-        //     const drillDown = drillDownForDimensions([
-        //         { column: col(PEOPLE_STATE_FIELD_ID) }
-        //     ], metadata);
-        //     expect(drillDown).toEqual({ breakouts: [
-        //         ["binning-strategy", ["field-id", PEOPLE_LATITUDE_FIELD_ID], "bin-width", 1],
-        //         ["binning-strategy", ["field-id", PEOPLE_LONGITUDE_FIELD_ID], "bin-width", 1],
-        //     ]});
-        // })
+    // GEO:
+    it("should return breakout by lat/lon for breakout by state", () => {
+      const drillDown = drillDownForDimensions(
+        [{ column: col(PEOPLE_STATE_FIELD_ID) }],
+        metadata,
+      );
+      expect(drillDown).toEqual({
+        breakouts: [
+          [
+            "binning-strategy",
+            ["field-id", PEOPLE_LATITUDE_FIELD_ID],
+            "bin-width",
+            1,
+          ],
+          [
+            "binning-strategy",
+            ["field-id", PEOPLE_LONGITUDE_FIELD_ID],
+            "bin-width",
+            1,
+          ],
+        ],
+      });
     });
+    it("should return breakout with 10 degree bin-width for lat/lon breakout with 30 degree bin-width", () => {
+      const drillDown = drillDownForDimensions(
+        [
+          {
+            column: col(PEOPLE_LATITUDE_FIELD_ID, {
+              binning_info: {
+                binning_strategy: "bin-width",
+                bin_width: 30,
+              },
+            }),
+          },
+          {
+            column: col(PEOPLE_LONGITUDE_FIELD_ID, {
+              binning_info: {
+                binning_strategy: "bin-width",
+                bin_width: 30,
+              },
+            }),
+          },
+        ],
+        metadata,
+      );
+      expect(drillDown).toEqual({
+        breakouts: [
+          [
+            "binning-strategy",
+            ["field-id", PEOPLE_LATITUDE_FIELD_ID],
+            "bin-width",
+            10,
+          ],
+          [
+            "binning-strategy",
+            ["field-id", PEOPLE_LONGITUDE_FIELD_ID],
+            "bin-width",
+            10,
+          ],
+        ],
+      });
+    });
+
+    // it("should return breakout by state for breakout by country", () => {
+    //     const drillDown = drillDownForDimensions([
+    //         { column: col(PEOPLE_STATE_FIELD_ID) }
+    //     ], metadata);
+    //     expect(drillDown).toEqual({ breakouts: [
+    //         ["binning-strategy", ["field-id", PEOPLE_LATITUDE_FIELD_ID], "bin-width", 1],
+    //         ["binning-strategy", ["field-id", PEOPLE_LONGITUDE_FIELD_ID], "bin-width", 1],
+    //     ]});
+    // })
+  });
 });
diff --git a/frontend/test/parameters/components/widgets/CategoryWidget.integ.spec.js b/frontend/test/parameters/components/widgets/CategoryWidget.integ.spec.js
index ca420533188b9322c6c0215f2d50afcbcd9caafc..246e34f1d8409339d7a9cfad5ed5ef96193d8f10 100644
--- a/frontend/test/parameters/components/widgets/CategoryWidget.integ.spec.js
+++ b/frontend/test/parameters/components/widgets/CategoryWidget.integ.spec.js
@@ -5,94 +5,80 @@ import React from "react";
 import CategoryWidget from "metabase/parameters/components/widgets/CategoryWidget";
 
 import { mount } from "enzyme";
-import {
-    click, clickButton
-} from "__support__/enzyme_utils"
+import { click, clickButton } from "__support__/enzyme_utils";
 
-const VALUES = [
-    ["First"],
-    ["Second"],
-    ["Third"],
-];
+const VALUES = [["First"], ["Second"], ["Third"]];
 
 const ON_SET_VALUE = jest.fn();
 
 function renderCategoryWidget(props) {
-    return mount(<CategoryWidget
-                    values={VALUES}
-                    setValue={ON_SET_VALUE}
-                    onClose={() => {}}
-                    {...props}
-                />
-    );
+  return mount(
+    <CategoryWidget
+      values={VALUES}
+      setValue={ON_SET_VALUE}
+      onClose={() => {}}
+      {...props}
+    />,
+  );
 }
 
 describe("CategoryWidget", () => {
-    describe("with a selected value", () => {
-        it("should render with selected value checked", () => {
-            let categoryWidget = renderCategoryWidget({ value: VALUES[0] });
-            expect(categoryWidget.find('.Icon-check').length).toEqual(1);
-            categoryWidget.find("label")
-                            .findWhere((label) => label.text().match(/First/))
-                            .find('.Icon-check')
-                            .exists();
-        });
+  describe("with a selected value", () => {
+    it("should render with selected value checked", () => {
+      let categoryWidget = renderCategoryWidget({ value: VALUES[0] });
+      expect(categoryWidget.find(".Icon-check").length).toEqual(1);
+      categoryWidget
+        .find("label")
+        .findWhere(label => label.text().match(/First/))
+        .find(".Icon-check")
+        .exists();
     });
+  });
 
-    describe("without a selected value", () => {
-        it("should render with selected value checked", () => {
-            let categoryWidget = renderCategoryWidget({ value: [] });
-            expect(categoryWidget.find('.Icon-check').length).toEqual(0);
-        });
+  describe("without a selected value", () => {
+    it("should render with selected value checked", () => {
+      let categoryWidget = renderCategoryWidget({ value: [] });
+      expect(categoryWidget.find(".Icon-check").length).toEqual(0);
     });
+  });
 
-    describe("selecting values", () => {
-        it("should mark the values as selected", () => {
-            let categoryWidget = renderCategoryWidget({ value: [] });
-            // Check option 1
-            click(
-                categoryWidget.find("label").at(0)
-            );
-            expect(categoryWidget.find('.Icon-check').length).toEqual(1);
+  describe("selecting values", () => {
+    it("should mark the values as selected", () => {
+      let categoryWidget = renderCategoryWidget({ value: [] });
+      // Check option 1
+      click(categoryWidget.find("label").at(0));
+      expect(categoryWidget.find(".Icon-check").length).toEqual(1);
 
-            // Check option 2
-            click(
-                categoryWidget.find("label").at(1)
-            );
-            expect(categoryWidget.find('.Icon-check').length).toEqual(2);
+      // Check option 2
+      click(categoryWidget.find("label").at(1));
+      expect(categoryWidget.find(".Icon-check").length).toEqual(2);
 
-            clickButton(categoryWidget.find(".Button"));
+      clickButton(categoryWidget.find(".Button"));
 
-            expect(ON_SET_VALUE).toHaveBeenCalledWith(['First', 'Second']);
+      expect(ON_SET_VALUE).toHaveBeenCalledWith(["First", "Second"]);
 
-            // Un-check option 1
-            click(
-                categoryWidget.find("label").at(0)
-            );
-            expect(categoryWidget.find('.Icon-check').length).toEqual(1);
+      // Un-check option 1
+      click(categoryWidget.find("label").at(0));
+      expect(categoryWidget.find(".Icon-check").length).toEqual(1);
 
-            clickButton(categoryWidget.find(".Button"));
+      clickButton(categoryWidget.find(".Button"));
 
-            expect(ON_SET_VALUE).toHaveBeenCalledWith(['Second']);
-        });
+      expect(ON_SET_VALUE).toHaveBeenCalledWith(["Second"]);
     });
-
-    describe("selecting no values", () => {
-        it("selected values should be null", () => {
-            let categoryWidget = renderCategoryWidget({ value: [] });
-            // Check option 1
-            click(
-                categoryWidget.find("label").at(0)
-            );
-            clickButton(categoryWidget.find(".Button"));
-            expect(ON_SET_VALUE).toHaveBeenCalledWith(['First']);
-
-            // un-check option 1
-            click(
-                categoryWidget.find("label").at(0)
-            );
-            clickButton(categoryWidget.find(".Button"));
-            expect(ON_SET_VALUE).toHaveBeenCalledWith(null);
-        });
+  });
+
+  describe("selecting no values", () => {
+    it("selected values should be null", () => {
+      let categoryWidget = renderCategoryWidget({ value: [] });
+      // Check option 1
+      click(categoryWidget.find("label").at(0));
+      clickButton(categoryWidget.find(".Button"));
+      expect(ON_SET_VALUE).toHaveBeenCalledWith(["First"]);
+
+      // un-check option 1
+      click(categoryWidget.find("label").at(0));
+      clickButton(categoryWidget.find(".Button"));
+      expect(ON_SET_VALUE).toHaveBeenCalledWith(null);
     });
+  });
 });
diff --git a/frontend/test/parameters/parameters.integ.spec.js b/frontend/test/parameters/parameters.integ.spec.js
index a148284a225cbf3507b59ce74e68dc7db56f5130..fc6c91fbac86169604884f213e5f91fefae6c5b3 100644
--- a/frontend/test/parameters/parameters.integ.spec.js
+++ b/frontend/test/parameters/parameters.integ.spec.js
@@ -1,44 +1,49 @@
 // Converted from an old Selenium E2E test
 import {
-    useSharedAdminLogin,
-    logout,
-    createTestStore,
-    restorePreviousLogin,
-    waitForRequestToComplete
+  useSharedAdminLogin,
+  logout,
+  createTestStore,
+  restorePreviousLogin,
+  waitForRequestToComplete,
 } from "__support__/integrated_tests";
-import {
-    click, clickButton,
-    setInputValue
-} from "__support__/enzyme_utils"
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 
 import { mount } from "enzyme";
 
 import { LOAD_CURRENT_USER } from "metabase/redux/user";
-import { INITIALIZE_SETTINGS, UPDATE_SETTING, updateSetting } from "metabase/admin/settings/settings";
+import {
+  INITIALIZE_SETTINGS,
+  UPDATE_SETTING,
+  updateSetting,
+} from "metabase/admin/settings/settings";
 import SettingToggle from "metabase/admin/settings/components/widgets/SettingToggle";
 import Toggle from "metabase/components/Toggle";
 import EmbeddingLegalese from "metabase/admin/settings/components/widgets/EmbeddingLegalese";
 import {
-    CREATE_PUBLIC_LINK,
-    INITIALIZE_QB,
-    API_CREATE_QUESTION,
-    QUERY_COMPLETED,
-    RUN_QUERY,
-    SET_QUERY_MODE,
-    setDatasetQuery,
-    UPDATE_EMBEDDING_PARAMS,
-    UPDATE_ENABLE_EMBEDDING,
-    UPDATE_TEMPLATE_TAG
+  CREATE_PUBLIC_LINK,
+  INITIALIZE_QB,
+  API_CREATE_QUESTION,
+  QUERY_COMPLETED,
+  RUN_QUERY,
+  SET_QUERY_MODE,
+  setDatasetQuery,
+  UPDATE_EMBEDDING_PARAMS,
+  UPDATE_ENABLE_EMBEDDING,
+  UPDATE_TEMPLATE_TAG,
 } from "metabase/query_builder/actions";
 import NativeQueryEditor from "metabase/query_builder/components/NativeQueryEditor";
 import { delay } from "metabase/lib/promise";
 import TagEditorSidebar from "metabase/query_builder/components/template_tags/TagEditorSidebar";
 import { getQuery } from "metabase/query_builder/selectors";
-import { ADD_PARAM_VALUES, FETCH_TABLE_METADATA } from "metabase/redux/metadata";
+import {
+  ADD_PARAM_VALUES,
+  FETCH_TABLE_METADATA,
+} from "metabase/redux/metadata";
 import RunButton from "metabase/query_builder/components/RunButton";
 import Scalar from "metabase/visualizations/visualizations/Scalar";
 import Parameters from "metabase/parameters/components/Parameters";
 import CategoryWidget from "metabase/parameters/components/widgets/CategoryWidget";
+import ParameterFieldWidget from "metabase/parameters/components/widgets/ParameterFieldWidget";
 import SaveQuestionModal from "metabase/containers/SaveQuestionModal";
 import { LOAD_COLLECTIONS } from "metabase/questions/collections";
 import SharingPane from "metabase/public/components/widgets/SharingPane";
@@ -51,234 +56,315 @@ import QuestionEmbedWidget from "metabase/query_builder/containers/QuestionEmbed
 import EmbedWidget from "metabase/public/components/widgets/EmbedWidget";
 
 async function updateQueryText(store, queryText) {
-    // We don't have Ace editor so we have to trigger the Redux action manually
-    const newDatasetQuery = getQuery(store.getState())
-        .updateQueryText(queryText)
-        .datasetQuery()
+  // We don't have Ace editor so we have to trigger the Redux action manually
+  const newDatasetQuery = getQuery(store.getState())
+    .updateQueryText(queryText)
+    .datasetQuery();
 
-    return store.dispatch(setDatasetQuery(newDatasetQuery))
+  return store.dispatch(setDatasetQuery(newDatasetQuery));
 }
 
-const getRelativeUrlWithoutHash = (url) =>
-    url.replace(/#.*$/, "").replace(/http:\/\/.*?\//, "/")
+const getRelativeUrlWithoutHash = url =>
+  url.replace(/#.*$/, "").replace(/http:\/\/.*?\//, "/");
 
 const COUNT_ALL = "200";
 const COUNT_DOOHICKEY = "51";
 const COUNT_GADGET = "47";
 
 describe("parameters", () => {
-    beforeAll(async () =>
-        useSharedAdminLogin()
-    );
-
-    describe("questions", () => {
-        let publicUrl = null;
-        let embedUrl = null;
-
-        it("should allow users to enable public sharing", async () => {
-            const store = await createTestStore();
-
-            // load public sharing settings
-            store.pushPath('/admin/settings/public_sharing');
-            const app = mount(store.getAppContainer())
-
-            await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS])
-
-            // // if enabled, disable it so we're in a known state
-            // // TODO Atte Keinänen 8/9/17: This should be done with a direct API call in afterAll instead
-            const enabledToggleContainer = app.find(SettingToggle).first();
-
-            expect(enabledToggleContainer.text()).toBe("Disabled");
-
-            // toggle it on
-            click(enabledToggleContainer.find(Toggle));
-            await store.waitForActions([UPDATE_SETTING])
-
-            // make sure it's enabled
-            expect(enabledToggleContainer.text()).toBe("Enabled");
-        })
-
-        it("should allow users to enable embedding", async () => {
-            const store = await createTestStore();
-
-            // load public sharing settings
-            store.pushPath('/admin/settings/embedding_in_other_applications');
-            const app = mount(store.getAppContainer())
-
-            await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS])
-
-            click(app.find(EmbeddingLegalese).find('button[children="Enable"]'));
-            await store.waitForActions([UPDATE_SETTING])
-
-            expect(app.find(EmbeddingLegalese).length).toBe(0);
-            const enabledToggleContainer = app.find(SettingToggle).first();
-            expect(enabledToggleContainer.text()).toBe("Enabled");
-        });
-
-        // Note: Test suite is sequential, so individual test cases can't be run individually
-        it("should allow users to create parameterized SQL questions", async () => {
-            // Don't render Ace editor in tests because it uses many DOM methods that aren't supported by jsdom
-            // NOTE Atte Keinänen 8/9/17: Ace provides a MockRenderer class which could be used for pseudo-rendering and
-            // testing Ace editor in tests, but it doesn't render stuff to DOM so I'm not sure how practical it would be
-            NativeQueryEditor.prototype.loadAceEditor = () => {
-            }
-
-            const store = await createTestStore();
-
-            // load public sharing settings
-            store.pushPath(Urls.plainQuestion());
-            const app = mount(store.getAppContainer())
-            await store.waitForActions([INITIALIZE_QB]);
-
-            click(app.find(".Icon-sql"));
-            await store.waitForActions([SET_QUERY_MODE]);
-
-            await updateQueryText(store, "select count(*) from products where {{category}}");
-
-            const tagEditorSidebar = app.find(TagEditorSidebar);
-
-            const fieldFilterVarType = tagEditorSidebar.find('.ColumnarSelector-row').at(3);
-            expect(fieldFilterVarType.text()).toBe("Field Filter");
-            click(fieldFilterVarType);
-
-            // there's an async error here for some reason
-            await store.waitForActions([UPDATE_TEMPLATE_TAG]);
+  beforeAll(async () => useSharedAdminLogin());
 
-            await delay(500);
+  describe("questions", () => {
+    let publicUrl = null;
+    let embedUrl = null;
 
-            const productsRow = tagEditorSidebar.find(".TestPopoverBody .List-section").at(4).find("a");
-            expect(productsRow.text()).toBe("Products");
-            click(productsRow);
+    it("should allow users to enable public sharing", async () => {
+      const store = await createTestStore();
 
-            // Table fields should be loaded on-the-fly before showing the field selector
-            await store.waitForActions(FETCH_TABLE_METADATA)
-            // Needed due to state update after fetching metadata
-            await delay(100)
+      // load public sharing settings
+      store.pushPath("/admin/settings/public_sharing");
+      const app = mount(store.getAppContainer());
 
-            const searchField = tagEditorSidebar.find(".TestPopoverBody").find(ListSearchField).find("input").first()
-            setInputValue(searchField, "cat")
+      await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
 
-            const categoryRow = tagEditorSidebar.find(".TestPopoverBody .List-section").at(2).find("a");
-            expect(categoryRow.text()).toBe("Category");
-            click(categoryRow);
+      // // if enabled, disable it so we're in a known state
+      // // TODO Atte Keinänen 8/9/17: This should be done with a direct API call in afterAll instead
+      const enabledToggleContainer = app.find(SettingToggle).first();
 
-            await store.waitForActions([UPDATE_TEMPLATE_TAG])
+      expect(enabledToggleContainer.text()).toBe("Disabled");
 
-            // close the template variable sidebar
-            click(tagEditorSidebar.find(".Icon-close"));
+      // toggle it on
+      click(enabledToggleContainer.find(Toggle));
+      await store.waitForActions([UPDATE_SETTING]);
 
+      // make sure it's enabled
+      expect(enabledToggleContainer.text()).toBe("Enabled");
+    });
 
-            // test without the parameter
-            click(app.find(RunButton));
-            await store.waitForActions([RUN_QUERY, QUERY_COMPLETED])
-            expect(app.find(Scalar).text()).toBe(COUNT_ALL);
-
-            // test the parameter
-            click(app.find(Parameters).find("a").first());
-            click(app.find(CategoryWidget).find('li h4[children="Doohickey"]'));
-            clickButton(app.find(CategoryWidget).find(".Button"));
-            click(app.find(RunButton));
-            await store.waitForActions([RUN_QUERY, QUERY_COMPLETED])
-            expect(app.find(Scalar).text()).toBe(COUNT_DOOHICKEY);
-
-            // save the question, required for public link/embedding
-            click(app.find(".Header-buttonSection a").first().find("a"))
-            await store.waitForActions([LOAD_COLLECTIONS]);
-
-            setInputValue(app.find(SaveQuestionModal).find("input[name='name']"), "sql parametrized");
-
-            clickButton(app.find(SaveQuestionModal).find("button").last());
-            await store.waitForActions([API_CREATE_QUESTION]);
-            await delay(100)
-
-            click(app.find('#QuestionSavedModal .Button[children="Not now"]'))
-            // wait for modal to close :'(
-            await delay(500);
-
-            // open sharing panel
-            click(app.find(QuestionEmbedWidget).find(EmbedWidget));
-
-            // "Embed this question in an application"
-            click(app.find(SharingPane).find("h3").last());
-
-            // make the parameter editable
-            click(app.find(".AdminSelect-content[children='Disabled']"));
-
-            click(app.find(".TestPopoverBody .Icon-pencil"))
-
-            await delay(500);
-
-            click(app.find("div[children='Publish']"));
-            await store.waitForActions([UPDATE_ENABLE_EMBEDDING, UPDATE_EMBEDDING_PARAMS])
-
-            // save the embed url for next tests
-            embedUrl = getRelativeUrlWithoutHash(app.find(PreviewPane).find("iframe").prop("src"));
-
-            // back to main share panel
-            click(app.find(EmbedTitle));
-
-            // toggle public link on
-            click(app.find(SharingPane).find(Toggle));
-            await store.waitForActions([CREATE_PUBLIC_LINK]);
-
-            // save the public url for next tests
-            publicUrl = getRelativeUrlWithoutHash(app.find(CopyWidget).find("input").first().prop("value"));
-        });
-
-        describe("as an anonymous user", () => {
-            beforeAll(() => logout());
-
-            async function runSharedQuestionTests(store, questionUrl, apiRegex) {
-                store.pushPath(questionUrl);
-                const app = mount(store.getAppContainer())
-
-                await store.waitForActions([ADD_PARAM_VALUES]);
-
-                // Loading the query results is done in PublicQuestion itself so we have to listen to API request instead of Redux action
-                await waitForRequestToComplete("GET", apiRegex)
-                // use `update()` because of setState
-                expect(app.update().find(Scalar).text()).toBe(COUNT_ALL + "sql parametrized");
+    it("should allow users to enable embedding", async () => {
+      const store = await createTestStore();
 
-                // manually click parameter (sadly the query results loading happens inline again)
-                click(app.find(Parameters).find("a").first());
-                click(app.find(CategoryWidget).find('li h4[children="Doohickey"]'));
-                clickButton(app.find(CategoryWidget).find(".Button"));
+      // load public sharing settings
+      store.pushPath("/admin/settings/embedding_in_other_applications");
+      const app = mount(store.getAppContainer());
 
-                await waitForRequestToComplete("GET", apiRegex)
-                expect(app.update().find(Scalar).text()).toBe(COUNT_DOOHICKEY + "sql parametrized");
+      await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
 
-                // set parameter via url
-                store.pushPath("/"); // simulate a page reload by visiting other page
-                store.pushPath(questionUrl + "?category=Gadget");
-                await waitForRequestToComplete("GET", apiRegex)
-                // use `update()` because of setState
-                expect(app.update().find(Scalar).text()).toBe(COUNT_GADGET + "sql parametrized");
-            }
+      click(app.find(EmbeddingLegalese).find('button[children="Enable"]'));
+      await store.waitForActions([UPDATE_SETTING]);
 
-            it("should allow seeing an embedded question", async () => {
-                if (!embedUrl) throw new Error("This test fails because previous tests didn't produce an embed url.")
-                const embedUrlTestStore = await createTestStore({ embedApp: true });
-                await runSharedQuestionTests(embedUrlTestStore, embedUrl, new RegExp("/api/embed/card/.*/query"))
-            })
+      expect(app.find(EmbeddingLegalese).length).toBe(0);
+      const enabledToggleContainer = app.find(SettingToggle).first();
+      expect(enabledToggleContainer.text()).toBe("Enabled");
+    });
 
-            it("should allow seeing a public question", async () => {
-                if (!publicUrl) throw new Error("This test fails because previous tests didn't produce a public url.")
-                const publicUrlTestStore = await createTestStore({ publicApp: true });
-                await runSharedQuestionTests(publicUrlTestStore, publicUrl, new RegExp("/api/public/card/.*/query"))
-            })
+    // Note: Test suite is sequential, so individual test cases can't be run individually
+    it("should allow users to create parameterized SQL questions", async () => {
+      // Don't render Ace editor in tests because it uses many DOM methods that aren't supported by jsdom
+      // NOTE Atte Keinänen 8/9/17: Ace provides a MockRenderer class which could be used for pseudo-rendering and
+      // testing Ace editor in tests, but it doesn't render stuff to DOM so I'm not sure how practical it would be
+      NativeQueryEditor.prototype.loadAceEditor = () => {};
+
+      const store = await createTestStore();
+
+      // load public sharing settings
+      store.pushPath(Urls.plainQuestion());
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([INITIALIZE_QB]);
+
+      click(app.find(".Icon-sql"));
+      await store.waitForActions([SET_QUERY_MODE]);
+
+      await updateQueryText(
+        store,
+        "select count(*) from products where {{category}}",
+      );
+
+      const tagEditorSidebar = app.find(TagEditorSidebar);
+
+      const fieldFilterVarType = tagEditorSidebar
+        .find(".ColumnarSelector-row")
+        .at(3);
+      expect(fieldFilterVarType.text()).toBe("Field Filter");
+      click(fieldFilterVarType);
+
+      // there's an async error here for some reason
+      await store.waitForActions([UPDATE_TEMPLATE_TAG]);
+
+      await delay(500);
+
+      const productsRow = tagEditorSidebar
+        .find(".TestPopoverBody .List-section")
+        .at(4)
+        .find("a");
+      expect(productsRow.text()).toBe("Products");
+      click(productsRow);
+
+      // Table fields should be loaded on-the-fly before showing the field selector
+      await store.waitForActions(FETCH_TABLE_METADATA);
+      // Needed due to state update after fetching metadata
+      await delay(100);
+
+      const searchField = tagEditorSidebar
+        .find(".TestPopoverBody")
+        .find(ListSearchField)
+        .find("input")
+        .first();
+      setInputValue(searchField, "cat");
+
+      const categoryRow = tagEditorSidebar
+        .find(".TestPopoverBody .List-section")
+        .at(2)
+        .find("a");
+      expect(categoryRow.text()).toBe("Category");
+      click(categoryRow);
+
+      await store.waitForActions([UPDATE_TEMPLATE_TAG]);
+
+      // close the template variable sidebar
+      click(tagEditorSidebar.find(".Icon-close"));
+
+      // test without the parameter
+      click(app.find(RunButton));
+      await store.waitForActions([RUN_QUERY, QUERY_COMPLETED]);
+      expect(app.find(Scalar).text()).toBe(COUNT_ALL);
+
+      // test the parameter
+      const parameter = app.find(ParameterFieldWidget).first();
+      click(parameter.find("div").first());
+      click(parameter.find('span[children="Doohickey"]'));
+      clickButton(parameter.find(".Button"));
+      click(app.find(RunButton));
+      await store.waitForActions([RUN_QUERY, QUERY_COMPLETED]);
+      expect(app.find(Scalar).text()).toBe(COUNT_DOOHICKEY);
+
+      // save the question, required for public link/embedding
+      click(
+        app
+          .find(".Header-buttonSection a")
+          .first()
+          .find("a"),
+      );
+      await store.waitForActions([LOAD_COLLECTIONS]);
+
+      setInputValue(
+        app.find(SaveQuestionModal).find("input[name='name']"),
+        "sql parametrized",
+      );
+
+      clickButton(
+        app
+          .find(SaveQuestionModal)
+          .find("button")
+          .last(),
+      );
+      await store.waitForActions([API_CREATE_QUESTION]);
+      await delay(100);
+
+      click(app.find('#QuestionSavedModal .Button[children="Not now"]'));
+      // wait for modal to close :'(
+      await delay(500);
+
+      // open sharing panel
+      click(app.find(QuestionEmbedWidget).find(EmbedWidget));
+
+      // "Embed this question in an application"
+      click(
+        app
+          .find(SharingPane)
+          .find("h3")
+          .last(),
+      );
+
+      // make the parameter editable
+      click(app.find(".AdminSelect-content[children='Disabled']"));
+
+      click(app.find(".TestPopoverBody .Icon-pencil"));
+
+      await delay(500);
+
+      click(app.find("div[children='Publish']"));
+      await store.waitForActions([
+        UPDATE_ENABLE_EMBEDDING,
+        UPDATE_EMBEDDING_PARAMS,
+      ]);
+
+      // save the embed url for next tests
+      embedUrl = getRelativeUrlWithoutHash(
+        app
+          .find(PreviewPane)
+          .find("iframe")
+          .prop("src"),
+      );
+
+      // back to main share panel
+      click(app.find(EmbedTitle));
+
+      // toggle public link on
+      click(app.find(SharingPane).find(Toggle));
+      await store.waitForActions([CREATE_PUBLIC_LINK]);
+
+      // save the public url for next tests
+      publicUrl = getRelativeUrlWithoutHash(
+        app
+          .find(CopyWidget)
+          .find("input")
+          .first()
+          .prop("value"),
+      );
+    });
 
-            // I think it's cleanest to restore the login here so that there are no surprises if you want to add tests
-            // that expect that we're already logged in
-            afterAll(() => restorePreviousLogin())
-        })
+    describe("as an anonymous user", () => {
+      beforeAll(() => logout());
+
+      async function runSharedQuestionTests(store, questionUrl, apiRegex) {
+        store.pushPath(questionUrl);
+        const app = mount(store.getAppContainer());
+
+        await store.waitForActions([ADD_PARAM_VALUES]);
+
+        // Loading the query results is done in PublicQuestion itself so we have to listen to API request instead of Redux action
+        await waitForRequestToComplete("GET", apiRegex);
+        // use `update()` because of setState
+        expect(
+          app
+            .update()
+            .find(Scalar)
+            .text(),
+        ).toBe(COUNT_ALL + "sql parametrized");
+
+        // manually click parameter (sadly the query results loading happens inline again)
+        click(
+          app
+            .find(Parameters)
+            .find("a")
+            .first(),
+        );
+        click(app.find(CategoryWidget).find('li h4[children="Doohickey"]'));
+        clickButton(app.find(CategoryWidget).find(".Button"));
+
+        await waitForRequestToComplete("GET", apiRegex);
+        expect(
+          app
+            .update()
+            .find(Scalar)
+            .text(),
+        ).toBe(COUNT_DOOHICKEY + "sql parametrized");
+
+        // set parameter via url
+        store.pushPath("/"); // simulate a page reload by visiting other page
+        store.pushPath(questionUrl + "?category=Gadget");
+        await waitForRequestToComplete("GET", apiRegex);
+        // use `update()` because of setState
+        expect(
+          app
+            .update()
+            .find(Scalar)
+            .text(),
+        ).toBe(COUNT_GADGET + "sql parametrized");
+      }
+
+      it("should allow seeing an embedded question", async () => {
+        if (!embedUrl)
+          throw new Error(
+            "This test fails because previous tests didn't produce an embed url.",
+          );
+        const embedUrlTestStore = await createTestStore({ embedApp: true });
+        await runSharedQuestionTests(
+          embedUrlTestStore,
+          embedUrl,
+          new RegExp("/api/embed/card/.*/query"),
+        );
+      });
+
+      it("should allow seeing a public question", async () => {
+        if (!publicUrl)
+          throw new Error(
+            "This test fails because previous tests didn't produce a public url.",
+          );
+        const publicUrlTestStore = await createTestStore({ publicApp: true });
+        await runSharedQuestionTests(
+          publicUrlTestStore,
+          publicUrl,
+          new RegExp("/api/public/card/.*/query"),
+        );
+      });
+
+      // I think it's cleanest to restore the login here so that there are no surprises if you want to add tests
+      // that expect that we're already logged in
+      afterAll(() => restorePreviousLogin());
+    });
 
-        afterAll(async () => {
-            const store = await createTestStore();
+    afterAll(async () => {
+      const store = await createTestStore();
 
-            // Disable public sharing and embedding after running tests
-            await store.dispatch(updateSetting({ key: "enable-public-sharing", value: false }))
-            await store.dispatch(updateSetting({ key: "enable-embedding", value: false }))
-        })
+      // Disable public sharing and embedding after running tests
+      await store.dispatch(
+        updateSetting({ key: "enable-public-sharing", value: false }),
+      );
+      await store.dispatch(
+        updateSetting({ key: "enable-embedding", value: false }),
+      );
     });
-
+  });
 });
diff --git a/frontend/test/public/public.integ.spec.js b/frontend/test/public/public.integ.spec.js
index 73f8098a6d76eb6a498ea0fe8c7dde6760ba4828..576a74606795ed647c1d03e6b7693ae0c1f165aa 100644
--- a/frontend/test/public/public.integ.spec.js
+++ b/frontend/test/public/public.integ.spec.js
@@ -1,9 +1,9 @@
-import { mount } from 'enzyme'
+import { mount } from "enzyme";
 
 import {
-    createDashboard,
-    createTestStore,
-    useSharedAdminLogin
+  createDashboard,
+  createTestStore,
+  useSharedAdminLogin,
 } from "__support__/integrated_tests";
 
 import { FETCH_DASHBOARD } from "metabase/dashboard/dashboard";
@@ -12,53 +12,51 @@ import * as Urls from "metabase/lib/urls";
 
 import { DashboardApi, SettingsApi } from "metabase/services";
 
-describe('public pages', () => {
-    beforeAll(async () => {
-        // needed to create the public dash
-        useSharedAdminLogin()
-    })
-
-    describe('public dashboards', () => {
-        let dashboard, store, publicDash
+describe("public pages", () => {
+  beforeAll(async () => {
+    // needed to create the public dash
+    useSharedAdminLogin();
+  });
 
-        beforeAll(async () => {
-            store = await createTestStore()
+  describe("public dashboards", () => {
+    let dashboard, store, publicDash;
 
-            // enable public sharing
-            await SettingsApi.put({ key: 'enable-public-sharing', value: true })
-
-            // create a dashboard
-            dashboard = await createDashboard({
-                name: 'Test public dash',
-                description: 'A dashboard for testing public things'
-            })
+    beforeAll(async () => {
+      store = await createTestStore();
 
-            // create the public link for that dashboard
-            publicDash = await DashboardApi.createPublicLink({ id: dashboard.id })
+      // enable public sharing
+      await SettingsApi.put({ key: "enable-public-sharing", value: true });
 
-        })
+      // create a dashboard
+      dashboard = await createDashboard({
+        name: "Test public dash",
+        description: "A dashboard for testing public things",
+      });
 
-        it('should be possible to view a public dashboard', async () => {
-            store.pushPath(Urls.publicDashboard(publicDash.uuid));
+      // create the public link for that dashboard
+      publicDash = await DashboardApi.createPublicLink({ id: dashboard.id });
+    });
 
-            const app = mount(store.getAppContainer());
+    it("should be possible to view a public dashboard", async () => {
+      store.pushPath(Urls.publicDashboard(publicDash.uuid));
 
-            await store.waitForActions([FETCH_DASHBOARD])
+      const app = mount(store.getAppContainer());
 
-            const headerText = app.find('.EmbedFrame-header .h4').text()
+      await store.waitForActions([FETCH_DASHBOARD]);
 
-            expect(headerText).toEqual('Test public dash')
-        })
+      const headerText = app.find(".EmbedFrame-header .h4").text();
 
-        afterAll(async () => {
-            // archive the dash so we don't impact other tests
-            await DashboardApi.update({
-                id: dashboard.id,
-                archived: true
-            })
-            // do some cleanup so that we don't impact other tests
-            await SettingsApi.put({ key: 'enable-public-sharing', value: false })
-        })
-    })
+      expect(headerText).toEqual("Test public dash");
+    });
 
-})
+    afterAll(async () => {
+      // archive the dash so we don't impact other tests
+      await DashboardApi.update({
+        id: dashboard.id,
+        archived: true,
+      });
+      // do some cleanup so that we don't impact other tests
+      await SettingsApi.put({ key: "enable-public-sharing", value: false });
+    });
+  });
+});
diff --git a/frontend/test/pulse/pulse.integ.spec.js b/frontend/test/pulse/pulse.integ.spec.js
index 2a010f105792d30ffd5f3be22297f8450611a5ad..756e0b2e4ee9cc7f97f6b705a08e12ed1f2b96f7 100644
--- a/frontend/test/pulse/pulse.integ.spec.js
+++ b/frontend/test/pulse/pulse.integ.spec.js
@@ -1,21 +1,14 @@
-
 import {
-    useSharedAdminLogin,
-    createTestStore,
-    createSavedQuestion
+  useSharedAdminLogin,
+  createTestStore,
+  createSavedQuestion,
 } from "__support__/integrated_tests";
-import {
-    click,
-    setInputValue
-} from "__support__/enzyme_utils"
+import { click, setInputValue } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import { mount } from "enzyme";
 
-import {
-    CardApi,
-    PulseApi
-} from "metabase/services";
+import { CardApi, PulseApi } from "metabase/services";
 import Question from "metabase-lib/lib/Question";
 
 import PulseListApp from "metabase/pulse/containers/PulseListApp";
@@ -25,121 +18,144 @@ import CardPicker from "metabase/pulse/components/CardPicker";
 import PulseCardPreview from "metabase/pulse/components/PulseCardPreview";
 import Toggle from "metabase/components/Toggle";
 
-import { FETCH_PULSES, SET_EDITING_PULSE, SAVE_EDITING_PULSE, FETCH_CARDS, FETCH_PULSE_CARD_PREVIEW } from "metabase/pulse/actions";
+import {
+  FETCH_PULSES,
+  SET_EDITING_PULSE,
+  SAVE_EDITING_PULSE,
+  FETCH_CARDS,
+  FETCH_PULSE_CARD_PREVIEW,
+} from "metabase/pulse/actions";
 
 describe("Pulse", () => {
   let questionCount, questionRaw;
-  const normalFormInput = PulseApi.form_input
+  const normalFormInput = PulseApi.form_input;
 
   beforeAll(async () => {
-    useSharedAdminLogin()
+    useSharedAdminLogin();
 
-    const formInput = await PulseApi.form_input()
+    const formInput = await PulseApi.form_input();
     PulseApi.form_input = () => ({
-        channels: {
+      channels: {
         ...formInput.channels,
-            "email": {
-                ...formInput.channels.email,
-                "configured": true
-            }
-        }
-    })
+        email: {
+          ...formInput.channels.email,
+          configured: true,
+        },
+      },
+    });
 
     questionCount = await createSavedQuestion(
-        Question.create({databaseId: 1, tableId: 1, metadata: null})
-            .query()
-            .addAggregation(["count"])
-            .question()
-            .setDisplay("scalar")
-            .setDisplayName("count")
-    )
+      Question.create({ databaseId: 1, tableId: 1, metadata: null })
+        .query()
+        .addAggregation(["count"])
+        .question()
+        .setDisplay("scalar")
+        .setDisplayName("count"),
+    );
 
     questionRaw = await createSavedQuestion(
-        Question.create({databaseId: 1, tableId: 1, metadata: null})
-            .query()
-            .question()
-            .setDisplay("table")
-            .setDisplayName("table")
-    )
+      Question.create({ databaseId: 1, tableId: 1, metadata: null })
+        .query()
+        .question()
+        .setDisplay("table")
+        .setDisplayName("table"),
+    );
 
     // possibly not necessary, but just to be sure we start with clean slate
     for (const pulse of await PulseApi.list()) {
-      await PulseApi.delete({ pulseId: pulse.id })
+      await PulseApi.delete({ pulseId: pulse.id });
     }
-  })
+  });
 
   afterAll(async () => {
-    PulseApi.form_input = normalFormInput
+    PulseApi.form_input = normalFormInput;
 
-    await CardApi.delete({ cardId: questionCount.id() })
-    await CardApi.delete({ cardId: questionRaw.id() })
+    await CardApi.delete({ cardId: questionCount.id() });
+    await CardApi.delete({ cardId: questionRaw.id() });
 
     for (const pulse of await PulseApi.list()) {
-      await PulseApi.delete({ pulseId: pulse.id })
+      await PulseApi.delete({ pulseId: pulse.id });
     }
-  })
+  });
 
   let store;
   beforeEach(async () => {
-    store = await createTestStore()
-  })
+    store = await createTestStore();
+  });
 
   it("should load pulses", async () => {
     store.pushPath("/pulse");
     const app = mount(store.connectContainer(<PulseListApp />));
     await store.waitForActions([FETCH_PULSES]);
 
-    const items = app.find(PulseListItem)
-    expect(items.length).toBe(0)
-  })
+    const items = app.find(PulseListItem);
+    expect(items.length).toBe(0);
+  });
 
   it("should load create pulse", async () => {
     store.pushPath("/pulse/create");
     const app = mount(store.connectContainer(<PulseEditApp />));
-    await store.waitForActions([SET_EDITING_PULSE,FETCH_CARDS]);
+    await store.waitForActions([SET_EDITING_PULSE, FETCH_CARDS]);
 
     // no previews yet
-    expect(app.find(PulseCardPreview).length).toBe(0)
+    expect(app.find(PulseCardPreview).length).toBe(0);
 
     // set name to 'foo'
-    setInputValue(app.find("input").first(), "foo")
+    setInputValue(app.find("input").first(), "foo");
 
     // email channel should be enabled
-    expect(app.find(Toggle).first().props().value).toBe(true);
+    expect(
+      app
+        .find(Toggle)
+        .first()
+        .props().value,
+    ).toBe(true);
 
     // add count card
-    app.find(CardPicker).first().props().onChange(questionCount.id())
+    app
+      .find(CardPicker)
+      .first()
+      .props()
+      .onChange(questionCount.id());
     await store.waitForActions([FETCH_PULSE_CARD_PREVIEW]);
 
     // add raw card
-    app.find(CardPicker).first().props().onChange(questionRaw.id())
+    app
+      .find(CardPicker)
+      .first()
+      .props()
+      .onChange(questionRaw.id());
     await store.waitForActions([FETCH_PULSE_CARD_PREVIEW]);
 
     let previews = app.find(PulseCardPreview);
-    expect(previews.length).toBe(2)
+    expect(previews.length).toBe(2);
 
     // NOTE: check text content since enzyme doesn't doesn't seem to work well with dangerouslySetInnerHTML
-    expect(previews.at(0).text()).toBe("count12,805")
-    expect(previews.at(0).find(".Icon-attachment").length).toBe(1)
-    expect(previews.at(1).text()).toBe("tableThis question will be added as a file attachment")
-    expect(previews.at(1).find(".Icon-attachment").length).toBe(0)
+    expect(previews.at(0).text()).toBe("count12,805");
+    expect(previews.at(0).find(".Icon-attachment").length).toBe(1);
+    expect(previews.at(1).text()).toEqual(
+      expect.stringContaining("Showing 20 of 12,805 rows"),
+    );
+    expect(previews.at(1).find(".Icon-attachment").length).toBe(0);
 
     // toggle email channel off
-    click(app.find(Toggle).first())
+    click(app.find(Toggle).first());
 
     previews = app.find(PulseCardPreview);
-    expect(previews.at(0).text()).toBe("count12,805")
-    expect(previews.at(0).find(".Icon-attachment").length).toBe(0)
-    expect(previews.at(1).text()).toBe("tableThis question won't be included in your Pulse")
-    expect(previews.at(1).find(".Icon-attachment").length).toBe(0)
+    expect(previews.at(0).text()).toBe("count12,805");
+    expect(previews.at(0).find(".Icon-attachment").length).toBe(0);
+    expect(previews.at(1).text()).toEqual(
+      expect.stringContaining("Showing 20 of 12,805 rows"),
+    );
+    expect(previews.at(1).find(".Icon-attachment").length).toBe(0);
 
     // toggle email channel on
-    click(app.find(Toggle).first())
+    click(app.find(Toggle).first());
 
     // save
     const saveButton = app.find(".PulseEdit-footer .Button").first();
-    expect(saveButton.hasClass("Button--primary")).toBe(true)
-    click(saveButton)
+    expect(saveButton.hasClass("Button--primary")).toBe(true);
+    click(saveButton);
 
     await store.waitForActions([SAVE_EDITING_PULSE]);
 
@@ -149,14 +165,14 @@ describe("Pulse", () => {
     expect(pulse.cards[1].id).toBe(questionRaw.id());
     expect(pulse.channels[0].channel_type).toBe("email");
     expect(pulse.channels[0].enabled).toBe(true);
-  })
+  });
 
   it("should load pulses", async () => {
     store.pushPath("/pulse");
     const app = mount(store.connectContainer(<PulseListApp />));
     await store.waitForActions([FETCH_PULSES]);
 
-    const items = app.find(PulseListItem)
-    expect(items.length).toBe(1)
-  })
-})
+    const items = app.find(PulseListItem);
+    expect(items.length).toBe(1);
+  });
+});
diff --git a/frontend/test/pulse/pulse.unit.spec.js b/frontend/test/pulse/pulse.unit.spec.js
index 971c0b7346954f1ab3e55babef12e1609f2c2989..54b730cdb9f6b37c52175a1d4f09e05a46064a2d 100644
--- a/frontend/test/pulse/pulse.unit.spec.js
+++ b/frontend/test/pulse/pulse.unit.spec.js
@@ -1,204 +1,62 @@
-import React from 'react'
-import { shallow } from 'enzyme'
+import React from "react";
+import { shallow } from "enzyme";
 
-import {
-    KEYCODE_DOWN,
-    KEYCODE_TAB,
-    KEYCODE_ENTER,
-    KEYCODE_COMMA
-} from "metabase/lib/keyboard"
-
-import Input from "metabase/components/Input"
-import UserAvatar from 'metabase/components/UserAvatar'
-import RecipientPicker from 'metabase/pulse/components/RecipientPicker'
+import RecipientPicker from "metabase/pulse/components/RecipientPicker";
+import TokenField from "metabase/components/TokenField";
 
 // We have to do some mocking here to avoid calls to GA and to Metabase settings
-jest.mock('metabase/lib/settings', () => ({
-    get: () => 'v'
-}))
-
-global.ga = jest.fn()
+jest.mock("metabase/lib/settings", () => ({
+  get: () => "v",
+}));
 
+global.ga = jest.fn();
 
 const TEST_USERS = [
-    { id: 1, common_name: 'Barb', email: 'barb_holland@hawkins.mail' }, // w
-    { id: 2, common_name: 'Dustin', email: 'dustin_henderson@hawkinsav.club' }, // w
-    { id: 3, common_name: 'El', email: '011@energy.gov' },
-    { id: 4, common_name: 'Lucas', email: 'lucas.sinclair@hawkins.mail' }, // w
-    { id: 5, common_name: 'Mike', email: 'dm_mike@hawkins.mail' }, // w
-    { id: 6, common_name: 'Nancy', email: '' },
-    { id: 7, common_name: 'Steve', email: '' },
-    { id: 8, common_name: 'Will', email: 'zombieboy@upside.down' }, // w
-]
-
-describe('recipient picker', () => {
-    describe('focus', () => {
-        it('should be focused if there are no recipients', () => {
-            const wrapper = shallow(
-                <RecipientPicker
-                    recipients={[]}
-                    users={TEST_USERS}
-                    isNewPulse={true}
-                    onRecipientsChange={() => alert('why?')}
-                />
-            )
-
-            expect(wrapper.state().focused).toBe(true)
-        })
-        it('should not be focused if there are existing recipients', () => {
-            const wrapper = shallow(
-                <RecipientPicker
-                    recipients={[TEST_USERS[0]]}
-                    users={TEST_USERS}
-                    isNewPulse={true}
-                    onRecipientsChange={() => alert('why?')}
-                />
-            )
-
-            expect(wrapper.state().focused).toBe(false)
-        })
-    })
-    describe('filtering', () => {
-        it('should properly filter users based on input', () => {
-            const wrapper = shallow(
-                <RecipientPicker
-                    recipients={[]}
-                    users={TEST_USERS}
-                    isNewPulse={true}
-                    onRecipientsChange={() => alert('why?')}
-                />
-            )
-
-            const spy = jest.spyOn(wrapper.instance(), 'setInputValue')
-            const input = wrapper.find(Input)
-
-            // we should start off with no users
-            expect(wrapper.state().filteredUsers.length).toBe(0)
-
-            // simulate typing 'w'
-            input.simulate('change', { target: { value: 'w' }})
-
-            expect(spy).toHaveBeenCalled()
-            expect(wrapper.state().inputValue).toEqual('w')
-
-            // 5 of the test users have a w in their name or email
-            expect(wrapper.state().filteredUsers.length).toBe(5)
-        })
-    })
-
-    describe('recipient selection', () => {
-        it('should allow the user to click to select a recipient', () => {
-            const spy = jest.fn()
-            const wrapper = shallow(
-                <RecipientPicker
-                    recipients={[]}
-                    users={TEST_USERS}
-                    isNewPulse={true}
-                    onRecipientsChange={spy}
-                />
-            )
-
-            const input = wrapper.find(Input)
-
-            // limit our options to one user by typing
-            input.simulate('change', { target: { value: 'steve' }})
-            expect(wrapper.state().filteredUsers.length).toBe(1)
-
-            const user = wrapper.find(UserAvatar).closest('li')
-            user.simulate('click', { target: {}})
-
-            expect(spy).toHaveBeenCalled()
-        })
-
-        describe('key selection', () => {
-            [KEYCODE_TAB, KEYCODE_ENTER, KEYCODE_COMMA].map(key =>
-                it(`should allow the user to use arrow keys and then ${key} to select a recipient`, () => {
-                    const spy = jest.fn()
-
-                    const wrapper = shallow(
-                        <RecipientPicker
-                            recipients={[]}
-                            users={TEST_USERS}
-                            isNewPulse={true}
-                            onRecipientsChange={spy}
-                        />
-                    )
-
-                    const input = wrapper.find(Input)
-
-
-                    // limit our options to  user by typing
-                    input.simulate('change', { target: { value: 'w' }})
-
-                    // the initially selected user should be the first user
-                    expect(wrapper.state().selectedUserID).toBe(TEST_USERS[0].id)
-
-                    input.simulate('keyDown', {
-                        keyCode: KEYCODE_DOWN,
-                        preventDefault: jest.fn()
-                    })
-
-                    // the next possible user should be selected now
-                    expect(wrapper.state().selectedUserID).toBe(TEST_USERS[1].id)
-
-                    input.simulate('keydown', {
-                        keyCode: key,
-                        preventDefalut: jest.fn()
-                    })
-
-                    expect(spy).toHaveBeenCalledTimes(1)
-                    expect(spy).toHaveBeenCalledWith([TEST_USERS[1]])
-                })
-            )
-        })
-
-        describe('usage', () => {
-            it('should all work good', () => {
-                class TestComponent extends React.Component {
-                    state = {
-                        recipients: []
-                    }
-
-                    render () {
-                        const { recipients } = this.state
-                        return (
-                            <RecipientPicker
-                                recipients={recipients}
-                                users={TEST_USERS}
-                                isNewPulse={true}
-                                onRecipientsChange={recipients => {
-                                    this.setState({ recipients })
-                                }}
-                            />
-                        )
-                    }
-                }
-                const wrapper = shallow(<TestComponent />)
-
-                // something about the popover code makes it not work with mount
-                // in the test env, so we have to use shallow and  dive here to
-                // actually get  the selection list to render anything that we
-                // can interact with
-                const picker = wrapper.find(RecipientPicker).dive()
-
-                const input = picker.find(Input)
-                input.simulate('change', { target: { value: 'will' }})
-
-                const user = picker.find(UserAvatar).closest('li')
-                user.simulate('click', { target: {}})
-
-
-                // there should be one user selected
-                expect(wrapper.state().recipients.length).toBe(1)
-
-                // grab the updated state of RecipientPicker
-                const postAddPicker = wrapper.find(RecipientPicker).dive()
-
-                // there should only be one user in the picker now , "Will" and then the input
-                // so there will be two list items
-                expect(postAddPicker.find('li').length).toBe(2)
-
-            })
-        })
-    })
-})
+  { id: 1, common_name: "Barb", email: "barb_holland@hawkins.mail" }, // w
+  { id: 2, common_name: "Dustin", email: "dustin_henderson@hawkinsav.club" }, // w
+  { id: 3, common_name: "El", email: "011@energy.gov" },
+  { id: 4, common_name: "Lucas", email: "lucas.sinclair@hawkins.mail" }, // w
+  { id: 5, common_name: "Mike", email: "dm_mike@hawkins.mail" }, // w
+  { id: 6, common_name: "Nancy", email: "" },
+  { id: 7, common_name: "Steve", email: "" },
+  { id: 8, common_name: "Will", email: "zombieboy@upside.down" }, // w
+];
+
+describe("recipient picker", () => {
+  describe("focus", () => {
+    it("should be focused if there are no recipients", () => {
+      const wrapper = shallow(
+        <RecipientPicker
+          recipients={[]}
+          users={TEST_USERS}
+          isNewPulse={true}
+          onRecipientsChange={() => alert("why?")}
+        />,
+      );
+
+      expect(
+        wrapper
+          .find(TokenField)
+          .dive()
+          .state().isFocused,
+      ).toBe(true);
+    });
+    it("should not be focused if there are existing recipients", () => {
+      const wrapper = shallow(
+        <RecipientPicker
+          recipients={[TEST_USERS[0]]}
+          users={TEST_USERS}
+          isNewPulse={true}
+          onRecipientsChange={() => alert("why?")}
+        />,
+      );
+
+      expect(
+        wrapper
+          .find(TokenField)
+          .dive()
+          .state().isFocused,
+      ).toBe(false);
+    });
+  });
+});
diff --git a/frontend/test/query_builder/actions.integ.spec.js b/frontend/test/query_builder/actions.integ.spec.js
index dd1a2b6a735715e8f0e249ef1c16d64176da926a..0fa766da227f51b8acfadb1ba282192e272d0f2c 100644
--- a/frontend/test/query_builder/actions.integ.spec.js
+++ b/frontend/test/query_builder/actions.integ.spec.js
@@ -1,123 +1,141 @@
 import {
-    ORDERS_TOTAL_FIELD_ID,
-    unsavedOrderCountQuestion
+  ORDERS_TOTAL_FIELD_ID,
+  unsavedOrderCountQuestion,
 } from "__support__/sample_dataset_fixture";
 import Question from "metabase-lib/lib/Question";
 import { parse as urlParse } from "url";
 import {
-    createSavedQuestion,
-    createTestStore,
-    useSharedAdminLogin
+  createSavedQuestion,
+  createTestStore,
+  useSharedAdminLogin,
 } from "__support__/integrated_tests";
 import { initializeQB } from "metabase/query_builder/actions";
-import { getCard, getOriginalCard, getQueryResults } from "metabase/query_builder/selectors";
+import {
+  getCard,
+  getOriginalCard,
+  getQueryResults,
+} from "metabase/query_builder/selectors";
 import _ from "underscore";
 
-jest.mock('metabase/lib/analytics');
+jest.mock("metabase/lib/analytics");
 
 // TODO: Convert completely to modern style
 
 describe("QueryBuilder", () => {
-    let savedCleanQuestion: Question = null;
-    let dirtyQuestion: Question = null;
-    let store = null;
+  let savedCleanQuestion: Question = null;
+  let dirtyQuestion: Question = null;
+  let store = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+    store = await createTestStore();
+  });
 
+  describe("initializeQb", () => {
     beforeAll(async () => {
-        useSharedAdminLogin();
-        store = await createTestStore()
-    })
-
-    describe("initializeQb", () => {
-        beforeAll(async () => {
-            savedCleanQuestion = await createSavedQuestion(unsavedOrderCountQuestion)
-
-            dirtyQuestion = savedCleanQuestion
-                .query()
-                .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
-                .question()
-        })
-
-        describe("for unsaved questions", () => {
-            it("completes successfully", async () => {
-                const location = urlParse(unsavedOrderCountQuestion.getUrl())
-                await store.dispatch(initializeQB(location, {}))
-            });
-
-            it("results in the correct card object in redux state", async () => {
-                expect(getCard(store.getState())).toMatchObject(unsavedOrderCountQuestion.card())
-            })
-
-            it("results in an empty original_card object in redux state", async () => {
-                expect(getOriginalCard(store.getState())).toEqual(null)
-            })
-
-            it("keeps the url same after initialization is finished", async () => {
-                expect(store.getPath()).toBe(unsavedOrderCountQuestion.getUrl())
-            })
-
-            // TODO: setTimeout for
-            xit("fetches the query results", async () => {
-                expect(getQueryResults(store.getState()) !== null).toBe(true)
-            })
-        })
-        describe("for saved questions", async () => {
-            describe("with clean state", () => {
-                it("completes successfully", async () => {
-                    const location = urlParse(savedCleanQuestion.getUrl(savedCleanQuestion))
-                    // pass the card id explicitly as we are not using react-router parameter resolution here
-                    await store.dispatch(initializeQB(location, {cardId: savedCleanQuestion.id()}))
-                });
-
-                it("results in the correct card object in redux state", async () => {
-                    expect(getCard(store.getState())).toMatchObject(_.omit(savedCleanQuestion.card(), "original_card_id"))
-                })
-
-                it("results in the correct original_card object in redux state", async () => {
-                    expect(getOriginalCard(store.getState())).toMatchObject(_.omit(savedCleanQuestion.card(), "original_card_id"))
-                })
-                it("keeps the url same after initialization is finished", async () => {
-                    expect(store.getPath()).toBe(savedCleanQuestion.getUrl(savedCleanQuestion))
-                })
-            })
-            describe("with dirty state", () => {
-                it("completes successfully", async () => {
-                    const location = urlParse(dirtyQuestion.getUrl(savedCleanQuestion))
-                    await store.dispatch(initializeQB(location, {}))
-                });
-
-                it("results in the correct card object in redux state", async () => {
-                    expect(dirtyQuestion.card()).toMatchObject(getCard(store.getState()))
-                })
-
-                it("results in the correct original_card object in redux state", async () => {
-                    expect(getOriginalCard(store.getState())).toMatchObject(_.omit(savedCleanQuestion.card(), "original_card_id"))
-                })
-                it("keeps the url same after initialization is finished", async () => {
-                    expect(store.getPath()).toBe(dirtyQuestion.getUrl())
-                })
-            })
-        })
-    })
-
-    describe("runQuestionQuery", () => {
-        it("returns the correct query results for a valid query", () => {
-            pending();
-        })
-        it("returns a correctly formatted error for invalid queries", () => {
-            pending();
-        })
-
-        // TODO: This would be really good to test but not exactly sure how
-        xit("ignores cache when `{ignoreCache = true}`", () => {
-            pending();
-        })
-
-        it("can be cancelled with `cancelQueryDeferred`", () => {
-            pending();
-        })
-    })
-
-    describe("navigateToNewCardInsideQB", () => {
-        // The full drill-trough flow including navigateToNewCardInsideQB is tested in Visualization.spec.js
-    })
+      savedCleanQuestion = await createSavedQuestion(unsavedOrderCountQuestion);
+
+      dirtyQuestion = savedCleanQuestion
+        .query()
+        .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
+        .question();
+    });
+
+    describe("for unsaved questions", () => {
+      it("completes successfully", async () => {
+        const location = urlParse(unsavedOrderCountQuestion.getUrl());
+        await store.dispatch(initializeQB(location, {}));
+      });
+
+      it("results in the correct card object in redux state", async () => {
+        expect(getCard(store.getState())).toMatchObject(
+          unsavedOrderCountQuestion.card(),
+        );
+      });
+
+      it("results in an empty original_card object in redux state", async () => {
+        expect(getOriginalCard(store.getState())).toEqual(null);
+      });
+
+      it("keeps the url same after initialization is finished", async () => {
+        expect(store.getPath()).toBe(unsavedOrderCountQuestion.getUrl());
+      });
+
+      // TODO: setTimeout for
+      xit("fetches the query results", async () => {
+        expect(getQueryResults(store.getState()) !== null).toBe(true);
+      });
+    });
+    describe("for saved questions", async () => {
+      describe("with clean state", () => {
+        it("completes successfully", async () => {
+          const location = urlParse(
+            savedCleanQuestion.getUrl(savedCleanQuestion),
+          );
+          // pass the card id explicitly as we are not using react-router parameter resolution here
+          await store.dispatch(
+            initializeQB(location, { cardId: savedCleanQuestion.id() }),
+          );
+        });
+
+        it("results in the correct card object in redux state", async () => {
+          expect(getCard(store.getState())).toMatchObject(
+            _.omit(savedCleanQuestion.card(), "original_card_id"),
+          );
+        });
+
+        it("results in the correct original_card object in redux state", async () => {
+          expect(getOriginalCard(store.getState())).toMatchObject(
+            _.omit(savedCleanQuestion.card(), "original_card_id"),
+          );
+        });
+        it("keeps the url same after initialization is finished", async () => {
+          expect(store.getPath()).toBe(
+            savedCleanQuestion.getUrl(savedCleanQuestion),
+          );
+        });
+      });
+      describe("with dirty state", () => {
+        it("completes successfully", async () => {
+          const location = urlParse(dirtyQuestion.getUrl(savedCleanQuestion));
+          await store.dispatch(initializeQB(location, {}));
+        });
+
+        it("results in the correct card object in redux state", async () => {
+          expect(dirtyQuestion.card()).toMatchObject(getCard(store.getState()));
+        });
+
+        it("results in the correct original_card object in redux state", async () => {
+          expect(getOriginalCard(store.getState())).toMatchObject(
+            _.omit(savedCleanQuestion.card(), "original_card_id"),
+          );
+        });
+        it("keeps the url same after initialization is finished", async () => {
+          expect(store.getPath()).toBe(dirtyQuestion.getUrl());
+        });
+      });
+    });
+  });
+
+  describe("runQuestionQuery", () => {
+    it("returns the correct query results for a valid query", () => {
+      pending();
+    });
+    it("returns a correctly formatted error for invalid queries", () => {
+      pending();
+    });
+
+    // TODO: This would be really good to test but not exactly sure how
+    xit("ignores cache when `{ignoreCache = true}`", () => {
+      pending();
+    });
+
+    it("can be cancelled with `cancelQueryDeferred`", () => {
+      pending();
+    });
+  });
+
+  describe("navigateToNewCardInsideQB", () => {
+    // The full drill-trough flow including navigateToNewCardInsideQB is tested in Visualization.spec.js
+  });
 });
diff --git a/frontend/test/query_builder/components/ActionsWidget.integ.spec.js b/frontend/test/query_builder/components/ActionsWidget.integ.spec.js
index 17d5eff1899b2c15eb494f918d9201aba64327dc..684e254b01a155105830f6456617c93951a33c32 100644
--- a/frontend/test/query_builder/components/ActionsWidget.integ.spec.js
+++ b/frontend/test/query_builder/components/ActionsWidget.integ.spec.js
@@ -1,82 +1,107 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click
-} from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
-import React from 'react'
-import { mount, shallow } from 'enzyme'
+import React from "react";
+import { mount, shallow } from "enzyme";
 
-import ActionsWidget from '../../../src/metabase/query_builder/components/ActionsWidget';
+import ActionsWidget from "../../../src/metabase/query_builder/components/ActionsWidget";
 import Question from "metabase-lib/lib/Question";
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
-import { INITIALIZE_QB, LOAD_TABLE_METADATA, QUERY_COMPLETED } from "metabase/query_builder/actions";
+import {
+  INITIALIZE_QB,
+  LOAD_TABLE_METADATA,
+  QUERY_COMPLETED,
+} from "metabase/query_builder/actions";
 import { MetricApi } from "metabase/services";
 
-const getActionsWidget = (question) =>
-    <ActionsWidget
-        question={question}
-        card={question.card()}
-        setCardAndRun={() => {}}
-        navigateToNewCardInsideQB={() => {}}
-    />
-
-describe('ActionsWidget', () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
+const getActionsWidget = question => (
+  <ActionsWidget
+    question={question}
+    card={question.card()}
+    setCardAndRun={() => {}}
+    navigateToNewCardInsideQB={() => {}}
+  />
+);
+
+describe("ActionsWidget", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  it("is visible for an empty question", () => {
+    const question: Question = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
     })
+      .query()
+      .question();
 
-    it("is visible for an empty question", () => {
-        const question: Question = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-            .query()
-            .question();
-
-        const component = shallow(getActionsWidget(question));
-        expect(component.children().children().length).toBeGreaterThan(0);
-    });
-
-    describe("for metrics", () => {
-        let activeMetricId;
-
-        beforeAll(async () => {
-            useSharedAdminLogin()
-
-            const metricDef = {name: "A Metric", description: "For testing new question flow", table_id: 1,show_in_getting_started: true,
-                definition: {database: 1, query: {aggregation: ["count"]}}}
-            activeMetricId = (await MetricApi.create(metricDef)).id;
+    const component = shallow(getActionsWidget(question));
+    expect(component.children().children().length).toBeGreaterThan(0);
+  });
 
-            const retiredMetricId = (await MetricApi.create(metricDef)).id;
-            // Retiring a metric is done with the `delete` endpoint
-            await MetricApi.delete({ metricId: retiredMetricId, revision_message: "Time to retire this buddy" })
-        })
+  describe("for metrics", () => {
+    let activeMetricId;
 
-        afterAll(async () => {
-            await MetricApi.delete({ metricId: activeMetricId, revision_message: "You are now a retired veteran too" })
-        })
-
-        it("shows metrics for the current table, excluding the retired ones", async () => {
-            const url = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-                .query()
-                .question()
-                .getUrl()
-
-            const store = await createTestStore()
-            store.pushPath(url)
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED, LOAD_TABLE_METADATA])
-
-            const actionsWidget = app.find(ActionsWidget)
-            click(actionsWidget.childAt(0))
+    beforeAll(async () => {
+      useSharedAdminLogin();
+
+      const metricDef = {
+        name: "A Metric",
+        description: "For testing new question flow",
+        table_id: 1,
+        show_in_getting_started: true,
+        definition: { database: 1, query: { aggregation: ["count"] } },
+      };
+      activeMetricId = (await MetricApi.create(metricDef)).id;
+
+      const retiredMetricId = (await MetricApi.create(metricDef)).id;
+      // Retiring a metric is done with the `delete` endpoint
+      await MetricApi.delete({
+        metricId: retiredMetricId,
+        revision_message: "Time to retire this buddy",
+      });
+    });
 
-            expect(actionsWidget.find('strong[children="A Metric"]').length).toBe(1)
-        })
-    })
+    afterAll(async () => {
+      await MetricApi.delete({
+        metricId: activeMetricId,
+        revision_message: "You are now a retired veteran too",
+      });
+    });
 
-});
\ No newline at end of file
+    it("shows metrics for the current table, excluding the retired ones", async () => {
+      const url = Question.create({
+        databaseId: DATABASE_ID,
+        tableId: ORDERS_TABLE_ID,
+        metadata,
+      })
+        .query()
+        .question()
+        .getUrl();
+
+      const store = await createTestStore();
+      store.pushPath(url);
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([
+        INITIALIZE_QB,
+        QUERY_COMPLETED,
+        LOAD_TABLE_METADATA,
+      ]);
+
+      const actionsWidget = app.find(ActionsWidget);
+      click(actionsWidget.childAt(0));
+
+      expect(actionsWidget.find('strong[children="A Metric"]').length).toBe(1);
+    });
+  });
+});
diff --git a/frontend/test/query_builder/components/FieldList.integ.spec.js b/frontend/test/query_builder/components/FieldList.integ.spec.js
index 2bac80cb4d80d0d462fbaffcb088a0302a264921..72257f3c0a86002e43b1af725c677cbfb93e736f 100644
--- a/frontend/test/query_builder/components/FieldList.integ.spec.js
+++ b/frontend/test/query_builder/components/FieldList.integ.spec.js
@@ -1,85 +1,118 @@
 // Important: import of integrated_tests always comes first in tests because of mocked modules
-import { createTestStore, useSharedAdminLogin } from "__support__/integrated_tests";
+import {
+  createTestStore,
+  useSharedAdminLogin,
+} from "__support__/integrated_tests";
 
-import React from 'react'
-import { mount } from 'enzyme'
+import React from "react";
+import { mount } from "enzyme";
 
-import FieldList from '../../../src/metabase/query_builder/components/FieldList';
+import FieldList from "../../../src/metabase/query_builder/components/FieldList";
 import Question from "metabase-lib/lib/Question";
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    orders_past_300_days_segment
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  orders_past_300_days_segment,
 } from "__support__/sample_dataset_fixture";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 import { createSegment } from "metabase/admin/datamodel/datamodel";
 import { getMetadata } from "metabase/selectors/metadata";
-import { fetchDatabases, fetchSegments, fetchTableMetadata } from "metabase/redux/metadata";
+import {
+  fetchDatabases,
+  fetchSegments,
+  fetchTableMetadata,
+} from "metabase/redux/metadata";
 import { TestTooltip, TestTooltipContent } from "metabase/components/Tooltip";
 import FilterWidget from "metabase/query_builder/components/filters/FilterWidget";
 
-const getFieldList = (query, fieldOptions, segmentOptions) =>
-    <FieldList
-        tableMetadata={query.tableMetadata()}
-        fieldOptions={fieldOptions}
-        segmentOptions={segmentOptions}
-        customFieldOptions={query.expressions()}
-        onFieldChange={() => {}}
-        enableSubDimensions={false}
-    />;
-
-describe('FieldList', () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    })
-
-    it("should allow using expression as aggregation dimension", async () => {
-        const store = await createTestStore()
-        await store.dispatch(fetchDatabases());
-        await store.dispatch(fetchTableMetadata(ORDERS_TABLE_ID));
-
-        const expressionName = "70% of subtotal";
-        const metadata = getMetadata(store.getState())
-
-        const query: StructuredQuery = Question.create({ databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata })
-            .query()
-            .updateExpression(expressionName, ["*", ["field-id", 4], 0.7])
-
-        // Use the count aggregation as an example case (this is equally valid for filters and groupings)
-        const fieldOptions = query.aggregationFieldOptions("sum");
-        const component = mount(getFieldList(query, fieldOptions));
-
-        expect(component.find(`.List-item-title[children="${expressionName}"]`).length).toBe(1);
-    });
-
-    it("should show the query definition tooltip correctly for a segment", async () => {
-        // TODO Atte Keinänen 6/27/17: Check why the result is wrapped in a promise that needs to be resolved manually
-        const segment = await (await createSegment(orders_past_300_days_segment)).payload;
-
-        const store = await createTestStore()
-        await store.dispatch(fetchDatabases());
-        await store.dispatch(fetchTableMetadata(ORDERS_TABLE_ID));
-        await store.dispatch(fetchSegments());
-        const metadata = getMetadata(store.getState())
-
-        const query: StructuredQuery = Question.create({ databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata }).query();
-        const component = mount(getFieldList(query, query.filterFieldOptions(), query.filterSegmentOptions()));
-
-        // TODO: This is pretty awkward – each list item could have its own React component for easier traversal
-        // Maybe also TestTooltip should provide an interface (like `tooltipWrapper.instance().show()`) for toggling it?
-        const tooltipTarget = component.find(`.List-item-title[children="${segment.name}"]`)
-            .first()
-            .closest('.List-item')
-            .find(".QuestionTooltipTarget")
-            .parent();
-
-        tooltipTarget.simulate("mouseenter");
-
-        const tooltipContent = tooltipTarget.closest(TestTooltip).find(TestTooltipContent);
-        expect(tooltipContent.length).toBe(1)
-
-        // eslint-disable-next-line no-irregular-whitespace
-        expect(tooltipContent.find(FilterWidget).last().text()).toMatch(/Created At -300day/);
+const getFieldList = (query, fieldOptions, segmentOptions) => (
+  <FieldList
+    tableMetadata={query.tableMetadata()}
+    fieldOptions={fieldOptions}
+    segmentOptions={segmentOptions}
+    customFieldOptions={query.expressions()}
+    onFieldChange={() => {}}
+    enableSubDimensions={false}
+  />
+);
+
+describe("FieldList", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  it("should allow using expression as aggregation dimension", async () => {
+    const store = await createTestStore();
+    await store.dispatch(fetchDatabases());
+    await store.dispatch(fetchTableMetadata(ORDERS_TABLE_ID));
+
+    const expressionName = "70% of subtotal";
+    const metadata = getMetadata(store.getState());
+
+    const query: StructuredQuery = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
     })
+      .query()
+      .updateExpression(expressionName, ["*", ["field-id", 4], 0.7]);
+
+    // Use the count aggregation as an example case (this is equally valid for filters and groupings)
+    const fieldOptions = query.aggregationFieldOptions("sum");
+    const component = mount(getFieldList(query, fieldOptions));
+
+    expect(
+      component.find(`.List-item-title[children="${expressionName}"]`).length,
+    ).toBe(1);
+  });
+
+  it("should show the query definition tooltip correctly for a segment", async () => {
+    // TODO Atte Keinänen 6/27/17: Check why the result is wrapped in a promise that needs to be resolved manually
+    const segment = await (await createSegment(orders_past_300_days_segment))
+      .payload;
+
+    const store = await createTestStore();
+    await store.dispatch(fetchDatabases());
+    await store.dispatch(fetchTableMetadata(ORDERS_TABLE_ID));
+    await store.dispatch(fetchSegments());
+    const metadata = getMetadata(store.getState());
+
+    const query: StructuredQuery = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
+    }).query();
+    const component = mount(
+      getFieldList(
+        query,
+        query.filterFieldOptions(),
+        query.filterSegmentOptions(),
+      ),
+    );
+
+    // TODO: This is pretty awkward – each list item could have its own React component for easier traversal
+    // Maybe also TestTooltip should provide an interface (like `tooltipWrapper.instance().show()`) for toggling it?
+    const tooltipTarget = component
+      .find(`.List-item-title[children="${segment.name}"]`)
+      .first()
+      .closest(".List-item")
+      .find(".QuestionTooltipTarget")
+      .parent();
+
+    tooltipTarget.simulate("mouseenter");
+
+    const tooltipContent = tooltipTarget
+      .closest(TestTooltip)
+      .find(TestTooltipContent);
+    expect(tooltipContent.length).toBe(1);
+
+    expect(
+      tooltipContent
+        .find(FilterWidget)
+        .last()
+        .text(),
+      // eslint-disable-next-line no-irregular-whitespace
+    ).toMatch(/Created At -300day/);
+  });
 });
diff --git a/frontend/test/query_builder/components/FieldName.unit.spec.js b/frontend/test/query_builder/components/FieldName.unit.spec.js
index 28881f5715174b2a269339a546386dcd59976e9d..355a00390414c63189c910b6ab2dca0f343b2f1f 100644
--- a/frontend/test/query_builder/components/FieldName.unit.spec.js
+++ b/frontend/test/query_builder/components/FieldName.unit.spec.js
@@ -1,42 +1,67 @@
-
-import React from 'react'
-import { mount } from 'enzyme'
+import React from "react";
+import { mount } from "enzyme";
 
 import {
-    metadata, // connected graph,
-    ORDERS_TABLE_ID,
-    ORDERS_CREATED_DATE_FIELD_ID, ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_CATEGORY_FIELD_ID
-} from '__support__/sample_dataset_fixture'
+  metadata, // connected graph,
+  ORDERS_TABLE_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
+  ORDERS_PRODUCT_FK_FIELD_ID,
+  PRODUCT_CATEGORY_FIELD_ID,
+} from "__support__/sample_dataset_fixture";
 
 import FieldName from "metabase/query_builder/components/FieldName.jsx";
 
 describe("FieldName", () => {
-    it("should render regular field correctly", () => {
-        let fieldName = mount(<FieldName field={ORDERS_CREATED_DATE_FIELD_ID} tableMetadata={metadata.tables[ORDERS_TABLE_ID]}/>);
-        expect(fieldName.text()).toEqual("Created At");
-    });
-    it("should render local field correctly", () => {
-        let fieldName = mount(<FieldName field={["field-id", ORDERS_CREATED_DATE_FIELD_ID]} tableMetadata={metadata.tables[ORDERS_TABLE_ID]}/>);
-        expect(fieldName.text()).toEqual("Created At");
-    });
-    it("should render foreign key correctly", () => {
-        let fieldName = mount(<FieldName field={["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_CATEGORY_FIELD_ID]} tableMetadata={metadata.tables[ORDERS_TABLE_ID]}/>);
-        expect(fieldName.text()).toEqual("ProductCategory");
-    });
-    it("should render datetime correctly", () => {
-        let fieldName = mount(<FieldName field={["datetime-field", ORDERS_CREATED_DATE_FIELD_ID, "week"]} tableMetadata={metadata.tables[ORDERS_TABLE_ID]}/>);
-        expect(fieldName.text()).toEqual("Created At: Week");
-    });
-    // TODO: How to test nested fields with the test dataset? Should we create a test mongo dataset?
-    it("should render nested field correctly", () => {
-        pending();
-        let fieldName = mount(<FieldName field={2} tableMetadata={ORDERS_TABLE_ID}/>);
-        expect(fieldName.text()).toEqual("Foo: Baz");
-    });
-    // TODO: How to test nested fields with the test dataset? Should we create a test mongo dataset?
-    it("should render nested fk field correctly", () => {
-        pending();
-        let fieldName = mount(<FieldName field={["fk->", 3, 2]} tableMetadata={ORDERS_TABLE_ID}/>);
-        expect(fieldName.text()).toEqual("BarFoo: Baz");
-    });
-});
\ No newline at end of file
+  it("should render regular field correctly", () => {
+    let fieldName = mount(
+      <FieldName
+        field={ORDERS_CREATED_DATE_FIELD_ID}
+        tableMetadata={metadata.tables[ORDERS_TABLE_ID]}
+      />,
+    );
+    expect(fieldName.text()).toEqual("Created At");
+  });
+  it("should render local field correctly", () => {
+    let fieldName = mount(
+      <FieldName
+        field={["field-id", ORDERS_CREATED_DATE_FIELD_ID]}
+        tableMetadata={metadata.tables[ORDERS_TABLE_ID]}
+      />,
+    );
+    expect(fieldName.text()).toEqual("Created At");
+  });
+  it("should render foreign key correctly", () => {
+    let fieldName = mount(
+      <FieldName
+        field={["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_CATEGORY_FIELD_ID]}
+        tableMetadata={metadata.tables[ORDERS_TABLE_ID]}
+      />,
+    );
+    expect(fieldName.text()).toEqual("ProductCategory");
+  });
+  it("should render datetime correctly", () => {
+    let fieldName = mount(
+      <FieldName
+        field={["datetime-field", ORDERS_CREATED_DATE_FIELD_ID, "week"]}
+        tableMetadata={metadata.tables[ORDERS_TABLE_ID]}
+      />,
+    );
+    expect(fieldName.text()).toEqual("Created At: Week");
+  });
+  // TODO: How to test nested fields with the test dataset? Should we create a test mongo dataset?
+  it("should render nested field correctly", () => {
+    pending();
+    let fieldName = mount(
+      <FieldName field={2} tableMetadata={ORDERS_TABLE_ID} />,
+    );
+    expect(fieldName.text()).toEqual("Foo: Baz");
+  });
+  // TODO: How to test nested fields with the test dataset? Should we create a test mongo dataset?
+  it("should render nested fk field correctly", () => {
+    pending();
+    let fieldName = mount(
+      <FieldName field={["fk->", 3, 2]} tableMetadata={ORDERS_TABLE_ID} />,
+    );
+    expect(fieldName.text()).toEqual("BarFoo: Baz");
+  });
+});
diff --git a/frontend/test/query_builder/components/GuiQueryEditor.unit.spec.jsx b/frontend/test/query_builder/components/GuiQueryEditor.unit.spec.jsx
index e7b215fe770aa21fa6c3b8bfc1b62d4ae36deba8..08038e4078f300ee851664ba8446a5353aa8de60 100644
--- a/frontend/test/query_builder/components/GuiQueryEditor.unit.spec.jsx
+++ b/frontend/test/query_builder/components/GuiQueryEditor.unit.spec.jsx
@@ -1,49 +1,60 @@
-import React from 'react'
-import { shallow } from 'enzyme'
+import React from "react";
+import { shallow } from "enzyme";
 
-import GuiQueryEditor, { BreakoutWidget } from '../../../src/metabase/query_builder/components/GuiQueryEditor';
+import GuiQueryEditor, {
+  BreakoutWidget,
+} from "../../../src/metabase/query_builder/components/GuiQueryEditor";
 import Question from "metabase-lib/lib/Question";
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    ORDERS_TOTAL_FIELD_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  ORDERS_TOTAL_FIELD_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
 
 import StructuredQuery from "metabase-lib/lib/queries/StructuredQuery";
 
-const getGuiQueryEditor = (query) =>
-    <GuiQueryEditor
-        query={query}
-        databases={metadata.databasesList()}
-        tables={metadata.tablesList()}
-        setDatabaseFn={() => {}}
-        setSourceTableFn={() => {}}
-        setDatasetQuery={() => {}}
-        isShowingTutorial={false}
-        isShowingDataReference={false}
-    />
-
-describe('GuiQueryEditor', () => {
-    it("should allow adding the first breakout", () => {
-        const query: StructuredQuery = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-            .query()
-            .addAggregation(["count"])
-
-        const component = shallow(getGuiQueryEditor(query));
-
-        // The add button is a BreakoutWidget React component
-        expect(component.find(BreakoutWidget).length).toBe(1);
-    });
-    it("should allow adding more than one breakout", () => {
-        const query: StructuredQuery = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-            .query()
-            .addAggregation(["count"])
-            .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID]);
-
-        const component = shallow(getGuiQueryEditor(query));
-
-        // Both the first breakout and the add button which both are the same BreakoutWidget React component
-        expect(component.find(BreakoutWidget).length).toBe(2);
-    });
-});
\ No newline at end of file
+const getGuiQueryEditor = query => (
+  <GuiQueryEditor
+    query={query}
+    databases={metadata.databasesList()}
+    tables={metadata.tablesList()}
+    setDatabaseFn={() => {}}
+    setSourceTableFn={() => {}}
+    setDatasetQuery={() => {}}
+    isShowingTutorial={false}
+    isShowingDataReference={false}
+  />
+);
+
+describe("GuiQueryEditor", () => {
+  it("should allow adding the first breakout", () => {
+    const query: StructuredQuery = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
+    })
+      .query()
+      .addAggregation(["count"]);
+
+    const component = shallow(getGuiQueryEditor(query));
+
+    // The add button is a BreakoutWidget React component
+    expect(component.find(BreakoutWidget).length).toBe(1);
+  });
+  it("should allow adding more than one breakout", () => {
+    const query: StructuredQuery = Question.create({
+      databaseId: DATABASE_ID,
+      tableId: ORDERS_TABLE_ID,
+      metadata,
+    })
+      .query()
+      .addAggregation(["count"])
+      .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID]);
+
+    const component = shallow(getGuiQueryEditor(query));
+
+    // Both the first breakout and the add button which both are the same BreakoutWidget React component
+    expect(component.find(BreakoutWidget).length).toBe(2);
+  });
+});
diff --git a/frontend/test/query_builder/components/NativeQueryEditor.integ.spec.jsx b/frontend/test/query_builder/components/NativeQueryEditor.integ.spec.jsx
index 9f34d328d9f63a94442591841ee752c9b73638aa..7aa5b99658e551f0ced96242c4462a77350cb0cf 100644
--- a/frontend/test/query_builder/components/NativeQueryEditor.integ.spec.jsx
+++ b/frontend/test/query_builder/components/NativeQueryEditor.integ.spec.jsx
@@ -1,5 +1,5 @@
 describe("NativeQueryEditor", () => {
-    it("lets you create a SQL question with a field filter variable", () => {
-        pending();
-    })
-})
\ No newline at end of file
+  it("lets you create a SQL question with a field filter variable", () => {
+    pending();
+  });
+});
diff --git a/frontend/test/query_builder/components/VisualizationSettings.integ.spec.js b/frontend/test/query_builder/components/VisualizationSettings.integ.spec.js
index 98b18b885b5a30df87d3bd010dc33f88fa1f4f46..18076d55c217d35a28c4ad61f26a2b140d4ccc48 100644
--- a/frontend/test/query_builder/components/VisualizationSettings.integ.spec.js
+++ b/frontend/test/query_builder/components/VisualizationSettings.integ.spec.js
@@ -1,24 +1,20 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-} from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import { mount } from "enzyme";
 import {
-    INITIALIZE_QB,
-    QUERY_COMPLETED,
-    setQueryDatabase,
-    setQuerySourceTable,
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  setQueryDatabase,
+  setQuerySourceTable,
 } from "metabase/query_builder/actions";
 
-import {
-    FETCH_TABLE_METADATA,
-} from "metabase/redux/metadata";
+import { FETCH_TABLE_METADATA } from "metabase/redux/metadata";
 
 import CheckBox from "metabase/components/CheckBox";
 import RunButton from "metabase/query_builder/components/RunButton";
@@ -28,60 +24,68 @@ import TableSimple from "metabase/visualizations/components/TableSimple";
 import * as Urls from "metabase/lib/urls";
 
 const initQbWithDbAndTable = (dbId, tableId) => {
-    return async () => {
-        const store = await createTestStore()
-        store.pushPath(Urls.plainQuestion());
-        const qb = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
+  return async () => {
+    const store = await createTestStore();
+    store.pushPath(Urls.plainQuestion());
+    const qb = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
 
-        // Use Products table
-        store.dispatch(setQueryDatabase(dbId));
-        store.dispatch(setQuerySourceTable(tableId));
-        await store.waitForActions([FETCH_TABLE_METADATA]);
+    // Use Products table
+    store.dispatch(setQueryDatabase(dbId));
+    store.dispatch(setQuerySourceTable(tableId));
+    await store.waitForActions([FETCH_TABLE_METADATA]);
 
-        return { store, qb }
-    }
-}
+    return { store, qb };
+  };
+};
 
-const initQBWithReviewsTable = initQbWithDbAndTable(1, 4)
+const initQBWithReviewsTable = initQbWithDbAndTable(1, 4);
 
 describe("QueryBuilder", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin()
-    })
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-    describe("visualization settings", () => {
-        it("lets you hide a field for a raw data table", async () => {
-            const { store, qb } = await initQBWithReviewsTable();
+  describe("visualization settings", () => {
+    it("lets you hide a field for a raw data table", async () => {
+      const { store, qb } = await initQBWithReviewsTable();
 
-            // Run the raw data query
-            click(qb.find(RunButton));
-            await store.waitForActions([QUERY_COMPLETED]);
+      // Run the raw data query
+      click(qb.find(RunButton));
+      await store.waitForActions([QUERY_COMPLETED]);
 
-            const vizSettings = qb.find(VisualizationSettings);
-            click(vizSettings.find(".Icon-gear"));
+      const vizSettings = qb.find(VisualizationSettings);
+      click(vizSettings.find(".Icon-gear"));
 
-            const settingsModal = vizSettings.find(".test-modal")
-            const table = settingsModal.find(TableSimple);
+      const settingsModal = vizSettings.find(".test-modal");
+      const table = settingsModal.find(TableSimple);
 
-            expect(table.find('div[children="Created At"]').length).toBe(1);
+      expect(table.find('div[children="Created At"]').length).toBe(1);
 
-            const doneButton = settingsModal.find(".Button--primary")
-            expect(doneButton.length).toBe(1)
+      const doneButton = settingsModal.find(".Button--primary");
+      expect(doneButton.length).toBe(1);
 
-            const fieldsToIncludeCheckboxes = settingsModal.find(CheckBox)
-            expect(fieldsToIncludeCheckboxes.length).toBe(7)
+      const fieldsToIncludeCheckboxes = settingsModal.find(CheckBox);
+      expect(fieldsToIncludeCheckboxes.length).toBe(7);
 
-            click(fieldsToIncludeCheckboxes.filterWhere((checkbox) => checkbox.parent().find("span").text() === "Created At"))
+      click(
+        fieldsToIncludeCheckboxes.filterWhere(
+          checkbox =>
+            checkbox
+              .parent()
+              .find("span")
+              .text() === "Created At",
+        ),
+      );
 
-            expect(table.find('div[children="Created At"]').length).toBe(0);
+      expect(table.find('div[children="Created At"]').length).toBe(0);
 
-            // Save the settings
-            click(doneButton);
-            expect(vizSettings.find(".test-modal").length).toBe(0);
+      // Save the settings
+      click(doneButton);
+      expect(vizSettings.find(".test-modal").length).toBe(0);
 
-            // Don't test the contents of actual table visualization here as react-virtualized doesn't seem to work
-            // very well together with Enzyme
-        })
-    })
-})
+      // Don't test the contents of actual table visualization here as react-virtualized doesn't seem to work
+      // very well together with Enzyme
+    });
+  });
+});
diff --git a/frontend/test/query_builder/components/dataref/FieldPane.integ.spec.js b/frontend/test/query_builder/components/dataref/FieldPane.integ.spec.js
index 47247976265f8df90df61dc5ce02899f2a800197..229231308eb27dd2390a3b7de6a148afe3f4d47a 100644
--- a/frontend/test/query_builder/components/dataref/FieldPane.integ.spec.js
+++ b/frontend/test/query_builder/components/dataref/FieldPane.integ.spec.js
@@ -1,17 +1,19 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import { mount } from "enzyme";
 
 import {
-    INITIALIZE_QB, QUERY_COMPLETED, setQuerySourceTable,
-    TOGGLE_DATA_REFERENCE
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  setQuerySourceTable,
+  TOGGLE_DATA_REFERENCE,
 } from "metabase/query_builder/actions";
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import DataReference from "metabase/query_builder/components/dataref/DataReference";
@@ -23,68 +25,72 @@ import * as Urls from "metabase/lib/urls";
 
 // Currently a lot of duplication with FieldPane tests
 describe("FieldPane", () => {
-    let store = null;
-    let queryBuilder = null;
+  let store = null;
+  let queryBuilder = null;
 
-    beforeAll(async () => {
-        useSharedAdminLogin();
-        store = await createTestStore()
+  beforeAll(async () => {
+    useSharedAdminLogin();
+    store = await createTestStore();
 
-        store.pushPath(Urls.plainQuestion());
-        queryBuilder = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
-    })
+    store.pushPath(Urls.plainQuestion());
+    queryBuilder = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
+  });
 
-    // NOTE: These test cases are intentionally stateful
-    // (doing the whole app rendering thing in every single test case would probably slow things down)
+  // NOTE: These test cases are intentionally stateful
+  // (doing the whole app rendering thing in every single test case would probably slow things down)
 
-    it("opens properly from QB", async () => {
-        // open data reference sidebar by clicking button
-        click(queryBuilder.find(".Icon-reference"));
-        await store.waitForActions([TOGGLE_DATA_REFERENCE]);
+  it("opens properly from QB", async () => {
+    // open data reference sidebar by clicking button
+    click(queryBuilder.find(".Icon-reference"));
+    await store.waitForActions([TOGGLE_DATA_REFERENCE]);
 
-        const dataReference = queryBuilder.find(DataReference);
-        expect(dataReference.length).toBe(1);
+    const dataReference = queryBuilder.find(DataReference);
+    expect(dataReference.length).toBe(1);
 
-        click(dataReference.find('a[children="Orders"]'));
+    click(dataReference.find('a[children="Orders"]'));
 
-        // TODO: Refactor TablePane so that it uses redux/metadata actions instead of doing inlined API calls
-        // then we can replace this with `store.waitForActions([FETCH_TABLE_FOREIGN_KEYS])` or similar
-        await delay(3000)
+    // TODO: Refactor TablePane so that it uses redux/metadata actions instead of doing inlined API calls
+    // then we can replace this with `store.waitForActions([FETCH_TABLE_FOREIGN_KEYS])` or similar
+    await delay(3000);
 
-        click(dataReference.find(`a[children="Created At"]`).first())
+    click(dataReference.find(`a[children="Created At"]`).first());
 
-        await store.waitForActions([FETCH_TABLE_METADATA]);
-    });
+    await store.waitForActions([FETCH_TABLE_METADATA]);
+  });
 
-    it("lets you group by Created At", async () => {
-        const getUseForButton = () => queryBuilder.find(DataReference).find(UseForButton);
+  it("lets you group by Created At", async () => {
+    const getUseForButton = () =>
+      queryBuilder.find(DataReference).find(UseForButton);
 
-        expect(getUseForButton().length).toBe(0);
+    expect(getUseForButton().length).toBe(0);
 
-        await store.dispatch(setQuerySourceTable(1))
-        // eslint-disable-line react/no-irregular-whitespace
-        expect(getUseForButton().text()).toMatch(/Group by/);
+    await store.dispatch(setQuerySourceTable(1));
+    // eslint-disable-line react/no-irregular-whitespace
+    expect(getUseForButton().text()).toMatch(/Group by/);
 
-        click(getUseForButton());
-        await store.waitForActions([QUERY_COMPLETED]);
+    click(getUseForButton());
+    await store.waitForActions([QUERY_COMPLETED]);
 
-        // after the breakout has been applied, the button shouldn't be visible anymore
-        expect(getUseForButton().length).toBe(0);
-    })
+    // after the breakout has been applied, the button shouldn't be visible anymore
+    expect(getUseForButton().length).toBe(0);
+  });
 
-    it("lets you see all distinct values of Created At", async () => {
-        const distinctValuesButton = queryBuilder.find(DataReference).find(QueryButton).at(0);
+  it("lets you see all distinct values of Created At", async () => {
+    const distinctValuesButton = queryBuilder
+      .find(DataReference)
+      .find(QueryButton)
+      .at(0);
 
-        try {
-            click(distinctValuesButton.children().first());
-        } catch(e) {
-            // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
-            // Now we are just using the onClick handler of Link so we don't have to care about that
-        }
+    try {
+      click(distinctValuesButton.children().first());
+    } catch (e) {
+      // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
+      // Now we are just using the onClick handler of Link so we don't have to care about that
+    }
 
-        await store.waitForActions([QUERY_COMPLETED]);
+    await store.waitForActions([QUERY_COMPLETED]);
 
-        expect(queryBuilder.find(Table).length).toBe(1)
-    });
+    expect(queryBuilder.find(Table).length).toBe(1);
+  });
 });
diff --git a/frontend/test/query_builder/components/dataref/MetricPane.integ.spec.js b/frontend/test/query_builder/components/dataref/MetricPane.integ.spec.js
index 6ad06b81687383ba37c0acf71058f3d57477f9df..d75b8bf68f6d72241b8d5d277aa675ad0bbeaa49 100644
--- a/frontend/test/query_builder/components/dataref/MetricPane.integ.spec.js
+++ b/frontend/test/query_builder/components/dataref/MetricPane.integ.spec.js
@@ -1,14 +1,18 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import { mount } from "enzyme";
 
-import { INITIALIZE_QB, QUERY_COMPLETED, TOGGLE_DATA_REFERENCE } from "metabase/query_builder/actions";
-import { delay } from "metabase/lib/promise"
+import {
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  TOGGLE_DATA_REFERENCE,
+} from "metabase/query_builder/actions";
+import { delay } from "metabase/lib/promise";
 
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import DataReference from "metabase/query_builder/components/dataref/DataReference";
@@ -21,62 +25,69 @@ import * as Urls from "metabase/lib/urls";
 import { MetricApi } from "metabase/services";
 
 describe("MetricPane", () => {
-    let store = null;
-    let queryBuilder = null;
-    let metricId = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin();
-        metricId = (await MetricApi.create(vendor_count_metric)).id;
-        store = await createTestStore()
-
-        store.pushPath(Urls.plainQuestion());
-        queryBuilder = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
-    })
-
-    afterAll(async () => {
-        await MetricApi.delete({ metricId, revision_message: "Let's exterminate this metric" })
-    })
-    // NOTE: These test cases are intentionally stateful
-    // (doing the whole app rendering thing in every single test case would probably slow things down)
+  let store = null;
+  let queryBuilder = null;
+  let metricId = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+    metricId = (await MetricApi.create(vendor_count_metric)).id;
+    store = await createTestStore();
+
+    store.pushPath(Urls.plainQuestion());
+    queryBuilder = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
+  });
+
+  afterAll(async () => {
+    await MetricApi.delete({
+      metricId,
+      revision_message: "Let's exterminate this metric",
+    });
+  });
+  // NOTE: These test cases are intentionally stateful
+  // (doing the whole app rendering thing in every single test case would probably slow things down)
 
-    it("opens properly from QB", async () => {
-        // open data reference sidebar by clicking button
-        click(queryBuilder.find(".Icon-reference"));
-        await store.waitForActions([TOGGLE_DATA_REFERENCE]);
+  it("opens properly from QB", async () => {
+    // open data reference sidebar by clicking button
+    click(queryBuilder.find(".Icon-reference"));
+    await store.waitForActions([TOGGLE_DATA_REFERENCE]);
 
-        const dataReference = queryBuilder.find(DataReference);
-        expect(dataReference.length).toBe(1);
+    const dataReference = queryBuilder.find(DataReference);
+    expect(dataReference.length).toBe(1);
 
-        click(dataReference.find('a[children="Products"]'));
+    click(dataReference.find('a[children="Products"]'));
 
-        // TODO: Refactor TablePane so that it uses redux/metadata actions instead of doing inlined API calls
-        // then we can replace this with `store.waitForActions([FETCH_TABLE_FOREIGN_KEYS])` or similar
-        await delay(3000)
+    // TODO: Refactor TablePane so that it uses redux/metadata actions instead of doing inlined API calls
+    // then we can replace this with `store.waitForActions([FETCH_TABLE_FOREIGN_KEYS])` or similar
+    await delay(3000);
 
-        click(dataReference.find(`a[children="${vendor_count_metric.name}"]`).first())
+    click(
+      dataReference.find(`a[children="${vendor_count_metric.name}"]`).first(),
+    );
 
-        await store.waitForActions([FETCH_TABLE_METADATA]);
-    });
+    await store.waitForActions([FETCH_TABLE_METADATA]);
+  });
 
-    it("shows you the correct definition", () => {
-        const queryDefinition = queryBuilder.find(DataReference).find(QueryDefinition);
-        expect(queryDefinition.text()).toMatch(/Number of distinct valuesofVendor/);
-    })
+  it("shows you the correct definition", () => {
+    const queryDefinition = queryBuilder
+      .find(DataReference)
+      .find(QueryDefinition);
+    expect(queryDefinition.text()).toMatch(/Number of distinct valuesofVendor/);
+  });
 
-    it("lets you see the vendor count", async () => {
-        const queryButton = queryBuilder.find(DataReference).find(QueryButton);
+  it("lets you see the vendor count", async () => {
+    const queryButton = queryBuilder.find(DataReference).find(QueryButton);
 
-        try {
-            click(queryButton.children().first());
-        } catch(e) {
-            // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
-            // Now we are just using the onClick handler of Link so we don't have to care about that
-        }
+    try {
+      click(queryButton.children().first());
+    } catch (e) {
+      // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
+      // Now we are just using the onClick handler of Link so we don't have to care about that
+    }
 
-        await store.waitForActions([QUERY_COMPLETED]);
+    await store.waitForActions([QUERY_COMPLETED]);
 
-        expect(queryBuilder.find(Scalar).text()).toBe("200")
-    });
+    expect(queryBuilder.find(Scalar).text()).toBe("200");
+  });
 });
diff --git a/frontend/test/query_builder/components/dataref/SegmentPane.integ.spec.js b/frontend/test/query_builder/components/dataref/SegmentPane.integ.spec.js
index 320dd35c184f9818f57d52fcbc37b41c516bc26f..fe7db890dfd1a342d13233f793a2569e9d4e25ee 100644
--- a/frontend/test/query_builder/components/dataref/SegmentPane.integ.spec.js
+++ b/frontend/test/query_builder/components/dataref/SegmentPane.integ.spec.js
@@ -1,17 +1,20 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import { mount } from "enzyme";
 
 import {
-    INITIALIZE_QB, LOAD_TABLE_METADATA, QUERY_COMPLETED, setQuerySourceTable,
-    TOGGLE_DATA_REFERENCE
+  INITIALIZE_QB,
+  LOAD_TABLE_METADATA,
+  QUERY_COMPLETED,
+  setQuerySourceTable,
+  TOGGLE_DATA_REFERENCE,
 } from "metabase/query_builder/actions";
-import { delay } from "metabase/lib/promise"
+import { delay } from "metabase/lib/promise";
 
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import DataReference from "metabase/query_builder/components/dataref/DataReference";
@@ -26,95 +29,110 @@ import * as Urls from "metabase/lib/urls";
 
 // Currently a lot of duplication with SegmentPane tests
 describe("SegmentPane", () => {
-    let store = null;
-    let queryBuilder = null;
-    let segment = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin();
-        segment = await SegmentApi.create(orders_past_300_days_segment);
-        store = await createTestStore()
-
-        store.pushPath(Urls.plainQuestion());
-        queryBuilder = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
-    })
-
-    afterAll(async() => {
-        await SegmentApi.delete({
-            segmentId: segment.id,
-            revision_message: "Please"
-        });
-    })
-
-    // NOTE: These test cases are intentionally stateful
-    // (doing the whole app rendering thing in every single test case would probably slow things down)
-
-    it("opens properly from QB", async () => {
-        // open data reference sidebar by clicking button
-        click(queryBuilder.find(".Icon-reference"));
-        await store.waitForActions([TOGGLE_DATA_REFERENCE]);
-
-        const dataReference = queryBuilder.find(DataReference);
-        expect(dataReference.length).toBe(1);
-
-        click(dataReference.find('a[children="Orders"]'));
-
-        // TODO: Refactor TablePane so that it uses redux/metadata actions instead of doing inlined API calls
-        // then we can replace this with `store.waitForActions([FETCH_TABLE_FOREIGN_KEYS])` or similar
-        await delay(3000)
-
-        click(dataReference.find(`a[children="${orders_past_300_days_segment.name}"]`).first())
-
-        await store.waitForActions([FETCH_TABLE_METADATA]);
-    });
-
-    it("shows you the correct segment definition", () => {
-        const queryDefinition = queryBuilder.find(DataReference).find(QueryDefinition);
-        // eslint-disable-next-line no-irregular-whitespace
-        expect(queryDefinition.text()).toMatch(/Created At -300day/);
-    })
-
-    it("lets you apply the filter to your current query", async () => {
-        await store.dispatch(setQuerySourceTable(1))
-        await store.waitForActions(LOAD_TABLE_METADATA);
-
-        const filterByButton = queryBuilder.find(DataReference).find(UseForButton).first();
-        click(filterByButton.children().first());
-
-        await store.waitForActions([QUERY_COMPLETED]);
-
-        expect(queryBuilder.find(DataReference).find(UseForButton).length).toBe(0);
-    });
-
-    it("lets you see count of rows for past 300 days", async () => {
-        const numberQueryButton = queryBuilder.find(DataReference).find(QueryButton).at(0);
-
-        try {
-            click(numberQueryButton.children().first());
-        } catch(e) {
-            // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
-            // Now we are just using the onClick handler of Link so we don't have to care about that
-        }
-
-        await store.waitForActions([QUERY_COMPLETED]);
-
-        // The value changes daily which wasn't originally taken into account
-        // expect(queryBuilder.find(Scalar).text()).toBe("1,236")
-    });
-
-    it("lets you see raw data for past 300 days", async () => {
-        const allQueryButton = queryBuilder.find(DataReference).find(QueryButton).at(1);
-
-        try {
-            click(allQueryButton.children().first());
-        } catch(e) {
-            // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
-            // Now we are just using the onClick handler of Link so we don't have to care about that
-        }
-
-        await store.waitForActions([QUERY_COMPLETED]);
-
-        expect(queryBuilder.find(Table).length).toBe(1)
+  let store = null;
+  let queryBuilder = null;
+  let segment = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+    segment = await SegmentApi.create(orders_past_300_days_segment);
+    store = await createTestStore();
+
+    store.pushPath(Urls.plainQuestion());
+    queryBuilder = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
+  });
+
+  afterAll(async () => {
+    await SegmentApi.delete({
+      segmentId: segment.id,
+      revision_message: "Please",
     });
+  });
+
+  // NOTE: These test cases are intentionally stateful
+  // (doing the whole app rendering thing in every single test case would probably slow things down)
+
+  it("opens properly from QB", async () => {
+    // open data reference sidebar by clicking button
+    click(queryBuilder.find(".Icon-reference"));
+    await store.waitForActions([TOGGLE_DATA_REFERENCE]);
+
+    const dataReference = queryBuilder.find(DataReference);
+    expect(dataReference.length).toBe(1);
+
+    click(dataReference.find('a[children="Orders"]'));
+
+    // TODO: Refactor TablePane so that it uses redux/metadata actions instead of doing inlined API calls
+    // then we can replace this with `store.waitForActions([FETCH_TABLE_FOREIGN_KEYS])` or similar
+    await delay(3000);
+
+    click(
+      dataReference
+        .find(`a[children="${orders_past_300_days_segment.name}"]`)
+        .first(),
+    );
+
+    await store.waitForActions([FETCH_TABLE_METADATA]);
+  });
+
+  it("shows you the correct segment definition", () => {
+    const queryDefinition = queryBuilder
+      .find(DataReference)
+      .find(QueryDefinition);
+    // eslint-disable-next-line no-irregular-whitespace
+    expect(queryDefinition.text()).toMatch(/Created At -300day/);
+  });
+
+  it("lets you apply the filter to your current query", async () => {
+    await store.dispatch(setQuerySourceTable(1));
+    await store.waitForActions(LOAD_TABLE_METADATA);
+
+    const filterByButton = queryBuilder
+      .find(DataReference)
+      .find(UseForButton)
+      .first();
+    click(filterByButton.children().first());
+
+    await store.waitForActions([QUERY_COMPLETED]);
+
+    expect(queryBuilder.find(DataReference).find(UseForButton).length).toBe(0);
+  });
+
+  it("lets you see count of rows for past 300 days", async () => {
+    const numberQueryButton = queryBuilder
+      .find(DataReference)
+      .find(QueryButton)
+      .at(0);
+
+    try {
+      click(numberQueryButton.children().first());
+    } catch (e) {
+      // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
+      // Now we are just using the onClick handler of Link so we don't have to care about that
+    }
+
+    await store.waitForActions([QUERY_COMPLETED]);
+
+    // The value changes daily which wasn't originally taken into account
+    // expect(queryBuilder.find(Scalar).text()).toBe("1,236")
+  });
+
+  it("lets you see raw data for past 300 days", async () => {
+    const allQueryButton = queryBuilder
+      .find(DataReference)
+      .find(QueryButton)
+      .at(1);
+
+    try {
+      click(allQueryButton.children().first());
+    } catch (e) {
+      // QueryButton uses react-router Link which always throws an error if it's called without a parent Router object
+      // Now we are just using the onClick handler of Link so we don't have to care about that
+    }
+
+    await store.waitForActions([QUERY_COMPLETED]);
+
+    expect(queryBuilder.find(Table).length).toBe(1);
+  });
 });
diff --git a/frontend/test/query_builder/components/filters/FilterPopover.unit.spec.js b/frontend/test/query_builder/components/filters/FilterPopover.unit.spec.js
index b31d68c71dd59ba72a23a84ce7d2803c2ceb238d..4f9b640d2b6cd337840726f5d8574a9946ef8fd5 100644
--- a/frontend/test/query_builder/components/filters/FilterPopover.unit.spec.js
+++ b/frontend/test/query_builder/components/filters/FilterPopover.unit.spec.js
@@ -1,86 +1,112 @@
-import "__support__/mocks"
-import React from 'react'
-import { shallow, mount } from 'enzyme'
+import "__support__/mocks";
+import React from "react";
+
+import { shallow, mount } from "enzyme";
 
 import Question from "metabase-lib/lib/Question";
 
-import FilterPopover from 'metabase/query_builder/components/filters/FilterPopover'
-import DatePicker from 'metabase/query_builder/components/filters/pickers/DatePicker'
-import CheckBox from 'metabase/components/CheckBox'
+import FilterPopover from "metabase/query_builder/components/filters/FilterPopover";
+import DatePicker from "metabase/query_builder/components/filters/pickers/DatePicker";
+import OperatorSelector from "metabase/query_builder/components/filters/OperatorSelector";
+import CheckBox from "metabase/components/CheckBox";
 
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    ORDERS_TOTAL_FIELD_ID,
-    ORDERS_CREATED_DATE_FIELD_ID,
-    metadata
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  ORDERS_TOTAL_FIELD_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
+  ORDERS_PRODUCT_FK_FIELD_ID,
+  PRODUCT_TILE_FIELD_ID,
+  metadata,
+  StaticMetadataProvider,
 } from "__support__/sample_dataset_fixture";
 
-const RELATIVE_DAY_FILTER = ["time-interval", ["field-id", ORDERS_CREATED_DATE_FIELD_ID], -30, "day"]
-
-const FILTER_WITH_CURRENT_PERIOD = RELATIVE_DAY_FILTER.concat([
-    {"include-current": true }
-])
+const RELATIVE_DAY_FILTER = [
+  "time-interval",
+  ["field-id", ORDERS_CREATED_DATE_FIELD_ID],
+  -30,
+  "day",
+];
+const RELATIVE_DAY_FILTER_WITH_CURRENT_PERIOD = RELATIVE_DAY_FILTER.concat([
+  { "include-current": true },
+]);
 
 const NUMERIC_FILTER = ["=", ["field-id", ORDERS_TOTAL_FIELD_ID], 1234];
 
+const STRING_CONTAINS_FILTER = [
+  "CONTAINS",
+  ["fk->", ORDERS_PRODUCT_FK_FIELD_ID, PRODUCT_TILE_FIELD_ID],
+  "asdf",
+];
+
 const QUERY = Question.create({
-    databaseId: DATABASE_ID,
-    tableId: ORDERS_TABLE_ID,
-    metadata
+  databaseId: DATABASE_ID,
+  tableId: ORDERS_TABLE_ID,
+  metadata,
 })
-.query()
-.addAggregation(["count"])
-.addFilter(RELATIVE_DAY_FILTER)
-.addFilter(NUMERIC_FILTER)
+  .query()
+  .addAggregation(["count"])
+  .addFilter(RELATIVE_DAY_FILTER)
+  .addFilter(NUMERIC_FILTER)
+  .addFilter(STRING_CONTAINS_FILTER);
 
-describe('FilterPopover', () => {
-    describe('existing filter', () => {
-        describe('DatePicker', () => {
-            it('should render', () => {
-                const wrapper = shallow(
-                    <FilterPopover
-                        query={QUERY}
-                        filter={QUERY.filters()[0]}
-                    />
-                )
-                expect(wrapper.find(DatePicker).length).toBe(1)
-            })
-        })
-        describe('including the current period', () => {
-            it('should not show a control to the user for the appropriate types of queries', () => {
-                const wrapper = mount(
-                    <FilterPopover
-                        query={QUERY}
-                        filter={QUERY.filters()[1]}
-                    />
-                )
-                expect(wrapper.find(CheckBox).length).toBe(0)
-            })
-            it('should show a control to the user for the appropriate types of queries', () => {
-                const wrapper = mount(
-                    <FilterPopover
-                        query={QUERY}
-                        filter={QUERY.filters()[0]}
-                    />
-                )
-                expect(wrapper.find(CheckBox).length).toBe(1)
-            })
-            it('should let the user toggle', () => {
-                const wrapper = mount(
-                    <FilterPopover
-                        query={QUERY}
-                        filter={QUERY.filters()[0]}
-                    />
-                )
+describe("FilterPopover", () => {
+  describe("existing filter", () => {
+    describe("DatePicker", () => {
+      it("should render", () => {
+        const wrapper = shallow(
+          <FilterPopover query={QUERY} filter={QUERY.filters()[0]} />,
+        );
+        expect(wrapper.find(DatePicker).length).toBe(1);
+      });
+    });
+    describe("filter operator selection", () => {
+      it("should have an operator selector", () => {
+        const wrapper = mount(
+          <StaticMetadataProvider>
+            <FilterPopover query={QUERY} filter={NUMERIC_FILTER} />
+          </StaticMetadataProvider>,
+        );
+        expect(wrapper.find(OperatorSelector).length).toEqual(1);
+      });
+    });
+    describe("filter options", () => {
+      it("should not show a control to the user if the filter has no options", () => {
+        const wrapper = mount(
+          <StaticMetadataProvider>
+            <FilterPopover query={QUERY} filter={QUERY.filters()[1]} />
+          </StaticMetadataProvider>,
+        );
+        expect(wrapper.find(CheckBox).length).toBe(0);
+      });
+      it('should show "current-period" option to the user for "time-intervals" filters', () => {
+        const wrapper = mount(
+          <FilterPopover query={QUERY} filter={RELATIVE_DAY_FILTER} />,
+        );
+        expect(wrapper.find(CheckBox).length).toBe(1);
+      });
+      it('should show "case-sensitive" option to the user for "contains" filters', () => {
+        const wrapper = mount(
+          <StaticMetadataProvider>
+            <FilterPopover query={QUERY} filter={STRING_CONTAINS_FILTER} />
+          </StaticMetadataProvider>,
+        );
+        expect(wrapper.find(CheckBox).length).toBe(1);
+      });
+      it("should let the user toggle an option", () => {
+        const wrapper = mount(
+          <FilterPopover query={QUERY} filter={RELATIVE_DAY_FILTER} />,
+        );
 
-                const toggle = wrapper.find(CheckBox)
-                expect(toggle.props().checked).toBe(false)
-                toggle.simulate('click')
+        const toggle = wrapper.find(CheckBox);
+        expect(toggle.props().checked).toBe(false);
+        toggle.simulate("click");
 
-                expect(wrapper.state().filter).toEqual(FILTER_WITH_CURRENT_PERIOD)
-                expect(wrapper.find(CheckBox).props().checked).toBe(true)
-            })
-        })
-    })
-})
+        expect(wrapper.state().filter).toEqual(
+          RELATIVE_DAY_FILTER_WITH_CURRENT_PERIOD,
+        );
+        expect(wrapper.find(CheckBox).props().checked).toBe(true);
+      });
+    });
+  });
+});
diff --git a/frontend/test/query_builder/components/filters/pickers/DatePicker.unit.spec.js b/frontend/test/query_builder/components/filters/pickers/DatePicker.unit.spec.js
index 2c3453f7b428ac6d396fefeaa4449c3e2c237fa6..f5ff8a215a21a9a130b2e6ad503c743dcaea9dc4 100644
--- a/frontend/test/query_builder/components/filters/pickers/DatePicker.unit.spec.js
+++ b/frontend/test/query_builder/components/filters/pickers/DatePicker.unit.spec.js
@@ -1,6 +1,5 @@
-
-import React from 'react'
-import { mount } from 'enzyme'
+import React from "react";
+import { mount } from "enzyme";
 
 import DatePicker from "metabase/query_builder/components/filters/pickers/DatePicker";
 import DateOperatorSelector from "metabase/query_builder/components/filters/DateOperatorSelector";
@@ -9,21 +8,36 @@ import DateUnitSelector from "metabase/query_builder/components/filters/DateUnit
 const nop = () => {};
 
 describe("DatePicker", () => {
-    it("should render 'Previous 30 Days'", () => {
-        let picker = mount(<DatePicker filter={["time-interval", ["field-id", 1], -30, "day"]} onFilterChange={nop}/> );
-        expect(picker.find(DateOperatorSelector).text()).toEqual("Previous");
-        expect(picker.find('input').props().value).toEqual("30");
-        expect(picker.find(DateUnitSelector).text()).toEqual("Days");
-    });
-    it("should render 'Next 1 Month'", () => {
-        let picker = mount(<DatePicker filter={["time-interval", ["field-id", 1], 1, "month"]} onFilterChange={nop}/> );
-        expect(picker.find(DateOperatorSelector).text()).toEqual("Next");
-        expect(picker.find('input').props().value).toEqual("1");
-        expect(picker.find(DateUnitSelector).text()).toEqual("Month");
-    });
-    it("should render 'Current Week'", () => {
-        let picker = mount(<DatePicker filter={["time-interval", ["field-id", 1], "current", "week"]} onFilterChange={nop}/> );
-        expect(picker.find(DateOperatorSelector).text()).toEqual("Current");
-        expect(picker.find(DateUnitSelector).text()).toEqual("Week");
-    });
+  it("should render 'Previous 30 Days'", () => {
+    let picker = mount(
+      <DatePicker
+        filter={["time-interval", ["field-id", 1], -30, "day"]}
+        onFilterChange={nop}
+      />,
+    );
+    expect(picker.find(DateOperatorSelector).text()).toEqual("Previous");
+    expect(picker.find("input").props().value).toEqual("30");
+    expect(picker.find(DateUnitSelector).text()).toEqual("Days");
+  });
+  it("should render 'Next 1 Month'", () => {
+    let picker = mount(
+      <DatePicker
+        filter={["time-interval", ["field-id", 1], 1, "month"]}
+        onFilterChange={nop}
+      />,
+    );
+    expect(picker.find(DateOperatorSelector).text()).toEqual("Next");
+    expect(picker.find("input").props().value).toEqual("1");
+    expect(picker.find(DateUnitSelector).text()).toEqual("Month");
+  });
+  it("should render 'Current Week'", () => {
+    let picker = mount(
+      <DatePicker
+        filter={["time-interval", ["field-id", 1], "current", "week"]}
+        onFilterChange={nop}
+      />,
+    );
+    expect(picker.find(DateOperatorSelector).text()).toEqual("Current");
+    expect(picker.find(DateUnitSelector).text()).toEqual("Week");
+  });
 });
diff --git a/frontend/test/query_builder/new_question.integ.spec.js b/frontend/test/query_builder/new_question.integ.spec.js
index ed8e6c33f723a442babb85a424ee7e632514385c..1ac5c40d16e0e74fe4eaccd9e1daaa9ade13038a 100644
--- a/frontend/test/query_builder/new_question.integ.spec.js
+++ b/frontend/test/query_builder/new_question.integ.spec.js
@@ -1,39 +1,41 @@
-import { mount } from "enzyme"
+import { mount } from "enzyme";
 
 import {
-    useSharedAdminLogin,
-    createTestStore, useSharedNormalLogin, forBothAdminsAndNormalUsers, withApiMocks, BROWSER_HISTORY_REPLACE,
+  useSharedAdminLogin,
+  createTestStore,
+  useSharedNormalLogin,
+  forBothAdminsAndNormalUsers,
+  withApiMocks,
+  BROWSER_HISTORY_REPLACE,
 } from "__support__/integrated_tests";
 
 import EntitySearch, {
-    SearchGroupingOption, SearchResultListItem,
-    SearchResultsGroup
+  SearchGroupingOption,
+  SearchResultListItem,
+  SearchResultsGroup,
 } from "metabase/containers/EntitySearch";
 
 import AggregationWidget from "metabase/query_builder/components/AggregationWidget";
 
-import {
-    click,
-} from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
 import { DETERMINE_OPTIONS } from "metabase/new_query/new_query";
 
 import { getQuery } from "metabase/query_builder/selectors";
 import DataSelector from "metabase/query_builder/components/DataSelector";
 
-import {
-    FETCH_DATABASES
-} from "metabase/redux/metadata"
+import { FETCH_DATABASES } from "metabase/redux/metadata";
 import NativeQuery from "metabase-lib/lib/queries/NativeQuery";
 
-import { delay } from 'metabase/lib/promise'
+import { delay } from "metabase/lib/promise";
 import * as Urls from "metabase/lib/urls";
 
 import {
-    INITIALIZE_QB,
-    UPDATE_URL,
-    REDIRECT_TO_NEW_QUESTION_FLOW, LOAD_METADATA_FOR_CARD,
-    QUERY_COMPLETED,
+  INITIALIZE_QB,
+  UPDATE_URL,
+  REDIRECT_TO_NEW_QUESTION_FLOW,
+  LOAD_METADATA_FOR_CARD,
+  QUERY_COMPLETED,
 } from "metabase/query_builder/actions";
 
 import { MetabaseApi, MetricApi, SegmentApi } from "metabase/services";
@@ -46,199 +48,251 @@ import NewQueryOption from "metabase/new_query/components/NewQueryOption";
 import NoDatabasesEmptyState from "metabase/reference/databases/NoDatabasesEmptyState";
 
 describe("new question flow", async () => {
-    // test an instance with segments, metrics, etc as an admin
-    describe("a rich instance", async () => {
-        let metricId = null;
-        let segmentId = null;
-
-        beforeAll(async () => {
-            // TODO: Move these test metric/segment definitions to a central place
-            const metricDef = {name: "A Metric", description: "For testing new question flow", table_id: 1,show_in_getting_started: true,
-                definition: {database: 1, query: {aggregation: ["count"]}}}
-            const segmentDef = {name: "A Segment", description: "For testing new question flow", table_id: 1, show_in_getting_started: true,
-                definition: {database: 1, query: {filter: ["abc"]}}}
-
-            // Needed for question creation flow
-            useSharedAdminLogin()
-            metricId = (await MetricApi.create(metricDef)).id;
-            segmentId = (await SegmentApi.create(segmentDef)).id;
-
-        })
-
-        afterAll(async () => {
-            useSharedAdminLogin()
-            await MetricApi.delete({ metricId, revision_message: "The lifetime of this metric was just a few seconds" })
-            await SegmentApi.delete({ segmentId, revision_message: "Sadly this segment didn't enjoy a long life either" })
-        })
-
-        it("redirects /question to /question/new", async () => {
-            useSharedNormalLogin()
-            const store = await createTestStore()
-            store.pushPath("/question");
-            mount(store.getAppContainer());
-            await store.waitForActions([REDIRECT_TO_NEW_QUESTION_FLOW])
-            expect(store.getPath()).toBe("/question/new")
-        })
-
-        it("renders all options for both admins and normal users if metrics & segments exist", async () => {
-            await forBothAdminsAndNormalUsers(async () => {
-                const store = await createTestStore()
-
-                store.pushPath(Urls.newQuestion());
-                const app = mount(store.getAppContainer());
-                await store.waitForActions([DETERMINE_OPTIONS]);
-
-                expect(app.find(NewQueryOption).length).toBe(3)
-            })
-        });
-
-        it("does not show Metrics option for normal users if there are no metrics", async () => {
-            useSharedNormalLogin()
-
-            await withApiMocks([
-                [MetricApi, "list", () => []],
-            ], async () => {
-                const store = await createTestStore()
-
-                store.pushPath(Urls.newQuestion());
-                const app = mount(store.getAppContainer());
-                await store.waitForActions([DETERMINE_OPTIONS]);
-
-                expect(app.find(NewQueryOption).filterWhere((c) => c.prop('title') === "Metrics").length).toBe(0)
-                expect(app.find(NewQueryOption).length).toBe(2)
-            })
-        });
-
-        it("does not show SQL option for normal user if SQL write permissions are missing", async () => {
-            useSharedNormalLogin()
-
-            const disableWritePermissionsForDb = (db) => ({ ...db, native_permissions: "read" })
-            const realDbListWithTables = MetabaseApi.db_list_with_tables
-
-            await withApiMocks([
-                [MetabaseApi, "db_list_with_tables", async () =>
-                    (await realDbListWithTables()).map(disableWritePermissionsForDb)
-                ],
-            ], async () => {
-                const store = await createTestStore()
-
-                store.pushPath(Urls.newQuestion());
-                const app = mount(store.getAppContainer());
-                await store.waitForActions([DETERMINE_OPTIONS]);
-
-                expect(app.find(NewQueryOption).length).toBe(2)
-            })
-        })
-
-        it("redirects to query builder if there are no segments/metrics and no write sql permissions", async () => {
-            useSharedNormalLogin()
-
-            const disableWritePermissionsForDb = (db) => ({ ...db, native_permissions: "read" })
-            const realDbListWithTables = MetabaseApi.db_list_with_tables
-
-            await withApiMocks([
-                [MetricApi, "list", () => []],
-                [MetabaseApi, "db_list_with_tables", async () =>
-                    (await realDbListWithTables()).map(disableWritePermissionsForDb)
-                ],
-            ], async () => {
-                const store = await createTestStore()
-                store.pushPath(Urls.newQuestion());
-                mount(store.getAppContainer());
-                await store.waitForActions(BROWSER_HISTORY_REPLACE, INITIALIZE_QB);
-            })
-        })
-
-        it("shows an empty state if there are no databases", async () => {
-            await forBothAdminsAndNormalUsers(async () => {
-                await withApiMocks([
-                    [MetabaseApi, "db_list_with_tables", () => []]
-                ], async () => {
-                    const store = await createTestStore()
-
-                    store.pushPath(Urls.newQuestion());
-                    const app = mount(store.getAppContainer());
-                    await store.waitForActions([DETERMINE_OPTIONS]);
-
-                    expect(app.find(NewQueryOption).length).toBe(0)
-                    expect(app.find(NoDatabasesEmptyState).length).toBe(1)
-                })
-            })
-        })
-
-        it("lets you start a custom gui question", async () => {
-            useSharedNormalLogin()
-            const store = await createTestStore()
-
-            store.pushPath(Urls.newQuestion());
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([DETERMINE_OPTIONS]);
-
-            click(app.find(NewQueryOption).filterWhere((c) => c.prop('title') === "Custom"))
-            await store.waitForActions(INITIALIZE_QB, UPDATE_URL, LOAD_METADATA_FOR_CARD);
-            expect(getQuery(store.getState()) instanceof StructuredQuery).toBe(true)
-        })
-
-        it("lets you start a custom native question", async () => {
-            useSharedNormalLogin()
-            // Don't render Ace editor in tests because it uses many DOM methods that aren't supported by jsdom
-            // see also parameters.integ.js for more notes about Ace editor testing
-            NativeQueryEditor.prototype.loadAceEditor = () => {}
-
-            const store = await createTestStore()
-
-            store.pushPath(Urls.newQuestion());
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([DETERMINE_OPTIONS]);
-
-            click(app.find(NewQueryOption).filterWhere((c) => c.prop('title') === "Native query"))
-            await store.waitForActions(INITIALIZE_QB);
-            expect(getQuery(store.getState()) instanceof NativeQuery).toBe(true)
-
-            // No database selector visible because in test environment we should
-            // only have a single database
-            expect(app.find(DataSelector).length).toBe(0)
-
-            // The name of the database should be displayed
-            expect(app.find(NativeQueryEditor).text()).toMatch(/Sample Dataset/)
-        })
-
-        it("lets you start a question from a metric", async () => {
-            useSharedNormalLogin()
-            const store = await createTestStore()
+  // test an instance with segments, metrics, etc as an admin
+  describe("a rich instance", async () => {
+    let metricId = null;
+    let segmentId = null;
+
+    beforeAll(async () => {
+      // TODO: Move these test metric/segment definitions to a central place
+      const metricDef = {
+        name: "A Metric",
+        description: "For testing new question flow",
+        table_id: 1,
+        show_in_getting_started: true,
+        definition: { database: 1, query: { aggregation: ["count"] } },
+      };
+      const segmentDef = {
+        name: "A Segment",
+        description: "For testing new question flow",
+        table_id: 1,
+        show_in_getting_started: true,
+        definition: { database: 1, query: { filter: ["abc"] } },
+      };
+
+      // Needed for question creation flow
+      useSharedAdminLogin();
+      metricId = (await MetricApi.create(metricDef)).id;
+      segmentId = (await SegmentApi.create(segmentDef)).id;
+    });
+
+    afterAll(async () => {
+      useSharedAdminLogin();
+      await MetricApi.delete({
+        metricId,
+        revision_message: "The lifetime of this metric was just a few seconds",
+      });
+      await SegmentApi.delete({
+        segmentId,
+        revision_message: "Sadly this segment didn't enjoy a long life either",
+      });
+    });
+
+    it("redirects /question to /question/new", async () => {
+      useSharedNormalLogin();
+      const store = await createTestStore();
+      store.pushPath("/question");
+      mount(store.getAppContainer());
+      await store.waitForActions([REDIRECT_TO_NEW_QUESTION_FLOW]);
+      expect(store.getPath()).toBe("/question/new");
+    });
+
+    it("renders all options for both admins and normal users if metrics & segments exist", async () => {
+      await forBothAdminsAndNormalUsers(async () => {
+        const store = await createTestStore();
+
+        store.pushPath(Urls.newQuestion());
+        const app = mount(store.getAppContainer());
+        await store.waitForActions([DETERMINE_OPTIONS]);
+
+        expect(app.find(NewQueryOption).length).toBe(3);
+      });
+    });
+
+    it("does not show Metrics option for normal users if there are no metrics", async () => {
+      useSharedNormalLogin();
+
+      await withApiMocks([[MetricApi, "list", () => []]], async () => {
+        const store = await createTestStore();
+
+        store.pushPath(Urls.newQuestion());
+        const app = mount(store.getAppContainer());
+        await store.waitForActions([DETERMINE_OPTIONS]);
+
+        expect(
+          app
+            .find(NewQueryOption)
+            .filterWhere(c => c.prop("title") === "Metrics").length,
+        ).toBe(0);
+        expect(app.find(NewQueryOption).length).toBe(2);
+      });
+    });
+
+    it("does not show SQL option for normal user if SQL write permissions are missing", async () => {
+      useSharedNormalLogin();
+
+      const disableWritePermissionsForDb = db => ({
+        ...db,
+        native_permissions: "read",
+      });
+      const realDbListWithTables = MetabaseApi.db_list_with_tables;
+
+      await withApiMocks(
+        [
+          [
+            MetabaseApi,
+            "db_list_with_tables",
+            async () =>
+              (await realDbListWithTables()).map(disableWritePermissionsForDb),
+          ],
+        ],
+        async () => {
+          const store = await createTestStore();
+
+          store.pushPath(Urls.newQuestion());
+          const app = mount(store.getAppContainer());
+          await store.waitForActions([DETERMINE_OPTIONS]);
+
+          expect(app.find(NewQueryOption).length).toBe(2);
+        },
+      );
+    });
+
+    it("redirects to query builder if there are no segments/metrics and no write sql permissions", async () => {
+      useSharedNormalLogin();
+
+      const disableWritePermissionsForDb = db => ({
+        ...db,
+        native_permissions: "read",
+      });
+      const realDbListWithTables = MetabaseApi.db_list_with_tables;
+
+      await withApiMocks(
+        [
+          [MetricApi, "list", () => []],
+          [
+            MetabaseApi,
+            "db_list_with_tables",
+            async () =>
+              (await realDbListWithTables()).map(disableWritePermissionsForDb),
+          ],
+        ],
+        async () => {
+          const store = await createTestStore();
+          store.pushPath(Urls.newQuestion());
+          mount(store.getAppContainer());
+          await store.waitForActions(BROWSER_HISTORY_REPLACE, INITIALIZE_QB);
+        },
+      );
+    });
+
+    it("shows an empty state if there are no databases", async () => {
+      await forBothAdminsAndNormalUsers(async () => {
+        await withApiMocks(
+          [[MetabaseApi, "db_list_with_tables", () => []]],
+          async () => {
+            const store = await createTestStore();
 
             store.pushPath(Urls.newQuestion());
             const app = mount(store.getAppContainer());
             await store.waitForActions([DETERMINE_OPTIONS]);
 
-            click(app.find(NewQueryOption).filterWhere((c) => c.prop('title') === "Metrics"))
-            await store.waitForActions(FETCH_DATABASES);
-            await store.waitForActions([SET_REQUEST_STATE]);
-            expect(store.getPath()).toBe("/question/new/metric")
-
-            const entitySearch = app.find(EntitySearch)
-            const viewByCreator = entitySearch.find(SearchGroupingOption).last()
-            expect(viewByCreator.text()).toBe("Creator");
-            click(viewByCreator)
-            expect(store.getPath()).toBe("/question/new/metric?grouping=creator")
-
-            const group = entitySearch.find(SearchResultsGroup)
-            expect(group.prop('groupName')).toBe("Bobby Tables")
-
-            const metricSearchResult = group.find(SearchResultListItem)
-                .filterWhere((item) => /A Metric/.test(item.text()))
-            click(metricSearchResult.childAt(0))
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-            await delay(100); // Trying to address random CI failures with a small delay
-
-            expect(
-                app.find(AggregationWidget).find(".View-section-aggregation").text()
-            ).toBe("A Metric")
-        })
-    })
-
-    describe("a newer instance", () => {
-
-    })
-})
+            expect(app.find(NewQueryOption).length).toBe(0);
+            expect(app.find(NoDatabasesEmptyState).length).toBe(1);
+          },
+        );
+      });
+    });
+
+    it("lets you start a custom gui question", async () => {
+      useSharedNormalLogin();
+      const store = await createTestStore();
+
+      store.pushPath(Urls.newQuestion());
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([DETERMINE_OPTIONS]);
+
+      click(
+        app.find(NewQueryOption).filterWhere(c => c.prop("title") === "Custom"),
+      );
+      await store.waitForActions(
+        INITIALIZE_QB,
+        UPDATE_URL,
+        LOAD_METADATA_FOR_CARD,
+      );
+      expect(getQuery(store.getState()) instanceof StructuredQuery).toBe(true);
+    });
+
+    it("lets you start a custom native question", async () => {
+      useSharedNormalLogin();
+      // Don't render Ace editor in tests because it uses many DOM methods that aren't supported by jsdom
+      // see also parameters.integ.js for more notes about Ace editor testing
+      NativeQueryEditor.prototype.loadAceEditor = () => {};
+
+      const store = await createTestStore();
+
+      store.pushPath(Urls.newQuestion());
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([DETERMINE_OPTIONS]);
+
+      click(
+        app
+          .find(NewQueryOption)
+          .filterWhere(c => c.prop("title") === "Native query"),
+      );
+      await store.waitForActions(INITIALIZE_QB);
+      expect(getQuery(store.getState()) instanceof NativeQuery).toBe(true);
+
+      // No database selector visible because in test environment we should
+      // only have a single database
+      expect(app.find(DataSelector).length).toBe(0);
+
+      // The name of the database should be displayed
+      expect(app.find(NativeQueryEditor).text()).toMatch(/Sample Dataset/);
+    });
+
+    it("lets you start a question from a metric", async () => {
+      useSharedNormalLogin();
+      const store = await createTestStore();
+
+      store.pushPath(Urls.newQuestion());
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([DETERMINE_OPTIONS]);
+
+      click(
+        app
+          .find(NewQueryOption)
+          .filterWhere(c => c.prop("title") === "Metrics"),
+      );
+      await store.waitForActions(FETCH_DATABASES);
+      await store.waitForActions([SET_REQUEST_STATE]);
+      expect(store.getPath()).toBe("/question/new/metric");
+
+      const entitySearch = app.find(EntitySearch);
+      const viewByCreator = entitySearch.find(SearchGroupingOption).last();
+      expect(viewByCreator.text()).toBe("Creator");
+      click(viewByCreator);
+      expect(store.getPath()).toBe("/question/new/metric?grouping=creator");
+
+      const group = entitySearch.find(SearchResultsGroup);
+      expect(group.prop("groupName")).toBe("Bobby Tables");
+
+      const metricSearchResult = group
+        .find(SearchResultListItem)
+        .filterWhere(item => /A Metric/.test(item.text()));
+      click(metricSearchResult.childAt(0));
+
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+      await delay(100); // Trying to address random CI failures with a small delay
+
+      expect(
+        app
+          .find(AggregationWidget)
+          .find(".View-section-aggregation")
+          .text(),
+      ).toBe("A Metric");
+    });
+  });
+
+  describe("a newer instance", () => {});
+});
diff --git a/frontend/test/query_builder/qb_drillthrough.integ.spec.js b/frontend/test/query_builder/qb_drillthrough.integ.spec.js
index 65b3d3c72cb72ad13fe0962e107cf6aade749f8b..3b6779c9a598b3a6e0aee9a50d4e5e5bf1e0a807 100644
--- a/frontend/test/query_builder/qb_drillthrough.integ.spec.js
+++ b/frontend/test/query_builder/qb_drillthrough.integ.spec.js
@@ -1,231 +1,276 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-} from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import { mount } from "enzyme";
 import {
-    INITIALIZE_QB,
-    QUERY_COMPLETED,
-    setQueryDatabase,
-    setQuerySourceTable,
-    setDatasetQuery,
-    NAVIGATE_TO_NEW_CARD,
-    UPDATE_URL,
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  setQueryDatabase,
+  setQuerySourceTable,
+  setDatasetQuery,
+  NAVIGATE_TO_NEW_CARD,
+  UPDATE_URL,
 } from "metabase/query_builder/actions";
 
 import QueryHeader from "metabase/query_builder/components/QueryHeader";
-import {
-    FETCH_TABLE_METADATA,
-} from "metabase/redux/metadata";
+import { FETCH_TABLE_METADATA } from "metabase/redux/metadata";
 
 import RunButton from "metabase/query_builder/components/RunButton";
 
 import BreakoutWidget from "metabase/query_builder/components/BreakoutWidget";
 import { getCard } from "metabase/query_builder/selectors";
 import { TestTable } from "metabase/visualizations/visualizations/Table";
-import ChartClickActions, { ChartClickAction } from "metabase/visualizations/components/ChartClickActions";
+import ChartClickActions, {
+  ChartClickAction,
+} from "metabase/visualizations/components/ChartClickActions";
 
 import { delay } from "metabase/lib/promise";
 import * as Urls from "metabase/lib/urls";
-import DataSelector, { TableTriggerContent } from "metabase/query_builder/components/DataSelector";
+import DataSelector, {
+  TableTriggerContent,
+} from "metabase/query_builder/components/DataSelector";
 import ObjectDetail from "metabase/visualizations/visualizations/ObjectDetail";
 
 const initQbWithDbAndTable = (dbId, tableId) => {
-    return async () => {
-        const store = await createTestStore()
-        store.pushPath(Urls.plainQuestion());
-        const qb = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB])
+  return async () => {
+    const store = await createTestStore();
+    store.pushPath(Urls.plainQuestion());
+    const qb = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
 
-        // Use Products table
-        store.dispatch(setQueryDatabase(dbId));
-        store.dispatch(setQuerySourceTable(tableId));
-        await store.waitForActions([FETCH_TABLE_METADATA]);
+    // Use Products table
+    store.dispatch(setQueryDatabase(dbId));
+    store.dispatch(setQuerySourceTable(tableId));
+    await store.waitForActions([FETCH_TABLE_METADATA]);
 
-        return { store, qb }
-    }
-}
+    return { store, qb };
+  };
+};
 
-const initQbWithOrdersTable = initQbWithDbAndTable(1, 1)
+const initQbWithOrdersTable = initQbWithDbAndTable(1, 1);
 
 describe("QueryBuilder", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin()
-    })
-
-    describe("drill-through", () => {
-        describe("View details action", () => {
-            it("works for foreign keys", async () => {
-                const {store, qb} = await initQbWithOrdersTable();
-                click(qb.find(RunButton));
-                await store.waitForActions([QUERY_COMPLETED]);
-                const table = qb.find(TestTable);
-
-                expect(qb.find(DataSelector).find(TableTriggerContent).text()).toBe("Orders")
-                const headerCells = table.find("thead th").map((cell) => cell.text())
-                const productIdIndex = headerCells.indexOf("Product ID")
-
-                const firstRowCells = table.find("tbody tr").first().find("td");
-                const productIdCell = firstRowCells.at(productIdIndex)
-                click(productIdCell.children().first());
-
-                // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
-                await delay(150);
-
-                const viewDetailsButton = qb.find(ChartClickActions)
-                    .find(ChartClickAction)
-                    .filterWhere(action => /View details/.test(action.text()))
-                    .first()
-
-                click(viewDetailsButton);
-
-                await store.waitForActions([NAVIGATE_TO_NEW_CARD, UPDATE_URL, QUERY_COMPLETED]);
-
-                expect(qb.find(ObjectDetail).length).toBe(1)
-                expect(qb.find(DataSelector).find(TableTriggerContent).text()).toBe("Products")
-            })
-        })
-        describe("Zoom In action for broken out fields", () => {
-            it("works for Count of rows aggregation and Subtotal 50 Bins breakout", async () => {
-                const {store, qb} = await initQbWithOrdersTable();
-                await store.dispatch(setDatasetQuery({
-                    database: 1,
-                    type: 'query',
-                    query: {
-                        source_table: 1,
-                        breakout: [['binning-strategy', ['field-id', 6], 'num-bins', 50]],
-                        aggregation: [['count']]
-                    }
-                }));
-
-                click(qb.find(RunButton));
-                await store.waitForActions([QUERY_COMPLETED]);
-
-                const table = qb.find(TestTable);
-                const firstRowCells = table.find("tbody tr").first().find("td");
-                expect(firstRowCells.length).toBe(2);
-
-                // NOTE: Commented out due to the randomness involved in sample dataset generation
-                // which sometimes causes the cell value to be different
-                // expect(firstRowCells.first().text()).toBe("4  –  6");
-
-                const countCell = firstRowCells.last();
-                expect(countCell.text()).toBe("2");
-                click(countCell.children().first());
-
-                // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
-                await delay(150);
-
-                click(qb.find(ChartClickActions).find('div[children="Zoom in"]'));
-
-                store.waitForActions([NAVIGATE_TO_NEW_CARD, UPDATE_URL, QUERY_COMPLETED]);
-
-                // Should reset to auto binning
-                const breakoutWidget = qb.find(BreakoutWidget).first();
-                expect(breakoutWidget.text()).toBe("Total: Auto binned");
-
-                // Expecting to see the correct lineage (just a simple sanity check)
-                const title = qb.find(QueryHeader).find("h1")
-                expect(title.text()).toBe("New question")
-            })
-
-            it("works for Count of rows aggregation and FK State breakout", async () => {
-                const {store, qb} = await initQbWithOrdersTable();
-                await store.dispatch(setDatasetQuery({
-                    database: 1,
-                    type: 'query',
-                    query: {
-                        source_table: 1,
-                        breakout: [['fk->', 7, 19]],
-                        aggregation: [['count']]
-                    }
-                }));
-
-                click(qb.find(RunButton));
-                await store.waitForActions([QUERY_COMPLETED]);
-
-                const table = qb.find(TestTable);
-                const firstRowCells = table.find("tbody tr").first().find("td");
-                expect(firstRowCells.length).toBe(2);
-
-                expect(firstRowCells.first().text()).toBe("AA");
-
-                const countCell = firstRowCells.last();
-                expect(countCell.text()).toBe("233");
-                click(countCell.children().first());
-
-                // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
-                await delay(150);
-
-                click(qb.find(ChartClickActions).find('div[children="Zoom in"]'));
-
-                store.waitForActions([NAVIGATE_TO_NEW_CARD, UPDATE_URL, QUERY_COMPLETED]);
-
-                // Should reset to auto binning
-                const breakoutWidgets = qb.find(BreakoutWidget);
-                expect(breakoutWidgets.length).toBe(3);
-                expect(breakoutWidgets.at(0).text()).toBe("Latitude: 1°");
-                expect(breakoutWidgets.at(1).text()).toBe("Longitude: 1°");
-
-                // Should have visualization type set to Pin map (temporary workaround until we have polished heat maps)
-                const card = getCard(store.getState())
-                expect(card.display).toBe("map");
-                expect(card.visualization_settings).toEqual({ "map.type": "pin" });
-            });
-
-            it("works for Count of rows aggregation and FK Latitude Auto binned breakout", async () => {
-                const {store, qb} = await initQbWithOrdersTable();
-                await store.dispatch(setDatasetQuery({
-                    database: 1,
-                    type: 'query',
-                    query: {
-                        source_table: 1,
-                        breakout: [["binning-strategy", ['fk->', 7, 14], "default"]],
-                        aggregation: [['count']]
-                    }
-                }));
-
-                click(qb.find(RunButton));
-                await store.waitForActions([QUERY_COMPLETED]);
-
-                const table = qb.find(TestTable);
-                const firstRowCells = table.find("tbody tr").first().find("td");
-                expect(firstRowCells.length).toBe(2);
-
-                expect(firstRowCells.first().text()).toBe("90° S  –  80° S");
-
-                const countCell = firstRowCells.last();
-                expect(countCell.text()).toBe("701");
-                click(countCell.children().first());
-
-                // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
-                await delay(150);
-
-                click(qb.find(ChartClickActions).find('div[children="Zoom in"]'));
-
-                store.waitForActions([NAVIGATE_TO_NEW_CARD, UPDATE_URL, QUERY_COMPLETED]);
-
-                // Should reset to auto binning
-                const breakoutWidgets = qb.find(BreakoutWidget);
-                expect(breakoutWidgets.length).toBe(2);
-
-                // Default location binning strategy currently has a bin width of 10° so
-                expect(breakoutWidgets.at(0).text()).toBe("Latitude: 1°");
-
-                // Should have visualization type set to the previous visualization
-                const card = getCard(store.getState())
-                expect(card.display).toBe("bar");
-
-                // Some part of visualization seems to be asynchronous, causing a cluster of errors
-                // about missing query results if this delay isn't present
-                await delay(100)
-            });
-        })
-    })
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("drill-through", () => {
+    describe("View details action", () => {
+      it("works for foreign keys", async () => {
+        const { store, qb } = await initQbWithOrdersTable();
+        click(qb.find(RunButton));
+        await store.waitForActions([QUERY_COMPLETED]);
+        const table = qb.find(TestTable);
+
+        expect(
+          qb
+            .find(DataSelector)
+            .find(TableTriggerContent)
+            .text(),
+        ).toBe("Orders");
+        const headerCells = table.find("thead th").map(cell => cell.text());
+        const productIdIndex = headerCells.indexOf("Product ID");
+
+        const firstRowCells = table
+          .find("tbody tr")
+          .first()
+          .find("td");
+        const productIdCell = firstRowCells.at(productIdIndex);
+        click(productIdCell.children().first());
+
+        // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
+        await delay(150);
+
+        const viewDetailsButton = qb
+          .find(ChartClickActions)
+          .find(ChartClickAction)
+          .filterWhere(action => /View details/.test(action.text()))
+          .first();
+
+        click(viewDetailsButton);
+
+        await store.waitForActions([
+          NAVIGATE_TO_NEW_CARD,
+          UPDATE_URL,
+          QUERY_COMPLETED,
+        ]);
+
+        expect(qb.find(ObjectDetail).length).toBe(1);
+        expect(
+          qb
+            .find(DataSelector)
+            .find(TableTriggerContent)
+            .text(),
+        ).toBe("Products");
+      });
+    });
+    describe("Zoom In action for broken out fields", () => {
+      it("works for Count of rows aggregation and Subtotal 50 Bins breakout", async () => {
+        const { store, qb } = await initQbWithOrdersTable();
+        await store.dispatch(
+          setDatasetQuery({
+            database: 1,
+            type: "query",
+            query: {
+              source_table: 1,
+              breakout: [["binning-strategy", ["field-id", 6], "num-bins", 50]],
+              aggregation: [["count"]],
+            },
+          }),
+        );
+
+        click(qb.find(RunButton));
+        await store.waitForActions([QUERY_COMPLETED]);
+
+        const table = qb.find(TestTable);
+        const firstRowCells = table
+          .find("tbody tr")
+          .first()
+          .find("td");
+        expect(firstRowCells.length).toBe(2);
+
+        // NOTE: Commented out due to the randomness involved in sample dataset generation
+        // which sometimes causes the cell value to be different
+        // expect(firstRowCells.first().text()).toBe("4  –  6");
+
+        const countCell = firstRowCells.last();
+        expect(countCell.text()).toBe("2");
+        click(countCell.children().first());
+
+        // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
+        await delay(150);
+
+        click(qb.find(ChartClickActions).find('div[children="Zoom in"]'));
+
+        store.waitForActions([
+          NAVIGATE_TO_NEW_CARD,
+          UPDATE_URL,
+          QUERY_COMPLETED,
+        ]);
+
+        // Should reset to auto binning
+        const breakoutWidget = qb.find(BreakoutWidget).first();
+        expect(breakoutWidget.text()).toBe("Total: Auto binned");
+
+        // Expecting to see the correct lineage (just a simple sanity check)
+        const title = qb.find(QueryHeader).find("h1");
+        expect(title.text()).toBe("New question");
+      });
+
+      it("works for Count of rows aggregation and FK State breakout", async () => {
+        const { store, qb } = await initQbWithOrdersTable();
+        await store.dispatch(
+          setDatasetQuery({
+            database: 1,
+            type: "query",
+            query: {
+              source_table: 1,
+              breakout: [["fk->", 7, 19]],
+              aggregation: [["count"]],
+            },
+          }),
+        );
+
+        click(qb.find(RunButton));
+        await store.waitForActions([QUERY_COMPLETED]);
+
+        const table = qb.find(TestTable);
+        const firstRowCells = table
+          .find("tbody tr")
+          .first()
+          .find("td");
+        expect(firstRowCells.length).toBe(2);
+
+        expect(firstRowCells.first().text()).toBe("AA");
+
+        const countCell = firstRowCells.last();
+        expect(countCell.text()).toBe("233");
+        click(countCell.children().first());
+
+        // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
+        await delay(150);
+
+        click(qb.find(ChartClickActions).find('div[children="Zoom in"]'));
+
+        store.waitForActions([
+          NAVIGATE_TO_NEW_CARD,
+          UPDATE_URL,
+          QUERY_COMPLETED,
+        ]);
+
+        // Should reset to auto binning
+        const breakoutWidgets = qb.find(BreakoutWidget);
+        expect(breakoutWidgets.length).toBe(3);
+        expect(breakoutWidgets.at(0).text()).toBe("Latitude: 1°");
+        expect(breakoutWidgets.at(1).text()).toBe("Longitude: 1°");
+
+        // Should have visualization type set to Pin map (temporary workaround until we have polished heat maps)
+        const card = getCard(store.getState());
+        expect(card.display).toBe("map");
+        expect(card.visualization_settings).toEqual({ "map.type": "pin" });
+      });
+
+      it("works for Count of rows aggregation and FK Latitude Auto binned breakout", async () => {
+        const { store, qb } = await initQbWithOrdersTable();
+        await store.dispatch(
+          setDatasetQuery({
+            database: 1,
+            type: "query",
+            query: {
+              source_table: 1,
+              breakout: [["binning-strategy", ["fk->", 7, 14], "default"]],
+              aggregation: [["count"]],
+            },
+          }),
+        );
+
+        click(qb.find(RunButton));
+        await store.waitForActions([QUERY_COMPLETED]);
+
+        const table = qb.find(TestTable);
+        const firstRowCells = table
+          .find("tbody tr")
+          .first()
+          .find("td");
+        expect(firstRowCells.length).toBe(2);
+
+        expect(firstRowCells.first().text()).toBe("90° S  –  80° S");
+
+        const countCell = firstRowCells.last();
+        expect(countCell.text()).toBe("701");
+        click(countCell.children().first());
+
+        // Drill-through is delayed in handleVisualizationClick of Visualization.jsx by 100ms
+        await delay(150);
+
+        click(qb.find(ChartClickActions).find('div[children="Zoom in"]'));
+
+        store.waitForActions([
+          NAVIGATE_TO_NEW_CARD,
+          UPDATE_URL,
+          QUERY_COMPLETED,
+        ]);
+
+        // Should reset to auto binning
+        const breakoutWidgets = qb.find(BreakoutWidget);
+        expect(breakoutWidgets.length).toBe(2);
+
+        // Default location binning strategy currently has a bin width of 10° so
+        expect(breakoutWidgets.at(0).text()).toBe("Latitude: 1°");
+
+        // Should have visualization type set to the previous visualization
+        const card = getCard(store.getState());
+        expect(card.display).toBe("bar");
+
+        // Some part of visualization seems to be asynchronous, causing a cluster of errors
+        // about missing query results if this delay isn't present
+        await delay(100);
+      });
+    });
+  });
 });
diff --git a/frontend/test/query_builder/qb_editor_bar.integ.spec.js b/frontend/test/query_builder/qb_editor_bar.integ.spec.js
index e5b9bed5f610dffaca08ad9a62193c17e05ecd75..712a7d1588ceca0ddcc967c138896a41a6d0d635 100644
--- a/frontend/test/query_builder/qb_editor_bar.integ.spec.js
+++ b/frontend/test/query_builder/qb_editor_bar.integ.spec.js
@@ -1,34 +1,35 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton, setInputValue
-} from "__support__/enzyme_utils"
+import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
+import { delay } from "metabase/lib/promise";
 
-import React from 'react';
+import React from "react";
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import { mount } from "enzyme";
 import {
-    INITIALIZE_QB,
-    QUERY_COMPLETED,
-    SET_DATASET_QUERY,
-    setQueryDatabase,
-    setQuerySourceTable,
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  SET_DATASET_QUERY,
+  setQueryDatabase,
+  setQuerySourceTable,
 } from "metabase/query_builder/actions";
 
 import {
-    FETCH_TABLE_METADATA,
+  FETCH_TABLE_METADATA,
+  FETCH_FIELD_VALUES,
 } from "metabase/redux/metadata";
 
-import FieldList, { DimensionPicker } from "metabase/query_builder/components/FieldList";
+import FieldList, {
+  DimensionPicker,
+} from "metabase/query_builder/components/FieldList";
 import FilterPopover from "metabase/query_builder/components/filters/FilterPopover";
 
-import CheckBox from "metabase/components/CheckBox";
 import FilterWidget from "metabase/query_builder/components/filters/FilterWidget";
 import FieldName from "metabase/query_builder/components/FieldName";
 import RunButton from "metabase/query_builder/components/RunButton";
+import { Option } from "metabase/components/Select";
 
 import OperatorSelector from "metabase/query_builder/components/filters/OperatorSelector";
 import BreakoutWidget from "metabase/query_builder/components/BreakoutWidget";
@@ -37,361 +38,356 @@ import { getQueryResults } from "metabase/query_builder/selectors";
 import * as Urls from "metabase/lib/urls";
 
 const initQbWithDbAndTable = (dbId, tableId) => {
-    return async () => {
-        const store = await createTestStore()
-        store.pushPath(Urls.plainQuestion());
-        const qb = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
+  return async () => {
+    const store = await createTestStore();
+    store.pushPath(Urls.plainQuestion());
+    const qb = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
 
-        // Use Products table
-        store.dispatch(setQueryDatabase(dbId));
-        store.dispatch(setQuerySourceTable(tableId));
-        await store.waitForActions([FETCH_TABLE_METADATA]);
+    // Use Products table
+    store.dispatch(setQueryDatabase(dbId));
+    store.dispatch(setQuerySourceTable(tableId));
+    await store.waitForActions([FETCH_TABLE_METADATA]);
 
-        return { store, qb }
-    }
-}
+    return { store, qb };
+  };
+};
 
-const initQbWithOrdersTable = initQbWithDbAndTable(1, 1)
-const initQBWithReviewsTable = initQbWithDbAndTable(1, 4)
+const initQbWithOrdersTable = initQbWithDbAndTable(1, 1);
+const initQBWithReviewsTable = initQbWithDbAndTable(1, 4);
 
 describe("QueryBuilder editor bar", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("for filtering by Rating category field in Reviews table", () => {
+    let store = null;
+    let qb = null;
     beforeAll(async () => {
-        useSharedAdminLogin()
-    })
-
-    describe("for filtering by Rating category field in Reviews table", () =>  {
-        let store = null;
-        let qb = null;
-        beforeAll(async () => {
-            ({ store, qb } = await initQBWithReviewsTable());
-        })
-
-        // NOTE: Sequential tests; these may fail in a cascading way but shouldn't affect other tests
-
-        it("lets you add Rating field as a filter", async () => {
-            // TODO Atte Keinänen 7/13/17: Extracting GuiQueryEditor's contents to smaller React components
-            // would make testing with selectors more natural
-            const filterSection = qb.find('.GuiBuilder-filtered-by');
-            const addFilterButton = filterSection.find('.AddButton');
-            click(addFilterButton);
-
-            const filterPopover = filterSection.find(FilterPopover);
-
-            const ratingFieldButton = filterPopover.find(FieldList).find('h4[children="Rating"]')
-            expect(ratingFieldButton.length).toBe(1);
-            click(ratingFieldButton);
-        })
-
-        it("lets you see its field values in filter popover", () => {
-            // Same as before applies to FilterPopover too: individual list items could be in their own components
-            const filterPopover = qb.find(FilterPopover);
-            const fieldItems = filterPopover.find('li');
-            expect(fieldItems.length).toBe(5);
-
-            // should be in alphabetical order
-            expect(fieldItems.first().text()).toBe("1")
-            expect(fieldItems.last().text()).toBe("5")
-        })
-
-        it("lets you set 'Rating is 5' filter", async () => {
-            const filterPopover = qb.find(FilterPopover);
-            const fieldItems = filterPopover.find('li');
-            const widgetFieldItem = fieldItems.last();
-            const widgetCheckbox = widgetFieldItem.find(CheckBox);
-
-            expect(widgetCheckbox.props().checked).toBe(false);
-            click(widgetFieldItem.children().first());
-            expect(widgetCheckbox.props().checked).toBe(true);
-
-            const addFilterButton = filterPopover.find('button[children="Add filter"]')
-            clickButton(addFilterButton);
+      ({ store, qb } = await initQBWithReviewsTable());
+    });
 
-            await store.waitForActions([SET_DATASET_QUERY])
-
-            expect(qb.find(FilterPopover).length).toBe(0);
-            const filterWidget = qb.find(FilterWidget);
-            expect(filterWidget.length).toBe(1);
-            expect(filterWidget.text()).toBe("Rating is equal to5");
-        })
+    // NOTE: Sequential tests; these may fail in a cascading way but shouldn't affect other tests
 
-        it("lets you set 'Rating is 5 or 4' filter", async () => {
-            // reopen the filter popover by clicking filter widget
-            const filterWidget = qb.find(FilterWidget);
-            click(filterWidget.find(FieldName));
+    it("lets you add Rating field as a filter", async () => {
+      // TODO Atte Keinänen 7/13/17: Extracting GuiQueryEditor's contents to smaller React components
+      // would make testing with selectors more natural
+      const filterSection = qb.find(".GuiBuilder-filtered-by");
+      const addFilterButton = filterSection.find(".AddButton");
+      click(addFilterButton);
 
-            const filterPopover = qb.find(FilterPopover);
-            const fieldItems = filterPopover.find('li');
-            const widgetFieldItem = fieldItems.at(3);
-            const gadgetCheckbox = widgetFieldItem.find(CheckBox);
+      const filterPopover = filterSection.find(FilterPopover);
+
+      const ratingFieldButton = filterPopover
+        .find(FieldList)
+        .find('h4[children="Rating"]');
+      expect(ratingFieldButton.length).toBe(1);
+      click(ratingFieldButton);
+    });
 
-            expect(gadgetCheckbox.props().checked).toBe(false);
-            click(widgetFieldItem.children().first());
-            expect(gadgetCheckbox.props().checked).toBe(true);
+    it("lets you see its field values in filter popover", async () => {
+      await store.waitForActions([FETCH_FIELD_VALUES]);
+
+      // FIXME: TokenField asynchronously updates displayed options :(
+      await delay(10);
+
+      // Same as before applies to FilterPopover too: individual list items could be in their own components
+      const filterPopover = qb.find(FilterPopover);
+      const fieldItems = filterPopover.find("li");
+      expect(fieldItems.length).toBe(5 + 1); // NOTE: bleh, one for the input
+
+      // should be in alphabetical order
+      expect(fieldItems.at(1).text()).toBe("1");
+      expect(fieldItems.last().text()).toBe("5");
+    });
 
-            const addFilterButton = filterPopover.find('button[children="Update filter"]')
-            clickButton(addFilterButton);
+    it("lets you set 'Rating is 5' filter", async () => {
+      const filterPopover = qb.find(FilterPopover);
+
+      setInputValue(filterPopover.find("input"), "5");
+
+      const addFilterButton = filterPopover.find(
+        'button[children="Add filter"]',
+      );
+      clickButton(addFilterButton);
+
+      await store.waitForActions([SET_DATASET_QUERY]);
+
+      expect(qb.find(FilterPopover).length).toBe(0);
+      const filterWidget = qb.find(FilterWidget);
+      expect(filterWidget.length).toBe(1);
+      expect(filterWidget.text()).toBe("Rating is equal to5");
+    });
+
+    it("lets you remove the added filter", async () => {
+      const filterWidget = qb.find(FilterWidget);
+      click(filterWidget.find(".Icon-close"));
+      await store.waitForActions([SET_DATASET_QUERY]);
+
+      expect(qb.find(FilterWidget).length).toBe(0);
+    });
+  });
 
-            await store.waitForActions([SET_DATASET_QUERY])
+  describe("for filtering by ID number field in Reviews table", () => {
+    let store = null;
+    let qb = null;
+    beforeAll(async () => {
+      ({ store, qb } = await initQBWithReviewsTable());
+    });
+
+    it("lets you add ID field as a filter", async () => {
+      const filterSection = qb.find(".GuiBuilder-filtered-by");
+      const addFilterButton = filterSection.find(".AddButton");
+      click(addFilterButton);
+
+      const filterPopover = filterSection.find(FilterPopover);
+
+      const ratingFieldButton = filterPopover
+        .find(FieldList)
+        .find('h4[children="ID"]');
+      expect(ratingFieldButton.length).toBe(1);
+      click(ratingFieldButton);
+    });
+
+    it("lets you see a correct number of operators in filter popover", () => {
+      const filterPopover = qb.find(FilterPopover);
+
+      // const optionsIcon = filterPopover.find(`a[children="Options"]`);
+      const operatorSelector = filterPopover.find(OperatorSelector);
+
+      click(operatorSelector);
+
+      expect(operatorSelector.find(Option).length).toBe(9);
+    });
+
+    it("lets you set 'ID is 10' filter", async () => {
+      const filterPopover = qb.find(FilterPopover);
+      const filterInput = filterPopover.find("input");
+      setInputValue(filterInput, "10");
+
+      const addFilterButton = filterPopover.find(
+        'button[children="Add filter"]',
+      );
+      clickButton(addFilterButton);
+
+      await store.waitForActions([SET_DATASET_QUERY]);
+
+      expect(qb.find(FilterPopover).length).toBe(0);
+      const filterWidget = qb.find(FilterWidget);
+      expect(filterWidget.length).toBe(1);
+      expect(filterWidget.text()).toBe("ID is equal to10");
+    });
 
-            expect(qb.find(FilterPopover).length).toBe(0);
-            expect(filterWidget.text()).toBe("Rating is equal to2 selections");
-        })
+    it("lets you update the filter to 'ID is between 1 or 100'", async () => {
+      const filterWidget = qb.find(FilterWidget);
+      click(filterWidget.find(FieldName));
 
-        it("lets you remove the added filter", async () => {
-            const filterWidget = qb.find(FilterWidget);
-            click(filterWidget.find(".Icon-close"))
-            await store.waitForActions([SET_DATASET_QUERY])
+      const filterPopover = qb.find(FilterPopover);
+      const operatorSelector = filterPopover.find(OperatorSelector);
+      click(operatorSelector);
+      clickButton(operatorSelector.find('[children="Between"]'));
 
-            expect(qb.find(FilterWidget).length).toBe(0);
-        })
-    })
+      const betweenInputs = filterPopover.find("input");
+      expect(betweenInputs.length).toBe(2);
 
-    describe("for filtering by ID number field in Reviews table", () => {
-        let store = null;
-        let qb = null;
-        beforeAll(async () => {
-            ({ store, qb } = await initQBWithReviewsTable());
-        })
-
-        it("lets you add ID field as a filter", async () => {
-            const filterSection = qb.find('.GuiBuilder-filtered-by');
-            const addFilterButton = filterSection.find('.AddButton');
-            click(addFilterButton);
+      setInputValue(betweenInputs.at(1), "asdasd");
+      const updateFilterButton = filterPopover.find(
+        'button[children="Update filter"]',
+      );
+      expect(updateFilterButton.props().className).toMatch(/disabled/);
 
-            const filterPopover = filterSection.find(FilterPopover);
-
-            const ratingFieldButton = filterPopover.find(FieldList).find('h4[children="ID"]')
-            expect(ratingFieldButton.length).toBe(1);
-            click(ratingFieldButton)
-        })
+      setInputValue(betweenInputs.at(0), "1");
+      setInputValue(betweenInputs.at(1), "100");
 
-        it("lets you see a correct number of operators in filter popover", () => {
-            const filterPopover = qb.find(FilterPopover);
+      clickButton(updateFilterButton);
 
-            const operatorSelector = filterPopover.find(OperatorSelector);
-            const moreOptionsIcon = operatorSelector.find(".Icon-chevrondown");
-            click(moreOptionsIcon);
-
-            expect(operatorSelector.find("button").length).toBe(9)
-        })
-
-        it("lets you set 'ID is 10' filter", async () => {
-            const filterPopover = qb.find(FilterPopover);
-            const filterInput = filterPopover.find("textarea");
-            setInputValue(filterInput, "10")
-
-            const addFilterButton = filterPopover.find('button[children="Add filter"]')
-            clickButton(addFilterButton);
-
-            await store.waitForActions([SET_DATASET_QUERY])
-
-            expect(qb.find(FilterPopover).length).toBe(0);
-            const filterWidget = qb.find(FilterWidget);
-            expect(filterWidget.length).toBe(1);
-            expect(filterWidget.text()).toBe("ID is equal to10");
-        })
-
-        it("lets you update the filter to 'ID is 10 or 11'", async () => {
-            const filterWidget = qb.find(FilterWidget);
-            click(filterWidget.find(FieldName))
-
-            const filterPopover = qb.find(FilterPopover);
-            const filterInput = filterPopover.find("textarea");
-
-            // Intentionally use a value with lots of extra spaces
-            setInputValue(filterInput, "  10,      11")
-
-            const addFilterButton = filterPopover.find('button[children="Update filter"]')
-            clickButton(addFilterButton);
-
-            await store.waitForActions([SET_DATASET_QUERY])
-
-            expect(qb.find(FilterPopover).length).toBe(0);
-            expect(filterWidget.text()).toBe("ID is equal to2 selections");
-        });
-
-        it("lets you update the filter to 'ID is between 1 or 100'", async () => {
-            const filterWidget = qb.find(FilterWidget);
-            click(filterWidget.find(FieldName))
-
-            const filterPopover = qb.find(FilterPopover);
-            const operatorSelector = filterPopover.find(OperatorSelector);
-            clickButton(operatorSelector.find('button[children="Between"]'));
-
-            const betweenInputs = filterPopover.find("textarea");
-            expect(betweenInputs.length).toBe(2);
-
-            expect(betweenInputs.at(0).props().value).toBe("10, 11");
-
-            setInputValue(betweenInputs.at(1), "asdasd")
-            const updateFilterButton = filterPopover.find('button[children="Update filter"]')
-            expect(updateFilterButton.props().className).toMatch(/disabled/);
-
-            setInputValue(betweenInputs.at(0), "1")
-            setInputValue(betweenInputs.at(1), "100")
-
-            clickButton(updateFilterButton);
-
-            await store.waitForActions([SET_DATASET_QUERY])
-            expect(qb.find(FilterPopover).length).toBe(0);
-            expect(filterWidget.text()).toBe("ID between1100");
-        });
-    })
-
-    describe("for grouping by Total in Orders table", async () => {
-        let store = null;
-        let qb = null;
-        beforeAll(async () => {
-            ({ store, qb } = await initQbWithOrdersTable());
-        })
-
-        it("lets you group by Total with the default binning option", async () => {
-            const breakoutSection = qb.find('.GuiBuilder-groupedBy');
-            const addBreakoutButton = breakoutSection.find('.AddButton');
-            click(addBreakoutButton);
-
-            const breakoutPopover = breakoutSection.find("#BreakoutPopover")
-            const subtotalFieldButton = breakoutPopover.find(FieldList).find('h4[children="Total"]')
-            expect(subtotalFieldButton.length).toBe(1);
-            click(subtotalFieldButton);
-
-            await store.waitForActions([SET_DATASET_QUERY])
-
-            const breakoutWidget = qb.find(BreakoutWidget).first();
-            expect(breakoutWidget.text()).toBe("Total: Auto binned");
-        });
-        it("produces correct results for default binning option", async () => {
-            // Run the raw data query
-            click(qb.find(RunButton));
-            await store.waitForActions([QUERY_COMPLETED]);
-
-            // We can use the visible row count as we have a low number of result rows
-            expect(qb.find(".ShownRowCount").text()).toBe("Showing 14 rows");
-
-            // Get the binning
-            const results = getQueryResults(store.getState())[0]
-            const breakoutBinningInfo = results.data.cols[0].binning_info;
-            expect(breakoutBinningInfo.binning_strategy).toBe("num-bins");
-            expect(breakoutBinningInfo.bin_width).toBe(20);
-            expect(breakoutBinningInfo.num_bins).toBe(8);
-        })
-        it("lets you change the binning strategy to 100 bins", async () => {
-            const breakoutWidget = qb.find(BreakoutWidget).first();
-            click(breakoutWidget.find(FieldName).children().first())
-            const breakoutPopover = qb.find("#BreakoutPopover")
-
-            const subtotalFieldButton = breakoutPopover.find(FieldList).find('.List-item--selected h4[children="Auto binned"]')
-            expect(subtotalFieldButton.length).toBe(1);
-            click(subtotalFieldButton)
-
-            click(qb.find(DimensionPicker).find('a[children="100 bins"]'));
-
-            await store.waitForActions([SET_DATASET_QUERY])
-            expect(breakoutWidget.text()).toBe("Total: 100 bins");
-        });
-        it("produces correct results for 100 bins", async () => {
-            click(qb.find(RunButton));
-            await store.waitForActions([QUERY_COMPLETED]);
-
-            expect(qb.find(".ShownRowCount").text()).toBe("Showing 253 rows");
-            const results = getQueryResults(store.getState())[0]
-            const breakoutBinningInfo = results.data.cols[0].binning_info;
-            expect(breakoutBinningInfo.binning_strategy).toBe("num-bins");
-            expect(breakoutBinningInfo.bin_width).toBe(1);
-            expect(breakoutBinningInfo.num_bins).toBe(100);
-        })
-        it("lets you disable the binning", async () => {
-            const breakoutWidget = qb.find(BreakoutWidget).first();
-            click(breakoutWidget.find(FieldName).children().first())
-            const breakoutPopover = qb.find("#BreakoutPopover")
-
-            const subtotalFieldButton = breakoutPopover.find(FieldList).find('.List-item--selected h4[children="100 bins"]')
-            expect(subtotalFieldButton.length).toBe(1);
-            click(subtotalFieldButton);
-
-            click(qb.find(DimensionPicker).find('a[children="Don\'t bin"]'));
-        });
-        it("produces the expected count of rows when no binning", async () => {
-            click(qb.find(RunButton));
-            await store.waitForActions([QUERY_COMPLETED]);
-
-            // We just want to see that there are a lot more rows than there would be if a binning was active
-            expect(qb.find(".ShownRowCount").text()).toBe("Showing first 2,000 rows");
-
-            const results = getQueryResults(store.getState())[0]
-            expect(results.data.cols[0].binning_info).toBe(undefined);
-        });
-    })
-
-    describe("for grouping by Latitude location field through Users FK in Orders table", async () => {
-        let store = null;
-        let qb = null;
-        beforeAll(async () => {
-            ({ store, qb } = await initQbWithOrdersTable());
-        })
-
-        it("lets you group by Latitude with the default binning option", async () => {
-            const breakoutSection = qb.find('.GuiBuilder-groupedBy');
-            const addBreakoutButton = breakoutSection.find('.AddButton');
-            click(addBreakoutButton);
-
-            const breakoutPopover = breakoutSection.find("#BreakoutPopover")
-
-            const userSectionButton = breakoutPopover.find(FieldList).find('h3[children="User"]')
-            expect(userSectionButton.length).toBe(1);
-            click(userSectionButton);
-
-            const subtotalFieldButton = breakoutPopover.find(FieldList).find('h4[children="Latitude"]')
-            expect(subtotalFieldButton.length).toBe(1);
-            click(subtotalFieldButton);
-
-            await store.waitForActions([SET_DATASET_QUERY])
-
-            const breakoutWidget = qb.find(BreakoutWidget).first();
-            expect(breakoutWidget.text()).toBe("Latitude: Auto binned");
-        });
-
-        it("produces correct results for default binning option", async () => {
-            // Run the raw data query
-            click(qb.find(RunButton));
-            await store.waitForActions([QUERY_COMPLETED]);
-
-            expect(qb.find(".ShownRowCount").text()).toBe("Showing 18 rows");
-
-            const results = getQueryResults(store.getState())[0]
-            const breakoutBinningInfo = results.data.cols[0].binning_info;
-            expect(breakoutBinningInfo.binning_strategy).toBe("bin-width");
-            expect(breakoutBinningInfo.bin_width).toBe(10);
-            expect(breakoutBinningInfo.num_bins).toBe(18);
-        })
-
-        it("lets you group by Latitude with the 'Bin every 1 degree'", async () => {
-            const breakoutWidget = qb.find(BreakoutWidget).first();
-            click(breakoutWidget.find(FieldName).children().first())
-            const breakoutPopover = qb.find("#BreakoutPopover")
-
-            const subtotalFieldButton = breakoutPopover.find(FieldList).find('.List-item--selected h4[children="Auto binned"]')
-            expect(subtotalFieldButton.length).toBe(1);
-            click(subtotalFieldButton);
-
-            click(qb.find(DimensionPicker).find('a[children="Bin every 1 degree"]'));
-
-            await store.waitForActions([SET_DATASET_QUERY])
-            expect(breakoutWidget.text()).toBe("Latitude: 1°");
-        });
-        it("produces correct results for 'Bin every 1 degree'", async () => {
-            // Run the raw data query
-            click(qb.find(RunButton));
-            await store.waitForActions([QUERY_COMPLETED]);
-
-            expect(qb.find(".ShownRowCount").text()).toBe("Showing 180 rows");
-
-            const results = getQueryResults(store.getState())[0]
-            const breakoutBinningInfo = results.data.cols[0].binning_info;
-            expect(breakoutBinningInfo.binning_strategy).toBe("bin-width");
-            expect(breakoutBinningInfo.bin_width).toBe(1);
-            expect(breakoutBinningInfo.num_bins).toBe(180);
-        })
+      await store.waitForActions([SET_DATASET_QUERY]);
+      expect(qb.find(FilterPopover).length).toBe(0);
+      expect(filterWidget.text()).toBe("ID between1100");
+    });
+  });
+
+  describe("for grouping by Total in Orders table", async () => {
+    let store = null;
+    let qb = null;
+    beforeAll(async () => {
+      ({ store, qb } = await initQbWithOrdersTable());
+    });
+
+    it("lets you group by Total with the default binning option", async () => {
+      const breakoutSection = qb.find(".GuiBuilder-groupedBy");
+      const addBreakoutButton = breakoutSection.find(".AddButton");
+      click(addBreakoutButton);
+
+      const breakoutPopover = breakoutSection.find("#BreakoutPopover");
+      const subtotalFieldButton = breakoutPopover
+        .find(FieldList)
+        .find('h4[children="Total"]');
+      expect(subtotalFieldButton.length).toBe(1);
+      click(subtotalFieldButton);
+
+      await store.waitForActions([SET_DATASET_QUERY]);
+
+      const breakoutWidget = qb.find(BreakoutWidget).first();
+      expect(breakoutWidget.text()).toBe("Total: Auto binned");
+    });
+    it("produces correct results for default binning option", async () => {
+      // Run the raw data query
+      click(qb.find(RunButton));
+      await store.waitForActions([QUERY_COMPLETED]);
+
+      // We can use the visible row count as we have a low number of result rows
+      expect(qb.find(".ShownRowCount").text()).toBe("Showing 14 rows");
+
+      // Get the binning
+      const results = getQueryResults(store.getState())[0];
+      const breakoutBinningInfo = results.data.cols[0].binning_info;
+      expect(breakoutBinningInfo.binning_strategy).toBe("num-bins");
+      expect(breakoutBinningInfo.bin_width).toBe(20);
+      expect(breakoutBinningInfo.num_bins).toBe(8);
+    });
+    it("lets you change the binning strategy to 100 bins", async () => {
+      const breakoutWidget = qb.find(BreakoutWidget).first();
+      click(
+        breakoutWidget
+          .find(FieldName)
+          .children()
+          .first(),
+      );
+      const breakoutPopover = qb.find("#BreakoutPopover");
+
+      const subtotalFieldButton = breakoutPopover
+        .find(FieldList)
+        .find('.List-item--selected h4[children="Auto binned"]');
+      expect(subtotalFieldButton.length).toBe(1);
+      click(subtotalFieldButton);
+
+      click(qb.find(DimensionPicker).find('a[children="100 bins"]'));
+
+      await store.waitForActions([SET_DATASET_QUERY]);
+      expect(breakoutWidget.text()).toBe("Total: 100 bins");
+    });
+    it("produces correct results for 100 bins", async () => {
+      click(qb.find(RunButton));
+      await store.waitForActions([QUERY_COMPLETED]);
+
+      expect(qb.find(".ShownRowCount").text()).toBe("Showing 253 rows");
+      const results = getQueryResults(store.getState())[0];
+      const breakoutBinningInfo = results.data.cols[0].binning_info;
+      expect(breakoutBinningInfo.binning_strategy).toBe("num-bins");
+      expect(breakoutBinningInfo.bin_width).toBe(1);
+      expect(breakoutBinningInfo.num_bins).toBe(100);
+    });
+    it("lets you disable the binning", async () => {
+      const breakoutWidget = qb.find(BreakoutWidget).first();
+      click(
+        breakoutWidget
+          .find(FieldName)
+          .children()
+          .first(),
+      );
+      const breakoutPopover = qb.find("#BreakoutPopover");
+
+      const subtotalFieldButton = breakoutPopover
+        .find(FieldList)
+        .find('.List-item--selected h4[children="100 bins"]');
+      expect(subtotalFieldButton.length).toBe(1);
+      click(subtotalFieldButton);
+
+      click(qb.find(DimensionPicker).find('a[children="Don\'t bin"]'));
+    });
+    it("produces the expected count of rows when no binning", async () => {
+      click(qb.find(RunButton));
+      await store.waitForActions([QUERY_COMPLETED]);
+
+      // We just want to see that there are a lot more rows than there would be if a binning was active
+      expect(qb.find(".ShownRowCount").text()).toBe("Showing first 2,000 rows");
+
+      const results = getQueryResults(store.getState())[0];
+      expect(results.data.cols[0].binning_info).toBe(undefined);
+    });
+  });
+
+  describe("for grouping by Latitude location field through Users FK in Orders table", async () => {
+    let store = null;
+    let qb = null;
+    beforeAll(async () => {
+      ({ store, qb } = await initQbWithOrdersTable());
+    });
+
+    it("lets you group by Latitude with the default binning option", async () => {
+      const breakoutSection = qb.find(".GuiBuilder-groupedBy");
+      const addBreakoutButton = breakoutSection.find(".AddButton");
+      click(addBreakoutButton);
+
+      const breakoutPopover = breakoutSection.find("#BreakoutPopover");
+
+      const userSectionButton = breakoutPopover
+        .find(FieldList)
+        .find('h3[children="User"]');
+      expect(userSectionButton.length).toBe(1);
+      click(userSectionButton);
+
+      const subtotalFieldButton = breakoutPopover
+        .find(FieldList)
+        .find('h4[children="Latitude"]');
+      expect(subtotalFieldButton.length).toBe(1);
+      click(subtotalFieldButton);
+
+      await store.waitForActions([SET_DATASET_QUERY]);
+
+      const breakoutWidget = qb.find(BreakoutWidget).first();
+      expect(breakoutWidget.text()).toBe("Latitude: Auto binned");
+    });
+
+    it("produces correct results for default binning option", async () => {
+      // Run the raw data query
+      click(qb.find(RunButton));
+      await store.waitForActions([QUERY_COMPLETED]);
+
+      expect(qb.find(".ShownRowCount").text()).toBe("Showing 18 rows");
+
+      const results = getQueryResults(store.getState())[0];
+      const breakoutBinningInfo = results.data.cols[0].binning_info;
+      expect(breakoutBinningInfo.binning_strategy).toBe("bin-width");
+      expect(breakoutBinningInfo.bin_width).toBe(10);
+      expect(breakoutBinningInfo.num_bins).toBe(18);
+    });
+
+    it("lets you group by Latitude with the 'Bin every 1 degree'", async () => {
+      const breakoutWidget = qb.find(BreakoutWidget).first();
+      click(
+        breakoutWidget
+          .find(FieldName)
+          .children()
+          .first(),
+      );
+      const breakoutPopover = qb.find("#BreakoutPopover");
+
+      const subtotalFieldButton = breakoutPopover
+        .find(FieldList)
+        .find('.List-item--selected h4[children="Auto binned"]');
+      expect(subtotalFieldButton.length).toBe(1);
+      click(subtotalFieldButton);
+
+      click(qb.find(DimensionPicker).find('a[children="Bin every 1 degree"]'));
+
+      await store.waitForActions([SET_DATASET_QUERY]);
+      expect(breakoutWidget.text()).toBe("Latitude: 1°");
+    });
+    it("produces correct results for 'Bin every 1 degree'", async () => {
+      // Run the raw data query
+      click(qb.find(RunButton));
+      await store.waitForActions([QUERY_COMPLETED]);
+
+      expect(qb.find(".ShownRowCount").text()).toBe("Showing 180 rows");
+
+      const results = getQueryResults(store.getState())[0];
+      const breakoutBinningInfo = results.data.cols[0].binning_info;
+      expect(breakoutBinningInfo.binning_strategy).toBe("bin-width");
+      expect(breakoutBinningInfo.bin_width).toBe(1);
+      expect(breakoutBinningInfo.num_bins).toBe(180);
     });
+  });
 });
diff --git a/frontend/test/query_builder/qb_question_states.integ.spec.js b/frontend/test/query_builder/qb_question_states.integ.spec.js
index f3db44c242b2f648cf52a2900962480396b16868..b94c0c0b36bcb6f93d45290f62406f4cb36dd8dd 100644
--- a/frontend/test/query_builder/qb_question_states.integ.spec.js
+++ b/frontend/test/query_builder/qb_question_states.integ.spec.js
@@ -1,24 +1,21 @@
 import {
-    useSharedAdminLogin,
-    whenOffline,
-    createSavedQuestion,
-    createTestStore
+  useSharedAdminLogin,
+  whenOffline,
+  createSavedQuestion,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton
-} from "__support__/enzyme_utils"
+import { click, clickButton } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import { mount } from "enzyme";
 import {
-    INITIALIZE_QB,
-    QUERY_COMPLETED,
-    QUERY_ERRORED,
-    RUN_QUERY,
-    CANCEL_QUERY,
-    API_UPDATE_QUESTION
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  QUERY_ERRORED,
+  RUN_QUERY,
+  CANCEL_QUERY,
+  API_UPDATE_QUESTION,
 } from "metabase/query_builder/actions";
 import { SET_ERROR_PAGE } from "metabase/redux/app";
 
@@ -30,8 +27,8 @@ import RunButton from "metabase/query_builder/components/RunButton";
 import Visualization from "metabase/visualizations/components/Visualization";
 
 import {
-    ORDERS_TOTAL_FIELD_ID,
-    unsavedOrderCountQuestion
+  ORDERS_TOTAL_FIELD_ID,
+  unsavedOrderCountQuestion,
 } from "__support__/sample_dataset_fixture";
 import VisualizationError from "metabase/query_builder/components/VisualizationError";
 import SaveQuestionModal from "metabase/containers/SaveQuestionModal";
@@ -39,135 +36,199 @@ import Radio from "metabase/components/Radio";
 import QuestionSavedModal from "metabase/components/QuestionSavedModal";
 
 describe("QueryBuilder", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+  describe("for saved questions", async () => {
+    let savedQuestion = null;
     beforeAll(async () => {
-        useSharedAdminLogin()
-    })
-    describe("for saved questions", async () => {
-        let savedQuestion = null;
-        beforeAll(async () => {
-            savedQuestion = await createSavedQuestion(unsavedOrderCountQuestion)
-        })
-
-        it("renders normally on page load", async () => {
-            const store = await createTestStore()
-            store.pushPath(savedQuestion.getUrl(savedQuestion));
-            const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-            expect(qbWrapper.find(QueryHeader).find("h1").text()).toBe(savedQuestion.displayName())
-        });
-        it("shows an error page if the server is offline", async () => {
-            const store = await createTestStore()
-
-            await whenOffline(async () => {
-                store.pushPath(savedQuestion.getUrl());
-                mount(store.connectContainer(<QueryBuilder />));
-                // only test here that the error page action is dispatched
-                // (it is set on the root level of application React tree)
-                await store.waitForActions([INITIALIZE_QB, SET_ERROR_PAGE]);
-            })
-        })
-        it("doesn't execute the query if user cancels it", async () => {
-            const store = await createTestStore()
-            store.pushPath(savedQuestion.getUrl());
-            const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-            await store.waitForActions([INITIALIZE_QB, RUN_QUERY]);
-
-            const runButton = qbWrapper.find(RunButton);
-            expect(runButton.text()).toBe("Cancel");
-            click(runButton);
-
-            await store.waitForActions([CANCEL_QUERY, QUERY_ERRORED]);
-            expect(qbWrapper.find(QueryHeader).find("h1").text()).toBe(savedQuestion.displayName())
-            expect(qbWrapper.find(VisualizationEmptyState).length).toBe(1)
-        })
+      savedQuestion = await createSavedQuestion(unsavedOrderCountQuestion);
     });
 
-    describe("for dirty questions", async () => {
-        describe("without original saved question", () => {
-            it("renders normally on page load", async () => {
-                const store = await createTestStore()
-                store.pushPath(unsavedOrderCountQuestion.getUrl());
-                const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-                await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-
-                expect(qbWrapper.find(QueryHeader).find("h1").text()).toBe("New question")
-                expect(qbWrapper.find(Visualization).length).toBe(1)
-            });
-            it("fails with a proper error message if the query is invalid", async () => {
-                const invalidQuestion = unsavedOrderCountQuestion.query()
-                    .addBreakout(["datetime-field", ["field-id", 12345], "day"])
-                    .question();
-
-                const store = await createTestStore()
-                store.pushPath(invalidQuestion.getUrl());
-                const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-                await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-
-                // TODO: How to get rid of the delay? There is asynchronous initialization in some of VisualizationError parent components
-                // Making the delay shorter causes Jest test runner to crash, see https://stackoverflow.com/a/44075568
-                expect(qbWrapper.find(QueryHeader).find("h1").text()).toBe("New question")
-                expect(qbWrapper.find(VisualizationError).length).toBe(1)
-                expect(qbWrapper.find(VisualizationError).text().includes("There was a problem with your question")).toBe(true)
-            });
-            it("fails with a proper error message if the server is offline", async () => {
-                const store = await createTestStore()
-
-                await whenOffline(async () => {
-                    store.pushPath(unsavedOrderCountQuestion.getUrl());
-                    const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-                    await store.waitForActions([INITIALIZE_QB, QUERY_ERRORED]);
-
-                    expect(qbWrapper.find(QueryHeader).find("h1").text()).toBe("New question")
-                    expect(qbWrapper.find(VisualizationError).length).toBe(1)
-                    expect(qbWrapper.find(VisualizationError).text().includes("We're experiencing server issues")).toBe(true)
-                })
-            })
-            it("doesn't execute the query if user cancels it", async () => {
-                const store = await createTestStore()
-                store.pushPath(unsavedOrderCountQuestion.getUrl());
-                const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-                await store.waitForActions([INITIALIZE_QB, RUN_QUERY]);
-
-                const runButton = qbWrapper.find(RunButton);
-                expect(runButton.text()).toBe("Cancel");
-                click(runButton);
-
-                await store.waitForActions([CANCEL_QUERY, QUERY_ERRORED]);
-                expect(qbWrapper.find(QueryHeader).find("h1").text()).toBe("New question")
-                expect(qbWrapper.find(VisualizationEmptyState).length).toBe(1)
-            })
-        })
-        describe("with original saved question", () => {
-            it("should let you replace the original question", async () => {
-                const store = await createTestStore()
-                const savedQuestion = await createSavedQuestion(unsavedOrderCountQuestion);
-
-                const dirtyQuestion = savedQuestion
-                    .query()
-                    .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
-                    .question()
-
-                store.pushPath(dirtyQuestion.getUrl(savedQuestion));
-                const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
-                await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-
-                const title = qbWrapper.find(QueryHeader).find("h1")
-                expect(title.text()).toBe("New question")
-                expect(title.parent().children().at(1).text()).toBe(`started from ${savedQuestion.displayName()}`)
-
-                // Click "SAVE" button
-                click(qbWrapper.find(".Header-buttonSection a").first().find("a"))
-
-                expect(qbWrapper.find(SaveQuestionModal).find(Radio).prop("value")).toBe("overwrite")
-                // Click Save in "Save question" dialog
-                clickButton(qbWrapper.find(SaveQuestionModal).find("button").last());
-                await store.waitForActions([API_UPDATE_QUESTION])
-
-                // Should not show a "add to dashboard" dialog in this case
-                // This is included because of regression #6541
-                expect(qbWrapper.find(QuestionSavedModal).length).toBe(0)
-            });
+    it("renders normally on page load", async () => {
+      const store = await createTestStore();
+      store.pushPath(savedQuestion.getUrl(savedQuestion));
+      const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+      expect(
+        qbWrapper
+          .find(QueryHeader)
+          .find("h1")
+          .text(),
+      ).toBe(savedQuestion.displayName());
+    });
+    it("shows an error page if the server is offline", async () => {
+      const store = await createTestStore();
+
+      await whenOffline(async () => {
+        store.pushPath(savedQuestion.getUrl());
+        mount(store.connectContainer(<QueryBuilder />));
+        // only test here that the error page action is dispatched
+        // (it is set on the root level of application React tree)
+        await store.waitForActions([INITIALIZE_QB, SET_ERROR_PAGE]);
+      });
+    });
+    it("doesn't execute the query if user cancels it", async () => {
+      const store = await createTestStore();
+      store.pushPath(savedQuestion.getUrl());
+      const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+      await store.waitForActions([INITIALIZE_QB, RUN_QUERY]);
+
+      const runButton = qbWrapper.find(RunButton);
+      expect(runButton.text()).toBe("Cancel");
+      click(runButton);
+
+      await store.waitForActions([CANCEL_QUERY, QUERY_ERRORED]);
+      expect(
+        qbWrapper
+          .find(QueryHeader)
+          .find("h1")
+          .text(),
+      ).toBe(savedQuestion.displayName());
+      expect(qbWrapper.find(VisualizationEmptyState).length).toBe(1);
+    });
+  });
+
+  describe("for dirty questions", async () => {
+    describe("without original saved question", () => {
+      it("renders normally on page load", async () => {
+        const store = await createTestStore();
+        store.pushPath(unsavedOrderCountQuestion.getUrl());
+        const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+        await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+
+        expect(
+          qbWrapper
+            .find(QueryHeader)
+            .find("h1")
+            .text(),
+        ).toBe("New question");
+        expect(qbWrapper.find(Visualization).length).toBe(1);
+      });
+      it("fails with a proper error message if the query is invalid", async () => {
+        const invalidQuestion = unsavedOrderCountQuestion
+          .query()
+          .addBreakout(["datetime-field", ["field-id", 12345], "day"])
+          .question();
+
+        const store = await createTestStore();
+        store.pushPath(invalidQuestion.getUrl());
+        const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+        await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+
+        // TODO: How to get rid of the delay? There is asynchronous initialization in some of VisualizationError parent components
+        // Making the delay shorter causes Jest test runner to crash, see https://stackoverflow.com/a/44075568
+        expect(
+          qbWrapper
+            .find(QueryHeader)
+            .find("h1")
+            .text(),
+        ).toBe("New question");
+        expect(qbWrapper.find(VisualizationError).length).toBe(1);
+        expect(
+          qbWrapper
+            .find(VisualizationError)
+            .text()
+            .includes("There was a problem with your question"),
+        ).toBe(true);
+      });
+      it("fails with a proper error message if the server is offline", async () => {
+        const store = await createTestStore();
+
+        await whenOffline(async () => {
+          store.pushPath(unsavedOrderCountQuestion.getUrl());
+          const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+          await store.waitForActions([INITIALIZE_QB, QUERY_ERRORED]);
+
+          expect(
+            qbWrapper
+              .find(QueryHeader)
+              .find("h1")
+              .text(),
+          ).toBe("New question");
+          expect(qbWrapper.find(VisualizationError).length).toBe(1);
+          expect(
+            qbWrapper
+              .find(VisualizationError)
+              .text()
+              .includes("We're experiencing server issues"),
+          ).toBe(true);
         });
+      });
+      it("doesn't execute the query if user cancels it", async () => {
+        const store = await createTestStore();
+        store.pushPath(unsavedOrderCountQuestion.getUrl());
+        const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+        await store.waitForActions([INITIALIZE_QB, RUN_QUERY]);
+
+        const runButton = qbWrapper.find(RunButton);
+        expect(runButton.text()).toBe("Cancel");
+        click(runButton);
+
+        await store.waitForActions([CANCEL_QUERY, QUERY_ERRORED]);
+        expect(
+          qbWrapper
+            .find(QueryHeader)
+            .find("h1")
+            .text(),
+        ).toBe("New question");
+        expect(qbWrapper.find(VisualizationEmptyState).length).toBe(1);
+      });
+    });
+    describe("with original saved question", () => {
+      it("should let you replace the original question", async () => {
+        const store = await createTestStore();
+        const savedQuestion = await createSavedQuestion(
+          unsavedOrderCountQuestion,
+        );
+
+        const dirtyQuestion = savedQuestion
+          .query()
+          .addBreakout(["field-id", ORDERS_TOTAL_FIELD_ID])
+          .question();
+
+        store.pushPath(dirtyQuestion.getUrl(savedQuestion));
+        const qbWrapper = mount(store.connectContainer(<QueryBuilder />));
+        await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+
+        const title = qbWrapper.find(QueryHeader).find("h1");
+        expect(title.text()).toBe("New question");
+        expect(
+          title
+            .parent()
+            .children()
+            .at(1)
+            .text(),
+        ).toBe(`started from ${savedQuestion.displayName()}`);
+
+        // Click "SAVE" button
+        click(
+          qbWrapper
+            .find(".Header-buttonSection a")
+            .first()
+            .find("a"),
+        );
+
+        expect(
+          qbWrapper
+            .find(SaveQuestionModal)
+            .find(Radio)
+            .prop("value"),
+        ).toBe("overwrite");
+        // Click Save in "Save question" dialog
+        clickButton(
+          qbWrapper
+            .find(SaveQuestionModal)
+            .find("button")
+            .last(),
+        );
+        await store.waitForActions([API_UPDATE_QUESTION]);
+
+        // Should not show a "add to dashboard" dialog in this case
+        // This is included because of regression #6541
+        expect(qbWrapper.find(QuestionSavedModal).length).toBe(0);
+      });
     });
+  });
 });
diff --git a/frontend/test/query_builder/qb_remapping.js b/frontend/test/query_builder/qb_remapping.js
index 0befbca831f478e715b97c103c20cc78ffb8937d..f88853b3fd18c270222bde6ca8292fa454893d89 100644
--- a/frontend/test/query_builder/qb_remapping.js
+++ b/frontend/test/query_builder/qb_remapping.js
@@ -1,31 +1,28 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import {
-    click,
-    clickButton
-} from "__support__/enzyme_utils"
+import { click, clickButton } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import { mount } from "enzyme";
 import {
-    INITIALIZE_QB,
-    QUERY_COMPLETED,
-    SET_DATASET_QUERY,
-    setQueryDatabase,
-    setQuerySourceTable,
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  SET_DATASET_QUERY,
+  setQueryDatabase,
+  setQuerySourceTable,
 } from "metabase/query_builder/actions";
 
 import {
-    deleteFieldDimension,
-    updateFieldDimension,
-    updateFieldValues,
-    FETCH_TABLE_METADATA,
+  deleteFieldDimension,
+  updateFieldDimension,
+  updateFieldValues,
+  FETCH_TABLE_METADATA,
 } from "metabase/redux/metadata";
 
-import FieldList  from "metabase/query_builder/components/FieldList";
+import FieldList from "metabase/query_builder/components/FieldList";
 import FilterPopover from "metabase/query_builder/components/filters/FilterPopover";
 
 import CheckBox from "metabase/components/CheckBox";
@@ -41,144 +38,177 @@ const REVIEW_RATING_ID = 33;
 const PRODUCT_TITLE_ID = 27;
 
 const initQbWithDbAndTable = (dbId, tableId) => {
-    return async () => {
-        const store = await createTestStore()
-        store.pushPath(Urls.plainQuestion());
-        const qb = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
+  return async () => {
+    const store = await createTestStore();
+    store.pushPath(Urls.plainQuestion());
+    const qb = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
 
-        // Use Products table
-        store.dispatch(setQueryDatabase(dbId));
-        store.dispatch(setQuerySourceTable(tableId));
-        await store.waitForActions([FETCH_TABLE_METADATA]);
+    // Use Products table
+    store.dispatch(setQueryDatabase(dbId));
+    store.dispatch(setQuerySourceTable(tableId));
+    await store.waitForActions([FETCH_TABLE_METADATA]);
 
-        return { store, qb }
-    }
-}
+    return { store, qb };
+  };
+};
 
-const initQBWithReviewsTable = initQbWithDbAndTable(1, 4)
+const initQBWithReviewsTable = initQbWithDbAndTable(1, 4);
 
 describe("QueryBuilder", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("remapping", () => {
     beforeAll(async () => {
-        useSharedAdminLogin()
-    })
-
-    describe("remapping", () => {
-        beforeAll(async () => {
-            // add remappings
-            const store = await createTestStore()
-
-            // NOTE Atte Keinänen 8/7/17:
-            // We test here the full dimension functionality which lets you enter a dimension name that differs
-            // from the field name. This is something that field settings UI doesn't let you to do yet.
-
-            await store.dispatch(updateFieldDimension(REVIEW_PRODUCT_ID, {
-                type: "external",
-                name: "Product Name",
-                human_readable_field_id: PRODUCT_TITLE_ID
-            }));
-
-            await store.dispatch(updateFieldDimension(REVIEW_RATING_ID, {
-                type: "internal",
-                name: "Rating Description",
-                human_readable_field_id: null
-            }));
-            await store.dispatch(updateFieldValues(REVIEW_RATING_ID, [
-                [1, 'Awful'], [2, 'Unpleasant'], [3, 'Meh'], [4, 'Enjoyable'], [5, 'Perfecto']
-            ]));
-        })
-
-        describe("for Rating category field with custom field values", () => {
-            // The following test case is very similar to earlier filter tests but in this case we use remapped values
-            it("lets you add 'Rating is Perfecto' filter", async () => {
-                const { store, qb } = await initQBWithReviewsTable();
-
-                // open filter popover
-                const filterSection = qb.find('.GuiBuilder-filtered-by');
-                const newFilterButton = filterSection.find('.AddButton');
-                click(newFilterButton);
-
-                // choose the field to be filtered
-                const filterPopover = filterSection.find(FilterPopover);
-                const ratingFieldButton = filterPopover.find(FieldList).find('h4[children="Rating Description"]')
-                expect(ratingFieldButton.length).toBe(1);
-                click(ratingFieldButton)
-
-                // check that field values seem correct
-                const fieldItems = filterPopover.find('li');
-                expect(fieldItems.length).toBe(5);
-                expect(fieldItems.first().text()).toBe("Awful")
-                expect(fieldItems.last().text()).toBe("Perfecto")
-
-                // select the last item (Perfecto)
-                const widgetFieldItem = fieldItems.last();
-                const widgetCheckbox = widgetFieldItem.find(CheckBox);
-                expect(widgetCheckbox.props().checked).toBe(false);
-                click(widgetFieldItem.children().first());
-                expect(widgetCheckbox.props().checked).toBe(true);
-
-                // add the filter
-                const addFilterButton = filterPopover.find('button[children="Add filter"]')
-                clickButton(addFilterButton);
-
-                await store.waitForActions([SET_DATASET_QUERY])
-
-                // validate the filter text value
-                expect(qb.find(FilterPopover).length).toBe(0);
-                const filterWidget = qb.find(FilterWidget);
-                expect(filterWidget.length).toBe(1);
-                expect(filterWidget.text()).toBe("Rating Description is equal toPerfecto");
-            })
-
-            it("shows remapped value correctly in Raw Data query with Table visualization", async () => {
-                const { store, qb } = await initQBWithReviewsTable();
-
-                clickButton(qb.find(RunButton));
-                await store.waitForActions([QUERY_COMPLETED]);
-
-                const table = qb.find(TestTable);
-                const headerCells = table.find("thead tr").first().find("th");
-                const firstRowCells = table.find("tbody tr").first().find("td");
-
-                expect(headerCells.length).toBe(6)
-                expect(headerCells.at(4).text()).toBe("Rating Description")
-
-                expect(firstRowCells.length).toBe(6);
-
-                expect(firstRowCells.at(4).text()).toBe("Perfecto");
-            })
-        });
-
-        describe("for Product ID FK field with a FK remapping", () => {
-            it("shows remapped values correctly in Raw Data query with Table visualization", async () => {
-                const { store, qb } = await initQBWithReviewsTable();
-
-                clickButton(qb.find(RunButton));
-                await store.waitForActions([QUERY_COMPLETED]);
-
-                const table = qb.find(TestTable);
-                const headerCells = table.find("thead tr").first().find("th");
-                const firstRowCells = table.find("tbody tr").first().find("td");
-
-                expect(headerCells.length).toBe(6)
-                expect(headerCells.at(3).text()).toBe("Product Name")
-
-                expect(firstRowCells.length).toBe(6);
-
-                expect(firstRowCells.at(3).text()).toBe("Awesome Wooden Pants");
-            })
-        });
-
-        afterAll(async () => {
-            const store = await createTestStore()
-
-            await store.dispatch(deleteFieldDimension(REVIEW_PRODUCT_ID));
-            await store.dispatch(deleteFieldDimension(REVIEW_RATING_ID));
-
-            await store.dispatch(updateFieldValues(REVIEW_RATING_ID, [
-                [1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5']
-            ]));
-        })
-
-    })
+      // add remappings
+      const store = await createTestStore();
+
+      // NOTE Atte Keinänen 8/7/17:
+      // We test here the full dimension functionality which lets you enter a dimension name that differs
+      // from the field name. This is something that field settings UI doesn't let you to do yet.
+
+      await store.dispatch(
+        updateFieldDimension(REVIEW_PRODUCT_ID, {
+          type: "external",
+          name: "Product Name",
+          human_readable_field_id: PRODUCT_TITLE_ID,
+        }),
+      );
+
+      await store.dispatch(
+        updateFieldDimension(REVIEW_RATING_ID, {
+          type: "internal",
+          name: "Rating Description",
+          human_readable_field_id: null,
+        }),
+      );
+      await store.dispatch(
+        updateFieldValues(REVIEW_RATING_ID, [
+          [1, "Awful"],
+          [2, "Unpleasant"],
+          [3, "Meh"],
+          [4, "Enjoyable"],
+          [5, "Perfecto"],
+        ]),
+      );
+    });
+
+    describe("for Rating category field with custom field values", () => {
+      // The following test case is very similar to earlier filter tests but in this case we use remapped values
+      it("lets you add 'Rating is Perfecto' filter", async () => {
+        const { store, qb } = await initQBWithReviewsTable();
+
+        // open filter popover
+        const filterSection = qb.find(".GuiBuilder-filtered-by");
+        const newFilterButton = filterSection.find(".AddButton");
+        click(newFilterButton);
+
+        // choose the field to be filtered
+        const filterPopover = filterSection.find(FilterPopover);
+        const ratingFieldButton = filterPopover
+          .find(FieldList)
+          .find('h4[children="Rating Description"]');
+        expect(ratingFieldButton.length).toBe(1);
+        click(ratingFieldButton);
+
+        // check that field values seem correct
+        const fieldItems = filterPopover.find("li");
+        expect(fieldItems.length).toBe(5);
+        expect(fieldItems.first().text()).toBe("Awful");
+        expect(fieldItems.last().text()).toBe("Perfecto");
+
+        // select the last item (Perfecto)
+        const widgetFieldItem = fieldItems.last();
+        const widgetCheckbox = widgetFieldItem.find(CheckBox);
+        expect(widgetCheckbox.props().checked).toBe(false);
+        click(widgetFieldItem.children().first());
+        expect(widgetCheckbox.props().checked).toBe(true);
+
+        // add the filter
+        const addFilterButton = filterPopover.find(
+          'button[children="Add filter"]',
+        );
+        clickButton(addFilterButton);
+
+        await store.waitForActions([SET_DATASET_QUERY]);
+
+        // validate the filter text value
+        expect(qb.find(FilterPopover).length).toBe(0);
+        const filterWidget = qb.find(FilterWidget);
+        expect(filterWidget.length).toBe(1);
+        expect(filterWidget.text()).toBe(
+          "Rating Description is equal toPerfecto",
+        );
+      });
+
+      it("shows remapped value correctly in Raw Data query with Table visualization", async () => {
+        const { store, qb } = await initQBWithReviewsTable();
+
+        clickButton(qb.find(RunButton));
+        await store.waitForActions([QUERY_COMPLETED]);
+
+        const table = qb.find(TestTable);
+        const headerCells = table
+          .find("thead tr")
+          .first()
+          .find("th");
+        const firstRowCells = table
+          .find("tbody tr")
+          .first()
+          .find("td");
+
+        expect(headerCells.length).toBe(6);
+        expect(headerCells.at(4).text()).toBe("Rating Description");
+
+        expect(firstRowCells.length).toBe(6);
+
+        expect(firstRowCells.at(4).text()).toBe("Perfecto");
+      });
+    });
+
+    describe("for Product ID FK field with a FK remapping", () => {
+      it("shows remapped values correctly in Raw Data query with Table visualization", async () => {
+        const { store, qb } = await initQBWithReviewsTable();
+
+        clickButton(qb.find(RunButton));
+        await store.waitForActions([QUERY_COMPLETED]);
+
+        const table = qb.find(TestTable);
+        const headerCells = table
+          .find("thead tr")
+          .first()
+          .find("th");
+        const firstRowCells = table
+          .find("tbody tr")
+          .first()
+          .find("td");
+
+        expect(headerCells.length).toBe(6);
+        expect(headerCells.at(3).text()).toBe("Product Name");
+
+        expect(firstRowCells.length).toBe(6);
+
+        expect(firstRowCells.at(3).text()).toBe("Awesome Wooden Pants");
+      });
+    });
+
+    afterAll(async () => {
+      const store = await createTestStore();
+
+      await store.dispatch(deleteFieldDimension(REVIEW_PRODUCT_ID));
+      await store.dispatch(deleteFieldDimension(REVIEW_RATING_ID));
+
+      await store.dispatch(
+        updateFieldValues(REVIEW_RATING_ID, [
+          [1, "1"],
+          [2, "2"],
+          [3, "3"],
+          [4, "4"],
+          [5, "5"],
+        ]),
+      );
+    });
+  });
 });
diff --git a/frontend/test/query_builder/qb_visualizations.integ.spec.js b/frontend/test/query_builder/qb_visualizations.integ.spec.js
index 214526a514af74aec5ba242b888736a453f8b71a..55a54eaa73e6b6b0036345b0a9b8f3cb21ad7534 100644
--- a/frontend/test/query_builder/qb_visualizations.integ.spec.js
+++ b/frontend/test/query_builder/qb_visualizations.integ.spec.js
@@ -1,17 +1,19 @@
 import {
-    useSharedAdminLogin,
-    createTestStore, createSavedQuestion
+  useSharedAdminLogin,
+  createTestStore,
+  createSavedQuestion,
 } from "__support__/integrated_tests";
 import { click, clickButton, setInputValue } from "__support__/enzyme_utils";
 
-import React from 'react';
+import React from "react";
 import QueryBuilder from "metabase/query_builder/containers/QueryBuilder";
 import { mount } from "enzyme";
 import {
-    API_CREATE_QUESTION,
-    API_UPDATE_QUESTION,
-    INITIALIZE_QB,
-    QUERY_COMPLETED, SET_CARD_VISUALIZATION,
+  API_CREATE_QUESTION,
+  API_UPDATE_QUESTION,
+  INITIALIZE_QB,
+  QUERY_COMPLETED,
+  SET_CARD_VISUALIZATION,
 } from "metabase/query_builder/actions";
 
 import Question from "metabase-lib/lib/Question";
@@ -22,83 +24,118 @@ import { LOAD_COLLECTIONS } from "metabase/questions/collections";
 import { CardApi } from "metabase/services";
 import * as Urls from "metabase/lib/urls";
 import VisualizationSettings from "metabase/query_builder/components/VisualizationSettings";
-import { TestPopover } from "metabase/components/Popover";
-
-const timeBreakoutQuestion = Question.create({databaseId: 1, tableId: 1, metadata: null})
-    .query()
-    .addAggregation(["count"])
-    .addBreakout(["datetime-field", ["field-id", 1], "day"])
-    .question()
-    .setDisplay("line")
-    .setDisplayName("Time breakout question")
+import Popover from "metabase/components/Popover";
+
+const timeBreakoutQuestion = Question.create({
+  databaseId: 1,
+  tableId: 1,
+  metadata: null,
+})
+  .query()
+  .addAggregation(["count"])
+  .addBreakout(["datetime-field", ["field-id", 1], "day"])
+  .question()
+  .setDisplay("line")
+  .setDisplayName("Time breakout question");
 
 describe("Query Builder visualization logic", () => {
-    let questionId = null
-    let savedTimeBreakoutQuestion = null
-
-    beforeAll(async () => {
-        useSharedAdminLogin()
-        savedTimeBreakoutQuestion = await createSavedQuestion(timeBreakoutQuestion)
-    })
-
-    it("should save the default x axis and y axis to `visualization_settings` when saving a new question in QB", async () => {
-        const store = await createTestStore()
-        store.pushPath(timeBreakoutQuestion.getUrl());
-        const app = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB]);
-
-        expect(getCard(store.getState()).visualization_settings).toEqual({})
-
-        await store.waitForActions([QUERY_COMPLETED]);
-        expect(getCard(store.getState()).visualization_settings).toEqual({})
-
-        // Click "SAVE" button
-        click(app.find(".Header-buttonSection a").first().find("a"))
-
-        await store.waitForActions([LOAD_COLLECTIONS]);
-
-        setInputValue(app.find(SaveQuestionModal).find("input[name='name']"), "test visualization question");
-        clickButton(app.find(SaveQuestionModal).find("button").last());
-        await store.waitForActions([API_CREATE_QUESTION]);
-
-        expect(getCard(store.getState()).visualization_settings).toEqual({
-            "graph.dimensions": ["CREATED_AT"],
-            "graph.metrics": ["count"]
-        })
-
-        questionId = getQuestion(store.getState()).id()
+  let questionId = null;
+  let savedTimeBreakoutQuestion = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+    savedTimeBreakoutQuestion = await createSavedQuestion(timeBreakoutQuestion);
+  });
+
+  it("should save the default x axis and y axis to `visualization_settings` when saving a new question in QB", async () => {
+    const store = await createTestStore();
+    store.pushPath(timeBreakoutQuestion.getUrl());
+    const app = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB]);
+
+    expect(getCard(store.getState()).visualization_settings).toEqual({});
+
+    await store.waitForActions([QUERY_COMPLETED]);
+    expect(getCard(store.getState()).visualization_settings).toEqual({});
+
+    // Click "SAVE" button
+    click(
+      app
+        .find(".Header-buttonSection a")
+        .first()
+        .find("a"),
+    );
+
+    await store.waitForActions([LOAD_COLLECTIONS]);
+
+    setInputValue(
+      app.find(SaveQuestionModal).find("input[name='name']"),
+      "test visualization question",
+    );
+    clickButton(
+      app
+        .find(SaveQuestionModal)
+        .find("button")
+        .last(),
+    );
+    await store.waitForActions([API_CREATE_QUESTION]);
+
+    expect(getCard(store.getState()).visualization_settings).toEqual({
+      "graph.dimensions": ["CREATED_AT"],
+      "graph.metrics": ["count"],
     });
 
-    it("should save the default x axis and y axis to `visualization_settings` when saving a new question in QB", async () => {
-        const store = await createTestStore()
-        store.pushPath(Urls.question(savedTimeBreakoutQuestion.id()));
-        const app = mount(store.connectContainer(<QueryBuilder />));
-        await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-        expect(getCard(store.getState()).visualization_settings).toEqual({})
-
-        // modify the question in the UI by switching visualization type
-        const vizSettings = app.find(VisualizationSettings)
-        const vizSettingsTrigger = vizSettings.find("a").first()
-        click(vizSettingsTrigger)
-        const areaChartOption = vizSettings.find(TestPopover).find('span').filterWhere((elem) => /Area/.test(elem.text()))
-        click(areaChartOption)
-        await store.waitForActions([SET_CARD_VISUALIZATION])
-
-        click(app.find(".Header-buttonSection a").first().find("a"))
-        expect(app.find(SaveQuestionModal).find(Radio).prop("value")).toBe("overwrite")
-        // Click Save in "Save question" dialog
-        clickButton(app.find(SaveQuestionModal).find("button").last());
-        await store.waitForActions([API_UPDATE_QUESTION])
-
-        expect(getCard(store.getState()).visualization_settings).toEqual({
-            "graph.dimensions": ["CREATED_AT"],
-            "graph.metrics": ["count"]
-        })
+    questionId = getQuestion(store.getState()).id();
+  });
+
+  it("should save the default x axis and y axis to `visualization_settings` when saving a new question in QB", async () => {
+    const store = await createTestStore();
+    store.pushPath(Urls.question(savedTimeBreakoutQuestion.id()));
+    const app = mount(store.connectContainer(<QueryBuilder />));
+    await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+    expect(getCard(store.getState()).visualization_settings).toEqual({});
+
+    // modify the question in the UI by switching visualization type
+    const vizSettings = app.find(VisualizationSettings);
+    const vizSettingsTrigger = vizSettings.find("a").first();
+    click(vizSettingsTrigger);
+    const areaChartOption = vizSettings
+      .find(Popover)
+      .find("span")
+      .filterWhere(elem => /Area/.test(elem.text()));
+    click(areaChartOption);
+    await store.waitForActions([SET_CARD_VISUALIZATION]);
+
+    click(
+      app
+        .find(".Header-buttonSection a")
+        .first()
+        .find("a"),
+    );
+    expect(
+      app
+        .find(SaveQuestionModal)
+        .find(Radio)
+        .prop("value"),
+    ).toBe("overwrite");
+    // Click Save in "Save question" dialog
+    clickButton(
+      app
+        .find(SaveQuestionModal)
+        .find("button")
+        .last(),
+    );
+    await store.waitForActions([API_UPDATE_QUESTION]);
+
+    expect(getCard(store.getState()).visualization_settings).toEqual({
+      "graph.dimensions": ["CREATED_AT"],
+      "graph.metrics": ["count"],
     });
-    afterAll(async () => {
-        if (questionId) {
-            await CardApi.delete({cardId: questionId})
-            await CardApi.delete({cardId: savedTimeBreakoutQuestion.id()})
-        }
-    })
+  });
+  afterAll(async () => {
+    if (questionId) {
+      await CardApi.delete({ cardId: questionId });
+      await CardApi.delete({ cardId: savedTimeBreakoutQuestion.id() });
+    }
+  });
 });
diff --git a/frontend/test/questions/CollectionEditorForm.unit.spec.js b/frontend/test/questions/CollectionEditorForm.unit.spec.js
index f735ea22d81e7950949c748ff80725c01a137bc7..0fcadde69c306f1e12435584aa852b0454b78fcf 100644
--- a/frontend/test/questions/CollectionEditorForm.unit.spec.js
+++ b/frontend/test/questions/CollectionEditorForm.unit.spec.js
@@ -1,38 +1,32 @@
 import {
-    getFormTitle,
-    getActionText
-} from '../../src/metabase/questions/containers/CollectionEditorForm'
+  getFormTitle,
+  getActionText,
+} from "../../src/metabase/questions/containers/CollectionEditorForm";
 
 const FORM_FIELDS = {
-    id: { value: 4 },
-    name: { value: 'Test collection' },
-    color: { value: '#409ee3' },
-    initialValues: {
-        color: '#409ee3'
-    }
-}
-const NEW_COLLECTION_FIELDS = { ...FORM_FIELDS, id: '', color: '' }
+  id: { value: 4 },
+  name: { value: "Test collection" },
+  color: { value: "#409ee3" },
+  initialValues: {
+    color: "#409ee3",
+  },
+};
+const NEW_COLLECTION_FIELDS = { ...FORM_FIELDS, id: "", color: "" };
 
-describe('CollectionEditorForm', () => {
+describe("CollectionEditorForm", () => {
+  describe("Title", () => {
+    it("should have a default title if no collection exists", () =>
+      expect(getFormTitle(NEW_COLLECTION_FIELDS)).toEqual("New collection"));
 
-    describe('Title', () => {
-        it('should have a default title if no collection exists', () =>
-            expect(getFormTitle(NEW_COLLECTION_FIELDS)).toEqual('New collection')
-        )
+    it("should have the title of the colleciton if one exists", () =>
+      expect(getFormTitle(FORM_FIELDS)).toEqual(FORM_FIELDS.name.value));
+  });
 
-        it('should have the title of the colleciton if one exists', () =>
-            expect(getFormTitle(FORM_FIELDS)).toEqual(FORM_FIELDS.name.value)
-        )
-    })
+  describe("Form actions", () => {
+    it('should have a "create" primary action if no collection exists', () =>
+      expect(getActionText(NEW_COLLECTION_FIELDS)).toEqual("Create"));
 
-    describe('Form actions', () => {
-        it('should have a "create" primary action if no collection exists', () =>
-            expect(getActionText(NEW_COLLECTION_FIELDS)).toEqual('Create')
-        )
-
-        it('should have an "update" primary action if no collection exists', () =>
-            expect(getActionText(FORM_FIELDS)).toEqual('Update')
-        )
-    })
-
-})
+    it('should have an "update" primary action if no collection exists', () =>
+      expect(getActionText(FORM_FIELDS)).toEqual("Update"));
+  });
+});
diff --git a/frontend/test/questions/QuestionIndex.unit.spec.js b/frontend/test/questions/QuestionIndex.unit.spec.js
index 2b284e2e4f83cb2e2161c198ba643d7024b04044..112d86499c35d846d4d0f8fbc899cfe677b34e3d 100644
--- a/frontend/test/questions/QuestionIndex.unit.spec.js
+++ b/frontend/test/questions/QuestionIndex.unit.spec.js
@@ -1,140 +1,158 @@
-import React from 'react'
-import {shallow, mount} from 'enzyme'
+import React from "react";
+import { shallow, mount } from "enzyme";
 
 import {
-    QuestionIndex,
-    CollectionEmptyState,
-    NoSavedQuestionsState,
-    QuestionIndexHeader
-} from '../../src/metabase/questions/containers/QuestionIndex';
+  QuestionIndex,
+  CollectionEmptyState,
+  NoSavedQuestionsState,
+  QuestionIndexHeader,
+} from "../../src/metabase/questions/containers/QuestionIndex";
 
 const someQuestions = [{}, {}, {}];
 const someCollections = [{}, {}];
 
 const dummyFunction = () => {};
 
-const getQuestionIndex = ({ questions, collections, isAdmin }) =>
-    <QuestionIndex
-        questions={questions}
-        collections={collections}
-        isAdmin={isAdmin}
-        replace={dummyFunction}
-        push={dummyFunction}
-        location={dummyFunction}
-        search={dummyFunction}
-        loadCollections={dummyFunction}
-    />;
-
-describe('QuestionIndex', () => {
-    describe('info box about collections', () => {
-        it("should be shown to admins if no collections", () => {
-            const component = shallow(getQuestionIndex({
-                questions: null,
-                collections: null,
-                isAdmin: true
-            }));
-
-            expect(component.find(CollectionEmptyState).length).toEqual(1);
-
-            const component2 = shallow(getQuestionIndex({
-                questions: someQuestions,
-                collections: null,
-                isAdmin: true
-            }));
-
-            expect(component2.find(CollectionEmptyState).length).toEqual(1);
-        });
-
-        it("should not be shown to admins if there are collections", () => {
-            const component = shallow(getQuestionIndex({
-                questions: null,
-                collections: someCollections,
-                isAdmin: false
-            }));
-
-            expect(component.find(CollectionEmptyState).length).toEqual(0);
-        });
-
-        it("should not be shown to non-admins", () => {
-            const component = shallow(getQuestionIndex({
-                questions: null,
-                collections: null,
-                isAdmin: false
-            }));
-
-            expect(component.find(CollectionEmptyState).length).toEqual(0);
-        })
+const getQuestionIndex = ({ questions, collections, isAdmin }) => (
+  <QuestionIndex
+    questions={questions}
+    collections={collections}
+    isAdmin={isAdmin}
+    replace={dummyFunction}
+    push={dummyFunction}
+    location={dummyFunction}
+    search={dummyFunction}
+    loadCollections={dummyFunction}
+  />
+);
+
+describe("QuestionIndex", () => {
+  describe("info box about collections", () => {
+    it("should be shown to admins if no collections", () => {
+      const component = shallow(
+        getQuestionIndex({
+          questions: null,
+          collections: null,
+          isAdmin: true,
+        }),
+      );
+
+      expect(component.find(CollectionEmptyState).length).toEqual(1);
+
+      const component2 = shallow(
+        getQuestionIndex({
+          questions: someQuestions,
+          collections: null,
+          isAdmin: true,
+        }),
+      );
+
+      expect(component2.find(CollectionEmptyState).length).toEqual(1);
     });
 
-    describe('no saved questions state', () => {
-       const eitherAdminOrNot = [true, false];
-
-       it("should be shown if no collections or questions", () => {
-           eitherAdminOrNot.forEach(isAdmin => {
-               const component = shallow(getQuestionIndex({
-                   questions: null,
-                   collections: null,
-                   isAdmin
-               }));
-
-               expect(component.find(NoSavedQuestionsState).length).toEqual(1);
-           })
-       });
-
-        it("should not be shown otherwise", () => {
-            eitherAdminOrNot.forEach(isAdmin => {
-                const component = shallow(getQuestionIndex({
-                    questions: someQuestions,
-                    collections: null,
-                    isAdmin
-                }));
-
-                expect(component.find(NoSavedQuestionsState).length).toEqual(0);
-
-                const component2 = shallow(getQuestionIndex({
-                    questions: null,
-                    collections: someCollections,
-                    isAdmin
-                }));
-
-                expect(component2.find(NoSavedQuestionsState).length).toEqual(0);
-            });
-        })
-    })
-
-    describe('collection actions', () => {
-       it("should let admins change permissions if collections exist", () => {
-           const component = mount(
-               <QuestionIndexHeader
-                   collections={someCollections}
-                   isAdmin={true}
-               />
-           );
-
-           // Why `find` does not work for matching React props: https://github.com/airbnb/enzyme/issues/582
-           expect(component.findWhere((node) => node.prop('to') === "/collections/permissions" ).length).toEqual(1);
-       });
-
-        it("should not let admins change permissions if no collections", () => {
-            const component = mount(
-                <QuestionIndexHeader
-                    collections={null}
-                    isAdmin={true}
-                />
-            );
-
-            expect(component.findWhere((node) => node.prop('to') === "/collections/permissions" ).length).toEqual(0);
-        });
-
-        it("should not let non-admins change permissions", () => {
-            const component = mount(
-                <QuestionIndexHeader
-                    collections={someCollections}
-                    isAdmin={false}
-                />
-            );
-
-            expect(component.findWhere((node) => node.prop('to') === "/collections/permissions" ).length).toEqual(0);
-        });
-    })
-});
\ No newline at end of file
+    it("should not be shown to admins if there are collections", () => {
+      const component = shallow(
+        getQuestionIndex({
+          questions: null,
+          collections: someCollections,
+          isAdmin: false,
+        }),
+      );
+
+      expect(component.find(CollectionEmptyState).length).toEqual(0);
+    });
+
+    it("should not be shown to non-admins", () => {
+      const component = shallow(
+        getQuestionIndex({
+          questions: null,
+          collections: null,
+          isAdmin: false,
+        }),
+      );
+
+      expect(component.find(CollectionEmptyState).length).toEqual(0);
+    });
+  });
+
+  describe("no saved questions state", () => {
+    const eitherAdminOrNot = [true, false];
+
+    it("should be shown if no collections or questions", () => {
+      eitherAdminOrNot.forEach(isAdmin => {
+        const component = shallow(
+          getQuestionIndex({
+            questions: null,
+            collections: null,
+            isAdmin,
+          }),
+        );
+
+        expect(component.find(NoSavedQuestionsState).length).toEqual(1);
+      });
+    });
+
+    it("should not be shown otherwise", () => {
+      eitherAdminOrNot.forEach(isAdmin => {
+        const component = shallow(
+          getQuestionIndex({
+            questions: someQuestions,
+            collections: null,
+            isAdmin,
+          }),
+        );
+
+        expect(component.find(NoSavedQuestionsState).length).toEqual(0);
+
+        const component2 = shallow(
+          getQuestionIndex({
+            questions: null,
+            collections: someCollections,
+            isAdmin,
+          }),
+        );
+
+        expect(component2.find(NoSavedQuestionsState).length).toEqual(0);
+      });
+    });
+  });
+
+  describe("collection actions", () => {
+    it("should let admins change permissions if collections exist", () => {
+      const component = mount(
+        <QuestionIndexHeader collections={someCollections} isAdmin={true} />,
+      );
+
+      // Why `find` does not work for matching React props: https://github.com/airbnb/enzyme/issues/582
+      expect(
+        component.findWhere(
+          node => node.prop("to") === "/collections/permissions",
+        ).length,
+      ).toEqual(1);
+    });
+
+    it("should not let admins change permissions if no collections", () => {
+      const component = mount(
+        <QuestionIndexHeader collections={null} isAdmin={true} />,
+      );
+
+      expect(
+        component.findWhere(
+          node => node.prop("to") === "/collections/permissions",
+        ).length,
+      ).toEqual(0);
+    });
+
+    it("should not let non-admins change permissions", () => {
+      const component = mount(
+        <QuestionIndexHeader collections={someCollections} isAdmin={false} />,
+      );
+
+      expect(
+        component.findWhere(
+          node => node.prop("to") === "/collections/permissions",
+        ).length,
+      ).toEqual(0);
+    });
+  });
+});
diff --git a/frontend/test/redux/metadata.integ.spec.js b/frontend/test/redux/metadata.integ.spec.js
index d0c0ad6bf3ecbd5ccfac8e858b0467354ecbd72a..e27c0e472b525e71035c653f7f89def09db7826f 100644
--- a/frontend/test/redux/metadata.integ.spec.js
+++ b/frontend/test/redux/metadata.integ.spec.js
@@ -4,126 +4,125 @@
  * - see if the metadata gets properly hydrated in `getMetadata` of selectors/metadata
  * - see if metadata loading actions can be safely used in isolation from each others
  */
-import { getMetadata } from "metabase/selectors/metadata"
+import { getMetadata } from "metabase/selectors/metadata";
 import {
-    createTestStore,
-    useSharedAdminLogin,
+  createTestStore,
+  useSharedAdminLogin,
 } from "__support__/integrated_tests";
 import {
-    fetchMetrics,
-    fetchDatabases,
-    fetchTables,
-} from "metabase/redux/metadata"
+  fetchMetrics,
+  fetchDatabases,
+  fetchTables,
+} from "metabase/redux/metadata";
 
-const metadata = (store) => getMetadata(store.getState())
+const metadata = store => getMetadata(store.getState());
 
 describe("metadata/redux", () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-    });
-
-    describe("METRIC ACTIONS", () => {
-        // TODO Atte Keinänen 6/23/17: Remove metrics after their creation in other tests
-        describe("fetchMetrics()", () => {
-            pending();
-            it("fetches no metrics in empty db", async () => {
-                const store = createTestStore();
-                await store.dispatch(fetchMetrics());
-                expect(metadata(store).metricsList().length).toBe(0)
-            })
-        })
-        describe("updateMetric(metric)", () => {
-            // await store.dispatch(updateMetric(metric));
-        })
-        describe("updateMetricImportantFields(...)", () => {
-            // await store.dispatch(updateMetricImportantFields(metricId, importantFieldIds));
-        })
-        describe("fetchMetricTable(metricId)", () => {
-            // await store.dispatch(fetchMetricTable(metricId));
-        })
-        describe("fetchMetricRevisions(metricId)", () => {
-            // await store.dispatch(fetchMetricRevisions(metricId));
-        })
-    })
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-    describe("SEGMENT ACTIONS", () => {
-        describe("fetchSegments()", () => {
-            // await store.dispatch(fetchSegments());
-        })
-        describe("updateSegment(segment)", () => {
-            // await store.dispatch(updateSegment(segment));
-        })
-    })
+  describe("METRIC ACTIONS", () => {
+    // TODO Atte Keinänen 6/23/17: Remove metrics after their creation in other tests
+    describe("fetchMetrics()", () => {
+      pending();
+      it("fetches no metrics in empty db", async () => {
+        const store = createTestStore();
+        await store.dispatch(fetchMetrics());
+        expect(metadata(store).metricsList().length).toBe(0);
+      });
+    });
+    describe("updateMetric(metric)", () => {
+      // await store.dispatch(updateMetric(metric));
+    });
+    describe("updateMetricImportantFields(...)", () => {
+      // await store.dispatch(updateMetricImportantFields(metricId, importantFieldIds));
+    });
+    describe("fetchMetricTable(metricId)", () => {
+      // await store.dispatch(fetchMetricTable(metricId));
+    });
+    describe("fetchMetricRevisions(metricId)", () => {
+      // await store.dispatch(fetchMetricRevisions(metricId));
+    });
+  });
 
-    describe("DATABASE ACTIONS", () => {
-        describe("fetchDatabases()", () => {
-            // TODO Atte Keinänen 6/23/17: Figure out why on CI two databases show up but locally only one
-            pending();
-            it("fetches the sample dataset", async () => {
+  describe("SEGMENT ACTIONS", () => {
+    describe("fetchSegments()", () => {
+      // await store.dispatch(fetchSegments());
+    });
+    describe("updateSegment(segment)", () => {
+      // await store.dispatch(updateSegment(segment));
+    });
+  });
 
-                const store = createTestStore();
-                expect(metadata(store).tablesList().length).toBe(0);
-                expect(metadata(store).databasesList().length).toBe(0);
+  describe("DATABASE ACTIONS", () => {
+    describe("fetchDatabases()", () => {
+      // TODO Atte Keinänen 6/23/17: Figure out why on CI two databases show up but locally only one
+      pending();
+      it("fetches the sample dataset", async () => {
+        const store = createTestStore();
+        expect(metadata(store).tablesList().length).toBe(0);
+        expect(metadata(store).databasesList().length).toBe(0);
 
-                await store.dispatch(fetchDatabases());
-                expect(metadata(store).databasesList().length).toBe(1);
-                expect(metadata(store).tablesList().length).toBe(4);
-                expect(metadata(store).databasesList()[0].tables.length).toBe(4);
-            })
-        })
-        describe("fetchDatabaseMetadata(dbId)", () => {
-            // await store.dispatch(fetchDatabaseMetadata(1));
-        })
-        describe("updateDatabase(database)", () => {
-            // await store.dispatch(updateDatabase(database));
-        })
-    })
+        await store.dispatch(fetchDatabases());
+        expect(metadata(store).databasesList().length).toBe(1);
+        expect(metadata(store).tablesList().length).toBe(4);
+        expect(metadata(store).databasesList()[0].tables.length).toBe(4);
+      });
+    });
+    describe("fetchDatabaseMetadata(dbId)", () => {
+      // await store.dispatch(fetchDatabaseMetadata(1));
+    });
+    describe("updateDatabase(database)", () => {
+      // await store.dispatch(updateDatabase(database));
+    });
+  });
 
-    describe("TABLE ACTIONS", () => {
-        describe("fetchTables()", () => {
-            // TODO Atte Keinänen 6/23/17: Figure out why on CI two databases show up but locally only one
-            pending();
-            it("fetches the sample dataset tables", async () => {
-                // what is the difference between fetchDatabases and fetchTables?
-                const store = createTestStore();
-                expect(metadata(store).tablesList().length).toBe(0);
-                expect(metadata(store).databasesList().length).toBe(0);
+  describe("TABLE ACTIONS", () => {
+    describe("fetchTables()", () => {
+      // TODO Atte Keinänen 6/23/17: Figure out why on CI two databases show up but locally only one
+      pending();
+      it("fetches the sample dataset tables", async () => {
+        // what is the difference between fetchDatabases and fetchTables?
+        const store = createTestStore();
+        expect(metadata(store).tablesList().length).toBe(0);
+        expect(metadata(store).databasesList().length).toBe(0);
 
-                await store.dispatch(fetchTables());
-                expect(metadata(store).tablesList().length).toBe(4);
-                expect(metadata(store).databasesList().length).toBe(1);
-            })
-            // await store.dispatch(fetchTables());
-        })
-        describe("updateTable(table)", () => {
-            // await store.dispatch(updateTable(table));
-        })
-        describe("fetchTableMetadata(tableId)", () => {
-            // await store.dispatch(fetchTableMetadata(tableId));
-        })
-        describe("fetchFieldValues(fieldId)", () => {
-            // await store.dispatch(fetchFieldValues());
-        })
-    })
+        await store.dispatch(fetchTables());
+        expect(metadata(store).tablesList().length).toBe(4);
+        expect(metadata(store).databasesList().length).toBe(1);
+      });
+      // await store.dispatch(fetchTables());
+    });
+    describe("updateTable(table)", () => {
+      // await store.dispatch(updateTable(table));
+    });
+    describe("fetchTableMetadata(tableId)", () => {
+      // await store.dispatch(fetchTableMetadata(tableId));
+    });
+    describe("fetchFieldValues(fieldId)", () => {
+      // await store.dispatch(fetchFieldValues());
+    });
+  });
 
-    describe("MISC ACTIONS", () => {
-        describe("addParamValues(paramValues)", () => {
-            // await store.dispatch(addParamValues(paramValues));
-        })
-        describe("updateField(field)", () => {
-            // await store.dispatch(updateField(field));
-        })
-        describe("fetchRevisions(type, id)", () => {
-            // await store.dispatch(fetchRevisions(type, id));
-        })
-        describe("fetchSegmentFields(segmentId)", () => {
-            // await store.dispatch(fetchSegmentFields(segmentId));
-        })
-        describe("fetchSegmentRevisions(segments)", () => {
-            // await store.dispatch(fetchSegmentRevisions(segmentId));
-        })
-        describe("fetchDatabasesWithMetadata()", () => {
-            // await store.dispatch(fetchDatabasesWithMetadata());
-        })
-    })
-})
\ No newline at end of file
+  describe("MISC ACTIONS", () => {
+    describe("addParamValues(paramValues)", () => {
+      // await store.dispatch(addParamValues(paramValues));
+    });
+    describe("updateField(field)", () => {
+      // await store.dispatch(updateField(field));
+    });
+    describe("fetchRevisions(type, id)", () => {
+      // await store.dispatch(fetchRevisions(type, id));
+    });
+    describe("fetchSegmentFields(segmentId)", () => {
+      // await store.dispatch(fetchSegmentFields(segmentId));
+    });
+    describe("fetchSegmentRevisions(segments)", () => {
+      // await store.dispatch(fetchSegmentRevisions(segmentId));
+    });
+    describe("fetchDatabasesWithMetadata()", () => {
+      // await store.dispatch(fetchDatabasesWithMetadata());
+    });
+  });
+});
diff --git a/frontend/test/reference/databases.integ.spec.js b/frontend/test/reference/databases.integ.spec.js
index 5bb45759126e92de55c0d96a86213330232902c4..66ce4addf3e1a409ea204995d029b5fd3384d11f 100644
--- a/frontend/test/reference/databases.integ.spec.js
+++ b/frontend/test/reference/databases.integ.spec.js
@@ -1,20 +1,20 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
-import React from 'react';
-import { mount } from 'enzyme';
+import React from "react";
+import { mount } from "enzyme";
 
-import { CardApi } from 'metabase/services'
+import { CardApi } from "metabase/services";
 
-import { 
-    FETCH_DATABASE_METADATA,
-    FETCH_REAL_DATABASES
+import {
+  FETCH_DATABASE_METADATA,
+  FETCH_REAL_DATABASES,
 } from "metabase/redux/metadata";
 
-import { END_LOADING } from "metabase/reference/reference"
+import { END_LOADING } from "metabase/reference/reference";
 
 import DatabaseListContainer from "metabase/reference/databases/DatabaseListContainer";
 import DatabaseDetailContainer from "metabase/reference/databases/DatabaseDetailContainer";
@@ -35,157 +35,166 @@ import { INITIALIZE_QB, QUERY_COMPLETED } from "metabase/query_builder/actions";
 import { getQuestion } from "metabase/query_builder/selectors";
 
 describe("The Reference Section", () => {
-    // Test data
-    const cardDef = { name :"A card", display: "scalar", 
-                      dataset_query: {database: 1, table_id: 1, type: "query", query: {source_table: 1, "aggregation": ["count"]}},
-                      visualization_settings: {}}
-
-    // Scaffolding
-    beforeAll(async () => {
-        useSharedAdminLogin();
-
-    })
-
-    describe("The Data Reference for the Sample Database", async () => {
-        
-        // database list
-        it("should see databases", async () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/");
-            var container = mount(store.connectContainer(<DatabaseListContainer />));
-            await store.waitForActions([FETCH_REAL_DATABASES, END_LOADING])
-            
-            expect(container.find(ReferenceHeader).length).toBe(1)
-            expect(container.find(DatabaseList).length).toBe(1)            
-            expect(container.find(AdminAwareEmptyState).length).toBe(0)
-            
-            expect(container.find(List).length).toBe(1)
-            expect(container.find(ListItem).length).toBeGreaterThanOrEqual(1)
-        })
-        
-        // database list
-        it("should not see saved questions in the database list", async () => {
-            var card = await CardApi.create(cardDef)
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/");
-            var container = mount(store.connectContainer(<DatabaseListContainer />));
-            await store.waitForActions([FETCH_REAL_DATABASES, END_LOADING])
-            
-            expect(container.find(ReferenceHeader).length).toBe(1)
-            expect(container.find(DatabaseList).length).toBe(1)            
-            expect(container.find(AdminAwareEmptyState).length).toBe(0)
-            
-            expect(container.find(List).length).toBe(1)
-            expect(container.find(ListItem).length).toBe(1)
-
-
-            expect(card.name).toBe(cardDef.name);
-            
-            await CardApi.delete({cardId: card.id})
-
-        })
-        
-        // database detail
-        it("should see a the detail view for the sample database", async ()=>{
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1");
-            mount(store.connectContainer(<DatabaseDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-
-        })
-        
-        // table list
-       it("should see the 4 tables in the sample database",async  () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables");
-            mount(store.connectContainer(<TableListContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
-        // table detail
-
-       it("should see the Orders table", async  () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/1");
-            mount(store.connectContainer(<TableDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
-
-       it("should see the Reviews table", async  () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/2");
-            mount(store.connectContainer(<TableDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
-       it("should see the Products table", async  () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/3");
-            mount(store.connectContainer(<TableDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
-       it("should see the People table", async  () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/4");
-            mount(store.connectContainer(<TableDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
-        // field list
-       it("should see the fields for the orders table", async  () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/1/fields");
-            mount(store.connectContainer(<FieldListContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-
-        })
-       it("should see the questions for the orders tables", async  () => {
-
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/1/questions");
-            mount(store.connectContainer(<TableQuestionsContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-            
-            var card = await CardApi.create(cardDef)
-
-            expect(card.name).toBe(cardDef.name);
-            
-            await CardApi.delete({cardId: card.id})
-        })
-
-        // field detail
-
-       it("should see the orders created_at timestamp field", async () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/1/fields/1");
-            mount(store.connectContainer(<FieldDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
-
-        it("should let you open a potentially useful question for created_at field without errors", async () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/1/fields/1");
-
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-            const fieldDetails = app.find(FieldDetailContainer);
-            expect(fieldDetails.length).toBe(1);
-
-            const usefulQuestionLink = fieldDetails.find(UsefulQuestions).find(QueryButton).first().find("a");
-            expect(usefulQuestionLink.text()).toBe("Number of Orders grouped by Created At")
-            click(usefulQuestionLink);
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-
-            const qbQuery = getQuestion(store.getState()).query();
-
-            // the granularity/subdimension should be applied correctly to the breakout
-            expect(qbQuery.breakouts()).toEqual([["datetime-field", ["field-id", 1], "day"]]);
-        })
-
-       it("should see the orders id field", async () => {
-            const store = await createTestStore()
-            store.pushPath("/reference/databases/1/tables/1/fields/25");
-            mount(store.connectContainer(<FieldDetailContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING])
-        })
+  // Test data
+  const cardDef = {
+    name: "A card",
+    display: "scalar",
+    dataset_query: {
+      database: 1,
+      table_id: 1,
+      type: "query",
+      query: { source_table: 1, aggregation: ["count"] },
+    },
+    visualization_settings: {},
+  };
+
+  // Scaffolding
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("The Data Reference for the Sample Database", async () => {
+    // database list
+    it("should see databases", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/");
+      var container = mount(store.connectContainer(<DatabaseListContainer />));
+      await store.waitForActions([FETCH_REAL_DATABASES, END_LOADING]);
+
+      expect(container.find(ReferenceHeader).length).toBe(1);
+      expect(container.find(DatabaseList).length).toBe(1);
+      expect(container.find(AdminAwareEmptyState).length).toBe(0);
+
+      expect(container.find(List).length).toBe(1);
+      expect(container.find(ListItem).length).toBeGreaterThanOrEqual(1);
+    });
+
+    // database list
+    it("should not see saved questions in the database list", async () => {
+      var card = await CardApi.create(cardDef);
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/");
+      var container = mount(store.connectContainer(<DatabaseListContainer />));
+      await store.waitForActions([FETCH_REAL_DATABASES, END_LOADING]);
+
+      expect(container.find(ReferenceHeader).length).toBe(1);
+      expect(container.find(DatabaseList).length).toBe(1);
+      expect(container.find(AdminAwareEmptyState).length).toBe(0);
+
+      expect(container.find(List).length).toBe(1);
+      expect(container.find(ListItem).length).toBe(1);
+
+      expect(card.name).toBe(cardDef.name);
+
+      await CardApi.delete({ cardId: card.id });
+    });
+
+    // database detail
+    it("should see a the detail view for the sample database", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1");
+      mount(store.connectContainer(<DatabaseDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+
+    // table list
+    it("should see the 4 tables in the sample database", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables");
+      mount(store.connectContainer(<TableListContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+    // table detail
+
+    it("should see the Orders table", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/1");
+      mount(store.connectContainer(<TableDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+
+    it("should see the Reviews table", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/2");
+      mount(store.connectContainer(<TableDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+    it("should see the Products table", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/3");
+      mount(store.connectContainer(<TableDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+    it("should see the People table", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/4");
+      mount(store.connectContainer(<TableDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+    // field list
+    it("should see the fields for the orders table", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/1/fields");
+      mount(store.connectContainer(<FieldListContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+    it("should see the questions for the orders tables", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/1/questions");
+      mount(store.connectContainer(<TableQuestionsContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+
+      var card = await CardApi.create(cardDef);
+
+      expect(card.name).toBe(cardDef.name);
+
+      await CardApi.delete({ cardId: card.id });
+    });
+
+    // field detail
+
+    it("should see the orders created_at timestamp field", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/1/fields/1");
+      mount(store.connectContainer(<FieldDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+    });
+
+    it("should let you open a potentially useful question for created_at field without errors", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/1/fields/1");
+
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
+      const fieldDetails = app.find(FieldDetailContainer);
+      expect(fieldDetails.length).toBe(1);
+
+      const usefulQuestionLink = fieldDetails
+        .find(UsefulQuestions)
+        .find(QueryButton)
+        .first()
+        .find("a");
+      expect(usefulQuestionLink.text()).toBe(
+        "Number of Orders grouped by Created At",
+      );
+      click(usefulQuestionLink);
+
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+
+      const qbQuery = getQuestion(store.getState()).query();
+
+      // the granularity/subdimension should be applied correctly to the breakout
+      expect(qbQuery.breakouts()).toEqual([
+        ["datetime-field", ["field-id", 1], "day"],
+      ]);
+    });
+
+    it("should see the orders id field", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/databases/1/tables/1/fields/25");
+      mount(store.connectContainer(<FieldDetailContainer />));
+      await store.waitForActions([FETCH_DATABASE_METADATA, END_LOADING]);
     });
-});
\ No newline at end of file
+  });
+});
diff --git a/frontend/test/reference/guide.integ.spec.js b/frontend/test/reference/guide.integ.spec.js
index ef9a8d74cc22013d94d1f9668609ee975633025b..8d3af63d80f18fb31b70fdeca7a487f815a22038 100644
--- a/frontend/test/reference/guide.integ.spec.js
+++ b/frontend/test/reference/guide.integ.spec.js
@@ -1,86 +1,115 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 
-import React from 'react';
-import { mount } from 'enzyme';
+import React from "react";
+import { mount } from "enzyme";
 
-import { SegmentApi, MetricApi } from 'metabase/services'
+import { SegmentApi, MetricApi } from "metabase/services";
 
-import { 
-    FETCH_DATABASE_METADATA,
-    FETCH_METRICS,
-    FETCH_SEGMENTS
+import {
+  FETCH_DATABASE_METADATA,
+  FETCH_METRICS,
+  FETCH_SEGMENTS,
 } from "metabase/redux/metadata";
 
 import GettingStartedGuideContainer from "metabase/reference/guide/GettingStartedGuideContainer";
 
-
-
 describe("The Reference Section", () => {
-    // Test data
-    const segmentDef = {name: "A Segment", description: "I did it!", table_id: 1, show_in_getting_started: true,
-                        definition: { source_table: 1, filter: ["time-interval", ["field-id", 1], -30, "day"] }}
-
-    const anotherSegmentDef = {name: "Another Segment", description: "I did it again!", table_id: 1, show_in_getting_started: true,
-                               definition: { source_table: 1, filter: ["time-interval", ["field-id", 1], -30, "day"] } }
-    const metricDef = {name: "A Metric", description: "I did it!", table_id: 1,show_in_getting_started: true,
-                        definition: {database: 1, query: {aggregation: ["count"]}}}
-
-    const anotherMetricDef = {name: "Another Metric", description: "I did it again!", table_id: 1,show_in_getting_started: true,
-                        definition: {database: 1, query: {aggregation: ["count"]}}}
-    
-    // Scaffolding
-    beforeAll(async () => {
-        useSharedAdminLogin();
-
-    })
-
-
-    describe("The Getting Started Guide", async ()=>{
-        
-        
-        it("Should show an empty guide for non-admin users", async () => {
-            const store = await createTestStore()    
-            store.pushPath("/reference/");
-            mount(store.connectContainer(<GettingStartedGuideContainer />));
-            await store.waitForActions([FETCH_DATABASE_METADATA, FETCH_SEGMENTS, FETCH_METRICS])
-        })
-        
-        xit("Should show an empty guide with a creation CTA for admin users", async () => {
-        })
-
-        xit("A non-admin attempting to edit the guide should get an error", async () => {
-        })
-
-        it("Adding metrics should to the guide should make them appear", async () => {
-            
-            expect(0).toBe(0)
-            var metric = await MetricApi.create(metricDef);
-            expect(1).toBe(1)
-            var metric2 = await MetricApi.create(anotherMetricDef);
-            expect(2).toBe(2)
-            await MetricApi.delete({metricId: metric.id, revision_message: "Please"})
-            expect(1).toBe(1)
-            await MetricApi.delete({metricId: metric2.id, revision_message: "Please"})
-            expect(0).toBe(0)
-        })
-
-        it("Adding segments should to the guide should make them appear", async () => {
-            expect(0).toBe(0)
-            var segment = await SegmentApi.create(segmentDef);
-            expect(1).toBe(1)
-            var anotherSegment = await SegmentApi.create(anotherSegmentDef);
-            expect(2).toBe(2)
-            await SegmentApi.delete({segmentId: segment.id, revision_message: "Please"})
-            expect(1).toBe(1)
-            await SegmentApi.delete({segmentId: anotherSegment.id, revision_message: "Please"})
-            expect(0).toBe(0)
-        })
-        
-    })
-
-
-
-});
\ No newline at end of file
+  // Test data
+  const segmentDef = {
+    name: "A Segment",
+    description: "I did it!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: {
+      source_table: 1,
+      filter: ["time-interval", ["field-id", 1], -30, "day"],
+    },
+  };
+
+  const anotherSegmentDef = {
+    name: "Another Segment",
+    description: "I did it again!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: {
+      source_table: 1,
+      filter: ["time-interval", ["field-id", 1], -30, "day"],
+    },
+  };
+  const metricDef = {
+    name: "A Metric",
+    description: "I did it!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: { database: 1, query: { aggregation: ["count"] } },
+  };
+
+  const anotherMetricDef = {
+    name: "Another Metric",
+    description: "I did it again!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: { database: 1, query: { aggregation: ["count"] } },
+  };
+
+  // Scaffolding
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("The Getting Started Guide", async () => {
+    it("Should show an empty guide for non-admin users", async () => {
+      const store = await createTestStore();
+      store.pushPath("/reference/");
+      mount(store.connectContainer(<GettingStartedGuideContainer />));
+      await store.waitForActions([
+        FETCH_DATABASE_METADATA,
+        FETCH_SEGMENTS,
+        FETCH_METRICS,
+      ]);
+    });
+
+    xit("Should show an empty guide with a creation CTA for admin users", async () => {});
+
+    xit("A non-admin attempting to edit the guide should get an error", async () => {});
+
+    it("Adding metrics should to the guide should make them appear", async () => {
+      expect(0).toBe(0);
+      var metric = await MetricApi.create(metricDef);
+      expect(1).toBe(1);
+      var metric2 = await MetricApi.create(anotherMetricDef);
+      expect(2).toBe(2);
+      await MetricApi.delete({
+        metricId: metric.id,
+        revision_message: "Please",
+      });
+      expect(1).toBe(1);
+      await MetricApi.delete({
+        metricId: metric2.id,
+        revision_message: "Please",
+      });
+      expect(0).toBe(0);
+    });
+
+    it("Adding segments should to the guide should make them appear", async () => {
+      expect(0).toBe(0);
+      var segment = await SegmentApi.create(segmentDef);
+      expect(1).toBe(1);
+      var anotherSegment = await SegmentApi.create(anotherSegmentDef);
+      expect(2).toBe(2);
+      await SegmentApi.delete({
+        segmentId: segment.id,
+        revision_message: "Please",
+      });
+      expect(1).toBe(1);
+      await SegmentApi.delete({
+        segmentId: anotherSegment.id,
+        revision_message: "Please",
+      });
+      expect(0).toBe(0);
+    });
+  });
+});
diff --git a/frontend/test/reference/metrics.integ.spec.js b/frontend/test/reference/metrics.integ.spec.js
index 62c7ae9be5c0fdcf2561baa025de48ad70d99b06..e28524e48a1ef1b21b115713d36c894b849dd02a 100644
--- a/frontend/test/reference/metrics.integ.spec.js
+++ b/frontend/test/reference/metrics.integ.spec.js
@@ -1,130 +1,136 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 
-import React from 'react';
-import { mount } from 'enzyme';
+import React from "react";
+import { mount } from "enzyme";
 
-import { CardApi, MetricApi } from 'metabase/services'
+import { CardApi, MetricApi } from "metabase/services";
 
-import { 
-    FETCH_METRICS,
-    FETCH_METRIC_TABLE,
-    FETCH_METRIC_REVISIONS
+import {
+  FETCH_METRICS,
+  FETCH_METRIC_TABLE,
+  FETCH_METRIC_REVISIONS,
 } from "metabase/redux/metadata";
 
-import { FETCH_GUIDE } from "metabase/reference/reference"
+import { FETCH_GUIDE } from "metabase/reference/reference";
 
 import MetricListContainer from "metabase/reference/metrics/MetricListContainer";
 import MetricDetailContainer from "metabase/reference/metrics/MetricDetailContainer";
 import MetricQuestionsContainer from "metabase/reference/metrics/MetricQuestionsContainer";
 import MetricRevisionsContainer from "metabase/reference/metrics/MetricRevisionsContainer";
 
-
 describe("The Reference Section", () => {
-    // Test data
-    const metricDef = {name: "A Metric", description: "I did it!", table_id: 1,show_in_getting_started: true,
-                        definition: {database: 1, query: {aggregation: ["count"]}}}
-
-    const anotherMetricDef = {name: "Another Metric", description: "I did it again!", table_id: 1,show_in_getting_started: true,
-                        definition: {database: 1, query: {aggregation: ["count"]}}}
-
-    const metricCardDef = { name :"A card", display: "scalar", 
-                      dataset_query: {database: 1, table_id: 1, type: "query", query: {source_table: 1, "aggregation": ["metric", 1]}},
-                      visualization_settings: {}}
-
-    // Scaffolding
-    beforeAll(async () => {
-        useSharedAdminLogin();
-
-    })
-
-
-    
-    describe("The Metrics section of the Data Reference", async ()=>{
-        describe("Empty State", async () => {
-
-            it("Should show no metrics in the list", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/metrics");
-                mount(store.connectContainer(<MetricListContainer />));
-                await store.waitForActions([FETCH_METRICS])
-            })
-
-        });
-
-        describe("With Metrics State", async () => {
-            var metricIds = []
-
-            beforeAll(async () => {            
-                // Create some metrics to have something to look at
-                var metric = await MetricApi.create(metricDef);
-                var metric2 = await MetricApi.create(anotherMetricDef);
-                
-                metricIds.push(metric.id)
-                metricIds.push(metric2.id)
-                })
-
-            afterAll(async () => {
-                // Delete the guide we created
-                // remove the metrics we created   
-                // This is a bit messy as technically these are just archived
-                for (const id of metricIds){
-                    await MetricApi.delete({metricId: id, revision_message: "Please"})
-                }
-            })
-            // metrics list
-            it("Should show no metrics in the list", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/metrics");
-                mount(store.connectContainer(<MetricListContainer />));
-                await store.waitForActions([FETCH_METRICS])
-            })
-            // metric detail
-            it("Should show the metric detail view for a specific id", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/metrics/"+metricIds[0]);
-                mount(store.connectContainer(<MetricDetailContainer />));
-                await store.waitForActions([FETCH_METRIC_TABLE, FETCH_GUIDE])
-            })
-            // metrics questions 
-            it("Should show no questions based on a new metric", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/metrics/"+metricIds[0]+'/questions');
-                mount(store.connectContainer(<MetricQuestionsContainer />));
-                await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE])
-            })
-            // metrics revisions
-            it("Should show revisions", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/metrics/"+metricIds[0]+'/revisions');
-                mount(store.connectContainer(<MetricRevisionsContainer />));
-                await store.waitForActions([FETCH_METRICS, FETCH_METRIC_REVISIONS])
-            })
-
-            it("Should see a newly asked question in its questions list", async () => {
-                    var card = await CardApi.create(metricCardDef)
-                    expect(card.name).toBe(metricCardDef.name);
-
-                    try {
-                        // see that there is a new question on the metric's questions page
-                        const store = await createTestStore()
-
-                        store.pushPath("/reference/metrics/"+metricIds[0]+'/questions');
-                        mount(store.connectContainer(<MetricQuestionsContainer />));
-                        await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE])
-                    } finally {
-                        // even if the code above results in an exception, try to delete the question
-                        await CardApi.delete({cardId: card.id})
-                    }
-            })
-
-                       
-        });
+  // Test data
+  const metricDef = {
+    name: "A Metric",
+    description: "I did it!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: { database: 1, query: { aggregation: ["count"] } },
+  };
+
+  const anotherMetricDef = {
+    name: "Another Metric",
+    description: "I did it again!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: { database: 1, query: { aggregation: ["count"] } },
+  };
+
+  const metricCardDef = {
+    name: "A card",
+    display: "scalar",
+    dataset_query: {
+      database: 1,
+      table_id: 1,
+      type: "query",
+      query: { source_table: 1, aggregation: ["metric", 1] },
+    },
+    visualization_settings: {},
+  };
+
+  // Scaffolding
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("The Metrics section of the Data Reference", async () => {
+    describe("Empty State", async () => {
+      it("Should show no metrics in the list", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/metrics");
+        mount(store.connectContainer(<MetricListContainer />));
+        await store.waitForActions([FETCH_METRICS]);
+      });
     });
-    
-
 
-
-});
\ No newline at end of file
+    describe("With Metrics State", async () => {
+      var metricIds = [];
+
+      beforeAll(async () => {
+        // Create some metrics to have something to look at
+        var metric = await MetricApi.create(metricDef);
+        var metric2 = await MetricApi.create(anotherMetricDef);
+
+        metricIds.push(metric.id);
+        metricIds.push(metric2.id);
+      });
+
+      afterAll(async () => {
+        // Delete the guide we created
+        // remove the metrics we created
+        // This is a bit messy as technically these are just archived
+        for (const id of metricIds) {
+          await MetricApi.delete({ metricId: id, revision_message: "Please" });
+        }
+      });
+      // metrics list
+      it("Should show no metrics in the list", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/metrics");
+        mount(store.connectContainer(<MetricListContainer />));
+        await store.waitForActions([FETCH_METRICS]);
+      });
+      // metric detail
+      it("Should show the metric detail view for a specific id", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/metrics/" + metricIds[0]);
+        mount(store.connectContainer(<MetricDetailContainer />));
+        await store.waitForActions([FETCH_METRIC_TABLE, FETCH_GUIDE]);
+      });
+      // metrics questions
+      it("Should show no questions based on a new metric", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/metrics/" + metricIds[0] + "/questions");
+        mount(store.connectContainer(<MetricQuestionsContainer />));
+        await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE]);
+      });
+      // metrics revisions
+      it("Should show revisions", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/metrics/" + metricIds[0] + "/revisions");
+        mount(store.connectContainer(<MetricRevisionsContainer />));
+        await store.waitForActions([FETCH_METRICS, FETCH_METRIC_REVISIONS]);
+      });
+
+      it("Should see a newly asked question in its questions list", async () => {
+        var card = await CardApi.create(metricCardDef);
+        expect(card.name).toBe(metricCardDef.name);
+
+        try {
+          // see that there is a new question on the metric's questions page
+          const store = await createTestStore();
+
+          store.pushPath("/reference/metrics/" + metricIds[0] + "/questions");
+          mount(store.connectContainer(<MetricQuestionsContainer />));
+          await store.waitForActions([FETCH_METRICS, FETCH_METRIC_TABLE]);
+        } finally {
+          // even if the code above results in an exception, try to delete the question
+          await CardApi.delete({ cardId: card.id });
+        }
+      });
+    });
+  });
+});
diff --git a/frontend/test/reference/segments.integ.spec.js b/frontend/test/reference/segments.integ.spec.js
index 3df654b20666c2b6e427c8c8c83f443f690b2f64..2d2232ad4f22ea7a1fe9dbe1c9d687ed533d1de1 100644
--- a/frontend/test/reference/segments.integ.spec.js
+++ b/frontend/test/reference/segments.integ.spec.js
@@ -1,22 +1,21 @@
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 
-import React from 'react';
-import { mount } from 'enzyme';
+import React from "react";
+import { mount } from "enzyme";
 
-import { CardApi, SegmentApi } from 'metabase/services'
+import { CardApi, SegmentApi } from "metabase/services";
 
-import { 
-    FETCH_SEGMENTS,
-    FETCH_SEGMENT_TABLE,
-    FETCH_SEGMENT_FIELDS,
-    FETCH_SEGMENT_REVISIONS
+import {
+  FETCH_SEGMENTS,
+  FETCH_SEGMENT_TABLE,
+  FETCH_SEGMENT_FIELDS,
+  FETCH_SEGMENT_REVISIONS,
 } from "metabase/redux/metadata";
 
-import { LOAD_ENTITIES } from "metabase/questions/questions"
-
+import { LOAD_ENTITIES } from "metabase/questions/questions";
 
 import SegmentListContainer from "metabase/reference/segments/SegmentListContainer";
 import SegmentDetailContainer from "metabase/reference/segments/SegmentDetailContainer";
@@ -26,123 +25,143 @@ import SegmentFieldListContainer from "metabase/reference/segments/SegmentFieldL
 import SegmentFieldDetailContainer from "metabase/reference/segments/SegmentFieldDetailContainer";
 
 describe("The Reference Section", () => {
-    // Test data
-    const segmentDef = {name: "A Segment", description: "I did it!", table_id: 1, show_in_getting_started: true,
-                        definition: {source_table: 1, filter: ["time-interval", ["field-id", 1], -30, "day"]}}
-
-    const anotherSegmentDef = {name: "Another Segment", description: "I did it again!", table_id: 1, show_in_getting_started: true,
-                               definition:{source_table: 1, filter: ["time-interval", ["field-id", 1], -15, "day"]}}
-    
-    const segmentCardDef = { name :"A card", display: "scalar",
-                      dataset_query: {database: 1, table_id: 1, type: "query", query: {source_table: 1, "aggregation": ["count"], "filter": ["segment", 1]}},
-                      visualization_settings: {}}
-
-    // Scaffolding
-    beforeAll(async () => {
-        useSharedAdminLogin();
-
-    })
-
-
-
-    
-    describe("The Segments section of the Data Reference", async ()=>{
-
-        describe("Empty State", async () => {
-                it("Should show no segments in the list", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments");
-                mount(store.connectContainer(<SegmentListContainer />));
-                await store.waitForActions([FETCH_SEGMENTS])
-            })
-
-        });
-
-        describe("With Segments State", async () => {
-            var segmentIds = []
-
-            beforeAll(async () => {            
-                // Create some segments to have something to look at
-                var segment = await SegmentApi.create(segmentDef);
-                var anotherSegment = await SegmentApi.create(anotherSegmentDef);
-                segmentIds.push(segment.id)
-                segmentIds.push(anotherSegment.id)
-
-                })
-
-            afterAll(async () => {
-                // Delete the guide we created
-                // remove the metrics  we created   
-                // This is a bit messy as technically these are just archived
-                for (const id of segmentIds){
-                    await SegmentApi.delete({segmentId: id, revision_message: "Please"})
-                }
-            })
-
-
-            // segments list
-            it("Should show the segments in the list", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments");
-                mount(store.connectContainer(<SegmentListContainer />));
-                await store.waitForActions([FETCH_SEGMENTS])
-            })
-            // segment detail
-            it("Should show the segment detail view for a specific id", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments/"+segmentIds[0]);
-                mount(store.connectContainer(<SegmentDetailContainer />));
-                await store.waitForActions([FETCH_SEGMENT_TABLE])
-            })
-
-            // segments field list
-            it("Should show the segment fields list", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments/"+segmentIds[0]+"/fields");
-                mount(store.connectContainer(<SegmentFieldListContainer />));
-                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS])
-            })
-            // segment detail
-            it("Should show the segment field detail view for a specific id", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments/"+segmentIds[0]+"/fields/" + 1);
-                mount(store.connectContainer(<SegmentFieldDetailContainer />));
-                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS])
-            })
-
-            // segment questions 
-            it("Should show no questions based on a new segment", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments/"+segmentIds[0]+'/questions');
-                mount(store.connectContainer(<SegmentQuestionsContainer />));
-                await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES])
-            })
-            // segment revisions
-            it("Should show revisions", async () => {
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments/"+segmentIds[0]+'/revisions');
-                mount(store.connectContainer(<SegmentRevisionsContainer />));
-                await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_REVISIONS])
-            })
-
-
-
-            it("Should see a newly asked question in its questions list", async () => {
-                var card = await CardApi.create(segmentCardDef)
-
-                expect(card.name).toBe(segmentCardDef.name);
-                
-                await CardApi.delete({cardId: card.id})
-
-                const store = await createTestStore()    
-                store.pushPath("/reference/segments/"+segmentIds[0]+'/questions');
-                mount(store.connectContainer(<SegmentQuestionsContainer />));
-                await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES])
-            })
-                      
-        });
+  // Test data
+  const segmentDef = {
+    name: "A Segment",
+    description: "I did it!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: {
+      source_table: 1,
+      filter: ["time-interval", ["field-id", 1], -30, "day"],
+    },
+  };
+
+  const anotherSegmentDef = {
+    name: "Another Segment",
+    description: "I did it again!",
+    table_id: 1,
+    show_in_getting_started: true,
+    definition: {
+      source_table: 1,
+      filter: ["time-interval", ["field-id", 1], -15, "day"],
+    },
+  };
+
+  const segmentCardDef = {
+    name: "A card",
+    display: "scalar",
+    dataset_query: {
+      database: 1,
+      table_id: 1,
+      type: "query",
+      query: {
+        source_table: 1,
+        aggregation: ["count"],
+        filter: ["segment", 1],
+      },
+    },
+    visualization_settings: {},
+  };
+
+  // Scaffolding
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  describe("The Segments section of the Data Reference", async () => {
+    describe("Empty State", async () => {
+      it("Should show no segments in the list", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments");
+        mount(store.connectContainer(<SegmentListContainer />));
+        await store.waitForActions([FETCH_SEGMENTS]);
+      });
     });
 
-
-
-});
\ No newline at end of file
+    describe("With Segments State", async () => {
+      var segmentIds = [];
+
+      beforeAll(async () => {
+        // Create some segments to have something to look at
+        var segment = await SegmentApi.create(segmentDef);
+        var anotherSegment = await SegmentApi.create(anotherSegmentDef);
+        segmentIds.push(segment.id);
+        segmentIds.push(anotherSegment.id);
+      });
+
+      afterAll(async () => {
+        // Delete the guide we created
+        // remove the metrics  we created
+        // This is a bit messy as technically these are just archived
+        for (const id of segmentIds) {
+          await SegmentApi.delete({
+            segmentId: id,
+            revision_message: "Please",
+          });
+        }
+      });
+
+      // segments list
+      it("Should show the segments in the list", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments");
+        mount(store.connectContainer(<SegmentListContainer />));
+        await store.waitForActions([FETCH_SEGMENTS]);
+      });
+      // segment detail
+      it("Should show the segment detail view for a specific id", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments/" + segmentIds[0]);
+        mount(store.connectContainer(<SegmentDetailContainer />));
+        await store.waitForActions([FETCH_SEGMENT_TABLE]);
+      });
+
+      // segments field list
+      it("Should show the segment fields list", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments/" + segmentIds[0] + "/fields");
+        mount(store.connectContainer(<SegmentFieldListContainer />));
+        await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS]);
+      });
+      // segment detail
+      it("Should show the segment field detail view for a specific id", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments/" + segmentIds[0] + "/fields/" + 1);
+        mount(store.connectContainer(<SegmentFieldDetailContainer />));
+        await store.waitForActions([FETCH_SEGMENT_TABLE, FETCH_SEGMENT_FIELDS]);
+      });
+
+      // segment questions
+      it("Should show no questions based on a new segment", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments/" + segmentIds[0] + "/questions");
+        mount(store.connectContainer(<SegmentQuestionsContainer />));
+        await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES]);
+      });
+      // segment revisions
+      it("Should show revisions", async () => {
+        const store = await createTestStore();
+        store.pushPath("/reference/segments/" + segmentIds[0] + "/revisions");
+        mount(store.connectContainer(<SegmentRevisionsContainer />));
+        await store.waitForActions([
+          FETCH_SEGMENT_TABLE,
+          FETCH_SEGMENT_REVISIONS,
+        ]);
+      });
+
+      it("Should see a newly asked question in its questions list", async () => {
+        var card = await CardApi.create(segmentCardDef);
+
+        expect(card.name).toBe(segmentCardDef.name);
+
+        await CardApi.delete({ cardId: card.id });
+
+        const store = await createTestStore();
+        store.pushPath("/reference/segments/" + segmentIds[0] + "/questions");
+        mount(store.connectContainer(<SegmentQuestionsContainer />));
+        await store.waitForActions([FETCH_SEGMENT_TABLE, LOAD_ENTITIES]);
+      });
+    });
+  });
+});
diff --git a/frontend/test/reference/utils.unit.spec.js b/frontend/test/reference/utils.unit.spec.js
index c96c4beb5971b808e3b689d1328d9e4914f511ff..ead5a1460bbccb126de3257d36355a46211e46d1 100644
--- a/frontend/test/reference/utils.unit.spec.js
+++ b/frontend/test/reference/utils.unit.spec.js
@@ -1,263 +1,275 @@
-import {
-    databaseToForeignKeys,
-    getQuestion
-} from 'metabase/reference/utils';
+import { databaseToForeignKeys, getQuestion } from "metabase/reference/utils";
 
-import { separateTablesBySchema } from "metabase/reference/databases/TableList"
+import { separateTablesBySchema } from "metabase/reference/databases/TableList";
 import { TYPE } from "metabase/lib/types";
 
 describe("Reference utils.js", () => {
+  describe("databaseToForeignKeys()", () => {
+    it("should build foreignKey viewmodels from database", () => {
+      const database = {
+        tables_lookup: {
+          1: {
+            id: 1,
+            display_name: "foo",
+            schema: "PUBLIC",
+            fields: [
+              {
+                id: 1,
+                special_type: TYPE.PK,
+                display_name: "bar",
+                description: "foobar",
+              },
+            ],
+          },
+          2: {
+            id: 2,
+            display_name: "bar",
+            schema: "public",
+            fields: [
+              {
+                id: 2,
+                special_type: TYPE.PK,
+                display_name: "foo",
+                description: "barfoo",
+              },
+            ],
+          },
+          3: {
+            id: 3,
+            display_name: "boo",
+            schema: "TEST",
+            fields: [
+              {
+                id: 3,
+                display_name: "boo",
+                description: "booboo",
+              },
+            ],
+          },
+        },
+      };
 
+      const foreignKeys = databaseToForeignKeys(database);
 
-    describe("databaseToForeignKeys()", () => {
-        it("should build foreignKey viewmodels from database", () => {
-            const database = {
-                tables_lookup: {
-                    1: {
-                        id: 1,
-                        display_name: 'foo',
-                        schema: 'PUBLIC',
-                        fields: [
-                            {
-                                id: 1,
-                                special_type: TYPE.PK,
-                                display_name: 'bar',
-                                description: 'foobar'
-                            }
-                        ]
-                    },
-                    2: {
-                        id: 2,
-                        display_name: 'bar',
-                        schema: 'public',
-                        fields: [
-                            {
-                                id: 2,
-                                special_type: TYPE.PK,
-                                display_name: 'foo',
-                                description: 'barfoo'
-                            }
-                        ]
-                    },
-                    3: {
-                        id: 3,
-                        display_name: 'boo',
-                        schema: 'TEST',
-                        fields: [
-                            {
-                                id: 3,
-                                display_name: 'boo',
-                                description: 'booboo'
-                            }
-                        ]
-                    }
-                }
-            };
-
-            const foreignKeys = databaseToForeignKeys(database);
-
-            expect(foreignKeys).toEqual({
-                1: { id: 1, name: 'Public.foo → bar', description: 'foobar' },
-                2: { id: 2, name: 'bar → foo', description: 'barfoo' }
-            });
-        });
+      expect(foreignKeys).toEqual({
+        1: { id: 1, name: "Public.foo → bar", description: "foobar" },
+        2: { id: 2, name: "bar → foo", description: "barfoo" },
+      });
     });
+  });
 
-    
-    describe("tablesToSchemaSeparatedTables()", () => {
-        it("should add schema separator to appropriate locations", () => {
-            const tables = {
-                1: { id: 1, name: 'table1', schema: 'foo' },
-                2: { id: 2, name: 'table2', schema: 'bar' },
-                3: { id: 3, name: 'table3', schema: 'boo' },
-                4: { id: 4, name: 'table4', schema: 'bar' },
-                5: { id: 5, name: 'table5', schema: 'foo' },
-                6: { id: 6, name: 'table6', schema: 'bar' }
-            };
+  describe("tablesToSchemaSeparatedTables()", () => {
+    it("should add schema separator to appropriate locations", () => {
+      const tables = {
+        1: { id: 1, name: "table1", schema: "foo" },
+        2: { id: 2, name: "table2", schema: "bar" },
+        3: { id: 3, name: "table3", schema: "boo" },
+        4: { id: 4, name: "table4", schema: "bar" },
+        5: { id: 5, name: "table5", schema: "foo" },
+        6: { id: 6, name: "table6", schema: "bar" },
+      };
 
-            const createSchemaSeparator = (table) => table.schema;
-            const createListItem = (table) => table;
+      const createSchemaSeparator = table => table.schema;
+      const createListItem = table => table;
 
-            const schemaSeparatedTables = separateTablesBySchema(
-                tables,
-                createSchemaSeparator,
-                createListItem
-            );
+      const schemaSeparatedTables = separateTablesBySchema(
+        tables,
+        createSchemaSeparator,
+        createListItem,
+      );
 
-            expect(schemaSeparatedTables).toEqual([
-                ["bar", { id: 2, name: 'table2', schema: 'bar' }],
-                { id: 4, name: 'table4', schema: 'bar' },
-                { id: 6, name: 'table6', schema: 'bar' },
-                ["boo", { id: 3, name: 'table3', schema: 'boo' }],
-                ["foo", { id: 1, name: 'table1', schema: 'foo' }],
-                { id: 5, name: 'table5', schema: 'foo' },
-            ]);
-        });
+      expect(schemaSeparatedTables).toEqual([
+        ["bar", { id: 2, name: "table2", schema: "bar" }],
+        { id: 4, name: "table4", schema: "bar" },
+        { id: 6, name: "table6", schema: "bar" },
+        ["boo", { id: 3, name: "table3", schema: "boo" }],
+        ["foo", { id: 1, name: "table1", schema: "foo" }],
+        { id: 5, name: "table5", schema: "foo" },
+      ]);
     });
+  });
 
-    describe("getQuestion()", () => {
-        const getNewQuestion = ({
-            database = 1,
-            table = 2,
-            display = "table",
-            aggregation,
-            breakout,
-            filter
-        }) => {
-            const card = {
-                "name": null,
-                "display": display,
-                "visualization_settings": {},
-                "dataset_query": {
-                    "database": database,
-                    "type": "query",
-                    "query": {
-                        "source_table": table
-                    }
-                }
-            };
-            if (aggregation != undefined) {
-                card.dataset_query.query.aggregation = aggregation;
-            }
-            if (breakout != undefined) {
-                card.dataset_query.query.breakout = breakout;
-            }
-            if (filter != undefined) {
-                card.dataset_query.query.filter = filter;
-            }
-            return card;
-        };
+  describe("getQuestion()", () => {
+    const getNewQuestion = ({
+      database = 1,
+      table = 2,
+      display = "table",
+      aggregation,
+      breakout,
+      filter,
+    }) => {
+      const card = {
+        name: null,
+        display: display,
+        visualization_settings: {},
+        dataset_query: {
+          database: database,
+          type: "query",
+          query: {
+            source_table: table,
+          },
+        },
+      };
+      if (aggregation != undefined) {
+        card.dataset_query.query.aggregation = aggregation;
+      }
+      if (breakout != undefined) {
+        card.dataset_query.query.breakout = breakout;
+      }
+      if (filter != undefined) {
+        card.dataset_query.query.filter = filter;
+      }
+      return card;
+    };
 
-        it("should generate correct question for table raw data", () => {
-            const question = getQuestion({
-                dbId: 3,
-                tableId: 4
-            });
+    it("should generate correct question for table raw data", () => {
+      const question = getQuestion({
+        dbId: 3,
+        tableId: 4,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 3,
-                table: 4
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 3,
+          table: 4,
+        }),
+      );
+    });
 
-        it("should generate correct question for table counts", () => {
-            const question = getQuestion({
-                dbId: 3,
-                tableId: 4,
-                getCount: true
-            });
+    it("should generate correct question for table counts", () => {
+      const question = getQuestion({
+        dbId: 3,
+        tableId: 4,
+        getCount: true,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 3,
-                table: 4,
-                aggregation: [ "count" ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 3,
+          table: 4,
+          aggregation: ["count"],
+        }),
+      );
+    });
 
-        it("should generate correct question for field raw data", () => {
-            const question = getQuestion({
-                dbId: 3,
-                tableId: 4,
-                fieldId: 5
-            });
+    it("should generate correct question for field raw data", () => {
+      const question = getQuestion({
+        dbId: 3,
+        tableId: 4,
+        fieldId: 5,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 3,
-                table: 4,
-                breakout: [ 5 ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 3,
+          table: 4,
+          breakout: [5],
+        }),
+      );
+    });
 
-        it("should generate correct question for field group by bar chart", () => {
-            const question = getQuestion({
-                dbId: 3,
-                tableId: 4,
-                fieldId: 5,
-                getCount: true,
-                visualization: 'bar'
-            });
+    it("should generate correct question for field group by bar chart", () => {
+      const question = getQuestion({
+        dbId: 3,
+        tableId: 4,
+        fieldId: 5,
+        getCount: true,
+        visualization: "bar",
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 3,
-                table: 4,
-                display: 'bar',
-                breakout: [ 5 ],
-                aggregation: [ "count" ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 3,
+          table: 4,
+          display: "bar",
+          breakout: [5],
+          aggregation: ["count"],
+        }),
+      );
+    });
 
-        it("should generate correct question for field group by pie chart", () => {
-            const question = getQuestion({
-                dbId: 3,
-                tableId: 4,
-                fieldId: 5,
-                getCount: true,
-                visualization: 'pie'
-            });
+    it("should generate correct question for field group by pie chart", () => {
+      const question = getQuestion({
+        dbId: 3,
+        tableId: 4,
+        fieldId: 5,
+        getCount: true,
+        visualization: "pie",
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 3,
-                table: 4,
-                display: 'pie',
-                breakout: [ 5 ],
-                aggregation: [ "count" ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 3,
+          table: 4,
+          display: "pie",
+          breakout: [5],
+          aggregation: ["count"],
+        }),
+      );
+    });
 
-        it("should generate correct question for metric raw data", () => {
-            const question = getQuestion({
-                dbId: 1,
-                tableId: 2,
-                metricId: 3
-            });
+    it("should generate correct question for metric raw data", () => {
+      const question = getQuestion({
+        dbId: 1,
+        tableId: 2,
+        metricId: 3,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                aggregation: [ "METRIC", 3 ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          aggregation: ["METRIC", 3],
+        }),
+      );
+    });
 
-        it("should generate correct question for metric group by fields", () => {
-            const question = getQuestion({
-                dbId: 1,
-                tableId: 2,
-                fieldId: 4,
-                metricId: 3
-            });
+    it("should generate correct question for metric group by fields", () => {
+      const question = getQuestion({
+        dbId: 1,
+        tableId: 2,
+        fieldId: 4,
+        metricId: 3,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                aggregation: [ "METRIC", 3 ],
-                breakout: [ 4 ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          aggregation: ["METRIC", 3],
+          breakout: [4],
+        }),
+      );
+    });
 
-        it("should generate correct question for segment raw data", () => {
-            const question = getQuestion({
-                dbId: 2,
-                tableId: 3,
-                segmentId: 4
-            });
+    it("should generate correct question for segment raw data", () => {
+      const question = getQuestion({
+        dbId: 2,
+        tableId: 3,
+        segmentId: 4,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 2,
-                table: 3,
-                filter: [ "AND", [ "SEGMENT", 4 ] ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 2,
+          table: 3,
+          filter: ["AND", ["SEGMENT", 4]],
+        }),
+      );
+    });
 
-        it("should generate correct question for segment counts", () => {
-            const question = getQuestion({
-                dbId: 2,
-                tableId: 3,
-                segmentId: 4,
-                getCount: true
-            });
+    it("should generate correct question for segment counts", () => {
+      const question = getQuestion({
+        dbId: 2,
+        tableId: 3,
+        segmentId: 4,
+        getCount: true,
+      });
 
-            expect(question).toEqual(getNewQuestion({
-                database: 2,
-                table: 3,
-                aggregation: [ "count" ],
-                filter: [ "AND", [ "SEGMENT", 4 ] ]
-            }));
-        });
+      expect(question).toEqual(
+        getNewQuestion({
+          database: 2,
+          table: 3,
+          aggregation: ["count"],
+          filter: ["AND", ["SEGMENT", 4]],
+        }),
+      );
     });
+  });
 });
diff --git a/frontend/test/selectors/metadata.integ.spec.js b/frontend/test/selectors/metadata.integ.spec.js
index e7c866182f981169aec4f08d58c60716609d4f8b..0e711dac711945fc93826d79f577675d6c05f335 100644
--- a/frontend/test/selectors/metadata.integ.spec.js
+++ b/frontend/test/selectors/metadata.integ.spec.js
@@ -1,8 +1,13 @@
-import { createTestStore, useSharedAdminLogin } from "__support__/integrated_tests";
 import {
-    deleteFieldDimension, fetchTableMetadata,
-    updateFieldDimension,
-    updateFieldValues,
+  createTestStore,
+  useSharedAdminLogin,
+} from "__support__/integrated_tests";
+import {
+  deleteFieldDimension,
+  fetchTableMetadata,
+  fetchFieldValues,
+  updateFieldDimension,
+  updateFieldValues,
 } from "metabase/redux/metadata";
 import { makeGetMergedParameterFieldValues } from "metabase/selectors/metadata";
 
@@ -12,65 +17,93 @@ const PRODUCT_CATEGORY_ID = 21;
 // NOTE Atte Keinänen 9/14/17: A hybrid of an integration test and a method unit test
 // I wanted to use a real state tree and have a realistic field remapping scenario
 
-describe('makeGetMergedParameterFieldValues', () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
-
-        // add remapping
-        const store = await createTestStore()
-
-        await store.dispatch(updateFieldDimension(REVIEW_RATING_ID, {
-            type: "internal",
-            name: "Rating Description",
-            human_readable_field_id: null
-        }));
-        await store.dispatch(updateFieldValues(REVIEW_RATING_ID, [
-            [1, 'Awful'], [2, 'Unpleasant'], [3, 'Meh'], [4, 'Enjoyable'], [5, 'Perfecto']
-        ]));
-    })
+describe("makeGetMergedParameterFieldValues", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
 
-    afterAll(async () => {
-        const store = await createTestStore()
+    // add remapping
+    const store = await createTestStore();
 
-        await store.dispatch(deleteFieldDimension(REVIEW_RATING_ID));
-        await store.dispatch(updateFieldValues(REVIEW_RATING_ID, [
-            [1, '1'], [2, '2'], [3, '3'], [4, '4'], [5, '5']
-        ]));
-    })
+    await store.dispatch(
+      updateFieldDimension(REVIEW_RATING_ID, {
+        type: "internal",
+        name: "Rating Description",
+        human_readable_field_id: null,
+      }),
+    );
+    await store.dispatch(
+      updateFieldValues(REVIEW_RATING_ID, [
+        [1, "Awful"],
+        [2, "Unpleasant"],
+        [3, "Meh"],
+        [4, "Enjoyable"],
+        [5, "Perfecto"],
+      ]),
+    );
+  });
 
-    it("should return empty array if no field ids", async () => {
-        const store = await createTestStore()
-        await store.dispatch(fetchTableMetadata(3))
+  afterAll(async () => {
+    const store = await createTestStore();
 
-        const getMergedParameterFieldValues = makeGetMergedParameterFieldValues()
-        expect(
-            getMergedParameterFieldValues(store.getState(), { parameter: { field_ids: [] } })
-        ).toEqual([])
+    await store.dispatch(deleteFieldDimension(REVIEW_RATING_ID));
+    await store.dispatch(
+      updateFieldValues(REVIEW_RATING_ID, [
+        [1, "1"],
+        [2, "2"],
+        [3, "3"],
+        [4, "4"],
+        [5, "5"],
+      ]),
+    );
+  });
 
-    })
+  it("should return empty array if no field ids", async () => {
+    const store = await createTestStore();
+    await store.dispatch(fetchTableMetadata(3));
 
-    it("should return the original field values if a single field id", async () => {
-        const store = await createTestStore()
-        await store.dispatch(fetchTableMetadata(3))
+    const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
+    expect(
+      getMergedParameterFieldValues(store.getState(), {
+        parameter: { field_ids: [] },
+      }),
+    ).toEqual([]);
+  });
 
-        const getMergedParameterFieldValues = makeGetMergedParameterFieldValues()
-        expect(
-            getMergedParameterFieldValues(store.getState(), { parameter: { field_ids: [PRODUCT_CATEGORY_ID] } })
-        ).toEqual([ [ 'Doohickey' ], [ 'Gadget' ], [ 'Gizmo' ], [ 'Widget' ] ])
-    })
+  it("should return the original field values if a single field id", async () => {
+    const store = await createTestStore();
+    await store.dispatch(fetchTableMetadata(3));
+    await store.dispatch(fetchFieldValues(PRODUCT_CATEGORY_ID));
 
-    it("should merge and sort field values if multiple field ids", async () => {
-        const store = await createTestStore()
-        await store.dispatch(fetchTableMetadata(3))
-        await store.dispatch(fetchTableMetadata(4))
+    const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
+    expect(
+      getMergedParameterFieldValues(store.getState(), {
+        parameter: { field_ids: [PRODUCT_CATEGORY_ID] },
+      }),
+    ).toEqual([["Doohickey"], ["Gadget"], ["Gizmo"], ["Widget"]]);
+  });
 
-        const getMergedParameterFieldValues = makeGetMergedParameterFieldValues()
-        expect(
-            getMergedParameterFieldValues(store.getState(), { parameter: { field_ids: [PRODUCT_CATEGORY_ID, REVIEW_RATING_ID] } })
-        ).toEqual([
-            [1, 'Awful'], ['Doohickey'], [4, 'Enjoyable'], ['Gadget'],
-            ['Gizmo'], [3, 'Meh'], [5, 'Perfecto'], [2, 'Unpleasant'], ['Widget']
-        ])
-    })
-})
+  it("should merge and sort field values if multiple field ids", async () => {
+    const store = await createTestStore();
+    await store.dispatch(fetchTableMetadata(3));
+    await store.dispatch(fetchTableMetadata(4));
+    await store.dispatch(fetchFieldValues(PRODUCT_CATEGORY_ID));
+    await store.dispatch(fetchFieldValues(REVIEW_RATING_ID));
 
+    const getMergedParameterFieldValues = makeGetMergedParameterFieldValues();
+    expect(
+      getMergedParameterFieldValues(store.getState(), {
+        parameter: { field_ids: [PRODUCT_CATEGORY_ID, REVIEW_RATING_ID] },
+      }),
+    ).toEqual([
+      [1, "Awful"],
+      ["Doohickey"],
+      [4, "Enjoyable"],
+      ["Gadget"],
+      ["Gizmo"],
+      [3, "Meh"],
+      [5, "Perfecto"],
+      [2, "Unpleasant"],
+      ["Widget"],
+    ]);
+  });
+});
diff --git a/frontend/test/selectors/metadata.unit.spec.js b/frontend/test/selectors/metadata.unit.spec.js
index e71d3c3f7dcbab014aa646caffa9e195f1e3abf1..cab616f3e00d92db22f91a41f8d91a5fff8d278a 100644
--- a/frontend/test/selectors/metadata.unit.spec.js
+++ b/frontend/test/selectors/metadata.unit.spec.js
@@ -1,75 +1,74 @@
-import Metadata from 'metabase-lib/lib/metadata/Metadata'
-import Database from 'metabase-lib/lib/metadata/Database'
+import Metadata from "metabase-lib/lib/metadata/Metadata";
+import Database from "metabase-lib/lib/metadata/Database";
 
 import {
-    metadata, // connected graph,
-    state, // the original non connected metadata objects,
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    ORDERS_CREATED_DATE_FIELD_ID
-} from '__support__/sample_dataset_fixture'
+  metadata, // connected graph,
+  state, // the original non connected metadata objects,
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  ORDERS_CREATED_DATE_FIELD_ID,
+} from "__support__/sample_dataset_fixture";
 
-import { copyObjects } from '../../src/metabase/selectors/metadata'
+import { copyObjects } from "../../src/metabase/selectors/metadata";
 
-const NUM_TABLES = Object.keys(state.metadata.tables).length
-const NUM_DBS = Object.keys(state.metadata.databases).length
-const NUM_FIELDS = Object.keys(state.metadata.fields).length
-const NUM_METRICS = Object.keys(state.metadata.metrics).length
-const NUM_SEGMENTS = Object.keys(state.metadata.segments).length
+const NUM_TABLES = Object.keys(state.metadata.tables).length;
+const NUM_DBS = Object.keys(state.metadata.databases).length;
+const NUM_FIELDS = Object.keys(state.metadata.fields).length;
+const NUM_METRICS = Object.keys(state.metadata.metrics).length;
+const NUM_SEGMENTS = Object.keys(state.metadata.segments).length;
 
 // NOTE: Also tests in redux/metadata.spec.js cover the use of metadata selectors
-describe('getMetadata', () => {
-    it('should properly transfer metadata', () => {
-        expect(metadata).toBeInstanceOf(Metadata)
+describe("getMetadata", () => {
+  it("should properly transfer metadata", () => {
+    expect(metadata).toBeInstanceOf(Metadata);
 
-        expect(Object.keys(metadata.databases).length).toEqual(NUM_DBS)
-        expect(Object.keys(metadata.tables).length).toEqual(NUM_TABLES)
-        expect(Object.keys(metadata.fields).length).toEqual(NUM_FIELDS)
-        expect(Object.keys(metadata.metrics).length).toEqual(NUM_METRICS)
-        expect(Object.keys(metadata.segments).length).toEqual(NUM_SEGMENTS)
-    })
+    expect(Object.keys(metadata.databases).length).toEqual(NUM_DBS);
+    expect(Object.keys(metadata.tables).length).toEqual(NUM_TABLES);
+    expect(Object.keys(metadata.fields).length).toEqual(NUM_FIELDS);
+    expect(Object.keys(metadata.metrics).length).toEqual(NUM_METRICS);
+    expect(Object.keys(metadata.segments).length).toEqual(NUM_SEGMENTS);
+  });
 
-    describe('connected database', () => {
-        it('should have the proper number of tables', () => {
-            const database = metadata.databases[DATABASE_ID]
-            expect(database.tables.length).toEqual(NUM_TABLES)
-        })
-    })
+  describe("connected database", () => {
+    it("should have the proper number of tables", () => {
+      const database = metadata.databases[DATABASE_ID];
+      expect(database.tables.length).toEqual(NUM_TABLES);
+    });
+  });
 
-    describe('connected table', () => {
-        const table = metadata.tables[ORDERS_TABLE_ID]
+  describe("connected table", () => {
+    const table = metadata.tables[ORDERS_TABLE_ID];
 
-        it('should have the proper number of fields', () => {
-            // TODO - make this more dynamic
-            expect(table.fields.length).toEqual(7)
-        })
+    it("should have the proper number of fields", () => {
+      // TODO - make this more dynamic
+      expect(table.fields.length).toEqual(7);
+    });
 
-        it('should have a parent database', () => {
-            expect(table.database).toEqual(metadata.databases[DATABASE_ID])
-        })
-    })
+    it("should have a parent database", () => {
+      expect(table.database).toEqual(metadata.databases[DATABASE_ID]);
+    });
+  });
 
-    describe('connected field', () => {
-        const field = metadata.fields[ORDERS_CREATED_DATE_FIELD_ID]
-        it('should have a parent table', () => {
-            expect(field.table).toEqual(metadata.tables[ORDERS_TABLE_ID])
-        })
-    })
-})
+  describe("connected field", () => {
+    const field = metadata.fields[ORDERS_CREATED_DATE_FIELD_ID];
+    it("should have a parent table", () => {
+      expect(field.table).toEqual(metadata.tables[ORDERS_TABLE_ID]);
+    });
+  });
+});
 
-describe('copyObjects', () => {
-    it('should clone each object in the provided mapping of objects', () => {
-        const meta = new Metadata()
-        const databases = state.metadata.databases
-        const copiedDatabases = copyObjects(meta, databases, Database)
+describe("copyObjects", () => {
+  it("should clone each object in the provided mapping of objects", () => {
+    const meta = new Metadata();
+    const databases = state.metadata.databases;
+    const copiedDatabases = copyObjects(meta, databases, Database);
 
-        expect(Object.keys(copiedDatabases).length).toEqual(NUM_DBS)
-
-        Object.values(copiedDatabases).map(db => {
-            expect(db).toBeInstanceOf(Database)
-            expect(db).toHaveProperty('metadata')
-            expect(db.metadata).toBeInstanceOf(Metadata)
-        })
-    })
-})
+    expect(Object.keys(copiedDatabases).length).toEqual(NUM_DBS);
 
+    Object.values(copiedDatabases).map(db => {
+      expect(db).toBeInstanceOf(Database);
+      expect(db).toHaveProperty("metadata");
+      expect(db.metadata).toBeInstanceOf(Metadata);
+    });
+  });
+});
diff --git a/frontend/test/setup/signup.integ.spec.js b/frontend/test/setup/signup.integ.spec.js
index aa8fa3eddcf96c53aac314b1c9302cf80a445daf..ff21362ed7464177ef31eef007dcd1c11ca4a093 100644
--- a/frontend/test/setup/signup.integ.spec.js
+++ b/frontend/test/setup/signup.integ.spec.js
@@ -1,23 +1,24 @@
 import {
-    createTestStore,
-    switchToPlainDatabase,
-    BROWSER_HISTORY_REPLACE, login
+  createTestStore,
+  switchToPlainDatabase,
+  BROWSER_HISTORY_REPLACE,
+  login,
 } from "__support__/integrated_tests";
 import {
-    chooseSelectOption,
-    click,
-    clickButton,
-    setInputValue
+  chooseSelectOption,
+  click,
+  clickButton,
+  setInputValue,
 } from "__support__/enzyme_utils";
 
 import {
-    COMPLETE_SETUP,
-    SET_ACTIVE_STEP,
-    SET_ALLOW_TRACKING,
-    SET_DATABASE_DETAILS,
-    SET_USER_DETAILS,
-    SUBMIT_SETUP,
-    VALIDATE_PASSWORD
+  COMPLETE_SETUP,
+  SET_ACTIVE_STEP,
+  SET_ALLOW_TRACKING,
+  SET_DATABASE_DETAILS,
+  SET_USER_DETAILS,
+  SUBMIT_SETUP,
+  VALIDATE_PASSWORD,
 } from "metabase/setup/actions";
 
 import path from "path";
@@ -37,186 +38,201 @@ import StepIndicators from "metabase/components/StepIndicators";
 
 jasmine.DEFAULT_TIMEOUT_INTERVAL = 60000;
 describe("setup wizard", () => {
-    let store = null;
-    let app = null;
-
-    const email = 'testy@metabase.com'
-    const strongPassword = 'QJbHYJN3tPW[29AoBM3#rsfB4@hshp>gC8mDmUTtbGTfExY]#nBjmtX@NmEJwxBc'
-
-    beforeAll(async () => {
-        switchToPlainDatabase();
-        store = await createTestStore()
-        store.pushPath("/");
-        app = mount(store.getAppContainer())
-    })
-
-    it("should start from the welcome page", async () => {
-        await store.waitForActions([BROWSER_HISTORY_REPLACE])
-        expect(store.getPath()).toBe("/setup")
-        expect(app.find(Setup).find("h1").text()).toBe("Welcome to Metabase")
-    });
-
-    it("should allow you to create an account", async () => {
-        clickButton(app.find(".Button.Button--primary"))
-        await store.waitForActions([SET_ACTIVE_STEP])
-
-        const userStep = app.find(UserStep)
-        expect(userStep.find('.SetupStep--active').length).toBe(1)
-
-        const nextButton = userStep.find('button[children="Next"]')
-        expect(nextButton.props().disabled).toBe(true)
-
-        setInputValue(userStep.find('input[name="first_name"]'), 'Testy')
-        setInputValue(userStep.find('input[name="last_name"]'), 'McTestface')
-        setInputValue(userStep.find('input[name="email"]'), email)
-        setInputValue(userStep.find('input[name="site_name"]'), 'Epic Team')
-
-        // test first with a weak password
-        setInputValue(userStep.find('input[name="password"]'), 'password')
-        await store.waitForActions([VALIDATE_PASSWORD])
-        setInputValue(userStep.find('input[name="password_confirm"]'), 'password')
-
-        // the form shouldn't be valid yet
-        expect(nextButton.props().disabled).toBe(true)
-
-        // then with a strong password, generated with my beloved password manager
-        setInputValue(userStep.find('input[name="password"]'), strongPassword)
-        await store.waitForActions([VALIDATE_PASSWORD])
-        setInputValue(userStep.find('input[name="password_confirm"]'), strongPassword)
-
-        // Due to the chained setState calls in UserStep we have to add a tiny delay here
-        await delay(500);
-
-        expect(nextButton.props().disabled).toBe(false)
-        clickButton(nextButton);
-        await store.waitForActions([SET_USER_DETAILS])
-        expect(app.find(DatabaseConnectionStep).find('.SetupStep--active').length).toBe(1)
-
-        // test that you can return to user settings if you want
-        click(userStep.find("h3"));
-        const newUserStep = app.find(UserStep)
-        expect(newUserStep.find('.SetupStep--active').length).toBe(1)
-        expect(userStep.find('input[name="first_name"]').prop('defaultValue')).toBe("Testy");
-        expect(userStep.find('input[name="password"]').prop('defaultValue')).toBe(strongPassword);
-
-        // re-enter database settings after that
-        clickButton(newUserStep.find('button[children="Next"]'));
-        await store.waitForActions([SET_ACTIVE_STEP])
-    })
-
-    it("should allow you to set connection settings for a new database", async () => {
-        // Try to address a rare test failure where `chooseSelectOption` fails because it couldn't find its parent option
-        app.update()
-
-        const databaseStep = app.find(DatabaseConnectionStep)
-        expect(databaseStep.find('.SetupStep--active').length).toBe(1)
-
-        // add h2 database
-        chooseSelectOption(app.find("option[value='h2']"));
-        setInputValue(databaseStep.find("input[name='name']"), "Metabase H2");
-
-        const nextButton = databaseStep.find('button[children="Next"]')
-        expect(nextButton.props().disabled).toBe(true);
-
-        const dbPath = path.resolve(__dirname, '../__runner__/test_db_fixture.db');
-        setInputValue(databaseStep.find("input[name='db']"), `file:${dbPath}`);
-
-        expect(nextButton.props().disabled).toBe(undefined);
-        clickButton(nextButton);
-        await store.waitForActions([SET_DATABASE_DETAILS])
-
-        const preferencesStep = app.find(PreferencesStep)
-        expect(preferencesStep.find('.SetupStep--active').length).toBe(1)
-    })
-
-    it("should show you scheduling step if you select \"Let me choose when Metabase syncs and scans\"", async () => {
-        // we can conveniently test returning to database settings now as well
-        const connectionStep = app.find(DatabaseConnectionStep)
-        click(connectionStep.find("h3"))
-        expect(connectionStep.find('.SetupStep--active').length).toBe(1)
-
-        const letUserControlSchedulingToggle = connectionStep
-            .find(FormField)
-            .filterWhere((f) => f.props().fieldName === "let-user-control-scheduling")
-            .find(Toggle);
-
-        expect(letUserControlSchedulingToggle.length).toBe(1);
-        expect(letUserControlSchedulingToggle.prop('value')).toBe(false);
-        click(letUserControlSchedulingToggle);
-        expect(letUserControlSchedulingToggle.prop('value')).toBe(true);
-
-        const nextButton = connectionStep.find('button[children="Next"]')
-        clickButton(nextButton);
-        await store.waitForActions([SET_DATABASE_DETAILS])
-
-        const schedulingStep = app.find(DatabaseSchedulingStep);
-        expect(schedulingStep.find('.SetupStep--active').length).toBe(1)
-
-        // disable the deep analysis
-        const syncOptions = schedulingStep.find(SyncOption);
-        const syncOptionsNever = syncOptions.at(1);
-        click(syncOptionsNever)
-
-        // proceed to tracking preferences step again
-        const nextButton2 = schedulingStep.find('button[children="Next"]')
-        clickButton(nextButton2);
-        await store.waitForActions([SET_DATABASE_DETAILS])
-    })
-
-    it("should let you opt in/out from user tracking", async () => {
-        const preferencesStep = app.find(PreferencesStep)
-        expect(preferencesStep.find('.SetupStep--active').length).toBe(1)
-
-        // tracking is enabled by default
-        const trackingToggle = preferencesStep.find(Toggle)
-        expect(trackingToggle.prop('value')).toBe(true)
-
-        click(trackingToggle)
-        await store.waitForActions([SET_ALLOW_TRACKING])
-        expect(trackingToggle.prop('value')).toBe(false)
-    })
-
-    // NOTE Atte Keinänen 8/15/17:
-    // If you want to develop tests incrementally, you should disable this step as this will complete the setup
-    // That is an irreversible action (you have to nuke the db in order to see the setup screen again)
-    it("should let you finish setup and subscribe to newsletter", async () => {
-        const preferencesStep = app.find(PreferencesStep)
-        const nextButton = preferencesStep.find('button[children="Next"]')
-        clickButton(nextButton)
-        await store.waitForActions([COMPLETE_SETUP, SUBMIT_SETUP])
-
-        const allSetUpSection = app.find(".SetupStep").last()
-        expect(allSetUpSection.find('.SetupStep--active').length).toBe(1)
-
-        expect(allSetUpSection.find('a[href="/?new"]').length).toBe(1)
-    });
-
-    it("should show you the onboarding modal", async () => {
-        // we can't persist the cookies of previous step so do the login manually here
-        await login({ username: email, password: strongPassword })
-        // redirect to `?new` caused some trouble in tests so create a new store for testing the modal interaction
-        const loggedInStore = await createTestStore();
-        loggedInStore.pushPath("/?new")
-        const loggedInApp = mount(loggedInStore.getAppContainer());
-
-        await loggedInStore.waitForActions([FETCH_ACTIVITY])
-
-        const modal = loggedInApp.find(NewUserOnboardingModal)
-        const stepIndicators = modal.find(StepIndicators)
-        expect(modal.length).toBe(1)
-        expect(stepIndicators.prop('currentStep')).toBe(1);
-
-        click(modal.find('a[children="Next"]'))
-        expect(stepIndicators.prop('currentStep')).toBe(2);
-
-        click(modal.find('a[children="Next"]'))
-        expect(stepIndicators.prop('currentStep')).toBe(3);
-
-        click(modal.find('a[children="Let\'s go"]'))
-        expect(loggedInApp.find(NewUserOnboardingModal).length).toBe(0);
-    })
-
-    afterAll(async () => {
-        // The challenge with setup guide test is that you can't reset the db to the initial state
-    })
+  let store = null;
+  let app = null;
+
+  const email = "testy@metabase.com";
+  const strongPassword =
+    "QJbHYJN3tPW[29AoBM3#rsfB4@hshp>gC8mDmUTtbGTfExY]#nBjmtX@NmEJwxBc";
+
+  beforeAll(async () => {
+    switchToPlainDatabase();
+    store = await createTestStore();
+    store.pushPath("/");
+    app = mount(store.getAppContainer());
+  });
+
+  it("should start from the welcome page", async () => {
+    await store.waitForActions([BROWSER_HISTORY_REPLACE]);
+    expect(store.getPath()).toBe("/setup");
+    expect(
+      app
+        .find(Setup)
+        .find("h1")
+        .text(),
+    ).toBe("Welcome to Metabase");
+  });
+
+  it("should allow you to create an account", async () => {
+    clickButton(app.find(".Button.Button--primary"));
+    await store.waitForActions([SET_ACTIVE_STEP]);
+
+    const userStep = app.find(UserStep);
+    expect(userStep.find(".SetupStep--active").length).toBe(1);
+
+    const nextButton = userStep.find('button[children="Next"]');
+    expect(nextButton.props().disabled).toBe(true);
+
+    setInputValue(userStep.find('input[name="first_name"]'), "Testy");
+    setInputValue(userStep.find('input[name="last_name"]'), "McTestface");
+    setInputValue(userStep.find('input[name="email"]'), email);
+    setInputValue(userStep.find('input[name="site_name"]'), "Epic Team");
+
+    // test first with a weak password
+    setInputValue(userStep.find('input[name="password"]'), "password");
+    await store.waitForActions([VALIDATE_PASSWORD]);
+    setInputValue(userStep.find('input[name="password_confirm"]'), "password");
+
+    // the form shouldn't be valid yet
+    expect(nextButton.props().disabled).toBe(true);
+
+    // then with a strong password, generated with my beloved password manager
+    setInputValue(userStep.find('input[name="password"]'), strongPassword);
+    await store.waitForActions([VALIDATE_PASSWORD]);
+    setInputValue(
+      userStep.find('input[name="password_confirm"]'),
+      strongPassword,
+    );
+
+    // Due to the chained setState calls in UserStep we have to add a tiny delay here
+    await delay(500);
+
+    expect(nextButton.props().disabled).toBe(false);
+    clickButton(nextButton);
+    await store.waitForActions([SET_USER_DETAILS]);
+    expect(
+      app.find(DatabaseConnectionStep).find(".SetupStep--active").length,
+    ).toBe(1);
+
+    // test that you can return to user settings if you want
+    click(userStep.find("h3"));
+    const newUserStep = app.find(UserStep);
+    expect(newUserStep.find(".SetupStep--active").length).toBe(1);
+    expect(userStep.find('input[name="first_name"]').prop("defaultValue")).toBe(
+      "Testy",
+    );
+    expect(userStep.find('input[name="password"]').prop("defaultValue")).toBe(
+      strongPassword,
+    );
+
+    // re-enter database settings after that
+    clickButton(newUserStep.find('button[children="Next"]'));
+    await store.waitForActions([SET_ACTIVE_STEP]);
+  });
+
+  it("should allow you to set connection settings for a new database", async () => {
+    // Try to address a rare test failure where `chooseSelectOption` fails because it couldn't find its parent option
+    app.update();
+
+    const databaseStep = app.find(DatabaseConnectionStep);
+    expect(databaseStep.find(".SetupStep--active").length).toBe(1);
+
+    // add h2 database
+    chooseSelectOption(app.find("option[value='h2']"));
+    setInputValue(databaseStep.find("input[name='name']"), "Metabase H2");
+
+    const nextButton = databaseStep.find('button[children="Next"]');
+    expect(nextButton.props().disabled).toBe(true);
+
+    const dbPath = path.resolve(__dirname, "../__runner__/test_db_fixture.db");
+    setInputValue(databaseStep.find("input[name='db']"), `file:${dbPath}`);
+
+    expect(nextButton.props().disabled).toBe(undefined);
+    clickButton(nextButton);
+    await store.waitForActions([SET_DATABASE_DETAILS]);
+
+    const preferencesStep = app.find(PreferencesStep);
+    expect(preferencesStep.find(".SetupStep--active").length).toBe(1);
+  });
+
+  it('should show you scheduling step if you select "Let me choose when Metabase syncs and scans"', async () => {
+    // we can conveniently test returning to database settings now as well
+    const connectionStep = app.find(DatabaseConnectionStep);
+    click(connectionStep.find("h3"));
+    expect(connectionStep.find(".SetupStep--active").length).toBe(1);
+
+    const letUserControlSchedulingToggle = connectionStep
+      .find(FormField)
+      .filterWhere(f => f.props().fieldName === "let-user-control-scheduling")
+      .find(Toggle);
+
+    expect(letUserControlSchedulingToggle.length).toBe(1);
+    expect(letUserControlSchedulingToggle.prop("value")).toBe(false);
+    click(letUserControlSchedulingToggle);
+    expect(letUserControlSchedulingToggle.prop("value")).toBe(true);
+
+    const nextButton = connectionStep.find('button[children="Next"]');
+    clickButton(nextButton);
+    await store.waitForActions([SET_DATABASE_DETAILS]);
+
+    const schedulingStep = app.find(DatabaseSchedulingStep);
+    expect(schedulingStep.find(".SetupStep--active").length).toBe(1);
+
+    // disable the deep analysis
+    const syncOptions = schedulingStep.find(SyncOption);
+    const syncOptionsNever = syncOptions.at(1);
+    click(syncOptionsNever);
+
+    // proceed to tracking preferences step again
+    const nextButton2 = schedulingStep.find('button[children="Next"]');
+    clickButton(nextButton2);
+    await store.waitForActions([SET_DATABASE_DETAILS]);
+  });
+
+  it("should let you opt in/out from user tracking", async () => {
+    const preferencesStep = app.find(PreferencesStep);
+    expect(preferencesStep.find(".SetupStep--active").length).toBe(1);
+
+    // tracking is enabled by default
+    const trackingToggle = preferencesStep.find(Toggle);
+    expect(trackingToggle.prop("value")).toBe(true);
+
+    click(trackingToggle);
+    await store.waitForActions([SET_ALLOW_TRACKING]);
+    expect(trackingToggle.prop("value")).toBe(false);
+  });
+
+  // NOTE Atte Keinänen 8/15/17:
+  // If you want to develop tests incrementally, you should disable this step as this will complete the setup
+  // That is an irreversible action (you have to nuke the db in order to see the setup screen again)
+  it("should let you finish setup and subscribe to newsletter", async () => {
+    const preferencesStep = app.find(PreferencesStep);
+    const nextButton = preferencesStep.find('button[children="Next"]');
+    clickButton(nextButton);
+    await store.waitForActions([COMPLETE_SETUP, SUBMIT_SETUP]);
+
+    const allSetUpSection = app.find(".SetupStep").last();
+    expect(allSetUpSection.find(".SetupStep--active").length).toBe(1);
+
+    expect(allSetUpSection.find('a[href="/?new"]').length).toBe(1);
+  });
+
+  it("should show you the onboarding modal", async () => {
+    // we can't persist the cookies of previous step so do the login manually here
+    await login({ username: email, password: strongPassword });
+    // redirect to `?new` caused some trouble in tests so create a new store for testing the modal interaction
+    const loggedInStore = await createTestStore();
+    loggedInStore.pushPath("/?new");
+    const loggedInApp = mount(loggedInStore.getAppContainer());
+
+    await loggedInStore.waitForActions([FETCH_ACTIVITY]);
+
+    const modal = loggedInApp.find(NewUserOnboardingModal);
+    const stepIndicators = modal.find(StepIndicators);
+    expect(modal.length).toBe(1);
+    expect(stepIndicators.prop("currentStep")).toBe(1);
+
+    click(modal.find('a[children="Next"]'));
+    expect(stepIndicators.prop("currentStep")).toBe(2);
+
+    click(modal.find('a[children="Next"]'));
+    expect(stepIndicators.prop("currentStep")).toBe(3);
+
+    click(modal.find('a[children="Let\'s go"]'));
+    expect(loggedInApp.find(NewUserOnboardingModal).length).toBe(0);
+  });
+
+  afterAll(async () => {
+    // The challenge with setup guide test is that you can't reset the db to the initial state
+  });
 });
diff --git a/frontend/test/visualizations/__support__/visualizations.js b/frontend/test/visualizations/__support__/visualizations.js
index 86712dfc23258eb4403b3b71c448fb8f2483521c..7aa257b4d00fd65613458a584e4763533b539c01 100644
--- a/frontend/test/visualizations/__support__/visualizations.js
+++ b/frontend/test/visualizations/__support__/visualizations.js
@@ -1,120 +1,162 @@
-
 export function makeCard(card) {
-    return {
-        name: "card",
-        dataset_query: {},
-        visualization_settings: {},
-        display: "scalar",
-        ...card
-    }
+  return {
+    name: "card",
+    dataset_query: {},
+    visualization_settings: {},
+    display: "scalar",
+    ...card,
+  };
 }
 
 export function makeData(cols, rows) {
-    return {
-        cols,
-        rows
-    };
+  return {
+    cols,
+    rows,
+  };
 }
 
 export const Column = (col = {}) => ({
-    ...col,
-    name: col.name || "column_name",
-    display_name: col.display_name || col.name || "column_display_name"
+  ...col,
+  name: col.name || "column_name",
+  display_name: col.display_name || col.name || "column_display_name",
 });
 
-export const DateTimeColumn = (col = {}) => Column({ "base_type" : "type/DateTime", "special_type" : null, ...col });
-export const NumberColumn   = (col = {}) => Column({ "base_type" : "type/Integer", "special_type" : "type/Number", ...col });
-export const StringColumn   = (col = {}) => Column({ "base_type" : "type/Text", "special_type" : null, ...col });
+export const DateTimeColumn = (col = {}) =>
+  Column({ base_type: "type/DateTime", special_type: null, ...col });
+export const NumberColumn = (col = {}) =>
+  Column({ base_type: "type/Integer", special_type: "type/Number", ...col });
+export const StringColumn = (col = {}) =>
+  Column({ base_type: "type/Text", special_type: null, ...col });
 
-export const Card = (name, ...overrides) => deepExtend({
-    card: {
+export const Card = (name, ...overrides) =>
+  deepExtend(
+    {
+      card: {
         name: name + "_name",
-        visualization_settings: {}
-    }
-}, ...overrides);
+        visualization_settings: {},
+      },
+    },
+    ...overrides,
+  );
 
 export const ScalarCard = (name, ...overrides) =>
-    Card(name, {
-        card: {
-            display: "scalar",
-        },
-        data: {
-            cols: [NumberColumn({ name: name + "_col0" })],
-            rows: [[1]]
-        }
-    }, ...overrides);
+  Card(
+    name,
+    {
+      card: {
+        display: "scalar",
+      },
+      data: {
+        cols: [NumberColumn({ name: name + "_col0" })],
+        rows: [[1]],
+      },
+    },
+    ...overrides,
+  );
 
 export const TableCard = (name, ...overrides) =>
-    Card(name, {
-        card: {
-            display: "table",
-        },
-        data: {
-            cols: [NumberColumn({ name: name + "_col0" })],
-            columns: ["id"],
-            rows: [[1]]
-        }
-    }, ...overrides);
+  Card(
+    name,
+    {
+      card: {
+        display: "table",
+      },
+      data: {
+        cols: [NumberColumn({ name: name + "_col0" })],
+        columns: ["id"],
+        rows: [[1]],
+      },
+    },
+    ...overrides,
+  );
 
 export const TextCard = (name, ...overrides) =>
-    Card(name, {
-        card: {
-            display: "text",
-            visualization_settings: {
-                text: ""
-            }
+  Card(
+    name,
+    {
+      card: {
+        display: "text",
+        visualization_settings: {
+          text: "",
         },
-        data: {
-            cols: [],
-            columns: [],
-            rows: []
-        }
-    }, ...overrides);
+      },
+      data: {
+        cols: [],
+        columns: [],
+        rows: [],
+      },
+    },
+    ...overrides,
+  );
 
 export const LineCard = (name, ...overrides) =>
-    Card(name, {
-        card: {
-            display: "line",
-        },
-        data: {
-            cols: [StringColumn({ name: name + "_col0" }), NumberColumn({ name: name + "_col1" })],
-            rows: [["a",0],["b",1]]
-        }
-    }, ...overrides);
+  Card(
+    name,
+    {
+      card: {
+        display: "line",
+      },
+      data: {
+        cols: [
+          StringColumn({ name: name + "_col0" }),
+          NumberColumn({ name: name + "_col1" }),
+        ],
+        rows: [["a", 0], ["b", 1]],
+      },
+    },
+    ...overrides,
+  );
 
 export const MultiseriesLineCard = (name, ...overrides) =>
-    Card(name, {
-        card: {
-            name: name + "_name",
-            display: "line",
-            visualization_settings: {}
-        },
-        data: {
-            cols: [StringColumn({ name: name + "_col0" }), StringColumn({ name: name + "_col1" }), NumberColumn({ name: name + "_col2" })],
-            rows: [
-                [name + "_cat1", "x", 0], [name + "_cat1", "y", 1], [name + "_cat1", "z", 1],
-                [name + "_cat2", "x", 2], [name + "_cat2", "y", 3], [name + "_cat2", "z", 4]
-            ]
-        }
-    }, ...overrides);
+  Card(
+    name,
+    {
+      card: {
+        name: name + "_name",
+        display: "line",
+        visualization_settings: {},
+      },
+      data: {
+        cols: [
+          StringColumn({ name: name + "_col0" }),
+          StringColumn({ name: name + "_col1" }),
+          NumberColumn({ name: name + "_col2" }),
+        ],
+        rows: [
+          [name + "_cat1", "x", 0],
+          [name + "_cat1", "y", 1],
+          [name + "_cat1", "z", 1],
+          [name + "_cat2", "x", 2],
+          [name + "_cat2", "y", 3],
+          [name + "_cat2", "z", 4],
+        ],
+      },
+    },
+    ...overrides,
+  );
 
 function deepExtend(target, ...sources) {
-    for (const source of sources) {
-        for (const prop in source) {
-            if (source.hasOwnProperty(prop)) {
-                if (target[prop] && typeof target[prop] === 'object' && source[prop] && typeof source[prop] === 'object') {
-                    deepExtend(target[prop], source[prop]);
-                } else {
-                    target[prop] = source[prop];
-                }
-            }
+  for (const source of sources) {
+    for (const prop in source) {
+      if (source.hasOwnProperty(prop)) {
+        if (
+          target[prop] &&
+          typeof target[prop] === "object" &&
+          source[prop] &&
+          typeof source[prop] === "object"
+        ) {
+          deepExtend(target[prop], source[prop]);
+        } else {
+          target[prop] = source[prop];
         }
+      }
     }
-    return target;
+  }
+  return target;
 }
 
 export function dispatchUIEvent(element, eventName) {
-    let e = document.createEvent("UIEvents");
-    e.initUIEvent(eventName, true, true, window, 1);
-    element.dispatchEvent(e);
+  let e = document.createEvent("UIEvents");
+  e.initUIEvent(eventName, true, true, window, 1);
+  element.dispatchEvent(e);
 }
diff --git a/frontend/test/visualizations/components/ChartSettings.unit.spec.js b/frontend/test/visualizations/components/ChartSettings.unit.spec.js
index 3ef2778f4632398eb077766d4fa6f80e47f5c2f1..664f01602a36b2b69e6443a9ea610e8cc88c64e9 100644
--- a/frontend/test/visualizations/components/ChartSettings.unit.spec.js
+++ b/frontend/test/visualizations/components/ChartSettings.unit.spec.js
@@ -5,69 +5,85 @@ import ChartSettings from "metabase/visualizations/components/ChartSettings";
 import { TableCard } from "../__support__/visualizations";
 
 import { mount } from "enzyme";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
 function renderChartSettings(enabled = true) {
-    const props = {
-        series: [
-            TableCard("Foo", {
-                card: {
-                    visualization_settings: {
-                        "table.columns": [{name: "Foo_col0", enabled: enabled}]
-                    }
-                }
-            }
-        )]
-    };
-    return mount(<ChartSettings {...props} onChange={(() => {})}/>);
+  const props = {
+    series: [
+      TableCard("Foo", {
+        card: {
+          visualization_settings: {
+            "table.columns": [{ name: "Foo_col0", enabled: enabled }],
+          },
+        },
+      }),
+    ],
+  };
+  return mount(<ChartSettings {...props} onChange={() => {}} />);
 }
 
 // The ExplicitSize component uses the matchMedia DOM API
 // which does not exist in jest's JSDOM
 Object.defineProperty(window, "matchMedia", {
-    value: jest.fn(() => { return {
-        matches: true,
-        addListener: (() => {}),
-        removeListener: (() => {}),
-    } })
+  value: jest.fn(() => {
+    return {
+      matches: true,
+      addListener: () => {},
+      removeListener: () => {},
+    };
+  }),
 });
 
 // We have to do some mocking here to avoid calls to GA and to Metabase settings
-jest.mock('metabase/lib/settings', () => ({
-    get: () => 'v'
-}))
+jest.mock("metabase/lib/settings", () => ({
+  get: () => "v",
+}));
 
 describe("ChartSettings", () => {
-    describe("toggling fields", () => {
-        describe("disabling all fields", () => {
-            it("should show null state", () => {
-                const chartSettings = renderChartSettings();
+  describe("toggling fields", () => {
+    describe("disabling all fields", () => {
+      it("should show null state", () => {
+        const chartSettings = renderChartSettings();
 
-                expect(chartSettings.find(".list-item [data-id=0] .Icon-check").length).toEqual(1);
-                expect(chartSettings.find("table").length).toEqual(1);
+        expect(
+          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
+        ).toEqual(1);
+        expect(chartSettings.find("table").length).toEqual(1);
 
-                click(chartSettings.find('.toggle-all .cursor-pointer'));
+        click(chartSettings.find(".toggle-all .cursor-pointer"));
 
-                expect(chartSettings.find(".list-item [data-id=0] .Icon-check").length).toEqual(0);
-                expect(chartSettings.find("table").length).toEqual(0);
-                expect(chartSettings.text()).toContain("Every field is hidden right now");
-            });
-        });
+        expect(
+          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
+        ).toEqual(0);
+        expect(chartSettings.find("table").length).toEqual(0);
+        expect(chartSettings.text()).toContain(
+          "Every field is hidden right now",
+        );
+      });
+    });
 
-        describe("enabling all fields", () => {
-            it("should show all columns", () => {
-                const chartSettings = renderChartSettings(false);
+    describe("enabling all fields", () => {
+      it("should show all columns", () => {
+        const chartSettings = renderChartSettings(false);
 
-                expect(chartSettings.find(".list-item [data-id=0] .Icon-check").length).toEqual(0);
-                expect(chartSettings.find("table").length).toEqual(0);
-                expect(chartSettings.text()).toContain("Every field is hidden right now");
+        expect(
+          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
+        ).toEqual(0);
+        expect(chartSettings.find("table").length).toEqual(0);
+        expect(chartSettings.text()).toContain(
+          "Every field is hidden right now",
+        );
 
-                click(chartSettings.find('.toggle-all .cursor-pointer'));
+        click(chartSettings.find(".toggle-all .cursor-pointer"));
 
-                expect(chartSettings.find(".list-item [data-id=0] .Icon-check").length).toEqual(1);
-                expect(chartSettings.find("table").length).toEqual(1);
-                expect(chartSettings.text()).not.toContain("Every field is hidden right now");
-            });
-        });
+        expect(
+          chartSettings.find(".list-item [data-id=0] .Icon-check").length,
+        ).toEqual(1);
+        expect(chartSettings.find("table").length).toEqual(1);
+        expect(chartSettings.text()).not.toContain(
+          "Every field is hidden right now",
+        );
+      });
     });
+  });
 });
diff --git a/frontend/test/visualizations/components/LegendVertical.unit.spec.js b/frontend/test/visualizations/components/LegendVertical.unit.spec.js
index dd7f39ae33a6cabb794dcba77c1e1bb90942e7e3..f75a8aec6fdaea8f9a4cf2159d58fe7457e9dd7a 100644
--- a/frontend/test/visualizations/components/LegendVertical.unit.spec.js
+++ b/frontend/test/visualizations/components/LegendVertical.unit.spec.js
@@ -3,12 +3,14 @@ import LegendVertical from "metabase/visualizations/components/LegendVertical.js
 import { mount } from "enzyme";
 
 describe("LegendVertical", () => {
-    it("should render string titles correctly", () => {
-        let legend = mount(<LegendVertical titles={["Hello"]} colors={["red"]} />);
-        expect(legend.text()).toEqual("Hello");
-    });
-    it("should render array titles correctly", () => {
-        let legend = mount(<LegendVertical titles={[["Hello", "world"]]} colors={["red"]} />);
-        expect(legend.text()).toEqual("Helloworld");
-    });
+  it("should render string titles correctly", () => {
+    let legend = mount(<LegendVertical titles={["Hello"]} colors={["red"]} />);
+    expect(legend.text()).toEqual("Hello");
+  });
+  it("should render array titles correctly", () => {
+    let legend = mount(
+      <LegendVertical titles={[["Hello", "world"]]} colors={["red"]} />,
+    );
+    expect(legend.text()).toEqual("Helloworld");
+  });
 });
diff --git a/frontend/test/visualizations/components/LineAreaBarChart.unit.spec.js b/frontend/test/visualizations/components/LineAreaBarChart.unit.spec.js
index 61e2e4b5f1ee1d1b160970729b6bee94a1794f64..4f28fd16a705d92d46d09a8175efd5990ddddb6c 100644
--- a/frontend/test/visualizations/components/LineAreaBarChart.unit.spec.js
+++ b/frontend/test/visualizations/components/LineAreaBarChart.unit.spec.js
@@ -3,623 +3,481 @@
 // HACK: Needed because of conflicts caused by circular dependencies
 import "metabase/visualizations/components/Visualization";
 
-import LineAreaBarChart from "metabase/visualizations/components/LineAreaBarChart"
+import LineAreaBarChart from "metabase/visualizations/components/LineAreaBarChart";
 
 const millisecondCard = {
-    "card": {
-        "description": null,
-        "archived": false,
-        "table_id": 1784,
-        "result_metadata": [
-            {
-                "base_type": "type/BigInteger",
-                "display_name": "Timestamp",
-                "name": "timestamp",
-                "special_type": "type/UNIXTimestampMilliseconds",
-                "unit": "week"
-            },
-            {
-                "base_type": "type/Integer",
-                "display_name": "count",
-                "name": "count",
-                "special_type": "type/Number"
-            }
-        ],
-        "creator": {
-            "email": "atte@metabase.com",
-            "first_name": "Atte",
-            "last_login": "2017-07-21T17:51:23.181Z",
-            "is_qbnewb": false,
-            "is_superuser": true,
-            "id": 1,
-            "last_name": "Keinänen",
-            "date_joined": "2017-03-17T03:37:27.396Z",
-            "common_name": "Atte Keinänen"
-        },
-        "database_id": 5,
-        "enable_embedding": false,
-        "collection_id": null,
-        "query_type": "query",
-        "name": "Toucan Incidents",
-        "query_average_duration": 501,
-        "creator_id": 1,
-        "updated_at": "2017-07-24T22:15:33.343Z",
-        "made_public_by_id": null,
-        "embedding_params": null,
-        "cache_ttl": null,
-        "dataset_query": {
-            "database": 5,
-            "type": "query",
-            "query": {
-                "source_table": 1784,
-                "aggregation": [
-                    [
-                        "count"
-                    ]
-                ],
-                "breakout": [
-                    [
-                        "datetime-field",
-                        [
-                            "field-id",
-                            8159
-                        ],
-                        "week"
-                    ]
-                ]
-            }
+  card: {
+    description: null,
+    archived: false,
+    table_id: 1784,
+    result_metadata: [
+      {
+        base_type: "type/BigInteger",
+        display_name: "Timestamp",
+        name: "timestamp",
+        special_type: "type/UNIXTimestampMilliseconds",
+        unit: "week",
+      },
+      {
+        base_type: "type/Integer",
+        display_name: "count",
+        name: "count",
+        special_type: "type/Number",
+      },
+    ],
+    creator: {
+      email: "atte@metabase.com",
+      first_name: "Atte",
+      last_login: "2017-07-21T17:51:23.181Z",
+      is_qbnewb: false,
+      is_superuser: true,
+      id: 1,
+      last_name: "Keinänen",
+      date_joined: "2017-03-17T03:37:27.396Z",
+      common_name: "Atte Keinänen",
+    },
+    database_id: 5,
+    enable_embedding: false,
+    collection_id: null,
+    query_type: "query",
+    name: "Toucan Incidents",
+    query_average_duration: 501,
+    creator_id: 1,
+    updated_at: "2017-07-24T22:15:33.343Z",
+    made_public_by_id: null,
+    embedding_params: null,
+    cache_ttl: null,
+    dataset_query: {
+      database: 5,
+      type: "query",
+      query: {
+        source_table: 1784,
+        aggregation: [["count"]],
+        breakout: [["datetime-field", ["field-id", 8159], "week"]],
+      },
+    },
+    id: 83,
+    display: "line",
+    visualization_settings: {
+      "graph.dimensions": ["timestamp"],
+      "graph.metrics": ["severity"],
+    },
+    created_at: "2017-07-21T19:40:40.102Z",
+    public_uuid: null,
+  },
+  data: {
+    rows: [
+      ["2015-05-31T00:00:00.000-07:00", 46],
+      ["2015-06-07T00:00:00.000-07:00", 47],
+      ["2015-06-14T00:00:00.000-07:00", 40],
+      ["2015-06-21T00:00:00.000-07:00", 60],
+      ["2015-06-28T00:00:00.000-07:00", 7],
+    ],
+    columns: ["timestamp", "count"],
+    native_form: {
+      query:
+        "SELECT count(*) AS \"count\", (date_trunc('week', CAST((CAST((TIMESTAMP '1970-01-01T00:00:00Z' + ((\"schema_126\".\"sad_toucan_incidents_incidents\".\"timestamp\" / 1000) * INTERVAL '1 second')) AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') AS \"timestamp\" FROM \"schema_126\".\"sad_toucan_incidents_incidents\" GROUP BY (date_trunc('week', CAST((CAST((TIMESTAMP '1970-01-01T00:00:00Z' + ((\"schema_126\".\"sad_toucan_incidents_incidents\".\"timestamp\" / 1000) * INTERVAL '1 second')) AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') ORDER BY (date_trunc('week', CAST((CAST((TIMESTAMP '1970-01-01T00:00:00Z' + ((\"schema_126\".\"sad_toucan_incidents_incidents\".\"timestamp\" / 1000) * INTERVAL '1 second')) AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') ASC",
+      params: null,
+    },
+    cols: [
+      {
+        description: null,
+        table_id: 1784,
+        schema_name: "schema_126",
+        special_type: "type/UNIXTimestampMilliseconds",
+        unit: "week",
+        name: "timestamp",
+        source: "breakout",
+        remapped_from: null,
+        extra_info: {},
+        fk_field_id: null,
+        remapped_to: null,
+        id: 8159,
+        visibility_type: "normal",
+        target: null,
+        display_name: "Timestamp",
+        base_type: "type/BigInteger",
+      },
+      {
+        description: null,
+        table_id: null,
+        special_type: "type/Number",
+        name: "count",
+        source: "aggregation",
+        remapped_from: null,
+        extra_info: {},
+        remapped_to: null,
+        id: null,
+        target: null,
+        display_name: "count",
+        base_type: "type/Integer",
+      },
+    ],
+    results_metadata: {
+      checksum: "H2XV8wuuBkFrxukvDt+Ehw==",
+      columns: [
+        {
+          base_type: "type/BigInteger",
+          display_name: "Timestamp",
+          name: "timestamp",
+          special_type: "type/UNIXTimestampMilliseconds",
+          unit: "week",
         },
-        "id": 83,
-        "display": "line",
-        "visualization_settings": {
-            "graph.dimensions": [
-                "timestamp"
-            ],
-            "graph.metrics": [
-                "severity"
-            ]
+        {
+          base_type: "type/Integer",
+          display_name: "count",
+          name: "count",
+          special_type: "type/Number",
         },
-        "created_at": "2017-07-21T19:40:40.102Z",
-        "public_uuid": null
+      ],
     },
-    "data": {
-        "rows": [
-            [
-                "2015-05-31T00:00:00.000-07:00",
-                46
-            ],
-            [
-                "2015-06-07T00:00:00.000-07:00",
-                47
-            ],
-            [
-                "2015-06-14T00:00:00.000-07:00",
-                40
-            ],
-            [
-                "2015-06-21T00:00:00.000-07:00",
-                60
-            ],
-            [
-                "2015-06-28T00:00:00.000-07:00",
-                7
-            ]
-        ],
-        "columns": [
-            "timestamp",
-            "count"
-        ],
-        "native_form": {
-            "query": "SELECT count(*) AS \"count\", (date_trunc('week', CAST((CAST((TIMESTAMP '1970-01-01T00:00:00Z' + ((\"schema_126\".\"sad_toucan_incidents_incidents\".\"timestamp\" / 1000) * INTERVAL '1 second')) AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') AS \"timestamp\" FROM \"schema_126\".\"sad_toucan_incidents_incidents\" GROUP BY (date_trunc('week', CAST((CAST((TIMESTAMP '1970-01-01T00:00:00Z' + ((\"schema_126\".\"sad_toucan_incidents_incidents\".\"timestamp\" / 1000) * INTERVAL '1 second')) AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') ORDER BY (date_trunc('week', CAST((CAST((TIMESTAMP '1970-01-01T00:00:00Z' + ((\"schema_126\".\"sad_toucan_incidents_incidents\".\"timestamp\" / 1000) * INTERVAL '1 second')) AS timestamp) + INTERVAL '1 day') AS timestamp)) - INTERVAL '1 day') ASC",
-            "params": null
-        },
-        "cols": [
-            {
-                "description": null,
-                "table_id": 1784,
-                "schema_name": "schema_126",
-                "special_type": "type/UNIXTimestampMilliseconds",
-                "unit": "week",
-                "name": "timestamp",
-                "source": "breakout",
-                "remapped_from": null,
-                "extra_info": {},
-                "fk_field_id": null,
-                "remapped_to": null,
-                "id": 8159,
-                "visibility_type": "normal",
-                "target": null,
-                "display_name": "Timestamp",
-                "base_type": "type/BigInteger"
-            },
-            {
-                "description": null,
-                "table_id": null,
-                "special_type": "type/Number",
-                "name": "count",
-                "source": "aggregation",
-                "remapped_from": null,
-                "extra_info": {},
-                "remapped_to": null,
-                "id": null,
-                "target": null,
-                "display_name": "count",
-                "base_type": "type/Integer"
-            }
-        ],
-        "results_metadata": {
-            "checksum": "H2XV8wuuBkFrxukvDt+Ehw==",
-            "columns": [
-                {
-                    "base_type": "type/BigInteger",
-                    "display_name": "Timestamp",
-                    "name": "timestamp",
-                    "special_type": "type/UNIXTimestampMilliseconds",
-                    "unit": "week"
-                },
-                {
-                    "base_type": "type/Integer",
-                    "display_name": "count",
-                    "name": "count",
-                    "special_type": "type/Number"
-                }
-            ]
-        }
-    }
+  },
 };
 
 const dateTimeCard = {
-    "card": {
-        "description": null,
-        "archived": false,
-        "table_id": 1,
-        "result_metadata": [
-            {
-                "base_type": "type/DateTime",
-                "display_name": "Created At",
-                "name": "CREATED_AT",
-                "description": "The date and time an order was submitted.",
-                "unit": "month"
-            },
-            {
-                "base_type": "type/Float",
-                "display_name": "sum",
-                "name": "sum",
-                "special_type": "type/Number"
-            }
-        ],
-        "creator": {
-            "email": "atte@metabase.com",
-            "first_name": "Atte",
-            "last_login": "2017-07-21T17:51:23.181Z",
-            "is_qbnewb": false,
-            "is_superuser": true,
-            "id": 1,
-            "last_name": "Keinänen",
-            "date_joined": "2017-03-17T03:37:27.396Z",
-            "common_name": "Atte Keinänen"
-        },
-        "database_id": 1,
-        "enable_embedding": false,
-        "collection_id": null,
-        "query_type": "query",
-        "name": "Orders over time",
-        "query_average_duration": 798,
-        "creator_id": 1,
-        "updated_at": "2017-07-24T22:15:33.603Z",
-        "made_public_by_id": null,
-        "embedding_params": null,
-        "cache_ttl": null,
-        "dataset_query": {
-            "database": 1,
-            "type": "query",
-            "query": {
-                "source_table": 1,
-                "aggregation": [
-                    [
-                        "sum",
-                        [
-                            "field-id",
-                            4
-                        ]
-                    ]
-                ],
-                "breakout": [
-                    [
-                        "datetime-field",
-                        [
-                            "field-id",
-                            1
-                        ],
-                        "month"
-                    ]
-                ]
-            }
+  card: {
+    description: null,
+    archived: false,
+    table_id: 1,
+    result_metadata: [
+      {
+        base_type: "type/DateTime",
+        display_name: "Created At",
+        name: "CREATED_AT",
+        description: "The date and time an order was submitted.",
+        unit: "month",
+      },
+      {
+        base_type: "type/Float",
+        display_name: "sum",
+        name: "sum",
+        special_type: "type/Number",
+      },
+    ],
+    creator: {
+      email: "atte@metabase.com",
+      first_name: "Atte",
+      last_login: "2017-07-21T17:51:23.181Z",
+      is_qbnewb: false,
+      is_superuser: true,
+      id: 1,
+      last_name: "Keinänen",
+      date_joined: "2017-03-17T03:37:27.396Z",
+      common_name: "Atte Keinänen",
+    },
+    database_id: 1,
+    enable_embedding: false,
+    collection_id: null,
+    query_type: "query",
+    name: "Orders over time",
+    query_average_duration: 798,
+    creator_id: 1,
+    updated_at: "2017-07-24T22:15:33.603Z",
+    made_public_by_id: null,
+    embedding_params: null,
+    cache_ttl: null,
+    dataset_query: {
+      database: 1,
+      type: "query",
+      query: {
+        source_table: 1,
+        aggregation: [["sum", ["field-id", 4]]],
+        breakout: [["datetime-field", ["field-id", 1], "month"]],
+      },
+    },
+    id: 25,
+    display: "line",
+    visualization_settings: {
+      "graph.colors": [
+        "#F1B556",
+        "#9cc177",
+        "#a989c5",
+        "#ef8c8c",
+        "#f9d45c",
+        "#F1B556",
+        "#A6E7F3",
+        "#7172AD",
+        "#7B8797",
+        "#6450e3",
+        "#55e350",
+        "#e35850",
+        "#77c183",
+        "#7d77c1",
+        "#c589b9",
+        "#bec589",
+        "#89c3c5",
+        "#c17777",
+        "#899bc5",
+        "#efce8c",
+        "#50e3ae",
+        "#be8cef",
+        "#8cefc6",
+        "#ef8cde",
+        "#b5f95c",
+        "#5cc2f9",
+        "#f95cd0",
+        "#c1a877",
+        "#f95c67",
+      ],
+    },
+    created_at: "2017-04-13T21:47:08.360Z",
+    public_uuid: null,
+  },
+  data: {
+    rows: [
+      ["2015-09-01T00:00:00.000-07:00", 533.45],
+      ["2015-10-01T00:00:00.000-07:00", 4130.049999999998],
+      ["2015-11-01T00:00:00.000-07:00", 6786.2599999999975],
+      ["2015-12-01T00:00:00.000-08:00", 12494.039999999994],
+      ["2016-01-01T00:00:00.000-08:00", 13594.169999999995],
+      ["2016-02-01T00:00:00.000-08:00", 16607.429999999997],
+      ["2016-03-01T00:00:00.000-08:00", 23600.45000000002],
+      ["2016-04-01T00:00:00.000-07:00", 24051.120000000024],
+      ["2016-05-01T00:00:00.000-07:00", 30163.87000000002],
+      ["2016-06-01T00:00:00.000-07:00", 30547.53000000002],
+      ["2016-07-01T00:00:00.000-07:00", 35808.49000000004],
+      ["2016-08-01T00:00:00.000-07:00", 43856.760000000075],
+      ["2016-09-01T00:00:00.000-07:00", 42831.96000000008],
+      ["2016-10-01T00:00:00.000-07:00", 50299.75000000006],
+      ["2016-11-01T00:00:00.000-07:00", 51861.37000000006],
+      ["2016-12-01T00:00:00.000-08:00", 55982.590000000106],
+      ["2017-01-01T00:00:00.000-08:00", 64462.70000000016],
+      ["2017-02-01T00:00:00.000-08:00", 58228.17000000016],
+      ["2017-03-01T00:00:00.000-08:00", 65618.70000000017],
+      ["2017-04-01T00:00:00.000-07:00", 66682.43000000018],
+      ["2017-05-01T00:00:00.000-07:00", 71817.04000000012],
+      ["2017-06-01T00:00:00.000-07:00", 72691.63000000018],
+      ["2017-07-01T00:00:00.000-07:00", 86210.1600000002],
+      ["2017-08-01T00:00:00.000-07:00", 81121.41000000008],
+      ["2017-09-01T00:00:00.000-07:00", 24811.320000000007],
+    ],
+    columns: ["CREATED_AT", "sum"],
+    native_form: {
+      query:
+        'SELECT sum("PUBLIC"."ORDERS"."SUBTOTAL") AS "sum", parsedatetime(formatdatetime("PUBLIC"."ORDERS"."CREATED_AT", \'yyyyMM\'), \'yyyyMM\') AS "CREATED_AT" FROM "PUBLIC"."ORDERS" GROUP BY parsedatetime(formatdatetime("PUBLIC"."ORDERS"."CREATED_AT", \'yyyyMM\'), \'yyyyMM\') ORDER BY parsedatetime(formatdatetime("PUBLIC"."ORDERS"."CREATED_AT", \'yyyyMM\'), \'yyyyMM\') ASC',
+      params: null,
+    },
+    cols: [
+      {
+        description: "The date and time an order was submitted.",
+        table_id: 1,
+        schema_name: "PUBLIC",
+        special_type: null,
+        unit: "month",
+        name: "CREATED_AT",
+        source: "breakout",
+        remapped_from: null,
+        extra_info: {},
+        fk_field_id: null,
+        remapped_to: null,
+        id: 1,
+        visibility_type: "normal",
+        target: null,
+        display_name: "Created At",
+        base_type: "type/DateTime",
+      },
+      {
+        description: null,
+        table_id: null,
+        special_type: "type/Number",
+        name: "sum",
+        source: "aggregation",
+        remapped_from: null,
+        extra_info: {},
+        remapped_to: null,
+        id: null,
+        target: null,
+        display_name: "sum",
+        base_type: "type/Float",
+      },
+    ],
+    results_metadata: {
+      checksum: "XIqamTTUJ9nbWlTwKc8Bpg==",
+      columns: [
+        {
+          base_type: "type/DateTime",
+          display_name: "Created At",
+          name: "CREATED_AT",
+          description: "The date and time an order was submitted.",
+          unit: "month",
         },
-        "id": 25,
-        "display": "line",
-        "visualization_settings": {
-            "graph.colors": [
-                "#F1B556",
-                "#9cc177",
-                "#a989c5",
-                "#ef8c8c",
-                "#f9d45c",
-                "#F1B556",
-                "#A6E7F3",
-                "#7172AD",
-                "#7B8797",
-                "#6450e3",
-                "#55e350",
-                "#e35850",
-                "#77c183",
-                "#7d77c1",
-                "#c589b9",
-                "#bec589",
-                "#89c3c5",
-                "#c17777",
-                "#899bc5",
-                "#efce8c",
-                "#50e3ae",
-                "#be8cef",
-                "#8cefc6",
-                "#ef8cde",
-                "#b5f95c",
-                "#5cc2f9",
-                "#f95cd0",
-                "#c1a877",
-                "#f95c67"
-            ]
+        {
+          base_type: "type/Float",
+          display_name: "sum",
+          name: "sum",
+          special_type: "type/Number",
         },
-        "created_at": "2017-04-13T21:47:08.360Z",
-        "public_uuid": null
+      ],
     },
-    "data": {
-        "rows": [
-            [
-                "2015-09-01T00:00:00.000-07:00",
-                533.45
-            ],
-            [
-                "2015-10-01T00:00:00.000-07:00",
-                4130.049999999998
-            ],
-            [
-                "2015-11-01T00:00:00.000-07:00",
-                6786.2599999999975
-            ],
-            [
-                "2015-12-01T00:00:00.000-08:00",
-                12494.039999999994
-            ],
-            [
-                "2016-01-01T00:00:00.000-08:00",
-                13594.169999999995
-            ],
-            [
-                "2016-02-01T00:00:00.000-08:00",
-                16607.429999999997
-            ],
-            [
-                "2016-03-01T00:00:00.000-08:00",
-                23600.45000000002
-            ],
-            [
-                "2016-04-01T00:00:00.000-07:00",
-                24051.120000000024
-            ],
-            [
-                "2016-05-01T00:00:00.000-07:00",
-                30163.87000000002
-            ],
-            [
-                "2016-06-01T00:00:00.000-07:00",
-                30547.53000000002
-            ],
-            [
-                "2016-07-01T00:00:00.000-07:00",
-                35808.49000000004
-            ],
-            [
-                "2016-08-01T00:00:00.000-07:00",
-                43856.760000000075
-            ],
-            [
-                "2016-09-01T00:00:00.000-07:00",
-                42831.96000000008
-            ],
-            [
-                "2016-10-01T00:00:00.000-07:00",
-                50299.75000000006
-            ],
-            [
-                "2016-11-01T00:00:00.000-07:00",
-                51861.37000000006
-            ],
-            [
-                "2016-12-01T00:00:00.000-08:00",
-                55982.590000000106
-            ],
-            [
-                "2017-01-01T00:00:00.000-08:00",
-                64462.70000000016
-            ],
-            [
-                "2017-02-01T00:00:00.000-08:00",
-                58228.17000000016
-            ],
-            [
-                "2017-03-01T00:00:00.000-08:00",
-                65618.70000000017
-            ],
-            [
-                "2017-04-01T00:00:00.000-07:00",
-                66682.43000000018
-            ],
-            [
-                "2017-05-01T00:00:00.000-07:00",
-                71817.04000000012
-            ],
-            [
-                "2017-06-01T00:00:00.000-07:00",
-                72691.63000000018
-            ],
-            [
-                "2017-07-01T00:00:00.000-07:00",
-                86210.1600000002
-            ],
-            [
-                "2017-08-01T00:00:00.000-07:00",
-                81121.41000000008
-            ],
-            [
-                "2017-09-01T00:00:00.000-07:00",
-                24811.320000000007
-            ]
-        ],
-        "columns": [
-            "CREATED_AT",
-            "sum"
-        ],
-        "native_form": {
-            "query": "SELECT sum(\"PUBLIC\".\"ORDERS\".\"SUBTOTAL\") AS \"sum\", parsedatetime(formatdatetime(\"PUBLIC\".\"ORDERS\".\"CREATED_AT\", 'yyyyMM'), 'yyyyMM') AS \"CREATED_AT\" FROM \"PUBLIC\".\"ORDERS\" GROUP BY parsedatetime(formatdatetime(\"PUBLIC\".\"ORDERS\".\"CREATED_AT\", 'yyyyMM'), 'yyyyMM') ORDER BY parsedatetime(formatdatetime(\"PUBLIC\".\"ORDERS\".\"CREATED_AT\", 'yyyyMM'), 'yyyyMM') ASC",
-            "params": null
-        },
-        "cols": [
-            {
-                "description": "The date and time an order was submitted.",
-                "table_id": 1,
-                "schema_name": "PUBLIC",
-                "special_type": null,
-                "unit": "month",
-                "name": "CREATED_AT",
-                "source": "breakout",
-                "remapped_from": null,
-                "extra_info": {},
-                "fk_field_id": null,
-                "remapped_to": null,
-                "id": 1,
-                "visibility_type": "normal",
-                "target": null,
-                "display_name": "Created At",
-                "base_type": "type/DateTime"
-            },
-            {
-                "description": null,
-                "table_id": null,
-                "special_type": "type/Number",
-                "name": "sum",
-                "source": "aggregation",
-                "remapped_from": null,
-                "extra_info": {},
-                "remapped_to": null,
-                "id": null,
-                "target": null,
-                "display_name": "sum",
-                "base_type": "type/Float"
-            }
-        ],
-        "results_metadata": {
-            "checksum": "XIqamTTUJ9nbWlTwKc8Bpg==",
-            "columns": [
-                {
-                    "base_type": "type/DateTime",
-                    "display_name": "Created At",
-                    "name": "CREATED_AT",
-                    "description": "The date and time an order was submitted.",
-                    "unit": "month"
-                },
-                {
-                    "base_type": "type/Float",
-                    "display_name": "sum",
-                    "name": "sum",
-                    "special_type": "type/Number"
-                }
-            ]
-        }
-    }
+  },
 };
 
 const numberCard = {
-    "card": {
-        "description": null,
-        "archived": false,
-        "labels": [],
-        "table_id": 4,
-        "result_metadata": [
-            {
-                "base_type": "type/Integer",
-                "display_name": "Ratings",
-                "name": "RATING",
-                "description": "The rating (on a scale of 1-5) the user left.",
-                "special_type": "type/Number"
-            },
-            {
-                "base_type": "type/Integer",
-                "display_name": "count",
-                "name": "count",
-                "special_type": "type/Number"
-            }
-        ],
-        "creator": {
-            "email": "atte@metabase.com",
-            "first_name": "Atte",
-            "last_login": "2017-07-21T17:51:23.181Z",
-            "is_qbnewb": false,
-            "is_superuser": true,
-            "id": 1,
-            "last_name": "Keinänen",
-            "date_joined": "2017-03-17T03:37:27.396Z",
-            "common_name": "Atte Keinänen"
-        },
-        "database_id": 1,
-        "enable_embedding": false,
-        "collection_id": 2,
-        "query_type": "query",
-        "name": "Reviews by Rating",
-        "creator_id": 1,
-        "updated_at": "2017-07-24T22:15:29.911Z",
-        "made_public_by_id": null,
-        "embedding_params": null,
-        "cache_ttl": null,
-        "dataset_query": {
-            "database": 1,
-            "type": "query",
-            "query": {
-                "source_table": 4,
-                "aggregation": [
-                    [
-                        "count"
-                    ]
-                ],
-                "breakout": [
-                    [
-                        "field-id",
-                        33
-                    ]
-                ]
-            }
+  card: {
+    description: null,
+    archived: false,
+    labels: [],
+    table_id: 4,
+    result_metadata: [
+      {
+        base_type: "type/Integer",
+        display_name: "Ratings",
+        name: "RATING",
+        description: "The rating (on a scale of 1-5) the user left.",
+        special_type: "type/Number",
+      },
+      {
+        base_type: "type/Integer",
+        display_name: "count",
+        name: "count",
+        special_type: "type/Number",
+      },
+    ],
+    creator: {
+      email: "atte@metabase.com",
+      first_name: "Atte",
+      last_login: "2017-07-21T17:51:23.181Z",
+      is_qbnewb: false,
+      is_superuser: true,
+      id: 1,
+      last_name: "Keinänen",
+      date_joined: "2017-03-17T03:37:27.396Z",
+      common_name: "Atte Keinänen",
+    },
+    database_id: 1,
+    enable_embedding: false,
+    collection_id: 2,
+    query_type: "query",
+    name: "Reviews by Rating",
+    creator_id: 1,
+    updated_at: "2017-07-24T22:15:29.911Z",
+    made_public_by_id: null,
+    embedding_params: null,
+    cache_ttl: null,
+    dataset_query: {
+      database: 1,
+      type: "query",
+      query: {
+        source_table: 4,
+        aggregation: [["count"]],
+        breakout: [["field-id", 33]],
+      },
+    },
+    id: 86,
+    display: "line",
+    visualization_settings: {},
+    collection: {
+      id: 2,
+      name: "Order Statistics",
+      slug: "order_statistics",
+      description: null,
+      color: "#7B8797",
+      archived: false,
+    },
+    favorite: false,
+    created_at: "2017-07-24T22:15:29.911Z",
+    public_uuid: null,
+  },
+  data: {
+    rows: [[1, 59], [2, 77], [3, 64], [4, 550], [5, 328]],
+    columns: ["RATING", "count"],
+    native_form: {
+      query:
+        'SELECT count(*) AS "count", "PUBLIC"."REVIEWS"."RATING" AS "RATING" FROM "PUBLIC"."REVIEWS" GROUP BY "PUBLIC"."REVIEWS"."RATING" ORDER BY "PUBLIC"."REVIEWS"."RATING" ASC',
+      params: null,
+    },
+    cols: [
+      {
+        description: "The rating (on a scale of 1-5) the user left.",
+        table_id: 4,
+        schema_name: "PUBLIC",
+        special_type: "type/Number",
+        name: "RATING",
+        source: "breakout",
+        remapped_from: null,
+        extra_info: {},
+        fk_field_id: null,
+        remapped_to: null,
+        id: 33,
+        visibility_type: "normal",
+        target: null,
+        display_name: "Ratings",
+        base_type: "type/Integer",
+      },
+      {
+        description: null,
+        table_id: null,
+        special_type: "type/Number",
+        name: "count",
+        source: "aggregation",
+        remapped_from: null,
+        extra_info: {},
+        remapped_to: null,
+        id: null,
+        target: null,
+        display_name: "count",
+        base_type: "type/Integer",
+      },
+    ],
+    results_metadata: {
+      checksum: "jTfxUHHttR31J8lQBqJ/EA==",
+      columns: [
+        {
+          base_type: "type/Integer",
+          display_name: "Ratings",
+          name: "RATING",
+          description: "The rating (on a scale of 1-5) the user left.",
+          special_type: "type/Number",
         },
-        "id": 86,
-        "display": "line",
-        "visualization_settings": {},
-        "collection": {
-            "id": 2,
-            "name": "Order Statistics",
-            "slug": "order_statistics",
-            "description": null,
-            "color": "#7B8797",
-            "archived": false
+        {
+          base_type: "type/Integer",
+          display_name: "count",
+          name: "count",
+          special_type: "type/Number",
         },
-        "favorite": false,
-        "created_at": "2017-07-24T22:15:29.911Z",
-        "public_uuid": null
+      ],
     },
-    "data": {
-        "rows": [
-            [
-                1,
-                59
-            ],
-            [
-                2,
-                77
-            ],
-            [
-                3,
-                64
-            ],
-            [
-                4,
-                550
-            ],
-            [
-                5,
-                328
-            ]
-        ],
-        "columns": [
-            "RATING",
-            "count"
-        ],
-        "native_form": {
-            "query": "SELECT count(*) AS \"count\", \"PUBLIC\".\"REVIEWS\".\"RATING\" AS \"RATING\" FROM \"PUBLIC\".\"REVIEWS\" GROUP BY \"PUBLIC\".\"REVIEWS\".\"RATING\" ORDER BY \"PUBLIC\".\"REVIEWS\".\"RATING\" ASC",
-            "params": null
-        },
-        "cols": [
-            {
-                "description": "The rating (on a scale of 1-5) the user left.",
-                "table_id": 4,
-                "schema_name": "PUBLIC",
-                "special_type": "type/Number",
-                "name": "RATING",
-                "source": "breakout",
-                "remapped_from": null,
-                "extra_info": {},
-                "fk_field_id": null,
-                "remapped_to": null,
-                "id": 33,
-                "visibility_type": "normal",
-                "target": null,
-                "display_name": "Ratings",
-                "base_type": "type/Integer"
-            },
-            {
-                "description": null,
-                "table_id": null,
-                "special_type": "type/Number",
-                "name": "count",
-                "source": "aggregation",
-                "remapped_from": null,
-                "extra_info": {},
-                "remapped_to": null,
-                "id": null,
-                "target": null,
-                "display_name": "count",
-                "base_type": "type/Integer"
-            }
-        ],
-        "results_metadata": {
-            "checksum": "jTfxUHHttR31J8lQBqJ/EA==",
-            "columns": [
-                {
-                    "base_type": "type/Integer",
-                    "display_name": "Ratings",
-                    "name": "RATING",
-                    "description": "The rating (on a scale of 1-5) the user left.",
-                    "special_type": "type/Number"
-                },
-                {
-                    "base_type": "type/Integer",
-                    "display_name": "count",
-                    "name": "count",
-                    "special_type": "type/Number"
-                }
-            ]
-        }
-    }
-}
+  },
+};
 
 describe("LineAreaBarChart", () => {
-    it("should let you combine series with datetimes only", () => {
-        expect(LineAreaBarChart.seriesAreCompatible(dateTimeCard, dateTimeCard)).toBe(true);
-    });
-    it("should let you combine series with UNIX millisecond timestamps only", () => {
-        expect(LineAreaBarChart.seriesAreCompatible(dateTimeCard, dateTimeCard)).toBe(true);
-    });
-    it("should let you combine series with numbers only", () => {
-        expect(LineAreaBarChart.seriesAreCompatible(numberCard, numberCard)).toBe(true);
-    });
-    it("should let you combine series with UNIX millisecond timestamps and datetimes", () => {
-        expect(LineAreaBarChart.seriesAreCompatible(millisecondCard, dateTimeCard)).toBe(true);
-        expect(LineAreaBarChart.seriesAreCompatible(dateTimeCard, millisecondCard)).toBe(true);
-    })
-    it("should not let you combine series with UNIX millisecond timestamps and numbers", () => {
-        expect(LineAreaBarChart.seriesAreCompatible(numberCard, millisecondCard)).toBe(false);
-        expect(LineAreaBarChart.seriesAreCompatible(millisecondCard, numberCard)).toBe(false);
-    })
-    it("should not let you combine series with datetimes and numbers", () => {
-        expect(LineAreaBarChart.seriesAreCompatible(numberCard, dateTimeCard)).toBe(false);
-        expect(LineAreaBarChart.seriesAreCompatible(dateTimeCard, numberCard)).toBe(false);
-    })
-})
\ No newline at end of file
+  it("should let you combine series with datetimes only", () => {
+    expect(
+      LineAreaBarChart.seriesAreCompatible(dateTimeCard, dateTimeCard),
+    ).toBe(true);
+  });
+  it("should let you combine series with UNIX millisecond timestamps only", () => {
+    expect(
+      LineAreaBarChart.seriesAreCompatible(dateTimeCard, dateTimeCard),
+    ).toBe(true);
+  });
+  it("should let you combine series with numbers only", () => {
+    expect(LineAreaBarChart.seriesAreCompatible(numberCard, numberCard)).toBe(
+      true,
+    );
+  });
+  it("should let you combine series with UNIX millisecond timestamps and datetimes", () => {
+    expect(
+      LineAreaBarChart.seriesAreCompatible(millisecondCard, dateTimeCard),
+    ).toBe(true);
+    expect(
+      LineAreaBarChart.seriesAreCompatible(dateTimeCard, millisecondCard),
+    ).toBe(true);
+  });
+  it("should not let you combine series with UNIX millisecond timestamps and numbers", () => {
+    expect(
+      LineAreaBarChart.seriesAreCompatible(numberCard, millisecondCard),
+    ).toBe(false);
+    expect(
+      LineAreaBarChart.seriesAreCompatible(millisecondCard, numberCard),
+    ).toBe(false);
+  });
+  it("should not let you combine series with datetimes and numbers", () => {
+    expect(LineAreaBarChart.seriesAreCompatible(numberCard, dateTimeCard)).toBe(
+      false,
+    );
+    expect(LineAreaBarChart.seriesAreCompatible(dateTimeCard, numberCard)).toBe(
+      false,
+    );
+  });
+});
diff --git a/frontend/test/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js b/frontend/test/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js
index 6dafe9873ab4faf4a076acd101588e869af04a5e..9f6d63747b3b856988baa0eab251e3d3faaa4743 100644
--- a/frontend/test/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js
+++ b/frontend/test/visualizations/components/LineAreaBarRenderer-bar.unit.spec.js
@@ -1,112 +1,141 @@
 import "__support__/mocks"; // included explicitly whereas with integrated tests it comes with __support__/integrated_tests
 
 import lineAreaBarRenderer from "metabase/visualizations/lib/LineAreaBarRenderer";
-import { NumberColumn, StringColumn, dispatchUIEvent } from "../__support__/visualizations";
+import {
+  NumberColumn,
+  StringColumn,
+  dispatchUIEvent,
+} from "../__support__/visualizations";
 
 const DEFAULT_SETTINGS = {
-    "graph.x_axis.scale": "ordinal",
-    "graph.y_axis.scale": "linear",
-    "graph.x_axis.axis_enabled": true,
-    "graph.y_axis.axis_enabled": true,
-    "graph.colors": ["#00FF00", "#FF0000"]
+  "graph.x_axis.scale": "ordinal",
+  "graph.y_axis.scale": "linear",
+  "graph.x_axis.axis_enabled": true,
+  "graph.y_axis.axis_enabled": true,
+  "graph.colors": ["#00FF00", "#FF0000"],
 };
 
 describe("LineAreaBarRenderer-bar", () => {
-    let element;
-    const qsa = (selector) => [...element.querySelectorAll(selector)];
+  let element;
+  const qsa = selector => [...element.querySelectorAll(selector)];
 
-    beforeEach(function() {
-        document.body.insertAdjacentHTML('afterbegin', '<div id="fixture" style="height: 800px; width: 1200px;">');
-        element = document.getElementById('fixture');
-    });
+  beforeEach(function() {
+    document.body.insertAdjacentHTML(
+      "afterbegin",
+      '<div id="fixture" style="height: 800px; width: 1200px;">',
+    );
+    element = document.getElementById("fixture");
+  });
 
-    afterEach(function() {
-        document.body.removeChild(document.getElementById('fixture'));
-    });
+  afterEach(function() {
+    document.body.removeChild(document.getElementById("fixture"));
+  });
 
-    ["area", "bar"].forEach(chartType =>
-        ["stacked", "normalized"].forEach(stack_type =>
-            it("should render a " + (stack_type || "") + " " + chartType + " chart with 2 series", function() {
-                return new Promise((resolve, reject) => {
-                    let hoverCount = 0;
-                    lineAreaBarRenderer(element, {
-                        chartType: chartType,
-                        series: [{
-                            card: {},
-                            data: {
-                                "cols": [StringColumn({
-                                    display_name: "Category",
-                                    source: "breakout"
-                                }), NumberColumn({display_name: "Sum", source: "aggregation"})],
-                                "rows": [["A", 1]]
-                            }
-                        }, {
-                            card: {},
-                            data: {
-                                "cols": [StringColumn({
-                                    display_name: "Category",
-                                    source: "breakout"
-                                }), NumberColumn({display_name: "Count", source: "aggregation"})],
-                                "rows": [["A", 2]]
-                            }
-                        }],
-                        settings: {
-                            ...DEFAULT_SETTINGS,
-                            "stackable.stack_type": stack_type
-                        },
-                        onHoverChange: (hover) => {
-                            try {
-                                const data = hover.data && hover.data.map(({key, value}) => ({key, value}));
-                                let standardDisplay
-                                let normalizedDisplay
+  ["area", "bar"].forEach(chartType =>
+    ["stacked", "normalized"].forEach(stack_type =>
+      it(
+        "should render a " +
+          (stack_type || "") +
+          " " +
+          chartType +
+          " chart with 2 series",
+        function() {
+          return new Promise((resolve, reject) => {
+            let hoverCount = 0;
+            lineAreaBarRenderer(element, {
+              chartType: chartType,
+              series: [
+                {
+                  card: {},
+                  data: {
+                    cols: [
+                      StringColumn({
+                        display_name: "Category",
+                        source: "breakout",
+                      }),
+                      NumberColumn({
+                        display_name: "Sum",
+                        source: "aggregation",
+                      }),
+                    ],
+                    rows: [["A", 1]],
+                  },
+                },
+                {
+                  card: {},
+                  data: {
+                    cols: [
+                      StringColumn({
+                        display_name: "Category",
+                        source: "breakout",
+                      }),
+                      NumberColumn({
+                        display_name: "Count",
+                        source: "aggregation",
+                      }),
+                    ],
+                    rows: [["A", 2]],
+                  },
+                },
+              ],
+              settings: {
+                ...DEFAULT_SETTINGS,
+                "stackable.stack_type": stack_type,
+              },
+              onHoverChange: hover => {
+                try {
+                  const data =
+                    hover.data &&
+                    hover.data.map(({ key, value }) => ({ key, value }));
+                  let standardDisplay;
+                  let normalizedDisplay;
 
+                  hoverCount++;
+                  if (hoverCount === 1) {
+                    standardDisplay = [
+                      { key: "Category", value: "A" },
+                      { key: "Sum", value: 1 },
+                    ];
 
-                                hoverCount++;
-                                if (hoverCount === 1) {
-                                    standardDisplay = [
-                                        {key: "Category", value: "A"},
-                                        {key: "Sum", value: 1}
-                                    ];
+                    normalizedDisplay = [
+                      { key: "Category", value: "A" },
+                      { key: "% Sum", value: "33%" },
+                    ];
 
-                                    normalizedDisplay = [
-                                        {key: "Category", value: "A"},
-                                        {key: "% Sum", value: "33%"}
-                                    ]
+                    expect(data).toEqual(
+                      stack_type === "normalized"
+                        ? normalizedDisplay
+                        : standardDisplay,
+                    );
+                    dispatchUIEvent(qsa(".bar, .dot")[1], "mousemove");
+                  } else if (hoverCount === 2) {
+                    standardDisplay = [
+                      { key: "Category", value: "A" },
+                      { key: "Count", value: 2 },
+                    ];
 
-                                    expect(data).toEqual(
-                                        stack_type === "normalized"
-                                            ? normalizedDisplay
-                                            : standardDisplay
-                                    )
-                                    dispatchUIEvent(qsa(".bar, .dot")[1], "mousemove");
-                                } else if (hoverCount === 2) {
+                    normalizedDisplay = [
+                      { key: "Category", value: "A" },
+                      { key: "% Count", value: "67%" },
+                    ];
 
-                                    standardDisplay = [
-                                        {key: "Category", value: "A"},
-                                        {key: "Count", value: 2}
-                                    ];
+                    expect(data).toEqual(
+                      stack_type === "normalized"
+                        ? normalizedDisplay
+                        : standardDisplay,
+                    );
 
-                                    normalizedDisplay = [
-                                        {key: "Category", value: "A"},
-                                        {key: "% Count", value: "67%"}
-                                    ]
-
-                                    expect(data).toEqual(
-                                        stack_type === "normalized"
-                                            ? normalizedDisplay
-                                            : standardDisplay
-                                    );
-
-                                    resolve()
-                                }
-                            } catch(e) {
-                                reject(e)
-                            }
-                        }
-                    });
-                    dispatchUIEvent(qsa(".bar, .dot")[0], "mousemove");
-                })
-            })
-        )
-    )
+                    resolve();
+                  }
+                } catch (e) {
+                  reject(e);
+                }
+              },
+            });
+            dispatchUIEvent(qsa(".bar, .dot")[0], "mousemove");
+          });
+        },
+      ),
+    ),
+  );
 });
diff --git a/frontend/test/visualizations/components/LineAreaBarRenderer-scatter.unit.spec.js b/frontend/test/visualizations/components/LineAreaBarRenderer-scatter.unit.spec.js
index 0827d013b4cb5e51779d04484219284f6479d8af..19960030a278cc012145ae15755866f5cffceb7d 100644
--- a/frontend/test/visualizations/components/LineAreaBarRenderer-scatter.unit.spec.js
+++ b/frontend/test/visualizations/components/LineAreaBarRenderer-scatter.unit.spec.js
@@ -5,77 +5,88 @@ import lineAreaBarRenderer from "metabase/visualizations/lib/LineAreaBarRenderer
 import { NumberColumn, dispatchUIEvent } from "../__support__/visualizations";
 
 const DEFAULT_SETTINGS = {
-    "graph.x_axis.scale": "linear",
-    "graph.y_axis.scale": "linear",
-    "graph.x_axis.axis_enabled": true,
-    "graph.y_axis.axis_enabled": true,
-    "graph.colors": ["#000000"]
+  "graph.x_axis.scale": "linear",
+  "graph.y_axis.scale": "linear",
+  "graph.x_axis.axis_enabled": true,
+  "graph.y_axis.axis_enabled": true,
+  "graph.colors": ["#000000"],
 };
 
 describe("LineAreaBarRenderer-scatter", () => {
-    let element;
-    const qsa = (selector) => [...window.document.documentElement.querySelectorAll(selector)];
+  let element;
+  const qsa = selector => [
+    ...window.document.documentElement.querySelectorAll(selector),
+  ];
 
-    beforeEach(function() {
-        document.body.insertAdjacentHTML('afterbegin', '<div id="fixture" style="height: 800px; width: 1200px;">');
-        element = document.getElementById('fixture');
-    });
-
-    afterEach(function() {
-        document.body.removeChild(document.getElementById('fixture'));
-    });
+  beforeEach(function() {
+    document.body.insertAdjacentHTML(
+      "afterbegin",
+      '<div id="fixture" style="height: 800px; width: 1200px;">',
+    );
+    element = document.getElementById("fixture");
+  });
 
-    it("should render a scatter chart with 2 dimensions", function(done) {
-        lineAreaBarRenderer(element, {
-            chartType: "scatter",
-            series: [{
-                data: {
-                    "cols" : [NumberColumn({ display_name: "A", source: "breakout" }), NumberColumn({ display_name: "B", source: "breakout" })],
-                    "rows" : [[1,2]]
-                }
-            }],
-            settings: DEFAULT_SETTINGS,
-            onHoverChange: (hover) => {
-                expect(hover.data.length).toBe(2);
-                expect(hover.data[0].key).toBe("A")
-                expect(hover.data[0].value).toBe(1)
-                expect(hover.data[1].key).toBe("B")
-                expect(hover.data[1].value).toBe(2)
+  afterEach(function() {
+    document.body.removeChild(document.getElementById("fixture"));
+  });
 
+  it("should render a scatter chart with 2 dimensions", function(done) {
+    lineAreaBarRenderer(element, {
+      chartType: "scatter",
+      series: [
+        {
+          data: {
+            cols: [
+              NumberColumn({ display_name: "A", source: "breakout" }),
+              NumberColumn({ display_name: "B", source: "breakout" }),
+            ],
+            rows: [[1, 2]],
+          },
+        },
+      ],
+      settings: DEFAULT_SETTINGS,
+      onHoverChange: hover => {
+        expect(hover.data.length).toBe(2);
+        expect(hover.data[0].key).toBe("A");
+        expect(hover.data[0].value).toBe(1);
+        expect(hover.data[1].key).toBe("B");
+        expect(hover.data[1].value).toBe(2);
 
-                done()
-            }
-        });
-
-        dispatchUIEvent(qsa(".bubble")[0], "mousemove");
+        done();
+      },
     });
 
-    it("should render a scatter chart with 2 dimensions and 1 metric", function(done) {
-        lineAreaBarRenderer(element, {
-            chartType: "scatter",
-            series: [{
-                data: {
-                    "cols" : [
-                        NumberColumn({ display_name: "A", source: "breakout" }),
-                        NumberColumn({ display_name: "B", source: "breakout" }),
-                        NumberColumn({ display_name: "C", source: "aggregation" })
-                    ],
-                    "rows" : [[1,2,3]]
-                }
-            }],
-            settings: DEFAULT_SETTINGS,
-            onHoverChange: (hover) => {
-                expect(hover.data.length).toBe(3);
-                expect(hover.data[0].key).toBe("A")
-                expect(hover.data[0].value).toBe(1)
-                expect(hover.data[1].key).toBe("B")
-                expect(hover.data[1].value).toBe(2)
-                expect(hover.data[2].key).toBe("C")
-                expect(hover.data[2].value).toBe(3)
-                done()
-            }
-        });
+    dispatchUIEvent(qsa(".bubble")[0], "mousemove");
+  });
 
-        dispatchUIEvent(qsa(".bubble")[0], "mousemove");
+  it("should render a scatter chart with 2 dimensions and 1 metric", function(done) {
+    lineAreaBarRenderer(element, {
+      chartType: "scatter",
+      series: [
+        {
+          data: {
+            cols: [
+              NumberColumn({ display_name: "A", source: "breakout" }),
+              NumberColumn({ display_name: "B", source: "breakout" }),
+              NumberColumn({ display_name: "C", source: "aggregation" }),
+            ],
+            rows: [[1, 2, 3]],
+          },
+        },
+      ],
+      settings: DEFAULT_SETTINGS,
+      onHoverChange: hover => {
+        expect(hover.data.length).toBe(3);
+        expect(hover.data[0].key).toBe("A");
+        expect(hover.data[0].value).toBe(1);
+        expect(hover.data[1].key).toBe("B");
+        expect(hover.data[1].value).toBe(2);
+        expect(hover.data[2].key).toBe("C");
+        expect(hover.data[2].value).toBe(3);
+        done();
+      },
     });
+
+    dispatchUIEvent(qsa(".bubble")[0], "mousemove");
+  });
 });
diff --git a/frontend/test/visualizations/components/LineAreaBarRenderer.unit.spec.js b/frontend/test/visualizations/components/LineAreaBarRenderer.unit.spec.js
index 34fd9d846ff89f23f1ff809cba2ca59f50fbca2d..ff003319c4c2c37164b5bc3e9fe5b362b985bc39 100644
--- a/frontend/test/visualizations/components/LineAreaBarRenderer.unit.spec.js
+++ b/frontend/test/visualizations/components/LineAreaBarRenderer.unit.spec.js
@@ -5,299 +5,305 @@ import { formatValue } from "metabase/lib/formatting";
 
 import d3 from "d3";
 
-import { NumberColumn, DateTimeColumn, StringColumn, dispatchUIEvent } from "../__support__/visualizations";
+import {
+  NumberColumn,
+  DateTimeColumn,
+  StringColumn,
+  dispatchUIEvent,
+} from "../__support__/visualizations";
 
-let formatTz = (offset) => (offset < 0 ? "-" : "+") + d3.format("02d")(Math.abs(offset)) + ":00"
+let formatTz = offset =>
+  (offset < 0 ? "-" : "+") + d3.format("02d")(Math.abs(offset)) + ":00";
 
-const BROWSER_TZ = formatTz(- new Date().getTimezoneOffset() / 60);
+const BROWSER_TZ = formatTz(-new Date().getTimezoneOffset() / 60);
 const ALL_TZS = d3.range(-1, 2).map(formatTz);
 
 describe("LineAreaBarRenderer", () => {
-    let element;
+  let element;
 
-    beforeEach(function() {
-        document.body.insertAdjacentHTML('afterbegin', '<div id="fixture-parent" style="height: 800px; width: 1200px;"><div id="fixture" /></div>');
-        element = document.getElementById('fixture');
+  beforeEach(function() {
+    document.body.insertAdjacentHTML(
+      "afterbegin",
+      '<div id="fixture-parent" style="height: 800px; width: 1200px;"><div id="fixture" /></div>',
+    );
+    element = document.getElementById("fixture");
+  });
+
+  afterEach(function() {
+    document.body.removeChild(document.getElementById("fixture-parent"));
+  });
+
+  it("should display numeric year in X-axis and tooltip correctly", () => {
+    return new Promise((resolve, reject) => {
+      renderTimeseriesLine({
+        rowsOfSeries: [[[2015, 1], [2016, 2], [2017, 3]]],
+        unit: "year",
+        onHoverChange: hover => {
+          try {
+            expect(
+              formatValue(hover.data[0].value, { column: hover.data[0].col }),
+            ).toEqual("2015");
+
+            // Doesn't return the correct ticks in Jest for some reason
+            // expect(qsa(".tick text").map(e => e.textContent)).toEqual([
+            //     "2015",
+            //     "2016",
+            //     "2017"
+            // ]);
+
+            resolve();
+          } catch (e) {
+            reject(e);
+          }
+        },
+      });
+
+      dispatchUIEvent(qs(".dot"), "mousemove");
     });
+  });
+
+  ["Z", ...ALL_TZS].forEach(tz =>
+    it(
+      "should display hourly data (in " +
+        tz +
+        " timezone) in X axis and tooltip consistently",
+      () => {
+        return new Promise((resolve, reject) => {
+          const rows = [
+            ["2016-10-03T20:00:00.000" + tz, 1],
+            ["2016-10-03T21:00:00.000" + tz, 1],
+          ];
+
+          renderTimeseriesLine({
+            rowsOfSeries: [rows],
+            unit: "hour",
+            onHoverChange: hover => {
+              try {
+                let expected = rows.map(row =>
+                  formatValue(row[0], {
+                    column: DateTimeColumn({ unit: "hour" }),
+                  }),
+                );
+                expect(
+                  formatValue(hover.data[0].value, {
+                    column: hover.data[0].col,
+                  }),
+                ).toEqual(expected[0]);
+                expect(
+                  qsa(".axis.x .tick text").map(e => e.textContent),
+                ).toEqual(expected);
+                resolve();
+              } catch (e) {
+                reject(e);
+              }
+            },
+          });
 
-    afterEach(function() {
-        document.body.removeChild(document.getElementById('fixture-parent'));
+          dispatchUIEvent(qs(".dot"), "mousemove");
+        });
+      },
+    ),
+  );
+
+  it("should display hourly data (in the browser's timezone) in X axis and tooltip consistently and correctly", () => {
+    return new Promise((resolve, reject) => {
+      const tz = BROWSER_TZ;
+      const rows = [
+        ["2016-01-01T01:00:00.000" + tz, 1],
+        ["2016-01-01T02:00:00.000" + tz, 1],
+        ["2016-01-01T03:00:00.000" + tz, 1],
+        ["2016-01-01T04:00:00.000" + tz, 1],
+      ];
+
+      renderTimeseriesLine({
+        rowsOfSeries: [rows],
+        unit: "hour",
+        onHoverChange: hover => {
+          try {
+            expect(
+              formatValue(rows[0][0], {
+                column: DateTimeColumn({ unit: "hour" }),
+              }),
+            ).toEqual("1 AM - January 1, 2016");
+            expect(
+              formatValue(hover.data[0].value, { column: hover.data[0].col }),
+            ).toEqual("1 AM - January 1, 2016");
+
+            expect(qsa(".axis.x .tick text").map(e => e.textContent)).toEqual([
+              "1 AM - January 1, 2016",
+              "2 AM - January 1, 2016",
+              "3 AM - January 1, 2016",
+              "4 AM - January 1, 2016",
+            ]);
+
+            resolve();
+          } catch (e) {
+            reject(e);
+          }
+        },
+      });
+
+      dispatchUIEvent(qs(".dot"), "mousemove");
     });
+  });
 
-    it("should display numeric year in X-axis and tooltip correctly", () => {
-        return new Promise((resolve, reject) => {
-            renderTimeseriesLine({
-                rowsOfSeries: [
-                    [
-                        [2015, 1],
-                        [2016, 2],
-                        [2017, 3]
-                    ]
-                ],
-                unit: "year",
-                onHoverChange: (hover) => {
-                    try {
-                        expect(formatValue(hover.data[0].value, { column: hover.data[0].col })).toEqual(
-                            "2015"
-                        );
-
-                        // Doesn't return the correct ticks in Jest for some reason
-                        // expect(qsa(".tick text").map(e => e.textContent)).toEqual([
-                        //     "2015",
-                        //     "2016",
-                        //     "2017"
-                        // ]);
-
-                        resolve();
-                    } catch(e) {
-                        reject(e);
-                    }
-                }
-            });
-
-            dispatchUIEvent(qs(".dot"), "mousemove");
-        })
-    });
+  describe("should render correctly a compound line graph", () => {
+    const rowsOfNonemptyCard = [[2015, 1], [2016, 2], [2017, 3]];
 
-    ["Z", ...ALL_TZS].forEach(tz =>
-        it("should display hourly data (in " + tz + " timezone) in X axis and tooltip consistently", () => {
-            return new Promise((resolve, reject) => {
-                const rows = [
-                    ["2016-10-03T20:00:00.000" + tz, 1],
-                    ["2016-10-03T21:00:00.000" + tz, 1],
-                ];
-
-                renderTimeseriesLine({
-                    rowsOfSeries: [rows],
-                    unit: "hour",
-                    onHoverChange: (hover) => {
-                        try {
-                            let expected = rows.map(row => formatValue(row[0], {column: DateTimeColumn({unit: "hour"})}));
-                            expect(formatValue(hover.data[0].value, {column: hover.data[0].col})).toEqual(
-                                expected[0]
-                            );
-                            expect(qsa(".axis.x .tick text").map(e => e.textContent)).toEqual(expected);
-                            resolve();
-                        } catch(e) {
-                            reject(e)
-                        }
-                    }
-                });
-
-                dispatchUIEvent(qs(".dot"), "mousemove");
-            });
-        })
-    );
+    it("when only second series is not empty", () => {
+      renderTimeseriesLine({
+        rowsOfSeries: [[], rowsOfNonemptyCard, [], []],
+        unit: "hour",
+      });
 
-    it("should display hourly data (in the browser's timezone) in X axis and tooltip consistently and correctly", () => {
-        return new Promise((resolve, reject) => {
-            const tz = BROWSER_TZ;
-            const rows = [
-                ["2016-01-01T01:00:00.000" + tz, 1],
-                ["2016-01-01T02:00:00.000" + tz, 1],
-                ["2016-01-01T03:00:00.000" + tz, 1],
-                ["2016-01-01T04:00:00.000" + tz, 1]
-            ];
-
-            renderTimeseriesLine({
-                rowsOfSeries: [rows],
-                unit: "hour",
-                onHoverChange: (hover) => {
-                    try {
-                        expect(formatValue(rows[0][0], {column: DateTimeColumn({unit: "hour"})})).toEqual(
-                            '1 AM - January 1, 2016'
-                        )
-                        expect(formatValue(hover.data[0].value, {column: hover.data[0].col})).toEqual(
-                            '1 AM - January 1, 2016'
-                        );
-
-                        expect(qsa(".axis.x .tick text").map(e => e.textContent)).toEqual([
-                            '1 AM - January 1, 2016',
-                            '2 AM - January 1, 2016',
-                            '3 AM - January 1, 2016',
-                            '4 AM - January 1, 2016'
-                        ]);
-
-                        resolve();
-                    } catch (e) {
-                        reject(e);
-                    }
-                }
-            });
-
-            dispatchUIEvent(qs(".dot"), "mousemove");
-        });
+      // A simple check to ensure that lines are rendered as expected
+      expect(qs(".line")).not.toBe(null);
     });
 
-    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(".line")).not.toBe(null);
-        });
+    it("when only first series is not empty", () => {
+      renderTimeseriesLine({
+        rowsOfSeries: [rowsOfNonemptyCard, [], [], []],
+        unit: "hour",
+      });
 
-        it("when only first series is not empty", () => {
-            renderTimeseriesLine({
-                rowsOfSeries: [
-                    rowsOfNonemptyCard, [], [], []
-                ],
-                unit: "hour"
-            });
+      expect(qs(".line")).not.toBe(null);
+    });
 
-            expect(qs(".line")).not.toBe(null);
-        });
+    it("when there are many empty and nonempty values ", () => {
+      renderTimeseriesLine({
+        rowsOfSeries: [
+          [],
+          rowsOfNonemptyCard,
+          [],
+          [],
+          rowsOfNonemptyCard,
+          [],
+          rowsOfNonemptyCard,
+        ],
+        unit: "hour",
+      });
+      expect(qs(".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(".bar")).not.toBe(null);
+    });
 
-        it("when there are many empty and nonempty values ", () => {
-            renderTimeseriesLine({
-                rowsOfSeries: [
-                    [], rowsOfNonemptyCard, [], [], rowsOfNonemptyCard, [], rowsOfNonemptyCard
-                ],
-                unit: "hour"
-            });
-            expect(qs(".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(".bar")).not.toBe(null);
-        });
+    it("when only first series is not empty", () => {
+      renderScalarBar({
+        scalars: [["Non-empty value", 15], ["Empty value", null]],
+      });
+      expect(qs(".bar")).not.toBe(null);
+    });
 
-        it("when only first series is not empty", () => {
-            renderScalarBar({
-                scalars: [
-                    ["Non-empty value", 15],
-                    ["Empty value", null]
-                ]
-            })
-            expect(qs(".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(".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(".goal .line")).not.toBe(null);
+      expect(qs(".goal text")).not.toBe(null);
+      expect(qs(".goal text").textContent).toEqual("Goal");
+    });
 
-        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(".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('.goal .line')).not.toBe(null)
-            expect(qs('.goal text')).not.toBe(null)
-            expect(qs('.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(".goal text"), "mouseenter");
-        })
-
-    })
-
-    // querySelector shortcut
-    const qs = (selector) => element.querySelector(selector);
-
-    // querySelectorAll shortcut, casts to Array
-    const qsa = (selector) => [...element.querySelectorAll(selector)];
-
-    // helper for timeseries line charts
-    const renderTimeseriesLine = ({ rowsOfSeries, onHoverChange, unit, settings }) => {
-        lineAreaBarRenderer(element, {
-            chartType: "line",
-            series: rowsOfSeries.map((rows) => ({
-                data: {
-                    "cols" : [DateTimeColumn({ unit }), NumberColumn()],
-                    "rows" : rows
-                },
-                card: {
-                }
-            })),
-            settings: {
-                "graph.x_axis.scale": "timeseries",
-                "graph.x_axis.axis_enabled": true,
-                "graph.colors": ["#000000"],
-                ...settings,
-            },
-            onHoverChange
-        });
-    }
-
-    const renderScalarBar = ({ scalars, onHoverChange, unit }) => {
-        lineAreaBarRenderer(element, {
-            chartType: "bar",
-            series: scalars.map((scalar) => ({
-                data: {
-                    "cols" : [StringColumn(), NumberColumn()],
-                    "rows" : [scalar]
-                },
-                card: {
-                }
-            })),
-            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
-        });
-    }
+    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(".goal text"), "mouseenter");
+    });
+  });
+
+  // querySelector shortcut
+  const qs = selector => element.querySelector(selector);
+
+  // querySelectorAll shortcut, casts to Array
+  const qsa = selector => [...element.querySelectorAll(selector)];
+
+  // helper for timeseries line charts
+  const renderTimeseriesLine = ({
+    rowsOfSeries,
+    onHoverChange,
+    unit,
+    settings,
+  }) => {
+    lineAreaBarRenderer(element, {
+      chartType: "line",
+      series: rowsOfSeries.map(rows => ({
+        data: {
+          cols: [DateTimeColumn({ unit }), NumberColumn()],
+          rows: rows,
+        },
+        card: {},
+      })),
+      settings: {
+        "graph.x_axis.scale": "timeseries",
+        "graph.x_axis.axis_enabled": true,
+        "graph.colors": ["#000000"],
+        ...settings,
+      },
+      onHoverChange,
+    });
+  };
+
+  const renderScalarBar = ({ scalars, onHoverChange, unit }) => {
+    lineAreaBarRenderer(element, {
+      chartType: "bar",
+      series: scalars.map(scalar => ({
+        data: {
+          cols: [StringColumn(), NumberColumn()],
+          rows: [scalar],
+        },
+        card: {},
+      })),
+      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/frontend/test/visualizations/components/ObjectDetail.integ.spec.js b/frontend/test/visualizations/components/ObjectDetail.integ.spec.js
index 87eea634ba071db48718f63870d4fb0ad62eb8d2..83d6c05191b3793229048dc20b028c34e3ed9a44 100644
--- a/frontend/test/visualizations/components/ObjectDetail.integ.spec.js
+++ b/frontend/test/visualizations/components/ObjectDetail.integ.spec.js
@@ -1,80 +1,77 @@
 import {
-    useSharedAdminLogin,
-    createSavedQuestion,
-    createTestStore
+  useSharedAdminLogin,
+  createSavedQuestion,
+  createTestStore,
 } from "__support__/integrated_tests";
 
-import {
-    click,
-    dispatchBrowserEvent
-} from "__support__/enzyme_utils"
+import { click, dispatchBrowserEvent } from "__support__/enzyme_utils";
 
-import { mount } from 'enzyme'
-import { delay } from 'metabase/lib/promise'
+import { mount } from "enzyme";
+import { delay } from "metabase/lib/promise";
 
-import {
-    INITIALIZE_QB,
-    QUERY_COMPLETED,
-} from "metabase/query_builder/actions";
+import { INITIALIZE_QB, QUERY_COMPLETED } from "metabase/query_builder/actions";
 
-import Question from "metabase-lib/lib/Question"
+import Question from "metabase-lib/lib/Question";
 
 import { getMetadata } from "metabase/selectors/metadata";
 
-describe('ObjectDetail', () => {
-
-    beforeAll(async () => {
-        useSharedAdminLogin()
-    })
+describe("ObjectDetail", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
 
-    describe('Increment and Decrement', () => {
-        it('should properly increment and decrement object deteail', async () => {
-            const store = await createTestStore()
-            const newQuestion = Question.create({databaseId: 1, tableId: 1, metadata: getMetadata(store.getState())})
-                .query()
-                .addFilter(["=", ["field-id", 2], 2])
-                .question()
-                .setDisplayName('Object Detail')
+  describe("Increment and Decrement", () => {
+    it("should properly increment and decrement object deteail", async () => {
+      const store = await createTestStore();
+      const newQuestion = Question.create({
+        databaseId: 1,
+        tableId: 1,
+        metadata: getMetadata(store.getState()),
+      })
+        .query()
+        .addFilter(["=", ["field-id", 2], 2])
+        .question()
+        .setDisplayName("Object Detail");
 
-            const savedQuestion = await createSavedQuestion(newQuestion);
+      const savedQuestion = await createSavedQuestion(newQuestion);
 
-            store.pushPath(savedQuestion.getUrl());
+      store.pushPath(savedQuestion.getUrl());
 
-            const app = mount(store.getAppContainer());
+      const app = mount(store.getAppContainer());
 
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
-            await delay(100); // Trying to address random CI failures with a small delay
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+      await delay(100); // Trying to address random CI failures with a small delay
 
-            expect(app.find('.ObjectDetail h1').text()).toEqual("2")
+      expect(app.find(".ObjectDetail h1").text()).toEqual("2");
 
-            const previousObjectTrigger = app.find('.Icon.Icon-backArrow')
-            click(previousObjectTrigger)
+      const previousObjectTrigger = app.find(".Icon.Icon-backArrow");
+      click(previousObjectTrigger);
 
-            await store.waitForActions([QUERY_COMPLETED]);
-            await delay(100); // Trying to address random CI failures with a small delay
+      await store.waitForActions([QUERY_COMPLETED]);
+      await delay(100); // Trying to address random CI failures with a small delay
 
-            expect(app.find('.ObjectDetail h1').text()).toEqual("1")
-            const nextObjectTrigger = app.find('.Icon.Icon-forwardArrow')
-            click(nextObjectTrigger)
+      expect(app.find(".ObjectDetail h1").text()).toEqual("1");
+      const nextObjectTrigger = app.find(".Icon.Icon-forwardArrow");
+      click(nextObjectTrigger);
 
-            await store.waitForActions([QUERY_COMPLETED]);
-            await delay(100); // Trying to address random CI failures with a small delay
+      await store.waitForActions([QUERY_COMPLETED]);
+      await delay(100); // Trying to address random CI failures with a small delay
 
-            expect(app.find('.ObjectDetail h1').text()).toEqual("2")
+      expect(app.find(".ObjectDetail h1").text()).toEqual("2");
 
-            // test keyboard shortcuts
+      // test keyboard shortcuts
 
-            // left arrow
-            dispatchBrowserEvent('keydown', { key: 'ArrowLeft' })
-            await store.waitForActions([QUERY_COMPLETED]);
-            await delay(100); // Trying to address random CI failures with a small delay
-            expect(app.find('.ObjectDetail h1').text()).toEqual("1")
+      // left arrow
+      dispatchBrowserEvent("keydown", { key: "ArrowLeft" });
+      await store.waitForActions([QUERY_COMPLETED]);
+      await delay(100); // Trying to address random CI failures with a small delay
+      expect(app.find(".ObjectDetail h1").text()).toEqual("1");
 
-            // left arrow
-            dispatchBrowserEvent('keydown', { key: 'ArrowRight' })
-            await store.waitForActions([QUERY_COMPLETED]);
-            await delay(100); // Trying to address random CI failures with a small delay
-            expect(app.find('.ObjectDetail h1').text()).toEqual("2")
-        })
-    })
-})
+      // left arrow
+      dispatchBrowserEvent("keydown", { key: "ArrowRight" });
+      await store.waitForActions([QUERY_COMPLETED]);
+      await delay(100); // Trying to address random CI failures with a small delay
+      expect(app.find(".ObjectDetail h1").text()).toEqual("2");
+    });
+  });
+});
diff --git a/frontend/test/visualizations/components/ObjectDetail.unit.spec.js b/frontend/test/visualizations/components/ObjectDetail.unit.spec.js
index a0d793654b7e9b1cd34441c69ec0b0a5c9f38e30..4cbed8acf2c846448750d00c8154cffd3f18886f 100644
--- a/frontend/test/visualizations/components/ObjectDetail.unit.spec.js
+++ b/frontend/test/visualizations/components/ObjectDetail.unit.spec.js
@@ -1,44 +1,41 @@
-import React from 'react'
-import { mount } from 'enzyme'
+import React from "react";
+import { mount } from "enzyme";
 
 // Needed due to wrong dependency resolution order
 // eslint-disable-next-line no-unused-vars
 import "metabase/visualizations/components/Visualization";
 
-import { ObjectDetail } from 'metabase/visualizations/visualizations/ObjectDetail'
+import { ObjectDetail } from "metabase/visualizations/visualizations/ObjectDetail";
 import { TYPE } from "metabase/lib/types";
 
 const objectDetailCard = {
-    card: {
-        display: "object"
-    },
-    data: {
-        cols: [{
-            display_name: "Details",
-            special_type: TYPE.SerializedJSON
-        }],
-        columns: [
-            "details"
-        ],
-        rows: [
-            [JSON.stringify({hey: "yo"})]
-        ]
-    }
-}
+  card: {
+    display: "object",
+  },
+  data: {
+    cols: [
+      {
+        display_name: "Details",
+        special_type: TYPE.SerializedJSON,
+      },
+    ],
+    columns: ["details"],
+    rows: [[JSON.stringify({ hey: "yo" })]],
+  },
+};
 
-describe('ObjectDetail', () => {
-    describe('json field rendering', () => {
-        it('should properly display JSON special type data as JSON', () => {
+describe("ObjectDetail", () => {
+  describe("json field rendering", () => {
+    it("should properly display JSON special type data as JSON", () => {
+      const detail = mount(
+        <ObjectDetail
+          data={objectDetailCard.data}
+          series={objectDetailCard}
+          loadObjectDetailFKReferences={() => ({})}
+        />,
+      );
 
-            const detail = mount(
-                <ObjectDetail
-                    data={objectDetailCard.data}
-                    series={objectDetailCard}
-                    loadObjectDetailFKReferences={() => ({})}
-                />
-            )
-
-            expect(detail.find('.ObjectJSON').length).toEqual(1)
-        })
-    })
-})
+      expect(detail.find(".ObjectJSON").length).toEqual(1);
+    });
+  });
+});
diff --git a/frontend/test/visualizations/components/Visualization.integ.spec.js b/frontend/test/visualizations/components/Visualization.integ.spec.js
index 5fec943175502b44bee22ab46600b39423289462..d01249e5f1055f86765090ba706dbc7025cb1909 100644
--- a/frontend/test/visualizations/components/Visualization.integ.spec.js
+++ b/frontend/test/visualizations/components/Visualization.integ.spec.js
@@ -7,170 +7,225 @@ import Visualization from "metabase/visualizations/components/Visualization";
 import LegendHeader from "metabase/visualizations/components/LegendHeader";
 import LegendItem from "metabase/visualizations/components/LegendItem";
 
-import { ScalarCard, LineCard, MultiseriesLineCard, TextCard } from "../__support__/visualizations";
+import {
+  ScalarCard,
+  LineCard,
+  MultiseriesLineCard,
+  TextCard,
+} from "../__support__/visualizations";
 
 import { mount } from "enzyme";
-import { click } from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
 function renderVisualization(props) {
-    return mount(<Visualization className="spread" {...props} />);
+  return mount(<Visualization className="spread" {...props} />);
 }
 
-function getScalarTitles (scalarComponent) {
-    return scalarComponent.find('.Scalar-title').map((title) => title.text())
+function getScalarTitles(scalarComponent) {
+  return scalarComponent.find(".Scalar-title").map(title => title.text());
 }
 
 function getTitles(viz) {
-    return viz.find(LegendHeader).map(header =>
-        header.find(LegendItem).map((item) => item.props().title)
-    )
+  return viz
+    .find(LegendHeader)
+    .map(header => header.find(LegendItem).map(item => item.props().title));
 }
 
 describe("Visualization", () => {
-    describe("not in dashboard", () => {
-        describe("scalar card", () => {
-            it("should not render title", () => {
-                let viz = renderVisualization({ rawSeries: [ScalarCard("Foo")] });
-                expect(getScalarTitles(viz)).toEqual([]);
-            });
+  describe("not in dashboard", () => {
+    describe("scalar card", () => {
+      it("should not render title", () => {
+        let viz = renderVisualization({ rawSeries: [ScalarCard("Foo")] });
+        expect(getScalarTitles(viz)).toEqual([]);
+      });
+    });
+
+    describe("line card", () => {
+      it("should not render card title", () => {
+        let viz = renderVisualization({ rawSeries: [LineCard("Foo")] });
+        expect(getTitles(viz)).toEqual([]);
+      });
+      it("should not render setting title", () => {
+        let viz = renderVisualization({
+          rawSeries: [
+            LineCard("Foo", {
+              card: { visualization_settings: { "card.title": "Foo_title" } },
+            }),
+          ],
+        });
+        expect(getTitles(viz)).toEqual([]);
+      });
+      it("should render breakout multiseries titles", () => {
+        let viz = renderVisualization({
+          rawSeries: [MultiseriesLineCard("Foo")],
+        });
+        expect(getTitles(viz)).toEqual([["Foo_cat1", "Foo_cat2"]]);
+      });
+    });
+  });
+
+  describe("in dashboard", () => {
+    describe("scalar card", () => {
+      it("should render a scalar title, not a legend title", () => {
+        let viz = renderVisualization({
+          rawSeries: [ScalarCard("Foo")],
+          showTitle: true,
+          isDashboard: true,
+        });
+        expect(getTitles(viz)).toEqual([]);
+        expect(getScalarTitles(viz).length).toEqual(1);
+      });
+      it("should render title when loading", () => {
+        let viz = renderVisualization({
+          rawSeries: [ScalarCard("Foo", { data: null })],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([["Foo_name"]]);
+      });
+      it("should render title when there's an error", () => {
+        let viz = renderVisualization({
+          rawSeries: [ScalarCard("Foo")],
+          showTitle: true,
+          error: "oops",
         });
+        expect(getTitles(viz)).toEqual([["Foo_name"]]);
+      });
+      it("should not render scalar title", () => {
+        let viz = renderVisualization({
+          rawSeries: [ScalarCard("Foo")],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([]);
+      });
+      it("should render multi scalar titles", () => {
+        let viz = renderVisualization({
+          rawSeries: [ScalarCard("Foo"), ScalarCard("Bar")],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([["Foo_name", "Bar_name"]]);
+      });
+    });
 
-        describe("line card", () => {
-            it("should not render card title", () => {
-                let viz = renderVisualization({ rawSeries: [LineCard("Foo")] });
-                expect(getTitles(viz)).toEqual([]);
-            });
-            it("should not render setting title", () => {
-                let viz = renderVisualization({ rawSeries: [LineCard("Foo", { card: { visualization_settings: { "card.title": "Foo_title" }}})] });
-                expect(getTitles(viz)).toEqual([]);
-            });
-            it("should render breakout multiseries titles", () => {
-                let viz = renderVisualization({ rawSeries: [MultiseriesLineCard("Foo")] });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_cat1", "Foo_cat2"]
-                ]);
-            });
+    describe("line card", () => {
+      it("should render normal title", () => {
+        let viz = renderVisualization({
+          rawSeries: [LineCard("Foo")],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([["Foo_name"]]);
+      });
+      it("should render normal title and breakout multiseries titles", () => {
+        let viz = renderVisualization({
+          rawSeries: [MultiseriesLineCard("Foo")],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([
+          ["Foo_name"],
+          ["Foo_cat1", "Foo_cat2"],
+        ]);
+      });
+      it("should render dashboard multiseries titles", () => {
+        let viz = renderVisualization({
+          rawSeries: [LineCard("Foo"), LineCard("Bar")],
+          showTitle: true,
         });
+        expect(getTitles(viz)).toEqual([["Foo_name", "Bar_name"]]);
+      });
+      it("should render dashboard multiseries titles and chart setting title", () => {
+        let viz = renderVisualization({
+          rawSeries: [
+            LineCard("Foo", {
+              card: { visualization_settings: { "card.title": "Foo_title" } },
+            }),
+            LineCard("Bar"),
+          ],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([
+          ["Foo_title"],
+          ["Foo_name", "Bar_name"],
+        ]);
+      });
+      it("should render multiple breakout multiseries titles (with both card titles and breakout values)", () => {
+        let viz = renderVisualization({
+          rawSeries: [MultiseriesLineCard("Foo"), MultiseriesLineCard("Bar")],
+          showTitle: true,
+        });
+        expect(getTitles(viz)).toEqual([
+          [
+            "Foo_name: Foo_cat1",
+            "Foo_name: Foo_cat2",
+            "Bar_name: Bar_cat1",
+            "Bar_name: Bar_cat2",
+          ],
+        ]);
+      });
     });
 
-    describe("in dashboard", () => {
-        describe("scalar card", () => {
-            it("should render a scalar title, not a legend title", () => {
-                let viz = renderVisualization({ rawSeries: [ScalarCard("Foo")], showTitle: true, isDashboard: true });
-                expect(getTitles(viz)).toEqual([]);
-                expect(getScalarTitles(viz).length).toEqual(1);
-            });
-            it("should render title when loading", () => {
-                let viz = renderVisualization({ rawSeries: [ScalarCard("Foo", { data: null })], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name"]
-                ]);
-            });
-            it("should render title when there's an error", () => {
-                let viz = renderVisualization({ rawSeries: [ScalarCard("Foo")], showTitle: true, error: "oops" });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name"]
-                ]);
-            });
-            it("should not render scalar title", () => {
-                let viz = renderVisualization({ rawSeries: [ScalarCard("Foo")], showTitle: true });
-                expect(getTitles(viz)).toEqual([]);
-            });
-            it("should render multi scalar titles", () => {
-                let viz = renderVisualization({ rawSeries: [ScalarCard("Foo"), ScalarCard("Bar")], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name", "Bar_name"]
-                ]);
-            });
+    describe("text card", () => {
+      describe("when not editing", () => {
+        it("should not render edit and preview actions", () => {
+          let viz = renderVisualization({
+            rawSeries: [TextCard("Foo")],
+            isEditing: false,
+          });
+          expect(viz.find(".Icon-editdocument").length).toEqual(0);
+          expect(viz.find(".Icon-eye").length).toEqual(0);
         });
 
-        describe("line card", () => {
-            it("should render normal title", () => {
-                let viz = renderVisualization({ rawSeries: [LineCard("Foo")], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name"]
-                ]);
-            });
-            it("should render normal title and breakout multiseries titles", () => {
-                let viz = renderVisualization({ rawSeries: [MultiseriesLineCard("Foo")], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name"],
-                    ["Foo_cat1", "Foo_cat2"]
-                ]);
-            });
-            it("should render dashboard multiseries titles", () => {
-                let viz = renderVisualization({ rawSeries: [LineCard("Foo"), LineCard("Bar")], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name", "Bar_name"]
-                ]);
-            });
-            it("should render dashboard multiseries titles and chart setting title", () => {
-                let viz = renderVisualization({ rawSeries: [
-                    LineCard("Foo", { card: { visualization_settings: { "card.title": "Foo_title" }}}),
-                    LineCard("Bar")
-                ], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_title"],
-                    ["Foo_name", "Bar_name"]
-                ]);
-            });
-            it("should render multiple breakout multiseries titles (with both card titles and breakout values)", () => {
-                let viz = renderVisualization({ rawSeries: [MultiseriesLineCard("Foo"), MultiseriesLineCard("Bar")], showTitle: true });
-                expect(getTitles(viz)).toEqual([
-                    ["Foo_name: Foo_cat1", "Foo_name: Foo_cat2", "Bar_name: Bar_cat1", "Bar_name: Bar_cat2"]
-                ]);
-            });
+        it("should render in the view mode", () => {
+          const textCard = TextCard("Foo", {
+            card: {
+              display: "text",
+              visualization_settings: {
+                text: "# Foobar",
+              },
+            },
+          });
+          let viz = renderVisualization({
+            rawSeries: [textCard],
+            isEditing: false,
+          });
+          expect(viz.find("textarea").length).toEqual(0);
+          expect(viz.find(".text-card-markdown").find("h1").length).toEqual(1);
+          expect(viz.find(".text-card-markdown").text()).toEqual("Foobar");
+        });
+      });
+
+      describe("when editing", () => {
+        it("should render edit and preview actions", () => {
+          let viz = renderVisualization({
+            rawSeries: [TextCard("Foo")],
+            isEditing: true,
+          });
+          expect(viz.find(".Icon-editdocument").length).toEqual(1);
+          expect(viz.find(".Icon-eye").length).toEqual(1);
         });
 
-        describe("text card", () => {
-            describe("when not editing", () => {
-                it("should not render edit and preview actions", () => {
-                    let viz = renderVisualization({ rawSeries: [TextCard("Foo")], isEditing: false});
-                    expect(viz.find(".Icon-editdocument").length).toEqual(0);
-                    expect(viz.find(".Icon-eye").length).toEqual(0);
-                });
-
-                it("should render in the view mode", () => {
-                    const textCard = TextCard("Foo", {
-                        card: {
-                            display: "text",
-                            visualization_settings: {
-                                text: "# Foobar"
-                            }
-                        },
-                    });
-                    let viz = renderVisualization({ rawSeries: [textCard], isEditing: false});
-                    expect(viz.find("textarea").length).toEqual(0);
-                    expect(viz.find(".text-card-markdown").find("h1").length).toEqual(1);
-                    expect(viz.find(".text-card-markdown").text()).toEqual("Foobar");
-                });
-            });
+        it("should render in the edit mode", () => {
+          let viz = renderVisualization({
+            rawSeries: [TextCard("Foo")],
+            isEditing: true,
+          });
+          expect(viz.find("textarea").length).toEqual(1);
+        });
 
-            describe("when editing", () => {
-                it("should render edit and preview actions", () => {
-                    let viz = renderVisualization({ rawSeries: [TextCard("Foo")], isEditing: true});
-                    expect(viz.find(".Icon-editdocument").length).toEqual(1);
-                    expect(viz.find(".Icon-eye").length).toEqual(1);
-                });
-
-                it("should render in the edit mode", () => {
-                    let viz = renderVisualization({ rawSeries: [TextCard("Foo")], isEditing: true});
-                    expect(viz.find("textarea").length).toEqual(1);
-                });
-
-                describe("toggling edit/preview modes", () => {
-                    it("should switch between rendered markdown and textarea input", () => {
-                        let viz = renderVisualization({ rawSeries: [TextCard("Foo")], isEditing: true});
-                        expect(viz.find("textarea").length).toEqual(1);
-                        click(viz.find(".Icon-eye"));
-                        expect(viz.find("textarea").length).toEqual(0);
-                        expect(viz.find(".text-card-markdown").length).toEqual(1);
-                        click(viz.find(".Icon-editdocument"));
-                        expect(viz.find(".text-card-markdown").length).toEqual(0);
-                        expect(viz.find("textarea").length).toEqual(1);
-                    });
-                });
-            });
+        describe("toggling edit/preview modes", () => {
+          it("should switch between rendered markdown and textarea input", () => {
+            let viz = renderVisualization({
+              rawSeries: [TextCard("Foo")],
+              isEditing: true,
+            });
+            expect(viz.find("textarea").length).toEqual(1);
+            click(viz.find(".Icon-eye"));
+            expect(viz.find("textarea").length).toEqual(0);
+            expect(viz.find(".text-card-markdown").length).toEqual(1);
+            click(viz.find(".Icon-editdocument"));
+            expect(viz.find(".text-card-markdown").length).toEqual(0);
+            expect(viz.find("textarea").length).toEqual(1);
+          });
         });
+      });
     });
+  });
 });
diff --git a/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js b/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js
index 051493df93b9f6416fc86f0b5e1edb2cc7f267ad..5436c97fdf7d06cc6c036ff735e964ac6b6ede85 100644
--- a/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js
+++ b/frontend/test/visualizations/components/settings/ChartSettingOrderedFields.unit.spec.js
@@ -5,56 +5,74 @@ import ChartSettingOrderedFields from "metabase/visualizations/components/settin
 import { mount } from "enzyme";
 
 function renderChartSettingOrderedFields(props) {
-    return mount(<ChartSettingOrderedFields {...props} onChange={(() => {})} />);
+  return mount(<ChartSettingOrderedFields {...props} onChange={() => {}} />);
 }
 
 describe("ChartSettingOrderedFields", () => {
-    describe("isAnySelected", () => {
-        describe("when on or more fields are enabled", () => {
-            it("should be true", () => {
-                const chartSettings = renderChartSettingOrderedFields({
-                    columnNames: {id: "ID", text: "Text"},
-                    value: [{name: 'id', enabled: true}, {name: 'text', enabled: false}]
-                });
-                expect(chartSettings.instance().isAnySelected()).toEqual(true);
-            });
+  describe("isAnySelected", () => {
+    describe("when on or more fields are enabled", () => {
+      it("should be true", () => {
+        const chartSettings = renderChartSettingOrderedFields({
+          columnNames: { id: "ID", text: "Text" },
+          value: [
+            { name: "id", enabled: true },
+            { name: "text", enabled: false },
+          ],
         });
+        expect(chartSettings.instance().isAnySelected()).toEqual(true);
+      });
+    });
 
-        describe("when no fields are enabled", () => {
-            it("should be false", () => {
-                const chartSettings = renderChartSettingOrderedFields({
-                    columnNames: {id: "ID", text: "Text"},
-                    value: [{name: 'id', enabled: false}, {name: 'text', enabled: false}]
-                });
-                expect(chartSettings.instance().isAnySelected()).toEqual(false);
-            });
+    describe("when no fields are enabled", () => {
+      it("should be false", () => {
+        const chartSettings = renderChartSettingOrderedFields({
+          columnNames: { id: "ID", text: "Text" },
+          value: [
+            { name: "id", enabled: false },
+            { name: "text", enabled: false },
+          ],
         });
+        expect(chartSettings.instance().isAnySelected()).toEqual(false);
+      });
     });
+  });
 
-    describe("toggleAll", () => {
-        describe("when passed false", () => {
-            it("should mark all fields as enabled", () => {
-                const chartSettings = renderChartSettingOrderedFields({
-                    columnNames: {id: "ID", text: "Text"},
-                    value: [{name: 'id', enabled: false}, {name: 'text', enabled: false}]
-                });
-                const chartSettingsInstance = chartSettings.instance();
-                chartSettingsInstance.toggleAll(false);
-                expect(chartSettingsInstance.state.data.items).toEqual([{name: 'id', enabled: true}, {name: 'text', enabled: true}]);
-            });
+  describe("toggleAll", () => {
+    describe("when passed false", () => {
+      it("should mark all fields as enabled", () => {
+        const chartSettings = renderChartSettingOrderedFields({
+          columnNames: { id: "ID", text: "Text" },
+          value: [
+            { name: "id", enabled: false },
+            { name: "text", enabled: false },
+          ],
         });
+        const chartSettingsInstance = chartSettings.instance();
+        chartSettingsInstance.toggleAll(false);
+        expect(chartSettingsInstance.state.data.items).toEqual([
+          { name: "id", enabled: true },
+          { name: "text", enabled: true },
+        ]);
+      });
+    });
 
-        describe("when passed true", () => {
-            it("should mark all fields as disabled", () => {
-                const chartSettings = renderChartSettingOrderedFields({
-                    columnNames: {id: "ID", text: "Text"},
-                    value: [{name: 'id', enabled: true}, {name: 'text', enabled: true}]
-                });
-
-                const chartSettingsInstance = chartSettings.instance();
-                chartSettingsInstance.toggleAll(true);
-                expect(chartSettingsInstance.state.data.items).toEqual([{name: 'id', enabled: false}, {name: 'text', enabled: false}]);
-            });
+    describe("when passed true", () => {
+      it("should mark all fields as disabled", () => {
+        const chartSettings = renderChartSettingOrderedFields({
+          columnNames: { id: "ID", text: "Text" },
+          value: [
+            { name: "id", enabled: true },
+            { name: "text", enabled: true },
+          ],
         });
+
+        const chartSettingsInstance = chartSettings.instance();
+        chartSettingsInstance.toggleAll(true);
+        expect(chartSettingsInstance.state.data.items).toEqual([
+          { name: "id", enabled: false },
+          { name: "text", enabled: false },
+        ]);
+      });
     });
+  });
 });
diff --git a/frontend/test/visualizations/drillthroughs.integ.spec.js b/frontend/test/visualizations/drillthroughs.integ.spec.js
index c4365aa9d05239ab2103830c05918d4d15d49e9d..f8fe5409e6d0aac989b0b9f089ee8d7b972e4a65 100644
--- a/frontend/test/visualizations/drillthroughs.integ.spec.js
+++ b/frontend/test/visualizations/drillthroughs.integ.spec.js
@@ -1,153 +1,139 @@
-import React from 'react';
-import { shallow } from 'enzyme';
+import React from "react";
+import { shallow } from "enzyme";
 import Visualization from "metabase/visualizations/components/Visualization";
 
-import { initializeQB, navigateToNewCardInsideQB } from "metabase/query_builder/actions";
+import {
+  initializeQB,
+  navigateToNewCardInsideQB,
+} from "metabase/query_builder/actions";
 import { parse as urlParse } from "url";
 
 import {
-    useSharedAdminLogin,
-    createTestStore
+  useSharedAdminLogin,
+  createTestStore,
 } from "__support__/integrated_tests";
 
 import Question from "metabase-lib/lib/Question";
 import {
-    DATABASE_ID,
-    ORDERS_TABLE_ID,
-    metadata,
+  DATABASE_ID,
+  ORDERS_TABLE_ID,
+  metadata,
 } from "__support__/sample_dataset_fixture";
 import ChartClickActions from "metabase/visualizations/components/ChartClickActions";
 
-const store = createTestStore()
+const store = createTestStore();
 
 const getVisualization = (question, results, onChangeCardAndRun) =>
-    store.connectContainer(
-        <Visualization
-            rawSeries={[{card: question.card(), data: results[0].data}]}
-            onChangeCardAndRun={navigateToNewCardInsideQB}
-            metadata={metadata}
-        />
-    );
-
-const question = Question.create({databaseId: DATABASE_ID, tableId: ORDERS_TABLE_ID, metadata})
-    .query()
-    .addAggregation(["count"])
-    .question()
-
-describe('Visualization drill-through', () => {
-    beforeAll(async () => {
-        useSharedAdminLogin();
+  store.connectContainer(
+    <Visualization
+      rawSeries={[{ card: question.card(), data: results[0].data }]}
+      onChangeCardAndRun={navigateToNewCardInsideQB}
+      metadata={metadata}
+    />,
+  );
+
+const question = Question.create({
+  databaseId: DATABASE_ID,
+  tableId: ORDERS_TABLE_ID,
+  metadata,
+})
+  .query()
+  .addAggregation(["count"])
+  .question();
+
+describe("Visualization drill-through", () => {
+  beforeAll(async () => {
+    useSharedAdminLogin();
+  });
+
+  // NOTE: Should this be here or somewhere in QB directory?
+  // see docstring of navigateToNewCardInsideQB for all possible scenarios
+  describe("drill-through action inside query builder", () => {
+    describe("for an unsaved question", () => {
+      pending();
+      it("results in a correct url", async () => {
+        // NON-TESTED CODE FOLLOWS, JUST DOCUMENTING THE IDEA
+
+        // initialize the query builder state
+        // (we are intentionally simplifying things by not rendering the QB but just focusing the redux state instead)
+        await store.dispatch(initializeQB(urlParse(question.getUrl()), {}));
+
+        const results = await question.apiGetResults();
+        const viz = shallow(
+          getVisualization(question, results, navigateToNewCardInsideQB),
+        );
+        const clickActions = viz.find(ChartClickActions).dive();
+
+        const action = {}; // gets some real mode action here
+        // we should make handleClickAction async so that we know when navigateToNewCardInsideQB is ready
+        // (that actually only applies to the saved card scenario)
+        await clickActions.instance().handleClickAction(action);
+        expect(history.getCurrentLocation().hash).toBe("this-value-is-fixed");
+      });
+
+      it("shows the name and lineage correctly", () => {
+        // requires writing a new selector for QB
+        const getLineage = store => {};
+        expect(getLineage(store.getState())).toBe("some value");
+      });
+
+      it("results in correct query result", () => {});
+    });
+
+    describe("for a clean saved question", () => {
+      pending();
+
+      it("results in a correct url", async () => {});
+      it("shows the name and lineage correctly", () => {});
+      it("results in correct query result", () => {});
+    });
+
+    describe("for a dirty saved question", () => {
+      pending();
+
+      it("results in a correct url", () => {});
+      it("shows the name and lineage correctly", () => {});
+      it("results in correct query result", () => {});
+    });
+  });
+
+  describe("title/legend click action from dashboard", () => {
+    pending();
+
+    // NOTE Atte Keinänen 6/21/17: Listing here the scenarios that would be nice to test
+    // although we should start with a representative subset of these
+
+    describe("from a scalar card title", () => {
+      it("results in a correct url", () => {});
+      it("shows the name lineage correctly", () => {});
+      it("results in correct query result", () => {});
+    });
+
+    describe("from a dashcard multiscalar legend", () => {
+      it("results in a correct url", () => {});
+      it("shows the name and lineage correctly", () => {});
+      it("results in correct query result", () => {});
+    });
+  });
+
+  describe("drill-through action from dashboard", () => {
+    pending();
+    describe("from a scalar card value", () => {
+      it("results in a correct url", () => {});
+      it("shows the name and lineage correctly", () => {});
+      it("results in correct query result", () => {});
     });
 
-    // NOTE: Should this be here or somewhere in QB directory?
-    // see docstring of navigateToNewCardInsideQB for all possible scenarios
-    describe("drill-through action inside query builder", () => {
-        describe("for an unsaved question", () => {
-            pending();
-            it("results in a correct url", async () => {
-
-                // NON-TESTED CODE FOLLOWS, JUST DOCUMENTING THE IDEA
-
-                // initialize the query builder state
-                // (we are intentionally simplifying things by not rendering the QB but just focusing the redux state instead)
-                await store.dispatch(initializeQB(urlParse(question.getUrl()), {}))
-
-                const results = await question.apiGetResults();
-                const viz = shallow(getVisualization(question, results, navigateToNewCardInsideQB));
-                const clickActions = viz.find(ChartClickActions).dive();
-
-                const action = {} // gets some real mode action here
-                // we should make handleClickAction async so that we know when navigateToNewCardInsideQB is ready
-                // (that actually only applies to the saved card scenario)
-                await clickActions.instance().handleClickAction(action)
-                expect(history.getCurrentLocation().hash).toBe("this-value-is-fixed")
-            })
-
-            it("shows the name and lineage correctly", () => {
-                // requires writing a new selector for QB
-                const getLineage = (store) => {}
-                expect(getLineage(store.getState())).toBe("some value")
-            })
-
-            it("results in correct query result", () => {
-            })
-        })
-
-        describe("for a clean saved question", () => {
-            pending();
-
-            it("results in a correct url", async () => {
-            })
-            it("shows the name and lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        })
-
-        describe("for a dirty saved question", () => {
-            pending();
-            
-            it("results in a correct url", () => {
-            })
-            it("shows the name and lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        })
-    })
-
-    describe("title/legend click action from dashboard", () => {
-        pending();
-
-        // NOTE Atte Keinänen 6/21/17: Listing here the scenarios that would be nice to test
-        // although we should start with a representative subset of these
-
-        describe("from a scalar card title", () => {
-            it("results in a correct url", () => {
-            })
-            it("shows the name lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        });
-
-        describe("from a dashcard multiscalar legend", () => {
-            it("results in a correct url", () => {
-            })
-            it("shows the name and lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        });
+    describe("from a scalar with active filter applied", () => {
+      it("results in a correct url", () => {});
+      it("shows the name and lineage correctly", () => {});
+      it("results in correct query result", () => {});
     });
 
-    describe("drill-through action from dashboard", () => {
-        pending();
-        describe("from a scalar card value", () => {
-            it("results in a correct url", () => {
-            })
-            it("shows the name and lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        });
-
-        describe("from a scalar with active filter applied", () => {
-            it("results in a correct url", () => {
-            })
-            it("shows the name and lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        });
-
-        describe("from a aggregation multiscalar legend", () => {
-            it("results in a correct url", () => {
-            })
-            it("shows the name and lineage correctly", () => {
-            })
-            it("results in correct query result", () => {
-            })
-        });
+    describe("from a aggregation multiscalar legend", () => {
+      it("results in a correct url", () => {});
+      it("shows the name and lineage correctly", () => {});
+      it("results in correct query result", () => {});
     });
+  });
 });
diff --git a/frontend/test/visualizations/lib/errors.unit.spec.js b/frontend/test/visualizations/lib/errors.unit.spec.js
index 62199125b519e56ba20c07564f99050a4488bb54..5ca813c3700ba833d3f149e83f4b471b8b2d54a9 100644
--- a/frontend/test/visualizations/lib/errors.unit.spec.js
+++ b/frontend/test/visualizations/lib/errors.unit.spec.js
@@ -1,10 +1,10 @@
-import { MinRowsError } from 'metabase/visualizations/lib/errors';
+import { MinRowsError } from "metabase/visualizations/lib/errors";
 
-describe('MinRowsError', () => {
-    it("should be an instanceof Error", () => {
-        expect(new MinRowsError(1, 0) instanceof Error).toBe(true);
-    });
-    it("should be an instanceof MinRowsError", () => {
-        expect(new MinRowsError(1, 0) instanceof MinRowsError).toBe(true);
-    });
+describe("MinRowsError", () => {
+  it("should be an instanceof Error", () => {
+    expect(new MinRowsError(1, 0) instanceof Error).toBe(true);
+  });
+  it("should be an instanceof MinRowsError", () => {
+    expect(new MinRowsError(1, 0) instanceof MinRowsError).toBe(true);
+  });
 });
diff --git a/frontend/test/visualizations/lib/numeric.unit.spec.js b/frontend/test/visualizations/lib/numeric.unit.spec.js
index 9c37e860835c733a4271676c7da2ed026bd5f77d..5f8bef35de686994837ba2b1c72ab673502e339a 100644
--- a/frontend/test/visualizations/lib/numeric.unit.spec.js
+++ b/frontend/test/visualizations/lib/numeric.unit.spec.js
@@ -1,47 +1,47 @@
 import {
-    precision,
-    computeNumericDataInverval
-} from 'metabase/visualizations/lib/numeric';
+  precision,
+  computeNumericDataInverval,
+} from "metabase/visualizations/lib/numeric";
 
-describe('visualization.lib.numeric', () => {
-    describe('precision', () => {
-        const CASES = [
-            [0,     0],
-            [10,    10],
-            [-10,   10],
-            [1,     1],
-            [-1,    1],
-            [0.1,   0.1],
-            [-0.1,  0.1],
-            [0.01,  0.01],
-            [-0.01, 0.01],
-            [1.1,   0.1],
-            [-1.1,  0.1],
-            [0.5,   0.1],
-            [0.9,   0.1],
-            [-0.5,  0.1],
-            [-0.9,  0.1],
-        ];
-        for (const c of CASES) {
-            it("precision of " + c[0] + " should be " + c[1], () => {
-                expect(precision(c[0])).toEqual(c[1]);
-            });
-        }
-    });
-    describe('computeNumericDataInverval', () => {
-        const CASES = [
-            [[0],       1],
-            [[1],       1],
-            [[0, 1],       1],
-            [[0.1, 1],  0.1],
-            [[0.1, 10], 0.1],
-            [[10, 1],   1],
-            [[0, null, 1], 1]
-        ];
-        for (const c of CASES) {
-            it("precision of " + c[0] + " should be " + c[1], () => {
-                expect(computeNumericDataInverval(c[0])).toEqual(c[1]);
-            });
-        }
-    });
+describe("visualization.lib.numeric", () => {
+  describe("precision", () => {
+    const CASES = [
+      [0, 0],
+      [10, 10],
+      [-10, 10],
+      [1, 1],
+      [-1, 1],
+      [0.1, 0.1],
+      [-0.1, 0.1],
+      [0.01, 0.01],
+      [-0.01, 0.01],
+      [1.1, 0.1],
+      [-1.1, 0.1],
+      [0.5, 0.1],
+      [0.9, 0.1],
+      [-0.5, 0.1],
+      [-0.9, 0.1],
+    ];
+    for (const c of CASES) {
+      it("precision of " + c[0] + " should be " + c[1], () => {
+        expect(precision(c[0])).toEqual(c[1]);
+      });
+    }
+  });
+  describe("computeNumericDataInverval", () => {
+    const CASES = [
+      [[0], 1],
+      [[1], 1],
+      [[0, 1], 1],
+      [[0.1, 1], 0.1],
+      [[0.1, 10], 0.1],
+      [[10, 1], 1],
+      [[0, null, 1], 1],
+    ];
+    for (const c of CASES) {
+      it("precision of " + c[0] + " should be " + c[1], () => {
+        expect(computeNumericDataInverval(c[0])).toEqual(c[1]);
+      });
+    }
+  });
 });
diff --git a/frontend/test/visualizations/lib/settings.unit.spec.js b/frontend/test/visualizations/lib/settings.unit.spec.js
index b633b54142098b5033a2d0825368fa0968131a53..ede5c52cc1f58d039dc066c98a5f0acb9165ae56 100644
--- a/frontend/test/visualizations/lib/settings.unit.spec.js
+++ b/frontend/test/visualizations/lib/settings.unit.spec.js
@@ -1,35 +1,43 @@
 // NOTE: need to load visualizations first for getSettings to work
 import "metabase/visualizations/index";
 
-import { getSettings } from 'metabase/visualizations/lib/settings';
+import { getSettings } from "metabase/visualizations/lib/settings";
 
 import { DateTimeColumn, NumberColumn } from "../__support__/visualizations";
 
-describe('visualization_settings', () => {
-    describe('getSettings', () => {
-        it("should default to unstacked stacked", () => {
-            const settings = getSettings([{
-                card: {
-                    display: "area",
-                    visualization_settings: {}
-                },
-                data: {
-                    cols: [DateTimeColumn({ unit: "month" }), NumberColumn()]
-                }
-            }])
-            expect(settings["stackable.stack_type"]).toBe(null);
-        })
-        it("should default area chart to stacked for 1 dimensions and 2 metrics", () => {
-            const settings = getSettings([{
-                card: {
-                    display: "area",
-                    visualization_settings: {}
-                },
-                data: {
-                    cols: [DateTimeColumn({ unit: "month" }), NumberColumn(), NumberColumn()]
-                }
-            }])
-            expect(settings["stackable.stack_type"]).toBe("stacked");
-        })
+describe("visualization_settings", () => {
+  describe("getSettings", () => {
+    it("should default to unstacked stacked", () => {
+      const settings = getSettings([
+        {
+          card: {
+            display: "area",
+            visualization_settings: {},
+          },
+          data: {
+            cols: [DateTimeColumn({ unit: "month" }), NumberColumn()],
+          },
+        },
+      ]);
+      expect(settings["stackable.stack_type"]).toBe(null);
     });
+    it("should default area chart to stacked for 1 dimensions and 2 metrics", () => {
+      const settings = getSettings([
+        {
+          card: {
+            display: "area",
+            visualization_settings: {},
+          },
+          data: {
+            cols: [
+              DateTimeColumn({ unit: "month" }),
+              NumberColumn(),
+              NumberColumn(),
+            ],
+          },
+        },
+      ]);
+      expect(settings["stackable.stack_type"]).toBe("stacked");
+    });
+  });
 });
diff --git a/frontend/test/visualizations/lib/table.unit.spec.js b/frontend/test/visualizations/lib/table.unit.spec.js
index 4cfac3a139b4af8ea627209ba6f62bd1c01bc0bb..7495d36e45edf197f22c6d37a67c1ef1f7043370 100644
--- a/frontend/test/visualizations/lib/table.unit.spec.js
+++ b/frontend/test/visualizations/lib/table.unit.spec.js
@@ -1,64 +1,120 @@
-import { getTableCellClickedObject, isColumnRightAligned } from "metabase/visualizations/lib/table";
+import {
+  getTableCellClickedObject,
+  isColumnRightAligned,
+} from "metabase/visualizations/lib/table";
 import { TYPE } from "metabase/lib/types";
 
 const RAW_COLUMN = {
-    source: "fields"
-}
+  source: "fields",
+};
 const METRIC_COLUMN = {
-    source: "aggregation"
-}
+  source: "aggregation",
+};
 const DIMENSION_COLUMN = {
-    source: "breakout"
-}
+  source: "breakout",
+};
 
 describe("metabase/visualization/lib/table", () => {
-    describe("getTableCellClickedObject", () => {
-        describe("normal table", () => {
-            it("should work with a raw data cell", () => {
-                expect(getTableCellClickedObject({ rows: [[0]], cols: [RAW_COLUMN]}, 0, 0, false)).toEqual({
-                    value: 0,
-                    column: RAW_COLUMN
-                });
-            })
-            it("should work with a dimension cell", () => {
-                expect(getTableCellClickedObject({ rows: [[1, 2]], cols: [DIMENSION_COLUMN, METRIC_COLUMN]}, 0, 0, false)).toEqual({
-                    value: 1,
-                    column: DIMENSION_COLUMN
-                });
-            })
-            it("should work with a metric cell", () => {
-                expect(getTableCellClickedObject({ rows: [[1, 2]], cols: [DIMENSION_COLUMN, METRIC_COLUMN]}, 0, 1, false)).toEqual({
-                    value: 2,
-                    column: METRIC_COLUMN,
-                    dimensions: [{
-                        value: 1,
-                        column: DIMENSION_COLUMN
-                    }]
-                });
-            })
-        })
-        describe("pivoted table", () => {
-            // TODO:
-        })
-    })
-
-    describe("isColumnRightAligned", () => {
-        it("should return true for numeric columns without a special type", () => {
-            expect(isColumnRightAligned({ base_type: TYPE.Integer })).toBe(true);
-        });
-        it("should return true for numeric columns with special type Number", () => {
-            expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.Number })).toBe(true);
+  describe("getTableCellClickedObject", () => {
+    describe("normal table", () => {
+      it("should work with a raw data cell", () => {
+        expect(
+          getTableCellClickedObject(
+            { rows: [[0]], cols: [RAW_COLUMN] },
+            0,
+            0,
+            false,
+          ),
+        ).toEqual({
+          value: 0,
+          column: RAW_COLUMN,
         });
-        it("should return true for numeric columns with special type latitude or longitude ", () => {
-            expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.Latitude })).toBe(true);
-            expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.Longitude })).toBe(true);
+      });
+      it("should work with a dimension cell", () => {
+        expect(
+          getTableCellClickedObject(
+            { rows: [[1, 2]], cols: [DIMENSION_COLUMN, METRIC_COLUMN] },
+            0,
+            0,
+            false,
+          ),
+        ).toEqual({
+          value: 1,
+          column: DIMENSION_COLUMN,
         });
-        it("should return false for numeric columns with special type zip code", () => {
-            expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.ZipCode })).toBe(false)
+      });
+      it("should work with a metric cell", () => {
+        expect(
+          getTableCellClickedObject(
+            { rows: [[1, 2]], cols: [DIMENSION_COLUMN, METRIC_COLUMN] },
+            0,
+            1,
+            false,
+          ),
+        ).toEqual({
+          value: 2,
+          column: METRIC_COLUMN,
+          dimensions: [
+            {
+              value: 1,
+              column: DIMENSION_COLUMN,
+            },
+          ],
         });
-        it("should return false for numeric columns with special type FK or PK", () => {
-            expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.FK })).toBe(false);
-            expect(isColumnRightAligned({ base_type: TYPE.Integer, special_type: TYPE.FK })).toBe(false);
-        });
-    })
-})
+      });
+    });
+    describe("pivoted table", () => {
+      // TODO:
+    });
+  });
+
+  describe("isColumnRightAligned", () => {
+    it("should return true for numeric columns without a special type", () => {
+      expect(isColumnRightAligned({ base_type: TYPE.Integer })).toBe(true);
+    });
+    it("should return true for numeric columns with special type Number", () => {
+      expect(
+        isColumnRightAligned({
+          base_type: TYPE.Integer,
+          special_type: TYPE.Number,
+        }),
+      ).toBe(true);
+    });
+    it("should return true for numeric columns with special type latitude or longitude ", () => {
+      expect(
+        isColumnRightAligned({
+          base_type: TYPE.Integer,
+          special_type: TYPE.Latitude,
+        }),
+      ).toBe(true);
+      expect(
+        isColumnRightAligned({
+          base_type: TYPE.Integer,
+          special_type: TYPE.Longitude,
+        }),
+      ).toBe(true);
+    });
+    it("should return false for numeric columns with special type zip code", () => {
+      expect(
+        isColumnRightAligned({
+          base_type: TYPE.Integer,
+          special_type: TYPE.ZipCode,
+        }),
+      ).toBe(false);
+    });
+    it("should return false for numeric columns with special type FK or PK", () => {
+      expect(
+        isColumnRightAligned({
+          base_type: TYPE.Integer,
+          special_type: TYPE.FK,
+        }),
+      ).toBe(false);
+      expect(
+        isColumnRightAligned({
+          base_type: TYPE.Integer,
+          special_type: TYPE.FK,
+        }),
+      ).toBe(false);
+    });
+  });
+});
diff --git a/frontend/test/visualizations/lib/timeseries.unit.spec.js b/frontend/test/visualizations/lib/timeseries.unit.spec.js
index b83323b42694a0ec81220a88473f17a195f4b0f9..8242ba6bfc2036a5e1603cf5db83e7fd367fe915 100644
--- a/frontend/test/visualizations/lib/timeseries.unit.spec.js
+++ b/frontend/test/visualizations/lib/timeseries.unit.spec.js
@@ -1,80 +1,147 @@
 import {
-    dimensionIsTimeseries,
-    computeTimeseriesDataInverval
-} from 'metabase/visualizations/lib/timeseries';
+  dimensionIsTimeseries,
+  computeTimeseriesDataInverval,
+} from "metabase/visualizations/lib/timeseries";
 
 import { TYPE } from "metabase/lib/types";
 
-describe('visualization.lib.timeseries', () => {
-    describe('dimensionIsTimeseries', () => {
-        // examples from https://en.wikipedia.org/wiki/ISO_8601
-        const ISO_8601_DATES = [
-            "2016-02-12",
-            "2016-02-12T03:21:55+00:00",
-            "2016-02-12T03:21:55Z",
-            "20160212T032155Z",
-            "2016-W06",
-            "2016-W06-5",
-            "2016-043"
-        ];
+describe("visualization.lib.timeseries", () => {
+  describe("dimensionIsTimeseries", () => {
+    // examples from https://en.wikipedia.org/wiki/ISO_8601
+    const ISO_8601_DATES = [
+      "2016-02-12",
+      "2016-02-12T03:21:55+00:00",
+      "2016-02-12T03:21:55Z",
+      "20160212T032155Z",
+      "2016-W06",
+      "2016-W06-5",
+      "2016-043",
+    ];
 
-        const NOT_DATES = [
-            "100",
-            "100 %",
-            "scanner 005"
-        ];
+    const NOT_DATES = ["100", "100 %", "scanner 005"];
 
-        it("should detect Date column as timeseries", () => {
-            expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Date }]})).toBe(true);
-        });
-        it("should detect Time column as timeseries", () => {
-            expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Time }]})).toBe(true);
-        });
-        it("should detect DateTime column as timeseries", () => {
-            expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.DateTime }]})).toBe(true);
-        });
-        ISO_8601_DATES.forEach(isoDate => {
-            it("should detect values with ISO 8601 formatted string '" + isoDate + "' as timeseries", () => {
-                expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Text }], rows: [[isoDate]]})).toBe(true);
-            })
-        });
-        NOT_DATES.forEach(notDate => {
-            it("should not detect value '" + notDate + "' as timeseries", () => {
-                expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Text }], rows: [[notDate]]})).toBe(false);
-            });
-        });
+    it("should detect Date column as timeseries", () => {
+      expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Date }] })).toBe(
+        true,
+      );
     });
+    it("should detect Time column as timeseries", () => {
+      expect(dimensionIsTimeseries({ cols: [{ base_type: TYPE.Time }] })).toBe(
+        true,
+      );
+    });
+    it("should detect DateTime column as timeseries", () => {
+      expect(
+        dimensionIsTimeseries({ cols: [{ base_type: TYPE.DateTime }] }),
+      ).toBe(true);
+    });
+    ISO_8601_DATES.forEach(isoDate => {
+      it(
+        "should detect values with ISO 8601 formatted string '" +
+          isoDate +
+          "' as timeseries",
+        () => {
+          expect(
+            dimensionIsTimeseries({
+              cols: [{ base_type: TYPE.Text }],
+              rows: [[isoDate]],
+            }),
+          ).toBe(true);
+        },
+      );
+    });
+    NOT_DATES.forEach(notDate => {
+      it("should not detect value '" + notDate + "' as timeseries", () => {
+        expect(
+          dimensionIsTimeseries({
+            cols: [{ base_type: TYPE.Text }],
+            rows: [[notDate]],
+          }),
+        ).toBe(false);
+      });
+    });
+  });
 
-    describe('computeTimeseriesDataInvervalIndex', () => {
-        const TEST_CASES = [
-            ["ms",      1, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:01.001Z"]]],
-            ["second",  1, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:01.000Z"]]],
-            ["second",  5, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:05.000Z"]]],
-            ["second", 15, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:15.000Z"]]],
-            ["second", 30, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:30.000Z"]]],
-            ["minute",  1, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:00.000Z"]]],
-            ["minute",  5, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:05:00.000Z"]]],
-            ["minute", 15, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:15:00.000Z"]]],
-            ["minute", 30, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:30:00.000Z"]]],
-            ["hour",    1, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T01:00:00.000Z"]]],
-            ["hour",    3, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:00:00.000Z"]]],
-            ["hour",    6, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T06:00:00.000Z"]]],
-            ["hour",   12, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T12:00:00.000Z"]]],
-            ["day",     1, [["2015-01-01T00:00:00.000Z"], ["2015-01-02T00:00:00.000Z"]]],
-            ["week",    1, [["2015-01-01T00:00:00.000Z"], ["2015-01-08T00:00:00.000Z"]]],
-            ["month",   1, [["2015-01-01T00:00:00.000Z"], ["2015-02-01T00:00:00.000Z"]]],
-            ["month",   3, [["2015-01-01T00:00:00.000Z"], ["2015-04-01T00:00:00.000Z"]]],
-            ["year",    1, [["2015-01-01T00:00:00.000Z"], ["2016-01-01T00:00:00.000Z"]]],
-            ["year",    5, [["2015-01-01T00:00:00.000Z"], ["2020-01-01T00:00:00.000Z"]]],
-            ["year",   10, [["2015-01-01T00:00:00.000Z"], ["2025-01-01T00:00:00.000Z"]]],
-        ];
+  describe("computeTimeseriesDataInvervalIndex", () => {
+    const TEST_CASES = [
+      ["ms", 1, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:01.001Z"]]],
+      [
+        "second",
+        1,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:01.000Z"]],
+      ],
+      [
+        "second",
+        5,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:05.000Z"]],
+      ],
+      [
+        "second",
+        15,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:15.000Z"]],
+      ],
+      [
+        "second",
+        30,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:30.000Z"]],
+      ],
+      [
+        "minute",
+        1,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:02:00.000Z"]],
+      ],
+      [
+        "minute",
+        5,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:05:00.000Z"]],
+      ],
+      [
+        "minute",
+        15,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:15:00.000Z"]],
+      ],
+      [
+        "minute",
+        30,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:30:00.000Z"]],
+      ],
+      ["hour", 1, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T01:00:00.000Z"]]],
+      ["hour", 3, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T03:00:00.000Z"]]],
+      ["hour", 6, [["2015-01-01T00:00:00.000Z"], ["2016-05-04T06:00:00.000Z"]]],
+      [
+        "hour",
+        12,
+        [["2015-01-01T00:00:00.000Z"], ["2016-05-04T12:00:00.000Z"]],
+      ],
+      ["day", 1, [["2015-01-01T00:00:00.000Z"], ["2015-01-02T00:00:00.000Z"]]],
+      ["week", 1, [["2015-01-01T00:00:00.000Z"], ["2015-01-08T00:00:00.000Z"]]],
+      [
+        "month",
+        1,
+        [["2015-01-01T00:00:00.000Z"], ["2015-02-01T00:00:00.000Z"]],
+      ],
+      [
+        "month",
+        3,
+        [["2015-01-01T00:00:00.000Z"], ["2015-04-01T00:00:00.000Z"]],
+      ],
+      ["year", 1, [["2015-01-01T00:00:00.000Z"], ["2016-01-01T00:00:00.000Z"]]],
+      ["year", 5, [["2015-01-01T00:00:00.000Z"], ["2020-01-01T00:00:00.000Z"]]],
+      [
+        "year",
+        10,
+        [["2015-01-01T00:00:00.000Z"], ["2025-01-01T00:00:00.000Z"]],
+      ],
+    ];
 
-        TEST_CASES.map(([expectedInterval, expectedCount, data]) => {
-            it("should return " + expectedCount + " " + expectedInterval, () => {
-                let { interval, count } = computeTimeseriesDataInverval(data.map(d => new Date(d)));
-                expect(interval).toBe(expectedInterval);
-                expect(count).toBe(expectedCount);
-            });
-        });
+    TEST_CASES.map(([expectedInterval, expectedCount, data]) => {
+      it("should return " + expectedCount + " " + expectedInterval, () => {
+        let { interval, count } = computeTimeseriesDataInverval(
+          data.map(d => new Date(d)),
+        );
+        expect(interval).toBe(expectedInterval);
+        expect(count).toBe(expectedCount);
+      });
     });
+  });
 });
diff --git a/frontend/test/visualizations/lib/utils.unit.spec.js b/frontend/test/visualizations/lib/utils.unit.spec.js
index 36753eb4c9cd4c88fda02bcb0bce26ebde5b2965..4ca063cf1f75b3802fc97d0674fde350332f96dd 100644
--- a/frontend/test/visualizations/lib/utils.unit.spec.js
+++ b/frontend/test/visualizations/lib/utils.unit.spec.js
@@ -1,8 +1,8 @@
 import {
-    cardHasBecomeDirty,
-    getCardAfterVisualizationClick,
-    getColumnCardinality,
-    getXValues
+  cardHasBecomeDirty,
+  getCardAfterVisualizationClick,
+  getColumnCardinality,
+  getXValues,
 } from "metabase/visualizations/lib/utils";
 
 import _ from "underscore";
@@ -10,229 +10,203 @@ import _ from "underscore";
 // TODO Atte Keinänen 5/31/17 Rewrite tests using metabase-lib methods instead of a raw format
 
 const baseQuery = {
-    "database": 1,
-    "type": "query",
-    "query": {
-        "source_table": 2,
-        "aggregation": [
-            [
-                "count"
-            ]
-        ],
-        "breakout": [
-            [
-                "field-id",
-                2
-            ]
-        ]
-    }
+  database: 1,
+  type: "query",
+  query: {
+    source_table: 2,
+    aggregation: [["count"]],
+    breakout: [["field-id", 2]],
+  },
 };
 const derivedQuery = {
-    ...baseQuery,
-    "query": {
-        ...baseQuery.query,
-        "filter": [
-            "time-interval",
-            [
-                "field-id",
-                1
-            ],
-            -7,
-            "day"
-        ]
-    }
+  ...baseQuery,
+  query: {
+    ...baseQuery.query,
+    filter: ["time-interval", ["field-id", 1], -7, "day"],
+  },
 };
 
 const breakoutMultiseriesQuery = {
-    ...baseQuery,
-    "query": {
-        ...baseQuery.query,
-        "breakout": [
-            ...baseQuery.query.breakout,
-            [
-                "fk->",
-                1,
-                10
-            ]
-        ]
-    }
+  ...baseQuery,
+  query: {
+    ...baseQuery.query,
+    breakout: [...baseQuery.query.breakout, ["fk->", 1, 10]],
+  },
 };
 const derivedBreakoutMultiseriesQuery = {
-    ...breakoutMultiseriesQuery,
-    "query": {
-        ...breakoutMultiseriesQuery.query,
-        "filter": [
-            "time-interval",
-            [
-                "field-id",
-                1
-            ],
-            -7,
-            "day"
-        ]
-    }
+  ...breakoutMultiseriesQuery,
+  query: {
+    ...breakoutMultiseriesQuery.query,
+    filter: ["time-interval", ["field-id", 1], -7, "day"],
+  },
 };
 
 const savedCard = {
-    id: 3,
-    dataset_query: baseQuery,
-    display: "line"
+  id: 3,
+  dataset_query: baseQuery,
+  display: "line",
 };
 const clonedSavedCard = {
-    id: 3,
-    dataset_query: _.clone(baseQuery),
-    display: "line"
+  id: 3,
+  dataset_query: _.clone(baseQuery),
+  display: "line",
 };
 const dirtyCardOnlyOriginalId = {
-    original_card_id: 7,
-    dataset_query: baseQuery,
-    display: "line"
+  original_card_id: 7,
+  dataset_query: baseQuery,
+  display: "line",
 };
 
 const derivedCard = {
-    ...dirtyCardOnlyOriginalId,
-    dataset_query: derivedQuery
+  ...dirtyCardOnlyOriginalId,
+  dataset_query: derivedQuery,
 };
 const derivedCardModifiedId = {
-    ...savedCard,
-    dataset_query: derivedQuery
+  ...savedCard,
+  dataset_query: derivedQuery,
 };
 const derivedDirtyCard = {
-    ...dirtyCardOnlyOriginalId,
-    dataset_query: derivedQuery
+  ...dirtyCardOnlyOriginalId,
+  dataset_query: derivedQuery,
 };
 
 const derivedCardWithDifferentDisplay = {
-    ...savedCard,
-    display: "table"
+  ...savedCard,
+  display: "table",
 };
 
 const savedMultiseriesCard = {
-    ...savedCard,
-    dataset_query: breakoutMultiseriesQuery
+  ...savedCard,
+  dataset_query: breakoutMultiseriesQuery,
 };
 const derivedMultiseriesCard = {
-    // id is not present when drilling through series / multiseries
-    dataset_query: derivedBreakoutMultiseriesQuery,
-    display: savedCard.display
+  // id is not present when drilling through series / multiseries
+  dataset_query: derivedBreakoutMultiseriesQuery,
+  display: savedCard.display,
 };
 const newCard = {
-    dataset_query: baseQuery,
-    display: "line"
+  dataset_query: baseQuery,
+  display: "line",
 };
 const modifiedNewCard = {
-    dataset_query: derivedQuery,
-    display: "line"
+  dataset_query: derivedQuery,
+  display: "line",
 };
 
 describe("metabase/visualization/lib/utils", () => {
-    describe("cardHasBecomeDirty", () => {
-        it("should consider cards with different display types dirty", () => {
-            // mostly for action widget actions that only change the display type
-            expect(cardHasBecomeDirty(derivedCardWithDifferentDisplay, savedCard)).toEqual(true);
-        });
-
-        it("should consider cards with different data data dirty", () => {
-            expect(cardHasBecomeDirty(derivedCard, savedCard)).toEqual(true);
-        });
-
-        it("should consider cards with same display type and data clean", () => {
-            // i.e. the card is practically the same as original card
-            expect(cardHasBecomeDirty(clonedSavedCard, savedCard)).toEqual(false);
-        });
-    });
-
-    describe("getCardAfterVisualizationClick", () => {
-        it("should use the id of a previous card in case of a multi-breakout visualization", () => {
-            expect(getCardAfterVisualizationClick(derivedMultiseriesCard, savedMultiseriesCard))
-                .toMatchObject({original_card_id: savedMultiseriesCard.id})
-        });
-
-        // TODO: Atte Keinänen 5/31/17 This scenario is a little fuzzy at the moment as there have been
-        // some specific corner cases where the id in previousCard is wrong/missing
-        // We should validate that previousCard always has an id as it should
-        it("if the new card contains the id it's more reliable to use it for initializing lineage", () => {
-            expect(getCardAfterVisualizationClick(derivedCardModifiedId, savedCard))
-                .toMatchObject({original_card_id: derivedCardModifiedId.id})
-        });
-
-        it("should be able to continue the lineage even if the previous question was dirty already", () => {
-            expect(getCardAfterVisualizationClick(derivedDirtyCard, dirtyCardOnlyOriginalId))
-                .toMatchObject({original_card_id: dirtyCardOnlyOriginalId.original_card_id})
-        });
-
-        it("should just pass the new question if the previous question was new", () => {
-            expect(getCardAfterVisualizationClick(modifiedNewCard, newCard))
-                .toMatchObject(modifiedNewCard)
-        });
-
-        it("should populate original_card_id even if the question isn't modified", () => {
-            // This is the hack to interoperate with questionUrlWithParameters when
-            // dashboard parameters are applied to a dashcards
-            expect(getCardAfterVisualizationClick(clonedSavedCard, savedCard))
-                .toMatchObject({original_card_id: savedCard.id})
-        });
-    });
-
-    describe('getXValues', () => {
-        it("should not change the order of a single series of ascending numbers", () => {
-            expect(getXValues([
-                [[1],[2],[11]]
-            ])).toEqual([1,2,11]);
-        });
-        it("should not change the order of a single series of descending numbers", () => {
-            expect(getXValues([
-                [[1],[2],[11]]
-            ])).toEqual([1,2,11]);
-        });
-        it("should not change the order of a single series of non-ordered numbers", () => {
-            expect(getXValues([
-                [[2],[1],[11]]
-            ])).toEqual([2,1,11]);
-        });
-
-        it("should not change the order of a single series of ascending strings", () => {
-            expect(getXValues([
-                [["1"],["2"],["11"]]
-            ])).toEqual(["1","2","11"]);
-        });
-        it("should not change the order of a single series of descending strings", () => {
-            expect(getXValues([
-                [["1"],["2"],["11"]]
-            ])).toEqual(["1","2","11"]);
-        });
-        it("should not change the order of a single series of non-ordered strings", () => {
-            expect(getXValues([
-                [["2"],["1"],["11"]]
-            ])).toEqual(["2","1","11"]);
-        });
-
-        it("should correctly merge multiple series of ascending numbers", () => {
-            expect(getXValues([
-                [[2],[11],[12]],
-                [[1],[2],[11]]
-            ])).toEqual([1,2,11,12]);
-        });
-        it("should correctly merge multiple series of descending numbers", () => {
-            expect(getXValues([
-                [[12],[11],[2]],
-                [[11],[2],[1]]
-            ])).toEqual([12,11,2,1]);
-        });
-    });
-
-    describe("getColumnCardinality", () => {
-        it("should get column cardinality", () => {
-            const cols = [{}];
-            const rows = [[1],[2],[3],[3]];
-            expect(getColumnCardinality(cols, rows, 0)).toEqual(3);
-        });
-        it("should get column cardinality for frozen column", () => {
-            const cols = [{}];
-            const rows = [[1],[2],[3],[3]];
-            Object.freeze(cols[0]);
-            expect(getColumnCardinality(cols, rows, 0)).toEqual(3);
-        });
+  describe("cardHasBecomeDirty", () => {
+    it("should consider cards with different display types dirty", () => {
+      // mostly for action widget actions that only change the display type
+      expect(
+        cardHasBecomeDirty(derivedCardWithDifferentDisplay, savedCard),
+      ).toEqual(true);
     });
-});
 
+    it("should consider cards with different data data dirty", () => {
+      expect(cardHasBecomeDirty(derivedCard, savedCard)).toEqual(true);
+    });
+
+    it("should consider cards with same display type and data clean", () => {
+      // i.e. the card is practically the same as original card
+      expect(cardHasBecomeDirty(clonedSavedCard, savedCard)).toEqual(false);
+    });
+  });
+
+  describe("getCardAfterVisualizationClick", () => {
+    it("should use the id of a previous card in case of a multi-breakout visualization", () => {
+      expect(
+        getCardAfterVisualizationClick(
+          derivedMultiseriesCard,
+          savedMultiseriesCard,
+        ),
+      ).toMatchObject({ original_card_id: savedMultiseriesCard.id });
+    });
+
+    // TODO: Atte Keinänen 5/31/17 This scenario is a little fuzzy at the moment as there have been
+    // some specific corner cases where the id in previousCard is wrong/missing
+    // We should validate that previousCard always has an id as it should
+    it("if the new card contains the id it's more reliable to use it for initializing lineage", () => {
+      expect(
+        getCardAfterVisualizationClick(derivedCardModifiedId, savedCard),
+      ).toMatchObject({ original_card_id: derivedCardModifiedId.id });
+    });
+
+    it("should be able to continue the lineage even if the previous question was dirty already", () => {
+      expect(
+        getCardAfterVisualizationClick(
+          derivedDirtyCard,
+          dirtyCardOnlyOriginalId,
+        ),
+      ).toMatchObject({
+        original_card_id: dirtyCardOnlyOriginalId.original_card_id,
+      });
+    });
+
+    it("should just pass the new question if the previous question was new", () => {
+      expect(
+        getCardAfterVisualizationClick(modifiedNewCard, newCard),
+      ).toMatchObject(modifiedNewCard);
+    });
+
+    it("should populate original_card_id even if the question isn't modified", () => {
+      // This is the hack to interoperate with questionUrlWithParameters when
+      // dashboard parameters are applied to a dashcards
+      expect(
+        getCardAfterVisualizationClick(clonedSavedCard, savedCard),
+      ).toMatchObject({ original_card_id: savedCard.id });
+    });
+  });
+
+  describe("getXValues", () => {
+    it("should not change the order of a single series of ascending numbers", () => {
+      expect(getXValues([[[1], [2], [11]]])).toEqual([1, 2, 11]);
+    });
+    it("should not change the order of a single series of descending numbers", () => {
+      expect(getXValues([[[1], [2], [11]]])).toEqual([1, 2, 11]);
+    });
+    it("should not change the order of a single series of non-ordered numbers", () => {
+      expect(getXValues([[[2], [1], [11]]])).toEqual([2, 1, 11]);
+    });
+
+    it("should not change the order of a single series of ascending strings", () => {
+      expect(getXValues([[["1"], ["2"], ["11"]]])).toEqual(["1", "2", "11"]);
+    });
+    it("should not change the order of a single series of descending strings", () => {
+      expect(getXValues([[["1"], ["2"], ["11"]]])).toEqual(["1", "2", "11"]);
+    });
+    it("should not change the order of a single series of non-ordered strings", () => {
+      expect(getXValues([[["2"], ["1"], ["11"]]])).toEqual(["2", "1", "11"]);
+    });
+
+    it("should correctly merge multiple series of ascending numbers", () => {
+      expect(getXValues([[[2], [11], [12]], [[1], [2], [11]]])).toEqual([
+        1,
+        2,
+        11,
+        12,
+      ]);
+    });
+    it("should correctly merge multiple series of descending numbers", () => {
+      expect(getXValues([[[12], [11], [2]], [[11], [2], [1]]])).toEqual([
+        12,
+        11,
+        2,
+        1,
+      ]);
+    });
+  });
+
+  describe("getColumnCardinality", () => {
+    it("should get column cardinality", () => {
+      const cols = [{}];
+      const rows = [[1], [2], [3], [3]];
+      expect(getColumnCardinality(cols, rows, 0)).toEqual(3);
+    });
+    it("should get column cardinality for frozen column", () => {
+      const cols = [{}];
+      const rows = [[1], [2], [3], [3]];
+      Object.freeze(cols[0]);
+      expect(getColumnCardinality(cols, rows, 0)).toEqual(3);
+    });
+  });
+});
diff --git a/frontend/test/xray/selectors.unit.spec.js b/frontend/test/xray/selectors.unit.spec.js
index 6d3f3badd6d07ad8216398a78bf76c770ebe65eb..8984b67c7ef1a2e33a5994cf8c628d60f4042c5f 100644
--- a/frontend/test/xray/selectors.unit.spec.js
+++ b/frontend/test/xray/selectors.unit.spec.js
@@ -1,70 +1,68 @@
-import {
-    getComparisonContributors
-} from 'metabase/xray/selectors'
+import { getComparisonContributors } from "metabase/xray/selectors";
 
-describe('xray selectors', () => {
-    describe('getComparisonContributors', () => {
-        it('should return the top contributors for a comparison', () => {
-            const GOOD_FIELD = {
-                field: {
-                    display_name: 'good'
-                },
-                histogram: {
-                    label: "Distribution",
-                    value: {}
-                }
-            }
+describe("xray selectors", () => {
+  describe("getComparisonContributors", () => {
+    it("should return the top contributors for a comparison", () => {
+      const GOOD_FIELD = {
+        field: {
+          display_name: "good",
+        },
+        histogram: {
+          label: "Distribution",
+          value: {},
+        },
+      };
 
-            const OTHER_FIELD = {
-                field: {
-                    display_name: 'other'
-                },
-                histogram: {
-                    label: "Distribution",
-                }
-            }
+      const OTHER_FIELD = {
+        field: {
+          display_name: "other",
+        },
+        histogram: {
+          label: "Distribution",
+        },
+      };
 
-            const state = {
-                xray: {
-                    comparison: {
-                        constituents: [
-                            {
-                                constituents: {
-                                    GOOD_FIELD,
-                                    OTHER_FIELD
-                                },
-                            },
-                            {
-                                constituents: {
-                                    GOOD_FIELD,
-                                    OTHER_FIELD
-                                }
-                            }
-                        ],
-                        'top-contributors': [
-                            {
-                                field: 'GOOD_FIELD',
-                                feature: 'histogram'
-                            },
-                        ]
-                    }
-                }
-            }
+      const state = {
+        xray: {
+          comparison: {
+            constituents: [
+              {
+                constituents: {
+                  GOOD_FIELD,
+                  OTHER_FIELD,
+                },
+              },
+              {
+                constituents: {
+                  GOOD_FIELD,
+                  OTHER_FIELD,
+                },
+              },
+            ],
+            "top-contributors": [
+              {
+                field: "GOOD_FIELD",
+                feature: "histogram",
+              },
+            ],
+          },
+        },
+      };
 
-            const expected = [
-                {
-                    feature: {
-                        label: "Distribution",
-                        type: 'histogram',
-                        value: {
-                            a: {},
-                            b: {}
-                        }
-                    },
-                    field: GOOD_FIELD
-                }
-            ]
-            expect(getComparisonContributors(state)).toEqual(expected)
-        })
-    })
-})
+      const expected = [
+        {
+          feature: {
+            label: "Distribution",
+            type: "histogram",
+            value: {
+              a: {},
+              b: {},
+            },
+          },
+          field: GOOD_FIELD,
+        },
+      ];
+      expect(getComparisonContributors(state)).toEqual(expected);
+    });
+  });
+});
diff --git a/frontend/test/xray/utils.unit.spec.js b/frontend/test/xray/utils.unit.spec.js
index 87d459b43ad2455b2ff0c31d683a80decf1b8f98..0294fc1bcdb615307420855c645c01968d5f87b8 100644
--- a/frontend/test/xray/utils.unit.spec.js
+++ b/frontend/test/xray/utils.unit.spec.js
@@ -1,10 +1,10 @@
-import { distanceToPhrase } from 'metabase/xray/utils'
+import { distanceToPhrase } from "metabase/xray/utils";
 
-describe('distanceToPhrase', () => {
-    it('should return the proper phrases', () => {
-        expect(distanceToPhrase(0.88)).toEqual('Very different')
-        expect(distanceToPhrase(0.5)).toEqual('Somewhat different')
-        expect(distanceToPhrase(0.36)).toEqual('Somewhat similar')
-        expect(distanceToPhrase(0.16)).toEqual('Very similar')
-    })
-})
+describe("distanceToPhrase", () => {
+  it("should return the proper phrases", () => {
+    expect(distanceToPhrase(0.88)).toEqual("Very different");
+    expect(distanceToPhrase(0.5)).toEqual("Somewhat different");
+    expect(distanceToPhrase(0.36)).toEqual("Somewhat similar");
+    expect(distanceToPhrase(0.16)).toEqual("Very similar");
+  });
+});
diff --git a/frontend/test/xray/xray.integ.spec.js b/frontend/test/xray/xray.integ.spec.js
index 1cc5330233c5b2d82b062a2b8e4ff03b6b753114..ddc16c604b116e43651fb72a6fcbffd78ee62305 100644
--- a/frontend/test/xray/xray.integ.spec.js
+++ b/frontend/test/xray/xray.integ.spec.js
@@ -1,27 +1,21 @@
 import {
-    useSharedAdminLogin,
-    createTestStore,
-    createSavedQuestion
+  useSharedAdminLogin,
+  createTestStore,
+  createSavedQuestion,
 } from "__support__/integrated_tests";
-import {
-    click
-} from "__support__/enzyme_utils"
+import { click } from "__support__/enzyme_utils";
 
 import { mount } from "enzyme";
-import {
-    CardApi,
-    SegmentApi,
-    SettingsApi
-} from "metabase/services";
+import { CardApi, SegmentApi, SettingsApi } from "metabase/services";
 
 import { delay } from "metabase/lib/promise";
 import {
-    FETCH_CARD_XRAY,
-    FETCH_FIELD_XRAY,
-    FETCH_SEGMENT_XRAY,
-    FETCH_SHARED_TYPE_COMPARISON_XRAY,
-    FETCH_TABLE_XRAY,
-    FETCH_TWO_TYPES_COMPARISON_XRAY
+  FETCH_CARD_XRAY,
+  FETCH_FIELD_XRAY,
+  FETCH_SEGMENT_XRAY,
+  FETCH_SHARED_TYPE_COMPARISON_XRAY,
+  FETCH_TABLE_XRAY,
+  FETCH_TWO_TYPES_COMPARISON_XRAY,
 } from "metabase/xray/xray";
 
 import FieldXray from "metabase/xray/containers/FieldXray";
@@ -38,448 +32,516 @@ import { INITIALIZE_QB, QUERY_COMPLETED } from "metabase/query_builder/actions";
 import ActionsWidget from "metabase/query_builder/components/ActionsWidget";
 
 // settings related actions for testing xray administration
-import { INITIALIZE_SETTINGS, UPDATE_SETTING } from "metabase/admin/settings/settings";
+import {
+  INITIALIZE_SETTINGS,
+  UPDATE_SETTING,
+} from "metabase/admin/settings/settings";
 import { LOAD_CURRENT_USER } from "metabase/redux/user";
 import { END_LOADING } from "metabase/reference/reference";
 
 import { getXrayEnabled, getMaxCost } from "metabase/xray/selectors";
 
-import Icon from "metabase/components/Icon"
-import Toggle from "metabase/components/Toggle"
-import { Link } from 'react-router'
+import Icon from "metabase/components/Icon";
+import Toggle from "metabase/components/Toggle";
+import { Link } from "react-router";
 import SettingsXrayForm from "metabase/admin/settings/components/SettingsXrayForm";
 import { ComparisonDropdown } from "metabase/xray/components/ComparisonDropdown";
-import { TestPopover } from "metabase/components/Popover";
+import Popover from "metabase/components/Popover";
 import ItemLink from "metabase/xray/components/ItemLink";
 import { TableLikeComparisonXRay } from "metabase/xray/containers/TableLikeComparison";
 import {
-    InsightCard,
-    NoisinessInsight,
-    NormalRangeInsight,
-    AutocorrelationInsight
+  InsightCard,
+  NoisinessInsight,
+  NormalRangeInsight,
+  AutocorrelationInsight,
 } from "metabase/xray/components/InsightCard";
 
 describe("xray integration tests", () => {
-    let segmentId = null;
-    let segmentId2 = null;
-    let timeBreakoutQuestion = null;
-    let segmentQuestion = null;
-    let segmentQuestion2 = null;
-
-    beforeAll(async () => {
-        useSharedAdminLogin()
-
-        const segmentDef = {name: "A Segment", description: "For testing xrays", table_id: 1, show_in_getting_started: true,
-            definition: { source_table: 1, filter: ["time-interval", ["field-id", 1], -30, "day"] }}
-        segmentId = (await SegmentApi.create(segmentDef)).id;
-
-        const segmentDef2 = {name: "A Segment", description: "For testing segment comparison", table_id: 1, show_in_getting_started: true,
-            definition: { source_table: 1, filter: ["time-interval", ["field-id", 1], -15, "day"] }}
-        segmentId2 = (await SegmentApi.create(segmentDef2)).id;
-
-        timeBreakoutQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata: null})
-                .query()
-                .addAggregation(["count"])
-                .addBreakout(["datetime-field", ["field-id", 1], "day"])
-                .question()
-                .setDisplay("line")
-                .setDisplayName("Time breakout question")
-        )
-
-        segmentQuestion = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata: null})
-                .query()
-                .addFilter(["SEGMENT", segmentId])
-                .question()
-                .setDisplayName("Segment question")
-        )
-
-        segmentQuestion2 = await createSavedQuestion(
-            Question.create({databaseId: 1, tableId: 1, metadata: null})
-                .query()
-                .addFilter(["SEGMENT", segmentId2])
-                .question()
-                .setDisplayName("Segment question")
-        )
-    })
-
-    afterAll(async () => {
-        await SegmentApi.delete({ segmentId, revision_message: "Sadly this segment didn't enjoy a long life either" })
-        await SegmentApi.delete({ segmentId: segmentId2, revision_message: "Sadly this segment didn't enjoy a long life either" })
-        await CardApi.delete({cardId: timeBreakoutQuestion.id()})
-        await CardApi.delete({cardId: segmentQuestion.id()})
-        await CardApi.delete({cardId: segmentQuestion2.id()})
-        await SettingsApi.put({ key: 'enable-xrays' }, true)
-    })
-
-    describe("table x-rays", async () => {
-        it("should render the table x-ray page without errors", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/table/1/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_TABLE_XRAY], { timeout: 20000 })
-
-            const tableXRay = app.find(TableXRay)
-            expect(tableXRay.length).toBe(1)
-            expect(tableXRay.find(CostSelect).length).toBe(1)
-            expect(tableXRay.find(Constituent).length).toBeGreaterThan(0)
-            expect(tableXRay.text()).toMatch(/Orders/);
-        })
-
-        it("should render the table-by-table comparison page without errors", async () => {
-            const store = await createTestStore()
-            // Compare the table naively with itself because we don't
-            // have anything real to compare with yet
-            store.pushPath(`/xray/compare/tables/1/1/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_SHARED_TYPE_COMPARISON_XRAY], { timeout: 20000 })
-
-            const tableComparisonXray = app.find(TableLikeComparisonXRay)
-            expect(tableComparisonXray.length).toBe(1)
-            expect(tableComparisonXray.find(CostSelect).length).toBe(1)
-
-        })
-    })
-
-    describe("field x-rays", async () => {
-        it("should render the field x-ray page with expected insights", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/field/2/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_FIELD_XRAY], { timeout: 20000 })
-
-            const fieldXRay = app.find(FieldXray)
-            expect(fieldXRay.length).toBe(1)
-            expect(fieldXRay.find(CostSelect).length).toBe(1)
-
-            expect(app.find(InsightCard).length > 0).toBe(true)
-            expect(app.find(NormalRangeInsight).length).toBe(1)
-        })
-    })
-
-    describe("question x-rays", async () => {
-        it("should render the comparison of two raw data questions without errors", async () => {
-            // NOTE: In the UI currently the only way to get here is to already be a comparison
-            // and then witch the compared items
-            const store = await createTestStore()
-            store.pushPath(`/xray/compare/cards/${segmentQuestion.id()}/${segmentQuestion2.id()}/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_SHARED_TYPE_COMPARISON_XRAY], { timeout: 20000 })
-
-            const segmentTableComparisonXray = app.find(TableLikeComparisonXRay)
-            expect(segmentTableComparisonXray.length).toBe(1)
-            expect(segmentTableComparisonXray.find(CostSelect).length).toBe(1)
-
-        })
-    })
-
-    describe("segment x-rays", async () => {
-        it("should render the segment x-ray page without errors", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/segment/${segmentId}/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_SEGMENT_XRAY], { timeout: 20000 })
-
-            const segmentXRay = app.find(SegmentXRay)
-            expect(segmentXRay.length).toBe(1)
-            expect(segmentXRay.find(CostSelect).length).toBe(1)
-
-            // check that we have the links to expected comparisons
-            click(segmentXRay.find(ComparisonDropdown).find('.Icon-compare'))
-            const comparisonPopover = segmentXRay.find(ComparisonDropdown).find(TestPopover)
-            expect(comparisonPopover.find(`a[href="/xray/compare/segment/${segmentId}/table/1/approximate"]`).length).toBe(1)
-            expect(comparisonPopover.find(`a[href="/xray/compare/segments/${segmentId}/${segmentId2}/approximate"]`).length).toBe(1)
-        })
-
-        it("should render the segment-by-segment comparison page without errors", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/compare/segments/${segmentId}/${segmentId2}/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_SHARED_TYPE_COMPARISON_XRAY], { timeout: 20000 })
-
-            const segmentComparisonXray = app.find(TableLikeComparisonXRay)
-            expect(segmentComparisonXray.length).toBe(1)
-            expect(segmentComparisonXray.find(CostSelect).length).toBe(1)
-
-        })
-
-        it("should render the segment-by-table comparison page without errors", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/compare/segment/${segmentId}/table/1/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_TWO_TYPES_COMPARISON_XRAY], { timeout: 20000 })
-
-            const segmentTableComparisonXray = app.find(TableLikeComparisonXRay)
-            expect(segmentTableComparisonXray.length).toBe(1)
-            expect(segmentTableComparisonXray.find(CostSelect).length).toBe(1)
-
-            // check that we have the links to expected comparisons
-            const comparisonDropdowns = segmentTableComparisonXray.find(ComparisonDropdown)
-            expect(comparisonDropdowns.length).toBe(2)
-            const leftSideDropdown = comparisonDropdowns.at(0)
-            const rightSideDropdown = comparisonDropdowns.at(1)
-
-            click(leftSideDropdown.find(ItemLink))
-            const leftSidePopover = leftSideDropdown.find(TestPopover)
-            console.log(leftSidePopover.debug())
-            expect(leftSidePopover.find(`a[href="/xray/compare/segment/${segmentId}/table/1/approximate"]`).length).toBe(0)
-            // should filter out the current table
-            expect(leftSidePopover.find(`a[href="/xray/compare/tables/1/1/approximate"]`).length).toBe(0)
-
-            // right side should be be table and show only segments options as comparision options atm
-            click(rightSideDropdown.find(ItemLink))
-            const rightSidePopover = rightSideDropdown.find(TestPopover)
-            console.log(rightSidePopover.debug())
-            expect(rightSidePopover.find(`a[href="/xray/compare/segments/${segmentId}/${segmentId2}/approximate"]`).length).toBe(1)
-            // should filter out the current segment
-            expect(rightSidePopover.find(`a[href="/xray/compare/segments/${segmentId}/${segmentId}/approximate"]`).length).toBe(0)
-        })
-
-        it("should render the segment - by - raw query question comparison page without errors", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/compare/segment/${segmentId}/card/${segmentQuestion.id()}/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_TWO_TYPES_COMPARISON_XRAY], { timeout: 20000 })
-
-            const segmentTableComparisonXray = app.find(TableLikeComparisonXRay)
-            expect(segmentTableComparisonXray.length).toBe(1)
-            expect(segmentTableComparisonXray.find(CostSelect).length).toBe(1)
-        })
-    })
-
-    describe("navigation", async () => {
-        it("should be possible to navigate between tables and their child fields", async () => {
-            const store = await createTestStore()
-            store.pushPath(`/xray/table/1/approximate`);
-
-            const app = mount(store.getAppContainer());
-            await store.waitForActions([FETCH_TABLE_XRAY], { timeout: 20000 })
-
-            const tableXray = app.find(TableXRay)
-            expect(tableXray.length).toBe(1)
-
-            const fieldLink = app.find(Constituent).first().find(Link)
-
-            click(fieldLink)
-
-            await store.waitForActions([FETCH_FIELD_XRAY], { timeout: 20000 })
-            const fieldXray = app.find(FieldXray)
-            expect(fieldXray.length).toBe(1)
-        })
-    })
-
-    // NOTE Atte Keinänen 8/24/17: I wanted to test both QB action widget xray action and the card/segment xray pages
-    // in the same tests so that we see that end-to-end user experience matches our expectations
-
-    describe("query builder actions", async () => {
-        beforeEach(async () => {
-            await SettingsApi.put({ key: 'enable-xrays', value: 'true' })
-        })
-
-        it("let you see card xray for a timeseries question", async () => {
-            await SettingsApi.put({ key: 'xray-max-cost', value: 'extended' })
-            const store = await createTestStore()
-            // make sure xrays are on and at the proper cost
-            store.pushPath(Urls.question(timeBreakoutQuestion.id()))
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED])
-            // NOTE Atte Keinänen: Not sure why we need this delay to get most of action widget actions to appear :/
-            await delay(500);
-
-            const actionsWidget = app.find(ActionsWidget)
-            click(actionsWidget.childAt(0))
-            const xrayOptionIcon = actionsWidget.find('.Icon.Icon-beaker')
-            click(xrayOptionIcon);
-
-
-            await store.waitForActions([FETCH_CARD_XRAY], {timeout: 20000})
-            expect(store.getPath()).toBe(`/xray/card/${timeBreakoutQuestion.id()}/extended`)
-
-            const cardXRay = app.find(CardXRay)
-            expect(cardXRay.length).toBe(1)
-            expect(cardXRay.text()).toMatch(/Time breakout question/);
-
-            // Should contain the expected insights
-            expect(app.find(InsightCard).length > 0).toBe(true)
-            expect(app.find(NoisinessInsight).length).toBe(1)
-            expect(app.find(AutocorrelationInsight).length).toBe(1)
-        })
-
-        it("let you see segment xray for a question containing a segment", async () => {
-            const store = await createTestStore()
-            store.pushPath(Urls.question(segmentQuestion.id()))
-            const app = mount(store.getAppContainer());
-
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED])
-
-            const actionsWidget = app.find(ActionsWidget)
-            click(actionsWidget.childAt(0))
-            const xrayOptionIcon = actionsWidget.find('.Icon.Icon-beaker')
-            click(xrayOptionIcon);
+  let segmentId = null;
+  let segmentId2 = null;
+  let timeBreakoutQuestion = null;
+  let segmentQuestion = null;
+  let segmentQuestion2 = null;
+
+  beforeAll(async () => {
+    useSharedAdminLogin();
+
+    const segmentDef = {
+      name: "A Segment",
+      description: "For testing xrays",
+      table_id: 1,
+      show_in_getting_started: true,
+      definition: {
+        source_table: 1,
+        filter: ["time-interval", ["field-id", 1], -30, "day"],
+      },
+    };
+    segmentId = (await SegmentApi.create(segmentDef)).id;
+
+    const segmentDef2 = {
+      name: "A Segment",
+      description: "For testing segment comparison",
+      table_id: 1,
+      show_in_getting_started: true,
+      definition: {
+        source_table: 1,
+        filter: ["time-interval", ["field-id", 1], -15, "day"],
+      },
+    };
+    segmentId2 = (await SegmentApi.create(segmentDef2)).id;
+
+    timeBreakoutQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata: null })
+        .query()
+        .addAggregation(["count"])
+        .addBreakout(["datetime-field", ["field-id", 1], "day"])
+        .question()
+        .setDisplay("line")
+        .setDisplayName("Time breakout question"),
+    );
+
+    segmentQuestion = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata: null })
+        .query()
+        .addFilter(["SEGMENT", segmentId])
+        .question()
+        .setDisplayName("Segment question"),
+    );
+
+    segmentQuestion2 = await createSavedQuestion(
+      Question.create({ databaseId: 1, tableId: 1, metadata: null })
+        .query()
+        .addFilter(["SEGMENT", segmentId2])
+        .question()
+        .setDisplayName("Segment question"),
+    );
+  });
+
+  afterAll(async () => {
+    await SegmentApi.delete({
+      segmentId,
+      revision_message: "Sadly this segment didn't enjoy a long life either",
+    });
+    await SegmentApi.delete({
+      segmentId: segmentId2,
+      revision_message: "Sadly this segment didn't enjoy a long life either",
+    });
+    await CardApi.delete({ cardId: timeBreakoutQuestion.id() });
+    await CardApi.delete({ cardId: segmentQuestion.id() });
+    await CardApi.delete({ cardId: segmentQuestion2.id() });
+    await SettingsApi.put({ key: "enable-xrays" }, true);
+  });
+
+  describe("table x-rays", async () => {
+    it("should render the table x-ray page without errors", async () => {
+      const store = await createTestStore();
+      store.pushPath(`/xray/table/1/approximate`);
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_TABLE_XRAY], { timeout: 20000 });
+
+      const tableXRay = app.find(TableXRay);
+      expect(tableXRay.length).toBe(1);
+      expect(tableXRay.find(CostSelect).length).toBe(1);
+      expect(tableXRay.find(Constituent).length).toBeGreaterThan(0);
+      expect(tableXRay.text()).toMatch(/Orders/);
+    });
+
+    it("should render the table-by-table comparison page without errors", async () => {
+      const store = await createTestStore();
+      // Compare the table naively with itself because we don't
+      // have anything real to compare with yet
+      store.pushPath(`/xray/compare/tables/1/1/approximate`);
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_SHARED_TYPE_COMPARISON_XRAY], {
+        timeout: 20000,
+      });
+
+      const tableComparisonXray = app.find(TableLikeComparisonXRay);
+      expect(tableComparisonXray.length).toBe(1);
+      expect(tableComparisonXray.find(CostSelect).length).toBe(1);
+    });
+  });
+
+  describe("field x-rays", async () => {
+    it("should render the field x-ray page with expected insights", async () => {
+      const store = await createTestStore();
+      store.pushPath(`/xray/field/2/approximate`);
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_FIELD_XRAY], { timeout: 20000 });
+
+      const fieldXRay = app.find(FieldXray);
+      expect(fieldXRay.length).toBe(1);
+      expect(fieldXRay.find(CostSelect).length).toBe(1);
+
+      expect(app.find(InsightCard).length > 0).toBe(true);
+      expect(app.find(NormalRangeInsight).length).toBe(1);
+    });
+  });
+
+  describe("question x-rays", async () => {
+    it("should render the comparison of two raw data questions without errors", async () => {
+      // NOTE: In the UI currently the only way to get here is to already be a comparison
+      // and then witch the compared items
+      const store = await createTestStore();
+      store.pushPath(
+        `/xray/compare/cards/${segmentQuestion.id()}/${segmentQuestion2.id()}/approximate`,
+      );
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_SHARED_TYPE_COMPARISON_XRAY], {
+        timeout: 20000,
+      });
+
+      const segmentTableComparisonXray = app.find(TableLikeComparisonXRay);
+      expect(segmentTableComparisonXray.length).toBe(1);
+      expect(segmentTableComparisonXray.find(CostSelect).length).toBe(1);
+    });
+  });
+
+  describe("segment x-rays", async () => {
+    it("should render the segment x-ray page without errors", async () => {
+      const store = await createTestStore();
+      store.pushPath(`/xray/segment/${segmentId}/approximate`);
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_SEGMENT_XRAY], { timeout: 20000 });
+
+      const segmentXRay = app.find(SegmentXRay);
+      expect(segmentXRay.length).toBe(1);
+      expect(segmentXRay.find(CostSelect).length).toBe(1);
+
+      // check that we have the links to expected comparisons
+      click(segmentXRay.find(ComparisonDropdown).find(".Icon-compare"));
+      const comparisonPopover = segmentXRay
+        .find(ComparisonDropdown)
+        .find(Popover);
+      expect(
+        comparisonPopover.find(
+          `a[href="/xray/compare/segment/${segmentId}/table/1/approximate"]`,
+        ).length,
+      ).toBe(1);
+      expect(
+        comparisonPopover.find(
+          `a[href="/xray/compare/segments/${segmentId}/${segmentId2}/approximate"]`,
+        ).length,
+      ).toBe(1);
+    });
+
+    it("should render the segment-by-segment comparison page without errors", async () => {
+      const store = await createTestStore();
+      store.pushPath(
+        `/xray/compare/segments/${segmentId}/${segmentId2}/approximate`,
+      );
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_SHARED_TYPE_COMPARISON_XRAY], {
+        timeout: 20000,
+      });
+
+      const segmentComparisonXray = app.find(TableLikeComparisonXRay);
+      expect(segmentComparisonXray.length).toBe(1);
+      expect(segmentComparisonXray.find(CostSelect).length).toBe(1);
+    });
+
+    it("should render the segment-by-table comparison page without errors", async () => {
+      const store = await createTestStore();
+      store.pushPath(`/xray/compare/segment/${segmentId}/table/1/approximate`);
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_TWO_TYPES_COMPARISON_XRAY], {
+        timeout: 20000,
+      });
+
+      const segmentTableComparisonXray = app.find(TableLikeComparisonXRay);
+      expect(segmentTableComparisonXray.length).toBe(1);
+      expect(segmentTableComparisonXray.find(CostSelect).length).toBe(1);
+
+      // check that we have the links to expected comparisons
+      const comparisonDropdowns = segmentTableComparisonXray.find(
+        ComparisonDropdown,
+      );
+      expect(comparisonDropdowns.length).toBe(2);
+      const leftSideDropdown = comparisonDropdowns.at(0);
+      const rightSideDropdown = comparisonDropdowns.at(1);
+
+      click(leftSideDropdown.find(ItemLink));
+      const leftSidePopover = leftSideDropdown.find(Popover);
+      expect(
+        leftSidePopover.find(
+          `a[href="/xray/compare/segment/${segmentId}/table/1/approximate"]`,
+        ).length,
+      ).toBe(0);
+      // should filter out the current table
+      expect(
+        leftSidePopover.find(`a[href="/xray/compare/tables/1/1/approximate"]`)
+          .length,
+      ).toBe(0);
+
+      // right side should be be table and show only segments options as comparision options atm
+      click(rightSideDropdown.find(ItemLink));
+      const rightSidePopover = rightSideDropdown.find(Popover);
+      expect(
+        rightSidePopover.find(
+          `a[href="/xray/compare/segments/${segmentId}/${segmentId2}/approximate"]`,
+        ).length,
+      ).toBe(1);
+      // should filter out the current segment
+      expect(
+        rightSidePopover.find(
+          `a[href="/xray/compare/segments/${segmentId}/${segmentId}/approximate"]`,
+        ).length,
+      ).toBe(0);
+    });
+
+    it("should render the segment - by - raw query question comparison page without errors", async () => {
+      const store = await createTestStore();
+      store.pushPath(
+        `/xray/compare/segment/${segmentId}/card/${segmentQuestion.id()}/approximate`,
+      );
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_TWO_TYPES_COMPARISON_XRAY], {
+        timeout: 20000,
+      });
+
+      const segmentTableComparisonXray = app.find(TableLikeComparisonXRay);
+      expect(segmentTableComparisonXray.length).toBe(1);
+      expect(segmentTableComparisonXray.find(CostSelect).length).toBe(1);
+    });
+  });
+
+  describe("navigation", async () => {
+    it("should be possible to navigate between tables and their child fields", async () => {
+      const store = await createTestStore();
+      store.pushPath(`/xray/table/1/approximate`);
+
+      const app = mount(store.getAppContainer());
+      await store.waitForActions([FETCH_TABLE_XRAY], { timeout: 20000 });
+
+      const tableXray = app.find(TableXRay);
+      expect(tableXray.length).toBe(1);
+
+      const fieldLink = app
+        .find(Constituent)
+        .first()
+        .find(Link);
+
+      click(fieldLink);
+
+      await store.waitForActions([FETCH_FIELD_XRAY], { timeout: 20000 });
+      const fieldXray = app.find(FieldXray);
+      expect(fieldXray.length).toBe(1);
+    });
+  });
+
+  // NOTE Atte Keinänen 8/24/17: I wanted to test both QB action widget xray action and the card/segment xray pages
+  // in the same tests so that we see that end-to-end user experience matches our expectations
+
+  describe("query builder actions", async () => {
+    beforeEach(async () => {
+      await SettingsApi.put({ key: "enable-xrays", value: "true" });
+    });
+
+    it("let you see card xray for a timeseries question", async () => {
+      await SettingsApi.put({ key: "xray-max-cost", value: "extended" });
+      const store = await createTestStore();
+      // make sure xrays are on and at the proper cost
+      store.pushPath(Urls.question(timeBreakoutQuestion.id()));
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+      // NOTE Atte Keinänen: Not sure why we need this delay to get most of action widget actions to appear :/
+      await delay(500);
+
+      const actionsWidget = app.find(ActionsWidget);
+      click(actionsWidget.childAt(0));
+      const xrayOptionIcon = actionsWidget.find(".Icon.Icon-beaker");
+      click(xrayOptionIcon);
+
+      await store.waitForActions([FETCH_CARD_XRAY], { timeout: 20000 });
+      expect(store.getPath()).toBe(
+        `/xray/card/${timeBreakoutQuestion.id()}/extended`,
+      );
+
+      const cardXRay = app.find(CardXRay);
+      expect(cardXRay.length).toBe(1);
+      expect(cardXRay.text()).toMatch(/Time breakout question/);
+
+      // Should contain the expected insights
+      expect(app.find(InsightCard).length > 0).toBe(true);
+      expect(app.find(NoisinessInsight).length).toBe(1);
+      expect(app.find(AutocorrelationInsight).length).toBe(1);
+    });
+
+    it("let you see segment xray for a question containing a segment", async () => {
+      const store = await createTestStore();
+      store.pushPath(Urls.question(segmentQuestion.id()));
+      const app = mount(store.getAppContainer());
+
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
 
-            await store.waitForActions([FETCH_SEGMENT_XRAY], { timeout: 20000 })
-            expect(store.getPath()).toBe(`/xray/segment/${segmentId}/approximate`)
+      const actionsWidget = app.find(ActionsWidget);
+      click(actionsWidget.childAt(0));
+      const xrayOptionIcon = actionsWidget.find(".Icon.Icon-beaker");
+      click(xrayOptionIcon);
 
-            const segmentXRay = app.find(SegmentXRay)
-            expect(segmentXRay.length).toBe(1)
-            expect(segmentXRay.find(CostSelect).length).toBe(1)
-            expect(segmentXRay.text()).toMatch(/A Segment/);
-        })
-    })
+      await store.waitForActions([FETCH_SEGMENT_XRAY], { timeout: 20000 });
+      expect(store.getPath()).toBe(`/xray/segment/${segmentId}/approximate`);
 
-    describe("admin management of xrays", async () => {
-        it("should allow an admin to manage xrays", async () => {
-            let app;
+      const segmentXRay = app.find(SegmentXRay);
+      expect(segmentXRay.length).toBe(1);
+      expect(segmentXRay.find(CostSelect).length).toBe(1);
+      expect(segmentXRay.text()).toMatch(/A Segment/);
+    });
+  });
 
-            const store = await createTestStore()
+  describe("admin management of xrays", async () => {
+    it("should allow an admin to manage xrays", async () => {
+      let app;
 
-            store.pushPath('/admin/settings/x_rays')
+      const store = await createTestStore();
 
-            app = mount(store.getAppContainer())
+      store.pushPath("/admin/settings/x_rays");
 
-            await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS], { timeout: 20000 })
+      app = mount(store.getAppContainer());
 
-            const xraySettings = app.find(SettingsXrayForm)
-            const xrayToggle = xraySettings.find(Toggle)
+      await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS], {
+        timeout: 20000,
+      });
 
-            // there should be a toggle
-            expect(xrayToggle.length).toEqual(1)
+      const xraySettings = app.find(SettingsXrayForm);
+      const xrayToggle = xraySettings.find(Toggle);
 
-            // things should be on
-            expect(getXrayEnabled(store.getState())).toEqual(true)
-            // the toggle should be on by default
-            expect(xrayToggle.props().value).toEqual(true)
+      // there should be a toggle
+      expect(xrayToggle.length).toEqual(1);
 
-            // toggle the... toggle
-            click(xrayToggle)
-            await store.waitForActions([UPDATE_SETTING])
-            await delay(100); // give the store UI some time to update (otherwise we see React errors in logs)
+      // things should be on
+      expect(getXrayEnabled(store.getState())).toEqual(true);
+      // the toggle should be on by default
+      expect(xrayToggle.props().value).toEqual(true);
 
-            expect(getXrayEnabled(store.getState())).toEqual(false)
+      // toggle the... toggle
+      click(xrayToggle);
+      await store.waitForActions([UPDATE_SETTING]);
+      await delay(100); // give the store UI some time to update (otherwise we see React errors in logs)
 
-            // navigate to a previosuly x-ray-able entity
-            store.pushPath(Urls.question(timeBreakoutQuestion.id()))
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED])
+      expect(getXrayEnabled(store.getState())).toEqual(false);
 
-            // for some reason a delay is needed to get the full action suite
-            await delay(500);
+      // navigate to a previosuly x-ray-able entity
+      store.pushPath(Urls.question(timeBreakoutQuestion.id()));
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
 
-            const actionsWidget = app.find(ActionsWidget)
-            click(actionsWidget.childAt(0))
+      // for some reason a delay is needed to get the full action suite
+      await delay(500);
 
-            // there should not be an xray option
-            const xrayOptionIcon = actionsWidget.find('.Icon.Icon-beaker')
-            expect(xrayOptionIcon.length).toEqual(0)
-        })
+      const actionsWidget = app.find(ActionsWidget);
+      click(actionsWidget.childAt(0));
 
-        it("should not show xray options for segments when xrays are disabled", async () => {
-            // turn off xrays
-            await SettingsApi.put({ key: 'enable-xrays', value: false })
+      // there should not be an xray option
+      const xrayOptionIcon = actionsWidget.find(".Icon.Icon-beaker");
+      expect(xrayOptionIcon.length).toEqual(0);
+    });
 
-            const store = await createTestStore()
+    it("should not show xray options for segments when xrays are disabled", async () => {
+      // turn off xrays
+      await SettingsApi.put({ key: "enable-xrays", value: false });
 
-            store.pushPath(Urls.question(segmentQuestion.id()))
-            const app = mount(store.getAppContainer())
+      const store = await createTestStore();
 
-            await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED])
-            await delay(500);
+      store.pushPath(Urls.question(segmentQuestion.id()));
+      const app = mount(store.getAppContainer());
 
-            const actionsWidget = app.find(ActionsWidget)
-            click(actionsWidget.childAt(0))
-            const xrayOptionIcon = actionsWidget.find('.Icon.Icon-beaker')
-            expect(xrayOptionIcon.length).toEqual(0)
-        })
+      await store.waitForActions([INITIALIZE_QB, QUERY_COMPLETED]);
+      await delay(500);
 
-        it("should properly reflect the an admin set the max cost of xrays", async () => {
-            await SettingsApi.put({ key: 'enable-xrays', value: true })
-            const store = await createTestStore()
+      const actionsWidget = app.find(ActionsWidget);
+      click(actionsWidget.childAt(0));
+      const xrayOptionIcon = actionsWidget.find(".Icon.Icon-beaker");
+      expect(xrayOptionIcon.length).toEqual(0);
+    });
 
-            store.pushPath('/admin/settings/x_rays')
+    it("should properly reflect the an admin set the max cost of xrays", async () => {
+      await SettingsApi.put({ key: "enable-xrays", value: true });
+      const store = await createTestStore();
 
-            const app = mount(store.getAppContainer())
+      store.pushPath("/admin/settings/x_rays");
 
-            await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS])
+      const app = mount(store.getAppContainer());
 
-            const xraySettings = app.find(SettingsXrayForm)
+      await store.waitForActions([LOAD_CURRENT_USER, INITIALIZE_SETTINGS]);
 
-            expect(xraySettings.find(Icon).length).toEqual(3)
+      const xraySettings = app.find(SettingsXrayForm);
 
-            const approximate = xraySettings.find('.text-measure li').first()
+      expect(xraySettings.find(Icon).length).toEqual(3);
 
-            click(approximate)
-            await store.waitForActions([UPDATE_SETTING])
-            await delay(100); // give the store UI some time to update (otherwise we see React errors in logs)
+      const approximate = xraySettings.find(".text-measure li").first();
 
-            expect(approximate.hasClass('text-brand')).toEqual(true)
-            expect(getMaxCost(store.getState())).toEqual('approximate')
+      click(approximate);
+      await store.waitForActions([UPDATE_SETTING]);
+      await delay(100); // give the store UI some time to update (otherwise we see React errors in logs)
 
-            store.pushPath(`/xray/table/1/approximate`);
+      expect(approximate.hasClass("text-brand")).toEqual(true);
+      expect(getMaxCost(store.getState())).toEqual("approximate");
 
-            await store.waitForActions(FETCH_TABLE_XRAY, { timeout: 20000 })
-            await delay(200)
+      store.pushPath(`/xray/table/1/approximate`);
 
-            const tableXRay = app.find(TableXRay)
-            expect(tableXRay.length).toBe(1)
-            expect(tableXRay.find(CostSelect).length).toBe(1)
-            // there should be two disabled states
-            expect(tableXRay.find('a.disabled').length).toEqual(2)
-        })
+      await store.waitForActions(FETCH_TABLE_XRAY, { timeout: 20000 });
+      await delay(200);
 
-    })
-    describe("data reference entry", async () => {
-        it("should be possible to access an Xray from the data reference", async () => {
-            // ensure xrays are on
-            await SettingsApi.put({ key: 'enable-xrays', value: true })
-            const store = await createTestStore()
+      const tableXRay = app.find(TableXRay);
+      expect(tableXRay.length).toBe(1);
+      expect(tableXRay.find(CostSelect).length).toBe(1);
+      // there should be two disabled states
+      expect(tableXRay.find("a.disabled").length).toEqual(2);
+    });
+  });
+  describe("data reference entry", async () => {
+    it("should be possible to access an Xray from the data reference", async () => {
+      // ensure xrays are on
+      await SettingsApi.put({ key: "enable-xrays", value: true });
+      const store = await createTestStore();
 
-            store.pushPath('/reference/databases/1/tables/1')
+      store.pushPath("/reference/databases/1/tables/1");
 
-            const app = mount(store.getAppContainer())
+      const app = mount(store.getAppContainer());
 
-            await store.waitForActions([END_LOADING])
+      await store.waitForActions([END_LOADING]);
 
-            const xrayTableSideBarItem = app.find('.Icon.Icon-beaker')
-            expect(xrayTableSideBarItem.length).toEqual(1)
+      const xrayTableSideBarItem = app.find(".Icon.Icon-beaker");
+      expect(xrayTableSideBarItem.length).toEqual(1);
 
-            store.pushPath('/reference/databases/1/tables/1/fields/1')
+      store.pushPath("/reference/databases/1/tables/1/fields/1");
 
-            await store.waitForActions([END_LOADING])
-            const xrayFieldSideBarItem = app.find('.Icon.Icon-beaker')
-            expect(xrayFieldSideBarItem.length).toEqual(1)
-        })
+      await store.waitForActions([END_LOADING]);
+      const xrayFieldSideBarItem = app.find(".Icon.Icon-beaker");
+      expect(xrayFieldSideBarItem.length).toEqual(1);
+    });
 
-        it("should not be possible to access an Xray from the data reference if xrays are disabled", async () => {
-            // turn off xrays
-            await SettingsApi.put({ key: 'enable-xrays', value: false })
-            const store = await createTestStore()
+    it("should not be possible to access an Xray from the data reference if xrays are disabled", async () => {
+      // turn off xrays
+      await SettingsApi.put({ key: "enable-xrays", value: false });
+      const store = await createTestStore();
 
-            const app = mount(store.getAppContainer())
+      const app = mount(store.getAppContainer());
 
-            store.pushPath('/reference/databases/1/tables/1')
+      store.pushPath("/reference/databases/1/tables/1");
 
-            await store.waitForActions([END_LOADING])
+      await store.waitForActions([END_LOADING]);
 
-            const xrayTableSideBarItem = app.find('.Icon.Icon-beaker')
-            expect(xrayTableSideBarItem.length).toEqual(0)
+      const xrayTableSideBarItem = app.find(".Icon.Icon-beaker");
+      expect(xrayTableSideBarItem.length).toEqual(0);
 
-            store.pushPath('/reference/databases/1/tables/1/fields/1')
-            await store.waitForActions([END_LOADING])
-            const xrayFieldSideBarItem = app.find('.Icon.Icon-beaker')
-            expect(xrayFieldSideBarItem.length).toEqual(0)
-        })
-    })
+      store.pushPath("/reference/databases/1/tables/1/fields/1");
+      await store.waitForActions([END_LOADING]);
+      const xrayFieldSideBarItem = app.find(".Icon.Icon-beaker");
+      expect(xrayFieldSideBarItem.length).toEqual(0);
+    });
+  });
 
-    afterAll(async () => {
-        await delay(2000)
-    })
+  afterAll(async () => {
+    await delay(2000);
+  });
 });
diff --git a/package.json b/package.json
index e85fcafe8ca59612fce3752093d57c754e055d9e..48056c026f4b35b132604f7d72774da620302c43 100644
--- a/package.json
+++ b/package.json
@@ -87,6 +87,7 @@
     "babel-loader": "^7.1.2",
     "babel-plugin-add-react-displayname": "^0.0.4",
     "babel-plugin-c-3po": "^0.5.8",
+    "babel-plugin-syntax-trailing-function-commas": "^6.22.0",
     "babel-plugin-transform-builtin-extend": "^1.1.2",
     "babel-plugin-transform-decorators-legacy": "^1.3.4",
     "babel-plugin-transform-flow-strip-types": "^6.8.0",
@@ -136,7 +137,7 @@
     "postcss-import": "^9.0.0",
     "postcss-loader": "^2.0.8",
     "postcss-url": "^6.0.4",
-    "prettier": "^0.22.0",
+    "prettier": "^1.10.2",
     "promise-loader": "^1.0.0",
     "react-test-renderer": "^15.5.4",
     "sauce-connect-launcher": "^1.1.1",
@@ -153,9 +154,9 @@
     "dev": "yarn && concurrently --kill-others -p name -n 'backend,frontend' -c 'blue,green' 'lein ring server' 'yarn run build-hot'",
     "lint": "yarn run lint-eslint && yarn run lint-prettier",
     "lint-eslint": "eslint --ext .js --ext .jsx --max-warnings 0 frontend/src frontend/test",
-    "lint-prettier": "prettier --tab-width 4 -l 'frontend/src/metabase/{qb,new_question}/**/*.js*' 'frontend/src/metabase-lib/**/*.js' || (echo '\nThese files are not formatted correctly. Did you forget to run \"yarn run prettier\"?' && false)",
+    "lint-prettier": "prettier -l 'frontend/**/*.{js,jsx,css}' || (echo '\nThese files are not formatted correctly. Did you forget to run \"yarn run prettier\"?' && false)",
     "flow": "flow check",
-    "test": "yarn run test-integrated && yarn run test-unit && yarn run test-karma",
+    "test": "yarn run test-unit && yarn run test-integrated && yarn run test-karma",
     "test-integrated": "babel-node ./frontend/test/__runner__/run_integrated_tests.js",
     "test-integrated-watch": "babel-node ./frontend/test/__runner__/run_integrated_tests.js --watch",
     "test-unit": "jest --maxWorkers=8 --config jest.unit.conf.json --coverage",
@@ -173,16 +174,12 @@
     "start": "yarn run build && lein ring server",
     "precommit": "lint-staged",
     "preinstall": "echo $npm_execpath | grep -q yarn || echo '\\033[0;33mSorry, npm is not supported. Please use Yarn (https://yarnpkg.com/).\\033[0m'",
-    "prettier": "prettier --tab-width 4 --write 'frontend/src/metabase/{qb,new_question}/**/*.js*' 'frontend/src/metabase-lib/**/*.js'",
+    "prettier": "prettier --write 'frontend/**/*.{js,jsx,css}'",
     "docs": "documentation build -f html -o frontend/docs frontend/src/metabase-lib/lib/**"
   },
   "lint-staged": {
-    "frontend/src/metabase/{qb,new_question}/**/*.js*": [
-      "prettier --tab-width 4 --write",
-      "git add"
-    ],
-    "frontend/src/metabase-lib/**/*.js*": [
-      "prettier --tab-width 4 --write",
+    "frontend/**/*.{js,jsx,css}": [
+      "prettier --write",
       "git add"
     ]
   }
diff --git a/project.clj b/project.clj
index 2b4a1ce7958dc9db3a22d8b6885013d55023ce62..90e3f44fcf2ab3f4a95f2b56a579bb4014f74285 100644
--- a/project.clj
+++ b/project.clj
@@ -64,6 +64,7 @@
                  [hiccup "1.0.5"]                                     ; HTML templating
                  [honeysql "0.8.2"]                                   ; Transform Clojure data structures to SQL
                  [io.crate/crate-jdbc "2.1.6"]                        ; Crate JDBC driver
+                 [instaparse "1.4.0"]                                 ; Insaparse parser generator
                  [kixi/stats "0.3.10"                                 ; Various statistic measures implemented as transducers
                   :exclusions [org.clojure/test.check                 ; test.check and AVL trees are used in kixi.stats.random. Remove exlusion if using.
                                org.clojure/data.avl]]
@@ -74,7 +75,7 @@
                                com.sun.jmx/jmxri]]
                  [medley "0.8.4"]                                     ; lightweight lib of useful functions
                  [metabase/throttle "1.0.1"]                          ; Tools for throttling access to API endpoints and other code pathways
-                 [mysql/mysql-connector-java "5.1.39"]                ;  !!! Don't upgrade to 6.0+ yet -- that's Java 8 only !!!
+                 [mysql/mysql-connector-java "5.1.45"]                ;  !!! Don't upgrade to 6.0+ yet -- that's Java 8 only !!!
                  [jdistlib "0.5.1"                                    ; Distribution statistic tests
                   :exclusions [com.github.wendykierp/JTransforms]]
                  [net.cgrand/xforms "0.13.0"                          ; Additional transducers
@@ -103,7 +104,7 @@
   :repositories [["bintray" "https://dl.bintray.com/crate/crate"]     ; Repo for Crate JDBC driver
                  ["redshift" "https://s3.amazonaws.com/redshift-driver-downloads"]]
   :plugins [[lein-environ "1.1.0"]                                    ; easy access to environment variables
-            [lein-ring "0.11.0"                                       ; start the HTTP server with 'lein ring server'
+            [lein-ring "0.12.3"                                       ; start the HTTP server with 'lein ring server'
              :exclusions [org.clojure/clojure]]                       ; TODO - should this be a dev dependency ?
             [puppetlabs/i18n "0.8.0"]]                                ; i18n helpers
   :main ^:skip-aot metabase.core
diff --git a/resources/migrations/000_migrations.yaml b/resources/migrations/000_migrations.yaml
index 1c99056f602d6848532f578595d588a8abf39440..f366997a09aa1fd7528ed5a3778ed8cf7bd8ba71 100644
--- a/resources/migrations/000_migrations.yaml
+++ b/resources/migrations/000_migrations.yaml
@@ -4020,3 +4020,29 @@ databaseChangeLog:
                   remarks: 'True if a XLS of the data should be included for this pulse card'
                   constraints:
                     nullable: false
+  - changeSet:
+      id: 73
+      author: camsaul
+      comment: 'Added 0.29.0'
+      changes:
+        # add a new 'options' (serialized JSON) column to Database to store things like whether we should default to
+        # making string searches case-insensitive
+        - addColumn:
+            tableName: metabase_database
+            columns:
+              - column:
+                  name: options
+                  type: text
+                  remarks: 'Serialized JSON containing various options like QB behavior.'
+  - changeSet:
+      id: 74
+      author: camsaul
+      comment: 'Added 0.29.0'
+      changes:
+        - addColumn:
+            tableName: metabase_field
+            columns:
+              - column:
+                  name: has_field_values
+                  type: text
+                  remarks: 'Whether we have FieldValues ("list"), should ad-hoc search ("search"), disable entirely ("none"), or infer dynamically (null)"'
diff --git a/src/metabase/api/card.clj b/src/metabase/api/card.clj
index aec3cd5e1b2bc461a53793e7d5d63f5f6c5f44bf..45814d0c2c7138dbc3713146ab8f97fec17bf55a 100644
--- a/src/metabase/api/card.clj
+++ b/src/metabase/api/card.clj
@@ -265,11 +265,11 @@
    metadata_checksum      (s/maybe su/NonBlankString)}
   ;; check that we have permissions to run the query that we're trying to save
   (api/check-403 (perms/set-has-full-permissions-for-set? @api/*current-user-permissions-set*
-                                                          (card/query-perms-set dataset_query :write)))
+                   (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
     (api/check-403 (perms/set-has-full-permissions? @api/*current-user-permissions-set*
-                                                    (perms/collection-readwrite-path collection_id))))
+                     (perms/collection-readwrite-path collection_id))))
   ;; everything is g2g, now save the card
   (let [card (db/insert! Card
                :creator_id             api/*current-user-id*
@@ -594,7 +594,7 @@
   "Run the query for Card with PARAMETERS and CONSTRAINTS, and return results in the usual format."
   {:style/indent 1}
   [card-id & {:keys [parameters constraints context dashboard-id]
-              :or   {constraints dataset-api/default-query-constraints
+              :or   {constraints qp/default-query-constraints
                      context     :question}}]
   {:pre [(u/maybe? sequential? parameters)]}
   (let [card    (api/read-check (hydrate (Card card-id) :in_public_dashboard))
@@ -669,5 +669,4 @@
   (api/check-embedding-enabled)
   (db/select [Card :name :id], :enable_embedding true, :archived false))
 
-(api/define-routes
-  (middleware/streaming-json-response (route-fn-name 'POST "/:card-id/query")))
+(api/define-routes)
diff --git a/src/metabase/api/common.clj b/src/metabase/api/common.clj
index 821fb05ddbe474fa66bc2973ffc0dd9d412ff0e1..ce5692bef31e813ab2f0321c39538120191bcf87 100644
--- a/src/metabase/api/common.clj
+++ b/src/metabase/api/common.clj
@@ -1,7 +1,10 @@
 (ns metabase.api.common
   "Dynamic variables and utility functions/macros for writing API functions."
   (:require [cheshire.core :as json]
-            [clojure.string :as s]
+            [clojure.core.async :as async]
+            [clojure.core.async.impl.protocols :as async-proto]
+            [clojure.java.io :as io]
+            [clojure.string :as str]
             [clojure.tools.logging :as log]
             [compojure.core :refer [defroutes]]
             [medley.core :as m]
@@ -13,6 +16,9 @@
             [ring.util
              [io :as rui]
              [response :as rr]]
+            [ring.core.protocols :as protocols]
+            [ring.util.response :as response]
+            [schema.core :as s]
             [toucan.db :as db])
   (:import [java.io BufferedWriter OutputStream OutputStreamWriter]
            [java.nio.charset Charset StandardCharsets]))
@@ -293,8 +299,8 @@
                      symb)]
     `(defroutes ~(vary-meta 'routes assoc :doc (format "Ring routes for %s:\n%s"
                                                        (-> (ns-name *ns*)
-                                                           (s/replace #"^metabase\." "")
-                                                           (s/replace #"\." "/"))
+                                                           (str/replace #"^metabase\." "")
+                                                           (str/replace #"\." "/"))
                                                        (u/pprint-to-str (concat api-routes additional-routes))))
        ~@additional-routes ~@api-routes)))
 
@@ -329,6 +335,113 @@
   ([entity id & other-conditions]
    (write-check (apply db/select-one entity :id id other-conditions))))
 
+;;; --------------------------------------------------- STREAMING ----------------------------------------------------
+
+(def ^:private ^:const streaming-response-keep-alive-interval-ms
+  "Interval between sending newline characters to keep Heroku from terminating requests like queries that take a long
+  time to complete."
+  (* 1 1000))
+
+;; Handle ring response maps that contain a core.async chan in the :body key:
+;;
+;; {:status 200
+;;  :body (async/chan)}
+;;
+;; and send strings (presumibly \n) as heartbeats to the client until the real results (a seq) is received, then
+;; stream that to the client
+(extend-protocol protocols/StreamableResponseBody
+  clojure.core.async.impl.channels.ManyToManyChannel
+  (write-body-to-stream [output-queue _ ^OutputStream output-stream]
+    (log/debug (u/format-color 'green "starting streaming request"))
+    (with-open [out (io/writer output-stream)]
+      (loop [chunk (async/<!! output-queue)]
+        (cond
+          (char? chunk)
+          (do
+            (try
+              (.write out (str chunk))
+              (.flush out)
+              (catch org.eclipse.jetty.io.EofException e
+                (log/info e (u/format-color 'yellow "connection closed, canceling request"))
+                (async/close! output-queue)
+                (throw e)))
+            (recur (async/<!! output-queue)))
+
+          ;; An error has occurred, let the user know
+          (instance? Exception chunk)
+          (json/generate-stream {:error (.getMessage ^Exception chunk)} out)
+
+          ;; We've recevied the response, write it to the output stream and we're done
+          (seq chunk)
+          (json/generate-stream chunk out)
+
+          ;;chunk is nil meaning the output channel has been closed
+          :else
+          out)))))
+
+(def ^:private InvokeWithKeepAliveSchema
+  {;; Channel that contains any number of newlines followed by the results of the invoked query thunk
+   :output-channel  (s/protocol async-proto/Channel)
+   ;; This channel will have an exception if that error condition is hit before the first heartbeat time, if a
+   ;; heartbeat has been sent, this channel is closed and its no longer useful
+   :error-channel   (s/protocol async-proto/Channel)
+   ;; Future that is invoking the query thunk. This is mainly useful for testing metadata to see if the future has been
+   ;; cancelled or was completed successfully
+   :response-future java.util.concurrent.Future})
+
+(s/defn ^:private invoke-thunk-with-keepalive :- InvokeWithKeepAliveSchema
+  "This function does the heavy lifting of invoking `query-thunk` on a background thread and returning it's results
+  along with a heartbeat while waiting for the results. This function returns a map that includes the relevate
+  execution information, see `InvokeWithKeepAliveSchema` for more information"
+  [query-thunk]
+  (let [response-chan (async/chan 1)
+        output-chan   (async/chan 1)
+        error-chan    (async/chan 1)
+        response-fut  (future
+                        (try
+                          (async/>!! response-chan (query-thunk))
+                          (catch Exception e
+                            (async/>!! error-chan e)
+                            (async/>!! response-chan e))
+                          (finally
+                            (async/close! error-chan))))]
+    (async/go-loop []
+      (let [[response-or-timeout c] (async/alts!! [response-chan (async/timeout streaming-response-keep-alive-interval-ms)])]
+        (if response-or-timeout
+          ;; We have a response since it's non-nil, write the results and close, we're done
+          (do
+            ;; If output-chan is closed, it's already too late, nothing else we need to do
+            (async/>!! output-chan response-or-timeout)
+            (async/close! output-chan))
+          (do
+            ;; We don't have a result yet, but enough time has passed, let's assume it's not an error
+            (async/close! error-chan)
+            ;; a newline padding character as it's harmless and will allow us to check if the client is connected. If
+            ;; sending this character fails because the connection is closed, the chan will then close.  Newlines are
+            ;; no-ops when reading JSON which this depends upon.
+            (log/debug (u/format-color 'blue "Response not ready, writing one byte & sleeping..."))
+            (if (async/>!! output-chan \newline)
+              ;; Success put the channel, wait and see if we get the response next time
+              (recur)
+              ;; The channel is closed, client has given up, we should give up too
+              (future-cancel response-fut))))))
+    {:output-channel  output-chan
+     :error-channel   error-chan
+     :response-future response-fut}))
+
+(defn cancellable-json-response
+  "Invokes `cancellable-thunk` in a future. If there's an immediate exception, throw it. If there's not an immediate
+  exception, return a ring response with a channel. The channel will potentially include newline characters before the
+  full response is delivered as a keepalive to the client. Eventually the results of `cancellable-thunk` will be put
+  to the channel"
+  [cancellable-thunk]
+  (let [{:keys [output-channel error-channel]} (invoke-thunk-with-keepalive cancellable-thunk)]
+    ;; If there's an immediate exception, it will be in `error-chan`, if not, `error-chan` will close and we'll assume
+    ;; the response is a success
+    (if-let [ex (async/<!! error-channel)]
+      (throw ex)
+      (assoc (response/response output-channel)
+        :content-type "applicaton/json"))))
 
 ;;; ------------------------------------------------ OTHER HELPER FNS ------------------------------------------------
 
diff --git a/src/metabase/api/dashboard.clj b/src/metabase/api/dashboard.clj
index 0b7a49f856f99dec95a807c0c7183ff386b6c431..02e994e4769acc3b55cb6d181d69d56af5cce236 100644
--- a/src/metabase/api/dashboard.clj
+++ b/src/metabase/api/dashboard.clj
@@ -4,6 +4,7 @@
             [compojure.core :refer [DELETE GET POST PUT]]
             [metabase
              [events :as events]
+             [query-processor :as qp]
              [util :as u]]
             [metabase.api
              [common :as api]
@@ -123,7 +124,7 @@
   [{:keys [dataset_query]}]
   (u/ignore-exceptions
     [(qp-util/query-hash dataset_query)
-     (qp-util/query-hash (assoc dataset_query :constraints dataset/default-query-constraints))]))
+     (qp-util/query-hash (assoc dataset_query :constraints qp/default-query-constraints))]))
 
 (defn- dashcard->query-hashes
   "Return a sequence of all the query hashes for this DASHCARD, including the top-level Card and any Series."
diff --git a/src/metabase/api/database.clj b/src/metabase/api/database.clj
index 97d03cd54e5d3f6ece2cfd277bc0fa542851ba26..62dde99f6d219772a6cacb39f6275f74c6cab081 100644
--- a/src/metabase/api/database.clj
+++ b/src/metabase/api/database.clj
@@ -193,7 +193,7 @@
 
 (defn- db-metadata [id]
   (-> (api/read-check Database id)
-      (hydrate [:tables [:fields :target :values] :segments :metrics])
+      (hydrate [:tables [:fields :target :has_field_values] :segments :metrics])
       (update :tables (fn [tables]
                         (for [table tables
                               :when (mi/can-read? table)]
diff --git a/src/metabase/api/dataset.clj b/src/metabase/api/dataset.clj
index 81ea0634449eb56bbfc05103b6c3a8c63fbcee3a..12daf2b9b789179693358d26edc511ea137f335d 100644
--- a/src/metabase/api/dataset.clj
+++ b/src/metabase/api/dataset.clj
@@ -22,34 +22,18 @@
              [schema :as su]]
             [schema.core :as s]))
 
-;;; --------------------------------------------------- Constants ----------------------------------------------------
-
-(def ^:private ^:const max-results-bare-rows
-  "Maximum number of rows to return specifically on :rows type queries via the API."
-  2000)
-
-(def ^:private ^:const max-results
-  "General maximum number of rows to return from an API query."
-  10000)
-
-(def ^:const default-query-constraints
-  "Default map of constraints that we apply on dataset queries executed by the api."
-  {:max-results           max-results
-   :max-results-bare-rows max-results-bare-rows})
-
-
 ;;; -------------------------------------------- Running a Query Normally --------------------------------------------
 
 (defn- query->source-card-id
-  "Return the ID of the Card used as the \"source\" query of this query, if applicable; otherwise return `nil`.
-   Used so `:card-id` context can be passed along with the query so Collections perms checking is done if appropriate."
+  "Return the ID of the Card used as the \"source\" query of this query, if applicable; otherwise return `nil`. Used so
+  `:card-id` context can be passed along with the query so Collections perms checking is done if appropriate. This fn
+  is a wrapper for the function of the same name in the QP util namespace; it adds additional permissions checking as
+  well."
   [outer-query]
-  (let [source-table (qputil/get-in-normalized outer-query [:query :source-table])]
-    (when (string? source-table)
-      (when-let [[_ card-id-str] (re-matches #"^card__(\d+$)" source-table)]
-        (log/info (str "Source query for this query is Card " card-id-str))
-        (u/prog1 (Integer/parseInt card-id-str)
-          (api/read-check Card <>))))))
+  (when-let [source-card-id (qputil/query->source-card-id outer-query)]
+    (log/info (str "Source query for this query is Card " source-card-id))
+    (api/read-check Card source-card-id)
+    source-card-id))
 
 (api/defendpoint POST "/"
   "Execute a query and retrieve the results in the usual format."
@@ -60,8 +44,10 @@
     (api/read-check Database database))
   ;; add sensible constraints for results limits on our query
   (let [source-card-id (query->source-card-id query)]
-    (qp/process-query-and-save-execution! (assoc query :constraints default-query-constraints)
-      {:executed-by api/*current-user-id*, :context :ad-hoc, :card-id source-card-id, :nested? (boolean source-card-id)})))
+    (api/cancellable-json-response
+     (fn []
+       (qp/process-query-and-save-with-max! query {:executed-by api/*current-user-id*, :context :ad-hoc,
+                                                   :card-id     source-card-id,        :nested? (boolean source-card-id)})))))
 
 
 ;;; ----------------------------------- Downloading Query Results in Other Formats -----------------------------------
@@ -155,8 +141,7 @@
   ;; 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))
-                (query/average-execution-time-ms (qputil/query-hash (assoc query :constraints default-query-constraints)))
+                (query/average-execution-time-ms (qputil/query-hash (assoc query :constraints qp/default-query-constraints)))
                 0)})
 
-(api/define-routes
-  (middleware/streaming-json-response (route-fn-name 'POST "/")))
+(api/define-routes)
diff --git a/src/metabase/api/field.clj b/src/metabase/api/field.clj
index d752a7251c7e6a70655467aa3d5e90182ede0758..5b700aef73f3ff24781acc19accf6f6affd4584f 100644
--- a/src/metabase/api/field.clj
+++ b/src/metabase/api/field.clj
@@ -5,13 +5,18 @@
             [metabase.models
              [dimension :refer [Dimension]]
              [field :as field :refer [Field]]
-             [field-values :as field-values :refer [FieldValues]]]
+             [field-values :as field-values :refer [FieldValues]]
+             [table :refer [Table]]]
+            [metabase.query-processor :as qp]
             [metabase.util :as u]
             [metabase.util.schema :as su]
             [schema.core :as s]
             [toucan
              [db :as db]
-             [hydrate :refer [hydrate]]]))
+             [hydrate :refer [hydrate]]])
+  (:import java.text.NumberFormat))
+
+;;; --------------------------------------------- Basic CRUD Operations ----------------------------------------------
 
 (def ^:private FieldType
   "Schema for a valid `Field` type."
@@ -27,7 +32,7 @@
   "Get `Field` with ID."
   [id]
   (-> (api/read-check Field id)
-      (hydrate [:table :db])))
+      (hydrate [:table :db] :has_field_values)))
 
 (defn- clear-dimension-on-fk-change! [{{dimension-id :id dimension-type :type} :dimensions :as field}]
   (when (and dimension-id (= :external dimension-type))
@@ -59,14 +64,17 @@
 
 (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}]
+  [id :as {{:keys [caveats description display_name fk_target_field_id points_of_interest special_type
+                   visibility_type has_field_values]
+            :as body} :body}]
   {caveats            (s/maybe su/NonBlankString)
    description        (s/maybe su/NonBlankString)
    display_name       (s/maybe su/NonBlankString)
    fk_target_field_id (s/maybe su/IntGreaterThanZero)
    points_of_interest (s/maybe su/NonBlankString)
    special_type       (s/maybe FieldType)
-   visibility_type    (s/maybe FieldVisibilityType)}
+   visibility_type    (s/maybe FieldVisibilityType)
+   has_field_values   (s/maybe (s/enum "search" "list" "none"))}
   (let [field              (hydrate (api/write-check Field id) :dimensions)
         new-special-type   (keyword (get body :special_type (:special_type field)))
         removed-fk?        (removed-fk-special-type? (:special_type field) new-special-type)
@@ -87,11 +95,15 @@
         (clear-dimension-on-type-change! field (:base_type field) new-special-type)
         (db/update! Field id
           (u/select-keys-when (assoc body :fk_target_field_id (when-not removed-fk? fk-target-field-id))
-            :present #{:caveats :description :fk_target_field_id :points_of_interest :special_type :visibility_type}
+            :present #{:caveats :description :fk_target_field_id :points_of_interest :special_type :visibility_type
+                       :has_field_values}
             :non-nil #{:display_name})))))
     ;; return updated field
     (hydrate (Field id) :dimensions)))
 
+
+;;; ------------------------------------------------- Field Metadata -------------------------------------------------
+
 (api/defendpoint GET "/:id/summary"
   "Get the count and distinct count of `Field` with ID."
   [id]
@@ -99,20 +111,8 @@
     [[:count     (metadata/field-count field)]
      [:distincts (metadata/field-distinct-count field)]]))
 
-(def ^:private empty-field-values
-  {: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 (api/read-check Field id)]
-    (if-let [field-values (and (field-values/field-should-have-field-values? field)
-                               (field-values/create-field-values-if-needed! field))]
-      (-> field-values
-          (assoc :values (field-values/field-values->pairs field-values))
-          (dissoc :human_readable_values))
-      {:values []})))
+;;; --------------------------------------------------- Dimensions ---------------------------------------------------
 
 (api/defendpoint POST "/:id/dimension"
   "Sets the dimension for the given field at ID"
@@ -144,6 +144,24 @@
     (db/delete! Dimension :field_id id)
     api/generic-204-no-content))
 
+
+;;; -------------------------------------------------- FieldValues ---------------------------------------------------
+
+(def ^:private empty-field-values
+  {: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 (api/read-check Field id)]
+    (if-let [field-values (and (field-values/field-should-have-field-values? field)
+                               (field-values/create-field-values-if-needed! field))]
+      (-> field-values
+          (assoc :values (field-values/field-values->pairs field-values))
+          (dissoc :human_readable_values))
+      {:values []})))
+
 ;; match things like GET /field-literal%2Ccreated_at%2Ctype%2FDatetime/values
 ;; (this is how things like [field-literal,created_at,type/Datetime] look when URL-encoded)
 (api/defendpoint GET "/field-literal%2C:field-name%2Ctype%2F:field-type/values"
@@ -212,4 +230,99 @@
   {:status :success})
 
 
+;;; --------------------------------------------------- Searching ----------------------------------------------------
+
+(defn- table-id [field]
+  (u/get-id (:table_id field)))
+
+(defn- db-id [field]
+  (u/get-id (db/select-one-field :db_id Table :id (table-id field))))
+
+(defn- follow-fks
+  "Automatically follow the target IDs in an FK `field` until we reach the PK it points to, and return that. For
+  non-FK Fields, returns them as-is. For example, with the Sample Dataset:
+
+     (follow-fks <PEOPLE.ID Field>)        ;-> <PEOPLE.ID Field>
+     (follow-fks <REVIEWS.REVIEWER Field>) ;-> <PEOPLE.ID Field>
+
+  This is used below to seamlessly handle either PK or FK Fields without having to think about which is which in the
+  `search-values` and `remapped-value` functions."
+  [{special-type :special_type, fk-target-field-id :fk_target_field_id, :as field}]
+  (if (and (isa? special-type :type/FK)
+           fk-target-field-id)
+    (db/select-one Field :id fk-target-field-id)
+    field))
+
+
+(s/defn ^:private search-values
+  "Search for values of `search-field` that start with `value` (up to `limit`, if specified), and return like
+
+      [<value-of-field> <matching-value-of-search-field>].
+
+   For example, with the Sample Dataset, you could search for the first three IDs & names of People whose name starts
+   with `Ma` as follows:
+
+      (search-values <PEOPLE.ID Field> <PEOPLE.NAME Field> \"Ma\" 3)
+      ;; -> ((14 \"Marilyne Mohr\")
+             (36 \"Margot Farrell\")
+             (48 \"Maryam Douglas\"))"
+  [field search-field value & [limit]]
+  (let [field   (follow-fks field)
+        results (qp/process-query
+                  {:database (db-id field)
+                   :type     :query
+                   :query    {:source-table (table-id field)
+                              :filter       [:starts-with [:field-id (u/get-id search-field)] value {:case-sensitive false}]
+                              :breakout     [[:field-id (u/get-id field)]]
+                              :fields       [[:field-id (u/get-id field)]
+                                             [:field-id (u/get-id search-field)]]
+                              :limit        limit}})]
+    ;; return rows if they exist
+    (get-in results [:data :rows])))
+
+(api/defendpoint GET "/:id/search/:search-id"
+  "Search for values of a Field that match values of another Field when breaking out by the "
+  [id search-id value limit]
+  {value su/NonBlankString
+   limit (s/maybe su/IntStringGreaterThanZero)}
+  (let [field        (api/read-check Field id)
+        search-field (api/read-check Field search-id)]
+    (search-values field search-field value (when limit (Integer/parseInt limit)))))
+
+
+(defn remapped-value
+  "Search for one specific remapping where the value of `field` exactly matches `value`. Returns a pair like
+
+      [<value-of-field> <value-of-remapped-field>]
+
+   if a match is found.
+
+   For example, with the Sample Dataset, you could find the name of the Person with ID 20 as follows:
+
+      (remapped-value <PEOPLE.ID Field> <PEOPLE.NAME Field> 20)
+      ;; -> [20 \"Peter Watsica\"]"
+  [field remapped-field value]
+  (let [field   (follow-fks field)
+        results (qp/process-query
+                  {:database (db-id field)
+                   :type     :query
+                   :query    {:source-table (table-id field)
+                              :filter       [:= [:field-id (u/get-id field)] value]
+                              :fields       [[:field-id (u/get-id field)]
+                                             [:field-id (u/get-id remapped-field)]]
+                              :limit        1}})]
+    ;; return first row if it exists
+    (first (get-in results [:data :rows]))))
+
+(api/defendpoint GET "/:id/remapping/:remapped-id"
+  "Fetch remapped Field values."
+  [id remapped-id, ^String value]
+  (let [field          (api/read-check Field id)
+        remapped-field (api/read-check Field remapped-id)
+        value          (if (isa? (:base_type field) :type/Number)
+                         (.parse (NumberFormat/getInstance) value)
+                         value)]
+    (remapped-value field remapped-field value)))
+
+
 (api/define-routes)
diff --git a/src/metabase/api/geojson.clj b/src/metabase/api/geojson.clj
index 327348354fccef864e271716eba537a223cf17b1..c6cfc6e1de3abc1e29714757fcb1dd6f5d3b94f8 100644
--- a/src/metabase/api/geojson.clj
+++ b/src/metabase/api/geojson.clj
@@ -28,7 +28,7 @@
 (defn- valid-json-url?
   "Is URL a valid HTTP URL and does it point to valid JSON?"
   [url]
-  (when (u/is-url? url)
+  (when (u/url? url)
     (valid-json? url)))
 
 (def ^:private valid-json-url-or-resource?
diff --git a/src/metabase/api/pulse.clj b/src/metabase/api/pulse.clj
index f6106dcdad95141c591c1388597025dc47d73a50..324216bd28d74f6426bbfdb8adb02a983781c9b8 100644
--- a/src/metabase/api/pulse.clj
+++ b/src/metabase/api/pulse.clj
@@ -133,7 +133,8 @@
      :pulse_card_html card-html
      :pulse_card_name (:name card)
      :pulse_card_url  (urls/card-url (:id card))
-     :row_count       (:row_count result)}))
+     :row_count       (:row_count result)
+     :col_count       (count (:cols (:data result)))}))
 
 (api/defendpoint GET "/preview_card_png/:id"
   "Get PNG rendering of a `Card` with ID."
diff --git a/src/metabase/api/session.clj b/src/metabase/api/session.clj
index 2ac66b87a9e9d8afa430c3029d32bdfd23da9780..3d313fca3ecf4ef6950665137c36f9f9b31cd247 100644
--- a/src/metabase/api/session.clj
+++ b/src/metabase/api/session.clj
@@ -187,7 +187,7 @@
   (last (re-find #"^.*@(.*$)" email)))
 
 (defn- email-in-domain? ^Boolean [email domain]
-  {:pre [(u/is-email? email)]}
+  {:pre [(u/email? email)]}
   (= (email->domain email) domain))
 
 (defn- autocreate-user-allowed-for-email? [email]
diff --git a/src/metabase/api/table.clj b/src/metabase/api/table.clj
index e2840ee2ef8441777af83ec8be19707f5e3f0b15..9ee39dbc73104a80997b1887fe451c4adbebf230 100644
--- a/src/metabase/api/table.clj
+++ b/src/metabase/api/table.clj
@@ -222,17 +222,16 @@
   (let [table (api/read-check Table id)
         driver (driver/database-id->driver (:db_id table))]
     (-> table
-        (hydrate :db [:fields :target :dimensions] :segments :metrics)
-        (update :fields with-normal-values)
+        (hydrate :db [:fields :target :dimensions :has_field_values] :segments :metrics)
         (m/dissoc-in [:db :details])
         (assoc-dimension-options driver)
         format-fields-for-response
-        (update-in [:fields] (if (Boolean/parseBoolean include_sensitive_fields)
-                               ;; If someone passes include_sensitive_fields return hydrated :fields as-is
-                               identity
-                               ;; Otherwise filter out all :sensitive fields
-                               (partial filter (fn [{:keys [visibility_type]}]
-                                                 (not= (keyword visibility_type) :sensitive))))))))
+        (update :fields (if (Boolean/parseBoolean include_sensitive_fields)
+                          ;; If someone passes include_sensitive_fields return hydrated :fields as-is
+                          identity
+                          ;; Otherwise filter out all :sensitive fields
+                          (partial filter (fn [{:keys [visibility_type]}]
+                                            (not= (keyword visibility_type) :sensitive))))))))
 
 (defn- card-result-metadata->virtual-fields
   "Return a sequence of 'virtual' fields metadata for the 'virtual' table for a Card in the Saved Questions 'virtual'
diff --git a/src/metabase/api/user.clj b/src/metabase/api/user.clj
index 15c59bcf882001cc91615b7dfd547af3bbb0320a..fdb7a6b7191c02fdf13da5a6bcf85fa60549c9c2 100644
--- a/src/metabase/api/user.clj
+++ b/src/metabase/api/user.clj
@@ -44,7 +44,7 @@
 
 
 (api/defendpoint POST "/"
-  "Create a new `User`, or or reäctivate an existing one."
+  "Create a new `User`, or reactivate an existing one."
   [:as {{:keys [first_name last_name email password]} :body}]
   {first_name su/NonBlankString
    last_name  su/NonBlankString
diff --git a/src/metabase/driver.clj b/src/metabase/driver.clj
index 920fe3e535fb63f7a6ab881875ce1fc4d55ba22e..e818becd072d113cc5908e7223e0073e63cfe8ef 100644
--- a/src/metabase/driver.clj
+++ b/src/metabase/driver.clj
@@ -143,12 +143,16 @@
       everything besides standard deviation is considered \"basic\"; only GA doesn't support this).
   *  `:standard-deviation-aggregations` - Does this driver support standard deviation aggregations?
   *  `:expressions` - Does this driver support expressions (e.g. adding the values of 2 columns together)?
-  *  `:dynamic-schema` -  Does this Database have no fixed definitions of schemas? (e.g. Mongo)
   *  `:native-parameters` - Does the driver support parameter substitution on native queries?
   *  `:expression-aggregations` - Does the driver support using expressions inside aggregations? e.g. something like
       \"sum(x) + count(y)\" or \"avg(x + y)\"
   *  `:nested-queries` - Does the driver support using a query as the `:source-query` of another MBQL query? Examples
-      are CTEs or subselects in SQL queries.")
+      are CTEs or subselects in SQL queries.
+  *  `:no-case-sensitivity-string-filter-options` - An anti-feature: does this driver not let you specify whether or not
+      our string search filter clauses (`:contains`, `:starts-with`, and `:ends-with`, collectively the equivalent of
+      SQL `LIKE` are case-senstive or not? This informs whether we should present you with the 'Case Sensitive' checkbox
+      in the UI. At the time of this writing SQLite, SQLServer, and MySQL have this 'feature' -- `LIKE` clauses are
+      always case-insensitive.")
 
   (format-custom-field-name ^String [this, ^String custom-field-name]
     "*OPTIONAL*. Return the custom name passed via an MBQL `:named` clause so it matches the way it is returned in the
@@ -212,7 +216,12 @@
      returned in any given order.")
 
   (current-db-time ^org.joda.time.DateTime [this ^DatabaseInstance database]
-    "Returns the current time and timezone from the perspective of `DATABASE`."))
+    "Returns the current time and timezone from the perspective of `DATABASE`.")
+
+  (default-to-case-sensitive? ^Boolean [this]
+    "Should this driver default to case-sensitive string search filter clauses (e.g. `starts-with` or `contains`)? The
+    default is `true` since that was the behavior of all drivers with the exception of GA before `0.29.0` when we
+    introduced case-insensitive string search filters as an option."))
 
 (def IDriverDefaultsMixin
   "Default implementations of `IDriver` methods marked *OPTIONAL*."
@@ -228,7 +237,8 @@
                                         (throw
                                          (NoSuchMethodException.
                                           (str (name driver) " does not implement table-rows-seq."))))
-   :current-db-time                   (constantly nil)})
+   :current-db-time                   (constantly nil)
+   :default-to-case-sensitive?        (constantly true)})
 
 
 ;;; ## CONFIG
diff --git a/src/metabase/driver/bigquery.clj b/src/metabase/driver/bigquery.clj
index 16b631df0372476f35b57b76bbac6cd9a4dc7f09..f41a2e19ce3f4752e11b02c2f275d29ba3460237 100644
--- a/src/metabase/driver/bigquery.clj
+++ b/src/metabase/driver/bigquery.clj
@@ -23,7 +23,9 @@
             [metabase.models
              [database :refer [Database]]
              [field :as field]]
-            [metabase.query-processor.util :as qputil]
+            [metabase.query-processor
+             [annotate :as annotate]
+             [util :as qputil]]
             [metabase.util.honeysql-extensions :as hx]
             [toucan.db :as db])
   (:import com.google.api.client.googleapis.auth.oauth2.GoogleCredential
@@ -32,7 +34,7 @@
             TableList TableList$Tables TableReference TableRow TableSchema]
            java.sql.Time
            [java.util Collections Date]
-           [metabase.query_processor.interface DateTimeValue TimeValue Value]))
+           [metabase.query_processor.interface AggregationWithField AggregationWithoutField DateTimeValue Expression TimeValue Value]))
 
 (defrecord BigQueryDriver []
   clojure.lang.Named
@@ -300,12 +302,48 @@
      :rows    (for [row rows]
                 (mapv row columns))}))
 
+;; From the dox: Fields must contain only letters, numbers, and underscores, start with a letter or underscore, and be
+;; at most 128 characters long.
+(defn- format-custom-field-name ^String [^String custom-field-name]
+  (let [replaced-str (-> (str/trim custom-field-name)
+                         (str/replace #"[^\w\d_]" "_")
+                         (str/replace #"(^\d)" "_$1"))]
+    (subs replaced-str 0 (min 128 (count replaced-str)))))
+
+(defn- agg-or-exp? [x]
+  (or (instance? Expression x)
+      (instance? AggregationWithField x)
+      (instance? AggregationWithoutField x)))
+
+(defn- bg-aggregate-name [aggregate]
+  (-> aggregate annotate/aggregation-name format-custom-field-name))
+
+(defn- pre-alias-aggregations
+  "Expressions are not allowed in the order by clauses of a BQ query. To sort by a custom expression, that custom
+  expression must be aliased from the order by. This code will find the aggregations and give them a name if they
+  don't already have one. This name can then be used in the order by if one is present."
+  [query]
+  (let [aliases (atom {})]
+    (walk/postwalk (fn [maybe-agg]
+                     (if-let [exp-name (and (agg-or-exp? maybe-agg)
+                                            (bg-aggregate-name maybe-agg))]
+                       (if-let [usage-count (get @aliases exp-name)]
+                         (let [new-custom-name (str exp-name "_" (inc usage-count))]
+                           (swap! aliases assoc
+                                  exp-name (inc usage-count)
+                                  new-custom-name 1)
+                           (assoc maybe-agg :custom-name new-custom-name))
+                         (do
+                           (swap! aliases assoc exp-name 1)
+                           (assoc maybe-agg :custom-name exp-name)))
+                       maybe-agg))
+                   query)))
+
 (defn- mbql->native [{{{:keys [dataset-id]} :details, :as database} :database, {{table-name :name} :source-table} :query, :as outer-query}]
   {:pre [(map? database) (seq dataset-id) (seq table-name)]}
-  (binding [sqlqp/*query* outer-query]
-    (let [honeysql-form (honeysql-form outer-query)
-          sql           (honeysql-form->sql honeysql-form)]
-      {:query      sql
+  (let [aliased-query (pre-alias-aggregations outer-query)]
+    (binding [sqlqp/*query* aliased-query]
+      {:query      (-> aliased-query honeysql-form honeysql-form->sql)
        :table-name table-name
        :mbql?      true})))
 
@@ -341,18 +379,25 @@
        (sqlqp/->honeysql driver)
        hx/->time))
 
-(defn- field->alias [{:keys [^String schema-name, ^String field-name, ^String table-name, ^Integer index, field], :as this}]
+(defn- field->alias [driver {:keys [^String schema-name, ^String field-name, ^String table-name, ^Integer index, field], :as this}]
   {:pre [(map? this) (or field
                          index
                          (and (seq schema-name) (seq field-name) (seq table-name))
                          (log/error "Don't know how to alias: " this))]}
   (cond
-    field (recur field) ; type/DateTime
-    index (name (let [{{aggregations :aggregation} :query} sqlqp/*query*
-                      {ag-type :aggregation-type}          (nth aggregations index)]
-                  (if (= ag-type :distinct)
-                    :count
-                    ag-type)))
+    field (recur driver field) ; type/DateTime
+    index (let [{{aggregations :aggregation} :query} sqlqp/*query*
+                {ag-type :aggregation-type :as agg}  (nth aggregations index)]
+            (cond
+              (= ag-type :distinct)
+              "count"
+
+              (instance? Expression agg)
+              (:custom-name agg)
+
+              :else
+              (name ag-type)))
+
     :else (str schema-name \. table-name \. field-name)))
 
 ;; TODO - Making 2 DB calls for each field to fetch its dataset is inefficient and makes me cry, but this method is
@@ -362,91 +407,13 @@
         dataset (:dataset-id (db/select-one-field :details Database, :id db-id))]
     (hsql/raw (apply format "[%s.%s.%s]" dataset (field/qualified-name-components field)))))
 
-;; We have to override the default SQL implementations of breakout and order-by because BigQuery propogates casting
-;; functions in SELECT
-;; BAD:
-;; SELECT msec_to_timestamp([sad_toucan_incidents.incidents.timestamp]) AS [sad_toucan_incidents.incidents.timestamp],
-;;       count(*) AS [count]
-;; FROM [sad_toucan_incidents.incidents]
-;; GROUP BY msec_to_timestamp([sad_toucan_incidents.incidents.timestamp])
-;; ORDER BY msec_to_timestamp([sad_toucan_incidents.incidents.timestamp]) ASC
-;; LIMIT 10
-;;
-;; GOOD:
-;; SELECT msec_to_timestamp([sad_toucan_incidents.incidents.timestamp]) AS [sad_toucan_incidents.incidents.timestamp],
-;;        count(*) AS [count]
-;; FROM [sad_toucan_incidents.incidents]
-;; GROUP BY [sad_toucan_incidents.incidents.timestamp]
-;; ORDER BY [sad_toucan_incidents.incidents.timestamp] ASC
-;; LIMIT 10
-
-(defn- deduplicate-aliases
-  "Given a sequence of aliases, return a sequence where duplicate aliases have been appropriately suffixed.
-
-     (deduplicate-aliases [\"sum\" \"count\" \"sum\" \"avg\" \"sum\" \"min\"])
-     ;; -> [\"sum\" \"count\" \"sum_2\" \"avg\" \"sum_3\" \"min\"]"
-  [aliases]
-  (loop [acc [], alias->use-count {}, [alias & more, :as aliases] aliases]
-    (let [use-count (get alias->use-count alias)]
-      (cond
-        (empty? aliases) acc
-        (not alias)      (recur (conj acc alias) alias->use-count more)
-        (not use-count)  (recur (conj acc alias) (assoc alias->use-count alias 1) more)
-        :else            (let [new-count (inc use-count)
-                               new-alias (str alias "_" new-count)]
-                           (recur (conj acc new-alias) (assoc alias->use-count alias new-count, new-alias 1) more))))))
-
-(defn- select-subclauses->aliases
-  "Return a vector of aliases used in HoneySQL SELECT-SUBCLAUSES.
-   (For clauses that aren't aliased, `nil` is returned as a placeholder)."
-  [select-subclauses]
-  (for [subclause select-subclauses]
-    (when (and (vector? subclause)
-               (= 2 (count subclause)))
-      (second subclause))))
-
-(defn update-select-subclause-aliases
-  "Given a vector of HoneySQL SELECT-SUBCLAUSES and a vector of equal length of NEW-ALIASES,
-   return a new vector with combining the original `SELECT` subclauses with the new aliases.
-
-   Subclauses that are not aliased are not modified; they are given a placeholder of `nil` in the NEW-ALIASES vector.
-
-     (update-select-subclause-aliases [[:user_id \"user_id\"] :venue_id]
-                                      [\"user_id_2\" nil])
-     ;; -> [[:user_id \"user_id_2\"] :venue_id]"
-  [select-subclauses new-aliases]
-  (for [[subclause new-alias] (partition 2 (interleave select-subclauses new-aliases))]
-    (if-not new-alias
-      subclause
-      [(first subclause) new-alias])))
-
-(defn- deduplicate-select-aliases
-  "Replace duplicate aliases in SELECT-SUBCLAUSES with appropriately suffixed aliases.
-
-  BigQuery doesn't allow duplicate aliases in `SELECT` statements; a statement like `SELECT sum(x) AS sum, sum(y) AS
-  sum` is invalid. (See #4089) To work around this, we'll modify the HoneySQL aliases to make sure the same one isn't
-  used twice by suffixing duplicates appropriately.
-  (We'll generate SQL like `SELECT sum(x) AS sum, sum(y) AS sum_2` instead.)"
-  [select-subclauses]
-  (let [aliases (select-subclauses->aliases select-subclauses)
-        deduped (deduplicate-aliases aliases)]
-    (update-select-subclause-aliases select-subclauses deduped)))
-
-(defn- apply-aggregation
-  "BigQuery's implementation of `apply-aggregation` just hands off to the normal Generic SQL implementation, but calls
-  `deduplicate-select-aliases` on the results."
-  [driver honeysql-form query]
-  (-> (sqlqp/apply-aggregation driver honeysql-form query)
-      (update :select deduplicate-select-aliases)))
-
-
-(defn- field->breakout-identifier [field]
-  (hsql/raw (str \[ (field->alias field) \])))
+(defn- field->breakout-identifier [driver field]
+  (hsql/raw (str \[ (field->alias driver field) \])))
 
 (defn- apply-breakout [driver honeysql-form {breakout-fields :breakout, fields-fields :fields}]
   (-> honeysql-form
       ;; Group by all the breakout fields
-      ((partial apply h/group)  (map field->breakout-identifier breakout-fields))
+      ((partial apply h/group)  (map #(field->breakout-identifier driver %) breakout-fields))
       ;; Add fields form only for fields that weren't specified in :fields clause -- we don't want to include it
       ;; twice, or HoneySQL will barf
       ((partial apply h/merge-select) (for [field breakout-fields
@@ -465,11 +432,11 @@
         (recur honeysql-form more)
         honeysql-form))))
 
-(defn- apply-order-by [honeysql-form {subclauses :order-by}]
+(defn- apply-order-by [driver honeysql-form {subclauses :order-by}]
   (loop [honeysql-form honeysql-form, [{:keys [field direction]} & more] subclauses]
-    (let [honeysql-form (h/merge-order-by honeysql-form [(field->breakout-identifier field) (case direction
-                                                                                              :ascending  :asc
-                                                                                              :descending :desc)])]
+    (let [honeysql-form (h/merge-order-by honeysql-form [(field->breakout-identifier driver field) (case direction
+                                                                                                     :ascending  :asc
+                                                                                                     :descending :desc)])]
       (if (seq more)
         (recur honeysql-form more)
         honeysql-form))))
@@ -477,13 +444,6 @@
 (defn- string-length-fn [field-key]
   (hsql/call :length field-key))
 
-;; From the dox: Fields must contain only letters, numbers, and underscores, start with a letter or underscore, and be
-;; at most 128 characters long.
-(defn- format-custom-field-name ^String [^String custom-field-name]
-  (str/join (take 128 (-> (str/trim custom-field-name)
-                        (str/replace #"[^\w\d_]" "_")
-                        (str/replace #"(^\d)" "_$1")))))
-
 (defn- date-interval [driver unit amount]
   (sqlqp/->honeysql driver (u/relative-date unit amount)))
 
@@ -497,16 +457,15 @@
 (u/strict-extend BigQueryDriver
   sql/ISQLDriver
   (merge (sql/ISQLDriverDefaultsMixin)
-         {:apply-aggregation         apply-aggregation
-          :apply-breakout            apply-breakout
+         {:apply-breakout            apply-breakout
           :apply-join-tables         (u/drop-first-arg apply-join-tables)
-          :apply-order-by            (u/drop-first-arg apply-order-by)
+          :apply-order-by            apply-order-by
           ;; these two are actually not applicable since we don't use JDBC
           :column->base-type         (constantly nil)
           :connection-details->spec  (constantly nil)
           :current-datetime-fn       (constantly :%current_timestamp)
           :date                      (u/drop-first-arg date)
-          :field->alias              (u/drop-first-arg field->alias)
+          :field->alias              field->alias
           :field->identifier         (u/drop-first-arg field->identifier)
           ;; we want identifiers quoted [like].[this] initially (we have to convert them to [like.this] before
           ;; executing)
@@ -551,7 +510,8 @@
                                                              :standard-deviation-aggregations
                                                              :native-parameters
                                                              :expression-aggregations
-                                                             :binning}
+                                                             :binning
+                                                             :native-query-params}
                                                            (when-not config/is-test?
                                                              ;; during unit tests don't treat bigquery as having FK
                                                              ;; support
diff --git a/src/metabase/driver/crate.clj b/src/metabase/driver/crate.clj
index 14bf85090c94dd3a00dc1db628d39510803bba66..7c85e0f8d37bc13baf26f2b09c176d2d583979ff 100644
--- a/src/metabase/driver/crate.clj
+++ b/src/metabase/driver/crate.clj
@@ -64,9 +64,9 @@
   (let [columns (jdbc/query
                  (sql/db->jdbc-connection-spec database)
                  [(format "select column_name, data_type as type_name
-                            from information_schema.columns
-                            where table_name like '%s' and table_schema like '%s'
-                            and data_type != 'object_array'" name schema)])] ; clojure jdbc can't handle fields of type "object_array" atm
+                           from information_schema.columns
+                           where table_name like '%s' and table_schema like '%s'
+                           and data_type != 'object_array'" name schema)])] ; clojure jdbc can't handle fields of type "object_array" atm
     (set (for [{:keys [column_name type_name]} columns]
            {:name      column_name
             :custom    {:column-type type_name}
diff --git a/src/metabase/driver/druid.clj b/src/metabase/driver/druid.clj
index 97264e8cfb285dfb4575cb9f67bf5bcd1a0bd2ec..61e9fdfe358cca40ae513abe2e29bbdf21c9d261 100644
--- a/src/metabase/driver/druid.clj
+++ b/src/metabase/driver/druid.clj
@@ -43,6 +43,7 @@
 
 (def ^:private ^{:arglists '([url & {:as options}])} GET  (partial do-request http/get))
 (def ^:private ^{:arglists '([url & {:as options}])} POST (partial do-request http/post))
+(def ^:private ^{:arglists '([url & {:as options}])} DELETE (partial do-request http/delete))
 
 
 ;;; ### Misc. Driver Fns
@@ -57,15 +58,39 @@
 (defn- do-query [details query]
   {:pre [(map? query)]}
   (ssh/with-ssh-tunnel [details-with-tunnel details]
-    (try (vec (POST (details->url details-with-tunnel "/druid/v2"), :body query))
-         (catch Throwable e
-           ;; try to extract the error
-           (let [message (or (u/ignore-exceptions
-                               (:error (json/parse-string (:body (:object (ex-data e))) keyword)))
-                             (.getMessage e))]
-             (log/error (u/format-color 'red "Error running query:\n%s" message))
-             ;; Re-throw a new exception with `message` set to the extracted message
-             (throw (Exception. message e)))))))
+    (try
+      (POST (details->url details-with-tunnel "/druid/v2"), :body query)
+      (catch Throwable e
+        ;; try to extract the error
+        (let [message (or (u/ignore-exceptions
+                            (:error (json/parse-string (:body (:object (ex-data e))) keyword)))
+                          (.getMessage e))]
+          (log/error (u/format-color 'red "Error running query:\n%s" message))
+          ;; Re-throw a new exception with `message` set to the extracted message
+          (throw (Exception. message e)))))))
+
+(defn- do-query-with-cancellation [details query]
+  (let [query-id  (get-in query [:context :queryId])
+        query-fut (future (do-query details query))]
+    (try
+      ;; Run the query in a future so that this thread will be interrupted, not the thread running the query (which is
+      ;; not interrupt aware)
+      @query-fut
+      (catch InterruptedException interrupted-ex
+        ;; The future has been cancelled, if we ahve a query id, try to cancel the query
+        (if-not query-id
+          (log/warn interrupted-ex "Client closed connection, no queryId found, can't cancel query")
+          (ssh/with-ssh-tunnel [details-with-tunnel details]
+            (log/warnf "Client closed connection, cancelling Druid queryId '%s'" query-id)
+            (try
+              ;; If we can't cancel the query, we don't want to hide the original exception, attempt to cancel, but if
+              ;; we can't, we should rethrow the InterruptedException, not an exception from the cancellation
+              (DELETE (details->url details-with-tunnel (format "/druid/v2/%s" query-id)))
+              (catch Exception cancel-ex
+                (log/warnf cancel-ex "Failed to cancel Druid query with queryId" query-id))
+              (finally
+                ;; Propogate the exception, will cause any other catch/finally clauses to fire
+                (throw interrupted-ex)))))))))
 
 
 ;;; ### Sync
@@ -134,7 +159,7 @@
                                              :display-name "Broker node port"
                                              :type         :integer
                                              :default      8082}]))
-          :execute-query     (fn [_ query] (qp/execute-query do-query query))
+          :execute-query     (fn [_ query] (qp/execute-query do-query-with-cancellation query))
           :features          (constantly #{:basic-aggregations :set-timezone :expression-aggregations})
           :mbql->native      (u/drop-first-arg qp/mbql->native)}))
 
diff --git a/src/metabase/driver/druid/query_processor.clj b/src/metabase/driver/druid/query_processor.clj
index 55699f3c573e0cab14675dfe6c657bca94184732..3a4de54aea4e97831b13bac43941a07d90431f2e 100644
--- a/src/metabase/driver/druid/query_processor.clj
+++ b/src/metabase/driver/druid/query_processor.clj
@@ -6,7 +6,7 @@
              [format :as tformat]]
             [clojure.core.match :refer [match]]
             [clojure.math.numeric-tower :as math]
-            [clojure.string :as s]
+            [clojure.string :as str]
             [clojure.tools.logging :as log]
             [metabase.driver.druid.js :as js]
             [metabase.query-processor
@@ -86,7 +86,8 @@
 (def ^:private ^:const query-type->default-query
   (let [defaults {:intervals   ["1900-01-01/2100-01-01"]
                   :granularity :all
-                  :context     {:timeout 60000}}]
+                  :context     {:timeout 60000
+                                :queryId (str (java.util.UUID/randomUUID))}}]
     {::select             (merge defaults {:queryType  :select
                                            :pagingSpec {:threshold i/absolute-max-results}})
      ::total              (merge defaults {:queryType :timeseries})
@@ -351,7 +352,7 @@
   [& function-str-parts]
   {:pre [(every? string? function-str-parts)]}
   {:type     :javascript
-   :function (s/replace (apply str function-str-parts) #"\s+" " ")})
+   :function (str/replace (apply str function-str-parts) #"\s+" " ")})
 
 ;; don't try to make this a ^:const map -- extract:timeFormat looks up timezone info at query time
 (defn- unit->extraction-fn [unit]
@@ -467,7 +468,7 @@
       (assoc-in [:query :dimensions] (mapv ->dimension-rvalue breakout-fields))))
 
 
-;;; ### handle-filter
+;;; ### handle-filter. See http://druid.io/docs/latest/querying/filters.html
 
 (defn- filter:and [filters]
   {:type   :and
@@ -493,11 +494,39 @@
                       :dimension nil
                       :metric    0))))
 
-(defn- filter:js [field fn-format-str & args]
-  {:pre [field (string? fn-format-str)]}
-  {:type      :javascript
-   :dimension (->rvalue field)
-   :function  (apply format fn-format-str args)})
+(defn- filter:like
+  "Build a `like` filter clause, which is almost just like a SQL `LIKE` clause."
+  [field pattern case-sensitive?]
+  {:type         :like
+   :dimension    (->rvalue field)
+   ;; tell Druid to use backslash as an escape character
+   :escape       "\\"
+   ;; if this is a case-insensitive search we'll lower-case the search pattern and add an extraction function to
+   ;; lower-case the dimension values we're matching against
+   :pattern      (cond-> pattern
+                   (not case-sensitive?) str/lower-case)
+   :extractionFn (when-not case-sensitive?
+                   {:type :lower})})
+
+(defn- escape-like-filter-pattern
+  "Escape `%`, `_`, and backslash symbols that aren't meant to have special meaning in `like` filters
+  patterns. Backslashes wouldn't normally have a special meaning, but we specify backslash as our escape character in
+  the `filter:like` function above, so they need to be escaped as well."
+  [s]
+  (str/replace s #"([%_\\])" "\\\\$1"))
+
+(defn- filter:bound
+  "Numeric `bound` filter, for finding values of `field` that are less than some value, greater than some value, or
+  both. Defaults to being `inclusive` (e.g. `<=` instead of `<`) but specify option `inclusive?` to change this."
+  [field & {:keys [lower upper inclusive?]
+            :or   {inclusive? true}}]
+  {:type        :bound
+   :ordering    :numeric
+   :dimension   (->rvalue field)
+   :lower       (num (->rvalue lower))
+   :upper       (num (->rvalue upper))
+   :lowerStrict (not inclusive?)
+   :upperStrict (not inclusive?)})
 
 (defn- check-filter-fields [filter-type & fields]
   (doseq [field fields]
@@ -507,7 +536,7 @@
         (u/format-color 'red "WARNING: Filtering only works on dimensions! '%s' is a metric. Ignoring %s filter."
           (->rvalue field) filter-type))))))
 
-(defn- parse-filter-subclause:filter [{:keys [filter-type field value] :as filter}]
+(defn- parse-filter-subclause:filter [{:keys [filter-type field value case-sensitive?] :as filter}]
   {:pre [filter]}
   ;; We'll handle :timestamp separately. It needs to go in :intervals instead
   (when-not (instance? DateTimeField field)
@@ -521,16 +550,13 @@
                    lat-field (:field lat)
                    lon-field (:field lon)]
                (check-filter-fields :inside lat-field lon-field)
-               {:type   :and
-                :fields [(filter:js lat-field "function (x) { return x >= %s && x <= %s; }"
-                                    (num (->rvalue (:min lat))) (num (->rvalue (:max lat))))
-                         (filter:js lon-field "function (x) { return x >= %s && x <= %s; }"
-                                    (num (->rvalue (:min lon))) (num (->rvalue (:max lon))))]})
+               (filter:and
+                [(filter:bound lat-field, :lower (:min lat), :upper (:max lat))
+                 (filter:bound lon-field, :lower (:min lon), :upper (:max lon))]))
 
              :between
              (let [{:keys [min-val max-val]} filter]
-               (filter:js field "function (x) { return x >= %s && x <= %s; }"
-                          (num (->rvalue (:value min-val))) (num (->rvalue (:value max-val)))))
+               (filter:bound field, :lower min-val, :upper max-val))
 
              :is-null
              (filter:nil? field)
@@ -541,25 +567,19 @@
              :contains
              {:type      :search
               :dimension (->rvalue field)
-              :query     {:type  :insensitive_contains
-                          :value value}}
-
-             :starts-with
-             (filter:js field
-                        "function (x) { return typeof x === 'string' && x.length >= %d && x.slice(0, %d) === '%s'; }"
-                        (count value) (count value) (s/replace value #"'" "\\\\'"))
-
-             :ends-with
-             (filter:js field
-                        "function (x) { return typeof x === 'string' && x.length >= %d && x.slice(-%d) === '%s'; }"
-                        (count value) (count value) (s/replace value #"'" "\\\\'"))
-
-             :=           (filter:= field value)
-             :!=          (filter:not (filter:= field value))
-             :<           (filter:js field "function (x) { return x < %s; }"  (num value))
-             :>           (filter:js field "function (x) { return x > %s; }"  (num value))
-             :<=          (filter:js field "function (x) { return x <= %s; }" (num value))
-             :>=          (filter:js field "function (x) { return x >= %s; }" (num value))))
+              :query     {:type          :contains
+                          :value         value
+                          :caseSensitive case-sensitive?}}
+
+             :starts-with (filter:like field (str (escape-like-filter-pattern value) \%) case-sensitive?)
+             :ends-with   (filter:like field (str \% (escape-like-filter-pattern value)) case-sensitive?)
+
+             :=  (filter:= field value)
+             :!= (filter:not (filter:= field value))
+             :<  (filter:bound field, :upper value, :inclusive? false)
+             :>  (filter:bound field, :lower value, :inclusive? false)
+             :<= (filter:bound field, :upper value)
+             :>= (filter:bound field, :lower value)))
          (catch Throwable e
            (log/warn (.getMessage e))))))
 
@@ -568,7 +588,7 @@
   (case compound-type
     :and {:type :and, :fields (filterv identity (map parse-filter-clause:filter subclauses))}
     :or  {:type :or,  :fields (filterv identity (map parse-filter-clause:filter subclauses))}
-    :not (when-let [subclause (parse-filter-subclause:filter subclause)]
+    :not (when-let [subclause (parse-filter-clause:filter subclause)]
            (filter:not subclause))
     nil  (parse-filter-subclause:filter clause)))
 
diff --git a/src/metabase/driver/generic_sql.clj b/src/metabase/driver/generic_sql.clj
index a97377a7e333ccf467fe3ecab861eff6273552ee..eb8d85a733c742674ea998a5538cc29d993475b2 100644
--- a/src/metabase/driver/generic_sql.clj
+++ b/src/metabase/driver/generic_sql.clj
@@ -277,7 +277,8 @@
             :expression-aggregations
             :native-parameters
             :nested-queries
-            :binning}
+            :binning
+            :native-query-params}
     (set-timezone-sql driver) (conj :set-timezone)))
 
 
diff --git a/src/metabase/driver/generic_sql/query_processor.clj b/src/metabase/driver/generic_sql/query_processor.clj
index 2164bb60e278a7fd0b20678ce62e20b996c9860e..c486249fb9846b9d37be9c711134db822ba2ac58 100644
--- a/src/metabase/driver/generic_sql/query_processor.clj
+++ b/src/metabase/driver/generic_sql/query_processor.clj
@@ -129,36 +129,6 @@
         (hx/* bin-width)
         (hx/+ min-value))))
 
-;; e.g. the ["aggregation" 0] fields we allow in order-by
-(defmethod ->honeysql [Object AgFieldRef]
-  [_ {index :index}]
-  (let [{:keys [aggregation-type]} (aggregation-at-index index)]
-    ;; For some arcane reason we name the results of a distinct aggregation "count",
-    ;; everything else is named the same as the aggregation
-    (if (= aggregation-type :distinct)
-      :count
-      aggregation-type)))
-
-(defmethod ->honeysql [Object Value]
-  [driver {:keys [value]}]
-  (->honeysql driver value))
-
-(defmethod ->honeysql [Object DateTimeValue]
-  [driver {{unit :unit} :field, value :value}]
-  (sql/date driver unit (->honeysql driver value)))
-
-(defmethod ->honeysql [Object RelativeDateTimeValue]
-  [driver {:keys [amount unit], {field-unit :unit} :field}]
-  (sql/date driver field-unit (if (zero? amount)
-                                (sql/current-datetime-fn driver)
-                                (driver/date-interval driver unit amount))))
-
-(defmethod ->honeysql [Object TimeValue]
-  [driver  {:keys [value]}]
-  (->honeysql driver value))
-
-;;; ## Clause Handlers
-
 (defn- aggregation->honeysql
   "Generate the HoneySQL form for an aggregation."
   [driver aggregation-type field]
@@ -181,7 +151,7 @@
       (->honeysql driver field))))
 
 ;; TODO - can't we just roll this into the ->honeysql method for `expression`?
-(defn- expression-aggregation->honeysql
+(defn expression-aggregation->honeysql
   "Generate the HoneySQL form for an expression aggregation."
   [driver expression]
   (->honeysql driver
@@ -193,6 +163,42 @@
                   (:aggregation-type arg) (aggregation->honeysql driver (:aggregation-type arg) (:field arg))
                   (:operator arg)         (expression-aggregation->honeysql driver arg)))))))
 
+;; e.g. the ["aggregation" 0] fields we allow in order-by
+(defmethod ->honeysql [Object AgFieldRef]
+  [driver {index :index}]
+  (let [{:keys [aggregation-type] :as aggregation} (aggregation-at-index index)]
+    (cond
+      ;; For some arcane reason we name the results of a distinct aggregation "count",
+      ;; everything else is named the same as the aggregation
+      (= aggregation-type :distinct)
+      :count
+
+      (instance? Expression aggregation)
+      (expression-aggregation->honeysql driver aggregation)
+
+      :else
+      aggregation-type)))
+
+(defmethod ->honeysql [Object Value]
+  [driver {:keys [value]}]
+  (->honeysql driver value))
+
+(defmethod ->honeysql [Object DateTimeValue]
+  [driver {{unit :unit} :field, value :value}]
+  (sql/date driver unit (->honeysql driver value)))
+
+(defmethod ->honeysql [Object RelativeDateTimeValue]
+  [driver {:keys [amount unit], {field-unit :unit} :field}]
+  (sql/date driver field-unit (if (zero? amount)
+                                (sql/current-datetime-fn driver)
+                                (driver/date-interval driver unit amount))))
+
+(defmethod ->honeysql [Object TimeValue]
+  [driver  {:keys [value]}]
+  (->honeysql driver value))
+
+;;; ## Clause Handlers
+
 (defn- apply-expression-aggregation [driver honeysql-form expression]
   (h/merge-select honeysql-form [(expression-aggregation->honeysql driver expression)
                                  (hx/escape-dots (driver/format-custom-field-name driver (annotate/aggregation-name expression)))]))
@@ -227,22 +233,39 @@
   (apply h/merge-select honeysql-form (for [field fields]
                                         (as driver (->honeysql driver field) field))))
 
+(defn- like-clause
+  "Generate a SQL `LIKE` clause. `value` is assumed to be a `Value` object (a record type with a key `:value` as well as
+  some sort of type info) or similar as opposed to a raw value literal."
+  [driver field value case-sensitive?]
+  ;; TODO - don't we need to escape underscores and percent signs in the pattern, since they have special meanings in
+  ;; LIKE clauses? That's what we're doing with Druid...
+  ;;
+  ;; TODO - Postgres supports `ILIKE`. Does that make a big enough difference performance-wise that we should do a
+  ;; custom implementation?
+  (if case-sensitive?
+    [:like field                    (->honeysql driver value)]
+    [:like (hsql/call :lower field) (->honeysql driver (update value :value str/lower-case))]))
+
 (defn filter-subclause->predicate
   "Given a filter SUBCLAUSE, return a HoneySQL filter predicate form for use in HoneySQL `where`."
-  [driver {:keys [filter-type field value], :as filter}]
+  [driver {:keys [filter-type field value case-sensitive?], :as filter}]
   {:pre [(map? filter) field]}
   (let [field (->honeysql driver field)]
     (case          filter-type
-      :between     [:between field (->honeysql driver (:min-val filter)) (->honeysql driver (:max-val filter))]
-      :starts-with [:like    field (->honeysql driver (update value :value (fn [s] (str    s \%)))) ]
-      :contains    [:like    field (->honeysql driver (update value :value (fn [s] (str \% s \%))))]
-      :ends-with   [:like    field (->honeysql driver (update value :value (fn [s] (str \% s))))]
-      :>           [:>       field (->honeysql driver value)]
-      :<           [:<       field (->honeysql driver value)]
-      :>=          [:>=      field (->honeysql driver value)]
-      :<=          [:<=      field (->honeysql driver value)]
-      :=           [:=       field (->honeysql driver value)]
-      :!=          [:not=    field (->honeysql driver value)])))
+      :between     [:between field
+                    (->honeysql driver (:min-val filter))
+                    (->honeysql driver (:max-val filter))]
+
+      :starts-with (like-clause driver field (update value :value #(str    % \%)) case-sensitive?)
+      :contains    (like-clause driver field (update value :value #(str \% % \%)) case-sensitive?)
+      :ends-with   (like-clause driver field (update value :value #(str \% %))    case-sensitive?)
+
+      :>           [:>    field (->honeysql driver value)]
+      :<           [:<    field (->honeysql driver value)]
+      :>=          [:>=   field (->honeysql driver value)]
+      :<=          [:<=   field (->honeysql driver value)]
+      :=           [:=    field (->honeysql driver value)]
+      :!=          [:not= field (->honeysql driver value)])))
 
 (defn filter-clause->predicate
   "Given a filter CLAUSE, return a HoneySQL filter predicate form for use in HoneySQL `where`.
@@ -428,16 +451,46 @@
               (jdbc/set-parameter value stmt i)))
           (rest (range)) params)))
 
+(defmacro ^:private with-ensured-connection
+  "In many of the clojure.java.jdbc functions, it checks to see if there's already a connection open before opening a
+  new one. This macro checks to see if one is open, or will open a new one. Will bind the connection to `conn-sym`."
+  [conn-sym db & body]
+  `(let [db# ~db]
+     (if-let [~conn-sym (jdbc/db-find-connection db#)]
+       (do ~@body)
+       (with-open [~conn-sym (jdbc/get-connection db#)]
+         ~@body))))
+
+(defn- cancellable-run-query
+  "Runs `sql` in such a way that it can be interrupted via a `future-cancel`"
+  [db sql params opts]
+  (with-ensured-connection conn db
+    ;; This is normally done for us by java.jdbc as a result of our `jdbc/query` call
+    (with-open [^PreparedStatement stmt (jdbc/prepare-statement conn sql opts)]
+      ;; Need to run the query in another thread so that this thread can cancel it if need be
+      (try
+        (let [query-future (future (jdbc/query conn (into [stmt] params) opts))]
+          ;; This thread is interruptable because it's awaiting the other thread (the one actually running the
+          ;; query). Interrupting this thread means that the client has disconnected (or we're shutting down) and so
+          ;; we can give up on the query running in the future
+          @query-future)
+        (catch InterruptedException e
+          (log/warn e "Client closed connection, cancelling query")
+          ;; This is what does the real work of cancelling the query. We aren't checking the result of
+          ;; `query-future` but this will cause an exception to be thrown, saying the query has been cancelled.
+          (.cancel stmt)
+          (throw e))))))
+
 (defn- run-query
   "Run the query itself."
   [{sql :query, params :params, remark :remark} timezone connection]
   (let [sql              (str "-- " remark "\n" (hx/unescape-dots sql))
         statement        (into [sql] params)
-        [columns & rows] (jdbc/query connection statement {:identifiers    identity, :as-arrays? true
-                                                           :read-columns   (read-columns-with-date-handling timezone)
-                                                           :set-parameters (set-parameters-with-timezone timezone)})]
-    {:rows    (or rows [])
-     :columns columns}))
+        [columns & rows] (cancellable-run-query connection sql params {:identifiers    identity, :as-arrays? true
+                                                                       :read-columns   (read-columns-with-date-handling timezone)
+                                                                       :set-parameters (set-parameters-with-timezone timezone)})]
+       {:rows    (or rows [])
+        :columns columns}))
 
 (defn- exception->nice-error-message ^String [^SQLException e]
   (or (->> (.getMessage e)     ; error message comes back like 'Column "ZID" not found; SQL statement: ... [error-code]' sometimes
diff --git a/src/metabase/driver/googleanalytics.clj b/src/metabase/driver/googleanalytics.clj
index 299148760efb733a9db332089d94c320d407beaf..d7e96570e5eab1322998e2c6c3b045e1824553e1 100644
--- a/src/metabase/driver/googleanalytics.clj
+++ b/src/metabase/driver/googleanalytics.clj
@@ -263,7 +263,8 @@
           :process-query-in-context          (u/drop-first-arg process-query-in-context)
           :mbql->native                      (u/drop-first-arg qp/mbql->native)
           :table-rows-seq                    (u/drop-first-arg table-rows-seq)
-          :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)}))
+          :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)
+          :default-to-case-sensitive?        (constantly false)}))
 
 (defn -init-driver
   "Register the Google Analytics driver"
diff --git a/src/metabase/driver/googleanalytics/query_processor.clj b/src/metabase/driver/googleanalytics/query_processor.clj
index 2ec26594ee94977da808aff82fd12fe050b53d0b..8c720d7620ed5283d480396766ac1e586ac7d1f6 100644
--- a/src/metabase/driver/googleanalytics/query_processor.clj
+++ b/src/metabase/driver/googleanalytics/query_processor.clj
@@ -1,5 +1,6 @@
 (ns metabase.driver.googleanalytics.query-processor
-  "The Query Processor is responsible for translating the Metabase Query Language into Google Analytics request format."
+  "The Query Processor is responsible for translating the Metabase Query Language into Google Analytics request format.
+  See https://developers.google.com/analytics/devguides/reporting/core/v3"
   (:require [clojure.string :as s]
             [clojure.tools.reader.edn :as edn]
             [medley.core :as m]
@@ -50,7 +51,7 @@
 (def ^:private ^{:arglists '([s])} escape-for-regex         (u/rpartial s/escape (char-escape-map ".\\+*?[^]$(){}=!<>|:-")))
 (def ^:private ^{:arglists '([s])} escape-for-filter-clause (u/rpartial s/escape (char-escape-map ",;\\")))
 
-(defn- ga-filter [& parts]
+(defn- ga-filter ^String [& parts]
   (escape-for-filter-clause (apply str parts)))
 
 
@@ -95,26 +96,31 @@
 ;;; ### filter
 
 ;; TODO: implement negate?
-(defn- parse-filter-subclause:filters [{:keys [filter-type field value] :as filter} & [negate?]]
-  (if negate? (throw (Exception. ":not is :not yet implemented")))
-  (when-not (instance? DateTimeField field)
-    (let [field (when field (->rvalue field))
-          value (when value (->rvalue value))]
-      (case filter-type
-        :contains    (ga-filter field "=@" value)
-        :starts-with (ga-filter field "=~^" (escape-for-regex value))
-        :ends-with   (ga-filter field "=~"  (escape-for-regex value) "$")
-        :=           (ga-filter field "==" value)
-        :!=          (ga-filter field "!=" value)
-        :>           (ga-filter field ">" value)
-        :<           (ga-filter field "<" value)
-        :>=          (ga-filter field ">=" value)
-        :<=          (ga-filter field "<=" value)
-        :between     (str (ga-filter field ">=" (->rvalue (:min-val filter)))
-                          ";"
-                          (ga-filter field "<=" (->rvalue (:max-val filter))))))))
-
-(defn- parse-filter-clause:filters [{:keys [compound-type subclause subclauses], :as clause}]
+(defn- parse-filter-subclause:filters
+  (^String [filter-clause negate?]
+   ;; if optional arg `negate?` is truthy then prepend a `!` to negate the filter.
+   ;; See https://developers.google.com/analytics/devguides/reporting/core/v3/segments-feature-reference#not-operator
+   (str (when negate? "!") (parse-filter-subclause:filters filter-clause)))
+
+  (^String [{:keys [filter-type field value case-sensitive?], :as filter-clause}]
+   (when-not (instance? DateTimeField field)
+     (let [field (when field (->rvalue field))
+           value (when value (->rvalue value))]
+       (case filter-type
+         :contains    (ga-filter field "=~" (if case-sensitive? "(?-i)" "(?i)")    (escape-for-regex value))
+         :starts-with (ga-filter field "=~" (if case-sensitive? "(?-i)" "(?i)") \^ (escape-for-regex value))
+         :ends-with   (ga-filter field "=~" (if case-sensitive? "(?-i)" "(?i)")    (escape-for-regex value) \$)
+         :=           (ga-filter field "==" value)
+         :!=          (ga-filter field "!=" value)
+         :>           (ga-filter field ">" value)
+         :<           (ga-filter field "<" value)
+         :>=          (ga-filter field ">=" value)
+         :<=          (ga-filter field "<=" value)
+         :between     (str (ga-filter field ">=" (->rvalue (:min-val filter-clause)))
+                           ";"
+                           (ga-filter field "<=" (->rvalue (:max-val filter-clause)))))))))
+
+(defn- parse-filter-clause:filters ^String [{:keys [compound-type subclause subclauses], :as clause}]
   (case compound-type
     :and (s/join ";" (remove nil? (map parse-filter-clause:filters subclauses)))
     :or  (s/join "," (remove nil? (map parse-filter-clause:filters subclauses)))
@@ -243,7 +249,7 @@
      :annotate mbql?}))
 
 
-;;; ------------------------------------------------------------ "transform-query" ------------------------------------------------------------
+;;; ----------------------------------------------- "transform-query" ------------------------------------------------
 
 ;;; metrics
 
@@ -281,7 +287,8 @@
     ;; if the top-level filter isn't compound check if it's built-in and return it if it is
     (when (built-in-segment? filter-clause)
       (second filter-clause))
-    ;; otherwise if it *is* compound return the first subclause that is built-in; if more than one is built-in throw exception
+    ;; otherwise if it *is* compound return the first subclause that is built-in; if more than one is built-in throw
+    ;; exception
     (when-let [[built-in-segment-name & more] (seq (for [subclause filter-clause
                                                          :when     (built-in-segment? subclause)]
                                                      (second subclause)))]
diff --git a/src/metabase/driver/mongo.clj b/src/metabase/driver/mongo.clj
index 0689e1797e7c5c41b8b6b7fa67f6203f64d826bc..635210b006b50a9e2f5387cdcee1722ff4d07d65 100644
--- a/src/metabase/driver/mongo.clj
+++ b/src/metabase/driver/mongo.clj
@@ -73,7 +73,7 @@
   (cond
     ;; 1. url?
     (and (string? field-value)
-         (u/is-url? field-value)) :type/URL
+         (u/url? field-value)) :type/URL
     ;; 2. json?
     (and (string? field-value)
          (or (.startsWith "{" field-value)
@@ -205,7 +205,7 @@
                                                              :display-name "Additional Mongo connection string options"
                                                              :placeholder  "readPreference=nearest&replicaSet=test"}]))
           :execute-query                     (u/drop-first-arg qp/execute-query)
-          :features                          (constantly #{:basic-aggregations :dynamic-schema :nested-fields})
+          :features                          (constantly #{:basic-aggregations :nested-fields})
           :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)
           :mbql->native                      (u/drop-first-arg qp/mbql->native)
           :process-query-in-context          (u/drop-first-arg process-query-in-context)
diff --git a/src/metabase/driver/mongo/query_processor.clj b/src/metabase/driver/mongo/query_processor.clj
index 7bd005897ff3de304601636d54b74eb13683aa03..05c99968fc9eccfc7b29116642fa46505b16b86b 100644
--- a/src/metabase/driver/mongo/query_processor.clj
+++ b/src/metabase/driver/mongo/query_processor.clj
@@ -217,15 +217,15 @@
 
 ;;; ### filter
 
-(defn- parse-filter-subclause [{:keys [filter-type field value] :as filter} & [negate?]]
+(defn- parse-filter-subclause [{:keys [filter-type field value case-sensitive?] :as filter} & [negate?]]
   (let [field (when field (->lvalue field))
         value (when value (->rvalue value))
         v     (case filter-type
                 :between     {$gte (->rvalue (:min-val filter))
                               $lte (->rvalue (:max-val filter))}
-                :contains    (re-pattern value)
-                :starts-with (re-pattern (str \^ value))
-                :ends-with   (re-pattern (str value \$))
+                :contains    (re-pattern (str (when-not case-sensitive? "(?i)")    value))
+                :starts-with (re-pattern (str (when-not case-sensitive? "(?i)") \^ value))
+                :ends-with   (re-pattern (str (when-not case-sensitive? "(?i)")    value \$))
                 :=           {"$eq" value}
                 :!=          {$ne  value}
                 :<           {$lt  value}
diff --git a/src/metabase/driver/mysql.clj b/src/metabase/driver/mysql.clj
index 13d1cbdd78bdb7facd1fc741b71fa181be82c341..52f42b5e2f5ec5b6de27ae5891f8bc21d6853909 100644
--- a/src/metabase/driver/mysql.clj
+++ b/src/metabase/driver/mysql.clj
@@ -234,49 +234,57 @@
 
 (u/strict-extend MySQLDriver
   driver/IDriver
-  (merge (sql/IDriverSQLDefaultsMixin)
-         {:date-interval                     (u/drop-first-arg date-interval)
-          :details-fields                    (constantly (ssh/with-tunnel-config
-                                                           [{:name         "host"
-                                                             :display-name "Host"
-                                                             :default      "localhost"}
-                                                            {:name         "port"
-                                                             :display-name "Port"
-                                                             :type         :integer
-                                                             :default      3306}
-                                                            {:name         "dbname"
-                                                             :display-name "Database name"
-                                                             :placeholder  "birds_of_the_word"
-                                                             :required     true}
-                                                            {:name         "user"
-                                                             :display-name "Database username"
-                                                             :placeholder  "What username do you use to login to the database?"
-                                                             :required     true}
-                                                            {:name         "password"
-                                                             :display-name "Database password"
-                                                             :type         :password
-                                                             :placeholder  "*******"}
-                                                            {:name         "additional-options"
-                                                             :display-name "Additional JDBC connection string options"
-                                                             :placeholder  "tinyInt1isBit=false"}]))
-          :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)
-          :current-db-time                   (driver/make-current-db-time-fn mysql-db-time-query mysql-date-formatters)})
+  (merge
+   (sql/IDriverSQLDefaultsMixin)
+   {:date-interval                     (u/drop-first-arg date-interval)
+    :details-fields                    (constantly (ssh/with-tunnel-config
+                                                     [{:name         "host"
+                                                       :display-name "Host"
+                                                       :default      "localhost"}
+                                                      {:name         "port"
+                                                       :display-name "Port"
+                                                       :type         :integer
+                                                       :default      3306}
+                                                      {:name         "dbname"
+                                                       :display-name "Database name"
+                                                       :placeholder  "birds_of_the_word"
+                                                       :required     true}
+                                                      {:name         "user"
+                                                       :display-name "Database username"
+                                                       :placeholder  "What username do you use to login to the database?"
+                                                       :required     true}
+                                                      {:name         "password"
+                                                       :display-name "Database password"
+                                                       :type         :password
+                                                       :placeholder  "*******"}
+                                                      {:name         "additional-options"
+                                                       :display-name "Additional JDBC connection string options"
+                                                       :placeholder  "tinyInt1isBit=false"}]))
+    :humanize-connection-error-message (u/drop-first-arg humanize-connection-error-message)
+    :current-db-time                   (driver/make-current-db-time-fn mysql-db-time-query mysql-date-formatters)
+    :features                          (fn [this]
+                                         ;; MySQL LIKE clauses are case-sensitive or not based on whether the
+                                         ;; collation of the server and the columns themselves. Since this isn't
+                                         ;; something we can really change in the query itself don't present the
+                                         ;; option to the users in the UI
+                                         (conj (sql/features this) :no-case-sensitivity-string-filter-options))})
 
   sql/ISQLDriver
-  (merge (sql/ISQLDriverDefaultsMixin)
-         {:active-tables             sql/post-filtered-active-tables
-          :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)
-          :excluded-schemas          (constantly #{"INFORMATION_SCHEMA"})
-          :quote-style               (constantly :mysql)
-          :string-length-fn          (u/drop-first-arg string-length-fn)
-          ;; If this fails you need to load the timezone definitions from your system into MySQL;
-          ;; 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 = %s;")
-          :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
+  (merge
+   (sql/ISQLDriverDefaultsMixin)
+   {:active-tables             sql/post-filtered-active-tables
+    :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)
+    :excluded-schemas          (constantly #{"INFORMATION_SCHEMA"})
+    :quote-style               (constantly :mysql)
+    :string-length-fn          (u/drop-first-arg string-length-fn)
+    ;; If this fails you need to load the timezone definitions from your system into MySQL; 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 = %s;")
+    :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
 
 (defn -init-driver
   "Register the MySQL driver"
diff --git a/src/metabase/driver/presto.clj b/src/metabase/driver/presto.clj
index 602738adf283a600ff0ab124dc8fd1cd2b03f153..eef652b53c9ab46fd53bf8e98918e29b9291d9b8 100644
--- a/src/metabase/driver/presto.clj
+++ b/src/metabase/driver/presto.clj
@@ -7,6 +7,7 @@
             [clojure
              [set :as set]
              [string :as str]]
+            [clojure.tools.logging :as log]
             [honeysql
              [core :as hsql]
              [helpers :as h]]
@@ -102,8 +103,8 @@
 
 (defn- execute-presto-query! [details query]
   (ssh/with-ssh-tunnel [details-with-tunnel details]
-    (let [{{:keys [columns data nextUri error]} :body :as foo} (http/post (details->uri details-with-tunnel "/v1/statement")
-                                                                          (assoc (details->request details-with-tunnel) :body query, :as :json))]
+    (let [{{:keys [columns data nextUri error id]} :body :as foo} (http/post (details->uri details-with-tunnel "/v1/statement")
+                                                                             (assoc (details->request details-with-tunnel) :body query, :as :json))]
 
       (when error
         (throw (ex-info (or (:message error) "Error preparing query.") error)))
@@ -112,7 +113,25 @@
                      :rows    rows}]
         (if (nil? nextUri)
           results
-          (fetch-presto-results! details-with-tunnel results nextUri))))))
+          ;; When executing the query, it doesn't return the results, but is geared toward async queries. After
+          ;; issuing the query, the below will ask for the results. Asking in a future so that this thread can be
+          ;; interrupted if the client disconnects
+          (let [results-future (future (fetch-presto-results! details-with-tunnel results nextUri))]
+            (try
+              @results-future
+              (catch InterruptedException e
+                (if id
+                  ;; If we have a query id, we can cancel the query
+                  (try
+                    (http/delete (details->uri details-with-tunnel (str "/v1/query/" id))
+                                 (details->request details-with-tunnel))
+                    ;; If we fail to cancel the query, log it but propogate the interrupted exception, instead of
+                    ;; covering it up with a failed cancel
+                    (catch Exception e
+                      (log/error e (str "Error cancelling query with id " id))))
+                  (log/warn "Client connection closed, no query-id found, can't cancel query"))
+                ;; Propogate the error so that any finalizers can still run
+                (throw e)))))))))
 
 
 ;;; Generic helpers
@@ -335,7 +354,8 @@
                                                                       :expressions
                                                                       :native-parameters
                                                                       :expression-aggregations
-                                                                      :binning}
+                                                                      :binning
+                                                                      :native-query-params}
                                                                     (when-not config/is-test?
                                                                       ;; during unit tests don't treat presto as having FK support
                                                                       #{:foreign-keys})))
diff --git a/src/metabase/driver/sqlite.clj b/src/metabase/driver/sqlite.clj
index 326550fd18a5f22a486677fc96fcbd791aaa9a11..557d6adc5fd8c0adc8d162071638fddcae8909e3 100644
--- a/src/metabase/driver/sqlite.clj
+++ b/src/metabase/driver/sqlite.clj
@@ -2,9 +2,7 @@
   (:require [clj-time
              [coerce :as tcoerce]
              [format :as tformat]]
-            [clojure
-             [set :as set]
-             [string :as s]]
+            [clojure.string :as str]
             [honeysql
              [core :as hsql]
              [format :as hformat]]
@@ -55,7 +53,7 @@
 ;; register the SQLite concatnation operator `||` with HoneySQL as `sqlite-concat`
 ;; (hsql/format (hsql/call :sqlite-concat :a :b)) -> "(a || b)"
 (defmethod hformat/fn-handler "sqlite-concat" [_ & args]
-  (str "(" (s/join " || " (map hformat/to-sql args)) ")"))
+  (str "(" (str/join " || " (map hformat/to-sql args)) ")"))
 
 (def ^:private ->date     (partial hsql/call :date))
 (def ^:private ->datetime (partial hsql/call :datetime))
@@ -175,32 +173,39 @@
 
 (u/strict-extend SQLiteDriver
   driver/IDriver
-  (merge (sql/IDriverSQLDefaultsMixin)
-         {:date-interval   (u/drop-first-arg date-interval)
-          :details-fields  (constantly [{:name         "db"
-                                         :display-name "Filename"
-                                         :placeholder  "/home/camsaul/toucan_sightings.sqlite 😋"
-                                         :required     true}])
-          :features        (fn [this]
-                             (set/difference (sql/features this)
-                                             ;; SQLite doesn't have a standard deviation function
-                                             #{:standard-deviation-aggregations}
-                                             ;; HACK SQLite doesn't support ALTER TABLE ADD CONSTRAINT FOREIGN KEY and
-                                             ;; I don't have all day to work around this so for now we'll just skip
-                                             ;; the foreign key stuff in the tests.
-                                             (when config/is-test?
-                                               #{:foreign-keys})))
-          :current-db-time (driver/make-current-db-time-fn sqlite-db-time-query sqlite-date-formatters)})
+  (merge
+   (sql/IDriverSQLDefaultsMixin)
+   {:date-interval   (u/drop-first-arg date-interval)
+    :details-fields  (constantly [{:name         "db"
+                                   :display-name "Filename"
+                                   :placeholder  "/home/camsaul/toucan_sightings.sqlite 😋"
+                                   :required     true}])
+    :features        (fn [this]
+                       (-> (sql/features this)
+                           ;; SQLite `LIKE` clauses are case-insensitive by default, and thus cannot be made
+                           ;; case-sensitive. So let people know we have this 'feature' so the frontend doesn't try to
+                           ;; present the option to you.
+                           (conj :no-case-sensitivity-string-filter-options)
+                           ;; SQLite doesn't have a standard deviation function
+                           (disj :standard-deviation-aggregations
+                                 ;; HACK SQLite doesn't support ALTER TABLE ADD CONSTRAINT FOREIGN KEY and I don't
+                                 ;; have all day to work around this so for now we'll just skip the foreign key stuff
+                                 ;; in the tests.
+                                 (when config/is-test?
+                                   :foreign-keys))))
+    :current-db-time (driver/make-current-db-time-fn sqlite-db-time-query sqlite-date-formatters)})
+
   sql/ISQLDriver
-  (merge (sql/ISQLDriverDefaultsMixin)
-         {:active-tables             sql/post-filtered-active-tables
-          :column->base-type         (sql/pattern-based-column->base-type pattern->type)
-          :connection-details->spec  (u/drop-first-arg connection-details->spec)
-          :current-datetime-fn       (constantly (hsql/call :datetime (hx/literal :now)))
-          :date                      (u/drop-first-arg date)
-          :prepare-sql-param         (u/drop-first-arg prepare-sql-param)
-          :string-length-fn          (u/drop-first-arg string-length-fn)
-          :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
+  (merge
+   (sql/ISQLDriverDefaultsMixin)
+   {:active-tables             sql/post-filtered-active-tables
+    :column->base-type         (sql/pattern-based-column->base-type pattern->type)
+    :connection-details->spec  (u/drop-first-arg connection-details->spec)
+    :current-datetime-fn       (constantly (hsql/call :datetime (hx/literal :now)))
+    :date                      (u/drop-first-arg date)
+    :prepare-sql-param         (u/drop-first-arg prepare-sql-param)
+    :string-length-fn          (u/drop-first-arg string-length-fn)
+    :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
 
 (defn -init-driver
   "Register the SQLite driver"
diff --git a/src/metabase/driver/sqlserver.clj b/src/metabase/driver/sqlserver.clj
index f2db2f24d5d7a4807a830210a0b2357c4139087e..0dccc3729eb1dd2aab0ecca25f6333fd75a61a4d 100644
--- a/src/metabase/driver/sqlserver.clj
+++ b/src/metabase/driver/sqlserver.clj
@@ -169,56 +169,62 @@
 
 (u/strict-extend SQLServerDriver
   driver/IDriver
-  (merge (sql/IDriverSQLDefaultsMixin)
-         {:date-interval  (u/drop-first-arg date-interval)
-          :details-fields (constantly (ssh/with-tunnel-config
-                                        [{:name         "host"
-                                          :display-name "Host"
-                                          :default      "localhost"}
-                                         {:name         "port"
-                                          :display-name "Port"
-                                          :type         :integer
-                                          :default      1433}
-                                         {:name         "db"
-                                          :display-name "Database name"
-                                          :placeholder  "BirdsOfTheWorld"
-                                          :required     true}
-                                         {:name         "instance"
-                                          :display-name "Database instance name"
-                                          :placeholder  "N/A"}
-                                         {:name         "domain"
-                                          :display-name "Windows domain"
-                                          :placeholder  "N/A"}
-                                         {:name         "user"
-                                          :display-name "Database username"
-                                          :placeholder  "What username do you use to login to the database?"
-                                          :required     true}
-                                         {:name         "password"
-                                          :display-name "Database password"
-                                          :type         :password
-                                          :placeholder  "*******"}
-                                         {:name         "ssl"
-                                          :display-name "Use a secure connection (SSL)?"
-                                          :type         :boolean
-                                          :default      false}
-                                         {:name         "additional-options"
-                                          :display-name "Additional JDBC connection string options"
-                                          :placeholder  "trustServerCertificate=false"}]))
-          :current-db-time (driver/make-current-db-time-fn sqlserver-db-time-query sqlserver-date-formatters)})
-
+  (merge
+   (sql/IDriverSQLDefaultsMixin)
+   {:date-interval  (u/drop-first-arg date-interval)
+    :details-fields (constantly (ssh/with-tunnel-config
+                                  [{:name         "host"
+                                    :display-name "Host"
+                                    :default      "localhost"}
+                                   {:name         "port"
+                                    :display-name "Port"
+                                    :type         :integer
+                                    :default      1433}
+                                   {:name         "db"
+                                    :display-name "Database name"
+                                    :placeholder  "BirdsOfTheWorld"
+                                    :required     true}
+                                   {:name         "instance"
+                                    :display-name "Database instance name"
+                                    :placeholder  "N/A"}
+                                   {:name         "domain"
+                                    :display-name "Windows domain"
+                                    :placeholder  "N/A"}
+                                   {:name         "user"
+                                    :display-name "Database username"
+                                    :placeholder  "What username do you use to login to the database?"
+                                    :required     true}
+                                   {:name         "password"
+                                    :display-name "Database password"
+                                    :type         :password
+                                    :placeholder  "*******"}
+                                   {:name         "ssl"
+                                    :display-name "Use a secure connection (SSL)?"
+                                    :type         :boolean
+                                    :default      false}
+                                   {:name         "additional-options"
+                                    :display-name "Additional JDBC connection string options"
+                                    :placeholder  "trustServerCertificate=false"}]))
+    :current-db-time (driver/make-current-db-time-fn sqlserver-db-time-query sqlserver-date-formatters)
+    :features        (fn [this]
+                       ;; SQLServer LIKE clauses are case-sensitive or not based on whether the collation of the
+                       ;; server and the columns themselves. Since this isn't something we can really change in the
+                       ;; query itself don't present the option to the users in the UI
+                       (conj (sql/features this) :no-case-sensitivity-string-filter-options))})
 
   sql/ISQLDriver
-  (merge (sql/ISQLDriverDefaultsMixin)
-         {:apply-limit               (u/drop-first-arg apply-limit)
-          :apply-page                (u/drop-first-arg apply-page)
-          :column->base-type         (u/drop-first-arg column->base-type)
-          :connection-details->spec  (u/drop-first-arg connection-details->spec)
-          :current-datetime-fn       (constantly :%getutcdate)
-          :date                      (u/drop-first-arg date)
-          :excluded-schemas          (constantly #{"sys" "INFORMATION_SCHEMA"})
-          :stddev-fn                 (constantly :stdev)
-          :string-length-fn          (u/drop-first-arg string-length-fn)
-          :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
+  (merge
+   (sql/ISQLDriverDefaultsMixin)
+   {:apply-limit               (u/drop-first-arg apply-limit)
+    :apply-page                (u/drop-first-arg apply-page)
+    :column->base-type         (u/drop-first-arg column->base-type)
+    :connection-details->spec  (u/drop-first-arg connection-details->spec)
+    :current-datetime-fn       (constantly :%getutcdate)
+    :date                      (u/drop-first-arg date)
+    :excluded-schemas          (constantly #{"sys" "INFORMATION_SCHEMA"})
+    :stddev-fn                 (constantly :stdev)
+    :string-length-fn          (u/drop-first-arg string-length-fn)
+    :unix-timestamp->timestamp (u/drop-first-arg unix-timestamp->timestamp)}))
 
 (defn -init-driver
   "Register the SQLServer driver"
diff --git a/src/metabase/email.clj b/src/metabase/email.clj
index 11ff649255ddca58f9a9561eea8ea603d70375b2..bed7e78cdefe3a68612ff4a26b2144481ffcc931 100644
--- a/src/metabase/email.clj
+++ b/src/metabase/email.clj
@@ -56,7 +56,7 @@
 (def ^:private EmailMessage
   (s/constrained
    {:subject      s/Str
-    :recipients   [(s/pred u/is-email?)]
+    :recipients   [(s/pred u/email?)]
     :message-type (s/enum :text :html :attachments)
     :message      (s/cond-pre s/Str [su/Map])} ; TODO - what should this be a sequence of?
    (fn [{:keys [message-type message]}]
diff --git a/src/metabase/email/_footer_pulse.mustache b/src/metabase/email/_footer_pulse.mustache
new file mode 100644
index 0000000000000000000000000000000000000000..725cae61054372fdcf844edacc1ec9ee8eb705f7
--- /dev/null
+++ b/src/metabase/email/_footer_pulse.mustache
@@ -0,0 +1,11 @@
+  </div>
+  {{#quotation}}
+    <div style="padding-top: 2em; padding-bottom: 1em; text-align: left; color: #CCCCCC; font-size: small;">"{{quotation}}"<br/>- {{quotationAuthor}}</div>
+  {{/quotation}}
+  {{#logoFooter}}
+    <div style="padding-bottom: 2em; padding-top: 1em; text-align: left;">
+      <img width="32" height="40" src="http://static.metabase.com/email_logo.png"/>
+    </div>
+  {{/logoFooter}}
+</body>
+</html>
diff --git a/src/metabase/email/_header.mustache b/src/metabase/email/_header.mustache
index 9541f783cac427b737057b370af6a5eae331689a..85c926ad42a85155f06ec1d9f7f7e93a55e1297f 100644
--- a/src/metabase/email/_header.mustache
+++ b/src/metabase/email/_header.mustache
@@ -14,4 +14,4 @@
       <img width="47" height="60" src="http://static.metabase.com/email_logo.png"/>
     </div>
   {{/logoHeader}}
-  <div class="container" style="margin: 0 auto; padding: 0 0 2em 0; max-width: 500px; font-size: 16px; line-height: 24px; color: #616D75;">
+  <div class="container" style="margin: 0; padding: 0 0 2em 0; max-width: 100%; font-size: 16px; line-height: 24px; color: #616D75;">
diff --git a/src/metabase/email/messages.clj b/src/metabase/email/messages.clj
index 22a2cc00592bb0cf6484c4c20cfc6346d917bab9..ffd585129da6bc978780f286cb05f72c820bb0ce 100644
--- a/src/metabase/email/messages.clj
+++ b/src/metabase/email/messages.clj
@@ -107,7 +107,7 @@
   "Format and send an email informing the user how to reset their password."
   [email google-auth? hostname password-reset-url]
   {:pre [(m/boolean? google-auth?)
-         (u/is-email? email)
+         (u/email? email)
          (string? hostname)
          (string? password-reset-url)]}
   (let [message-body (stencil/render-file "metabase/email/password_reset"
@@ -149,7 +149,7 @@
 (defn send-notification-email!
   "Format and send an email informing the user about changes to objects in the system."
   [email context]
-  {:pre [(u/is-email? email) (map? context)]}
+  {:pre [(u/email? email) (map? context)]}
   (let [context      (merge (update context :dependencies build-dependencies)
                             notification-context
                             (random-quote-context))
@@ -163,7 +163,7 @@
 (defn send-follow-up-email!
   "Format and send an email to the system admin following up on the installation."
   [email msg-type]
-  {:pre [(u/is-email? email) (contains? #{"abandon" "follow-up"} msg-type)]}
+  {:pre [(u/email? email) (contains? #{"abandon" "follow-up"} msg-type)]}
   (let [subject      (if (= "abandon" msg-type)
                        "[Metabase] Help make Metabase better."
                        "[Metabase] Tell us how things are going.")
@@ -204,19 +204,21 @@
      :content-type content-type
      :file-name    (format "%s.%s" card-name ext)
      :content      (-> attachment-file .toURI .toURL)
-     :description  (format "Full results for '%s'" card-name)}))
+     :description  (format "More results for '%s'" card-name)}))
 
 (defn- result-attachments [results]
   (remove nil?
           (apply concat
-                 (for [{{card-name :name, csv? :include_csv, xls? :include_xls} :card :as result} results
-                       :when (and (or csv? xls?)
-                                  (seq (get-in result [:result :data :rows])))]
-                   [(when-let [temp-file (and csv? (create-temp-file "csv"))]
+                 (for [{{card-name :name, :as card} :card :as result} results
+                       :let [{:keys [rows] :as result-data} (get-in result [:result :data])]
+                       :when (seq rows)]
+                   [(when-let [temp-file (and (render/include-csv-attachment? card result-data)
+                                              (create-temp-file "csv"))]
                       (export/export-to-csv-writer temp-file result)
                       (create-result-attachment-map "csv" card-name temp-file))
 
-                    (when-let [temp-file (and xls? (create-temp-file "xlsx"))]
+                    (when-let [temp-file (and (render/include-xls-attachment? card result-data)
+                                              (create-temp-file "xlsx"))]
                       (export/export-to-xlsx-file temp-file result)
                       (create-result-attachment-map "xlsx" card-name temp-file))]))))
 
diff --git a/src/metabase/email/pulse.mustache b/src/metabase/email/pulse.mustache
index d2f5e05cc7a2e1518f250bae9bdee722dfff6a20..e024ababd09f0a304f9949164db62eaea1515d6f 100644
--- a/src/metabase/email/pulse.mustache
+++ b/src/metabase/email/pulse.mustache
@@ -1,8 +1,8 @@
 {{> metabase/email/_header}}
   {{#pulseName}}
-    <h1 style="{{sectionStyle}} margin: 16px; color: {{colorGrey4}};">
+    <h1 style="{{sectionStyle}} margin: 16px; color: {{color-brand}};">
       {{pulseName}}
     </h1>
   {{/pulseName}}
   {{{pulse}}}
-{{> metabase/email/_footer}}
+{{> metabase/email/_footer_pulse}}
diff --git a/src/metabase/integrations/ldap.clj b/src/metabase/integrations/ldap.clj
index e9a04b4f9fffb761067da67edd41198e4f1ff61e..6f4541f3f2c34b680394c70db88068ac3df7ba1a 100644
--- a/src/metabase/integrations/ldap.clj
+++ b/src/metabase/integrations/ldap.clj
@@ -49,15 +49,15 @@
   :default "(&(objectClass=inetOrgPerson)(|(uid={login})(mail={login})))")
 
 (defsetting ldap-attribute-email
-  (tru "Attribute to use for the user's email. (usually 'mail', 'email' or 'userPrincipalName')")
+  (tru "Attribute to use for the user's email. (usually ''mail'', ''email'' or ''userPrincipalName'')")
   :default "mail")
 
 (defsetting ldap-attribute-firstname
-  (tru "Attribute to use for the user's first name. (usually 'givenName')")
+  (tru "Attribute to use for the user''s first name. (usually ''givenName'')")
   :default "givenName")
 
 (defsetting ldap-attribute-lastname
-  (tru "Attribute to use for the user's last name. (usually 'sn')")
+  (tru "Attribute to use for the user''s last name. (usually ''sn'')")
   :default "sn")
 
 (defsetting ldap-group-sync
@@ -66,7 +66,7 @@
   :default false)
 
 (defsetting ldap-group-base
-  (tru "Search base for groups, not required if your LDAP directory provides a 'memberOf' overlay. (Will be searched recursively)"))
+  (tru "Search base for groups, not required if your LDAP directory provides a ''memberOf'' overlay. (Will be searched recursively)"))
 
 (defsetting ldap-group-mappings
   ;; Should be in the form: {"cn=Some Group,dc=...": [1, 2, 3]} where keys are LDAP groups and values are lists of MB groups IDs
diff --git a/src/metabase/metabot.clj b/src/metabase/metabot.clj
index 2a748c4725a359e2fc9b524b0117811e6cdac36c..f5eacf8ca79bc0cf2eed3cf5a7dc500f086ddaed 100644
--- a/src/metabase/metabot.clj
+++ b/src/metabase/metabot.clj
@@ -71,11 +71,11 @@
                              dispatch-token) varr}))]
     (fn dispatch*
       ([]
-       (keys-description (tru "Here's what I can {0}:" verb) fn-map))
+       (keys-description (tru "Here''s what I can {0}:" verb) fn-map))
       ([what & args]
        (if-let [f (fn-map (keyword what))]
          (apply f args)
-         (tru "I don't know how to {0} `{1}`.\n{2}"
+         (tru "I don''t know how to {0} `{1}`.\n{2}"
                  verb
                  (if (instance? clojure.lang.Named what)
                    (name what)
@@ -105,7 +105,7 @@
   [& _]
   (let [cards (with-metabot-permissions
                 (filterv mi/can-read? (db/select [Card :id :name :dataset_query], {:order-by [[:id :desc]], :limit 20})))]
-    (tru "Here's your {0} most recent cards:\n{1}" (count cards) (format-cards cards))))
+    (tru "Here''s your {0} most recent cards:\n{1}" (count cards) (format-cards cards))))
 
 (defn- card-with-name [card-name]
   (first (u/prog1 (db/select [Card :id :name], :%lower.name [:like (str \% (str/lower-case card-name) \%)])
@@ -118,13 +118,13 @@
     (integer? card-id-or-name)     (db/select-one [Card :id :name], :id card-id-or-name)
     (or (string? card-id-or-name)
         (symbol? card-id-or-name)) (card-with-name card-id-or-name)
-    :else                          (throw (Exception. (str (tru "I don't know what Card `{0}` is. Give me a Card ID or name." card-id-or-name))))))
+    :else                          (throw (Exception. (str (tru "I don''t know what Card `{0}` is. Give me a Card ID or name." card-id-or-name))))))
 
 
 (defn ^:metabot show
   "Implementation of the `metabot show card <name-or-id>` command."
   ([]
-   (tru "Show which card? Give me a part of a card name or its ID and I can show it to you. If you don't know which card you want, try `metabot list`."))
+   (tru "Show which card? Give me a part of a card name or its ID and I can show it to you. If you don''t know which card you want, try `metabot list`."))
   ([card-id-or-name]
    (if-let [{card-id :id} (id-or-name->card card-id-or-name)]
      (do
diff --git a/src/metabase/middleware.clj b/src/metabase/middleware.clj
index f427c4346cde1ab78a50784fc26f00270f2ab31f..ab1bf81a2ec1a9866f35f92cf0cbb3eaaa325e04 100644
--- a/src/metabase/middleware.clj
+++ b/src/metabase/middleware.clj
@@ -1,10 +1,6 @@
 (ns metabase.middleware
   "Metabase-specific middleware functions & configuration."
-  (:require [cheshire
-             [core :as json]
-             [generate :refer [add-encoder encode-nil encode-str]]]
-            [clojure.core.async :as async]
-            [clojure.java.io :as io]
+  (:require [cheshire.generate :refer [add-encoder encode-nil encode-str]]
             [clojure.tools.logging :as log]
             [metabase
              [config :as config]
@@ -20,8 +16,6 @@
              [setting :refer [defsetting]]
              [user :as user :refer [User]]]
             monger.json
-            [ring.core.protocols :as protocols]
-            [ring.util.response :as response]
             [toucan
              [db :as db]
              [models :as models]])
@@ -377,74 +371,3 @@
            (handler request))
          (catch Throwable e
            {:status 400, :body (.getMessage e)}))))
-
-
-;;; --------------------------------------------------- STREAMING ----------------------------------------------------
-
-(def ^:private ^:const streaming-response-keep-alive-interval-ms
-  "Interval between sending newline characters to keep Heroku from terminating requests like queries that take a long
-  time to complete."
-  (* 1 1000))
-
-;; Handle ring response maps that contain a core.async chan in the :body key:
-;;
-;; {:status 200
-;;  :body (async/chan)}
-;;
-;; and send each string sent to that queue back to the browser as it arrives
-;; this avoids output buffering in the default stream handling which was not sending
-;; any responses until ~5k characters where in the queue.
-(extend-protocol protocols/StreamableResponseBody
-  clojure.core.async.impl.channels.ManyToManyChannel
-  (write-body-to-stream [output-queue _ ^OutputStream output-stream]
-    (log/debug (u/format-color 'green "starting streaming request"))
-    (with-open [out (io/writer output-stream)]
-      (loop [chunk (async/<!! output-queue)]
-        (when-not (= chunk ::EOF)
-          (.write out (str chunk))
-          (try
-            (.flush out)
-            (catch org.eclipse.jetty.io.EofException e
-              (log/info (u/format-color 'yellow "connection closed, canceling request %s" (type e)))
-              (async/close! output-queue)
-              (throw e)))
-          (recur (async/<!! output-queue)))))))
-
-(defn streaming-json-response
-  "This midelware assumes handlers fail early or return success Run the handler in a future and send newlines to keep
-  the connection open and help detect when the browser is no longer listening for the response. Waits for one second
-  to see if the handler responds immediately, If it does then there is no need to stream the response and it is sent
-  back directly. In cases where it takes longer than a second, assume the eventual result will be a success and start
-  sending newlines to keep the connection open."
-  [handler]
-  (fn [request]
-    (let [response            (future (handler request))
-          optimistic-response (deref response streaming-response-keep-alive-interval-ms ::no-immediate-response)]
-      (if (= optimistic-response ::no-immediate-response)
-        ;; if we didn't get a normal response in the first poling interval assume it's going to be slow
-        ;; and start sending keepalive packets.
-        (let [output (async/chan 1)]
-          ;; the output channel will be closed by the adapter when the incoming connection is closed.
-          (future
-            (loop []
-              (Thread/sleep streaming-response-keep-alive-interval-ms)
-              (when-not (realized? response)
-                (log/debug (u/format-color 'blue "Response not ready, writing one byte & sleeping..."))
-                ;; a newline padding character is used because it forces output flushing in jetty.
-                ;; if sending this character fails because the connection is closed, the chan will then close.
-                ;; Newlines are no-ops when reading JSON which this depends upon.
-                (when-not (async/>!! output "\n")
-                  (log/info (u/format-color 'yellow "canceled request %s" (future-cancel response)))
-                  (future-cancel response)) ;; try our best to kill the thread running the query.
-                (recur))))
-          (future
-            (try
-              ;; This is the part where we make this assume it's a JSON response we are sending.
-              (async/>!! output (json/encode (:body @response)))
-              (finally
-                (async/>!! output ::EOF)
-                (async/close! response))))
-          ;; here we assume a successful response will be written to the output channel.
-          (assoc (response/response output)
-            :content-type "applicaton/json"))
-          optimistic-response))))
diff --git a/src/metabase/models/card.clj b/src/metabase/models/card.clj
index dbe47eec12ab2da9d1c567282e568e824c1ce8c9..e840cb7cfc09e68831cfba31fb35ef009f0a83c0 100644
--- a/src/metabase/models/card.clj
+++ b/src/metabase/models/card.clj
@@ -101,18 +101,33 @@
                               (:schema table)
                               (or (:id table) (:table-id table)))))))
 
+(declare perms-objects-set)
+
+(defn- source-card-perms
+  "If `outer-query` is based on a source Card (if `:source-table` uses a psuedo-source-table like `card__<id>`) then
+  return the permissions needed to *read* that Card. Running or saving a Card that uses another Card as a source query
+  simply requires read permissions for that Card; e.g. if you are allowed to view a query you can save a new query
+  that uses it as a source. Thus the distinction between read and write permissions in not important here.
+
+  See issue #6845 for further discussion."
+  [outer-query]
+  (when-let [source-card-id (qputil/query->source-card-id outer-query)]
+    (perms-objects-set (Card source-card-id) :read)))
+
 (defn- mbql-permissions-path-set
   "Return the set of required permissions needed to run QUERY."
   [read-or-write query]
   {:pre [(map? query) (map? (:query query))]}
-  (try (let [{:keys [query database]} (qp/expand query)]
-         (tables->permissions-path-set read-or-write database (query->source-and-join-tables query)))
-       ;; if for some reason we can't expand the Card (i.e. it's an invalid legacy card)
-       ;; just return a set of permissions that means no one will ever get to see it
-       (catch Throwable e
-         (log/warn "Error getting permissions for card:" (.getMessage e) "\n"
-                   (u/pprint-to-str (u/filtered-stacktrace e)))
-         #{"/db/0/"})))                        ; DB 0 will never exist
+  (try
+    (or (source-card-perms query)
+        (let [{:keys [query database]} (qp/expand query)]
+          (tables->permissions-path-set read-or-write database (query->source-and-join-tables query))))
+    ;; if for some reason we can't expand the Card (i.e. it's an invalid legacy card)
+    ;; just return a set of permissions that means no one will ever get to see it
+    (catch Throwable e
+      (log/warn "Error getting permissions for card:" (.getMessage e) "\n"
+                (u/pprint-to-str (u/filtered-stacktrace e)))
+      #{"/db/0/"})))                    ; DB 0 will never exist
 
 ;; it takes a lot of DB calls and function calls to expand/resolve a query, and since they're pure functions we can
 ;; save ourselves some a lot of DB calls by caching the results. Cache the permissions reqquired to run a given query
@@ -154,12 +169,14 @@
 ;;; -------------------------------------------------- Dependencies --------------------------------------------------
 
 (defn card-dependencies
-  "Calculate any dependent objects for a given `Card`."
-  [this id {:keys [dataset_query]}]
-  (when (and dataset_query
-             (= :query (keyword (:type dataset_query))))
-    {:Metric  (q/extract-metric-ids (:query dataset_query))
-     :Segment (q/extract-segment-ids (:query dataset_query))}))
+  "Calculate any dependent objects for a given `card`."
+  ([_ _ card]
+   (card-dependencies card))
+  ([{:keys [dataset_query]}]
+   (when (and dataset_query
+              (= :query (keyword (:type dataset_query))))
+     {:Metric  (q/extract-metric-ids (:query dataset_query))
+      :Segment (q/extract-segment-ids (:query dataset_query))})))
 
 
 ;;; -------------------------------------------------- Revisions --------------------------------------------------
diff --git a/src/metabase/models/database.clj b/src/metabase/models/database.clj
index de6eba94cea6779f0e2559f149c733a43f08957c..a1740aae14d2da774628dedde383f3ff35dc277b 100644
--- a/src/metabase/models/database.clj
+++ b/src/metabase/models/database.clj
@@ -110,6 +110,7 @@
   (merge models/IModelDefaults
          {:hydration-keys (constantly [:database :db])
           :types          (constantly {:details                     :encrypted-json
+                                       :options                     :json
                                        :engine                      :keyword
                                        :metadata_sync_schedule      :cron-string
                                        :cache_field_values_schedule :cron-string})
@@ -160,6 +161,8 @@
   "The string to replace passwords with when serializing Databases."
   "**MetabasePass**")
 
+;; when encoding a Database as JSON remove the `details` for any non-admin User. For admin users they can still see
+;; the `details` but remove the password. No one gets to see this in an API response!
 (add-encoder
  DatabaseInstance
  (fn [db json-generator]
diff --git a/src/metabase/models/field.clj b/src/metabase/models/field.clj
index 978bfad2bf66d8a966cb28d9efe8b7427ce02b7a..8a995ff01cab4bc2ccade04f93fd8fdd6f1a1e0f 100644
--- a/src/metabase/models/field.clj
+++ b/src/metabase/models/field.clj
@@ -157,6 +157,27 @@
     (for [field fields]
       (assoc field :dimensions (get id->dimensions (:id field) [])))))
 
+(defn with-has-field-values
+  "Infer what the value of the `has_field_values` should be for Fields where it's not set. Admins can set this to one
+  of the values below, but if it's `nil` in the DB we'll infer it automatically.
+
+  *  `list`   = has an associated FieldValues object
+  *  `search` = does not have FieldValues
+  *  `none`   = admin has explicitly disabled search behavior for this Field"
+  {:batched-hydrate :has_field_values}
+  [fields]
+  (let [fields-without-has-field-values-ids (set (for [field fields
+                                                       :when (nil? (:has_field_values field))]
+                                                   (:id field)))
+        fields-with-fieldvalues-ids         (when (seq fields-without-has-field-values-ids)
+                                              (db/select-field :field_id FieldValues
+                                                :field_id [:in fields-without-has-field-values-ids]))]
+    (for [field fields]
+      (assoc field :has_field_values (or (:has_field_values field)
+                                         (if (contains? fields-with-fieldvalues-ids (u/get-id field))
+                                           :list
+                                           :search))))))
+
 (defn readable-fields-only
   "Efficiently checks if each field is readable and returns only readable fields"
   [fields]
diff --git a/src/metabase/models/permissions.clj b/src/metabase/models/permissions.clj
index af5bfc5ef55c1c2de27752eaf6260a6b3588cd87..1bb3ac67e9f999875dea08cc0a6a8a2765b23201 100644
--- a/src/metabase/models/permissions.clj
+++ b/src/metabase/models/permissions.clj
@@ -145,17 +145,20 @@
 
 (defn set-has-full-permissions?
   "Does PERMISSIONS-SET grant *full* access to object with PATH?"
+  {:style/indent 1}
   ^Boolean [permissions-set path]
   (boolean (some (u/rpartial is-permissions-for-object? path) permissions-set)))
 
 (defn set-has-partial-permissions?
   "Does PERMISSIONS-SET grant access full access to object with PATH *or* to a descendant of it?"
+  {:style/indent 1}
   ^Boolean [permissions-set path]
   (boolean (some (u/rpartial is-partial-permissions-for-object? path) permissions-set)))
 
 
 (defn ^Boolean set-has-full-permissions-for-set?
   "Do the permissions paths in PERMISSIONS-SET grant *full* access to all the object paths in OBJECT-PATHS-SET?"
+  {:style/indent 1}
   [permissions-set object-paths-set]
   {:pre [(is-permissions-set? permissions-set) (is-permissions-set? object-paths-set)]}
   (every? (partial set-has-full-permissions? permissions-set)
@@ -164,6 +167,7 @@
 (defn ^Boolean set-has-partial-permissions-for-set?
   "Do the permissions paths in PERMISSIONS-SET grant *partial* access to all the object paths in OBJECT-PATHS-SET?
    (PERMISSIONS-SET must grant partial access to *every* object in OBJECT-PATH-SETS set)."
+  {:style/indent 1}
   [permissions-set object-paths-set]
   {:pre [(is-permissions-set? permissions-set) (is-permissions-set? object-paths-set)]}
   (every? (partial set-has-partial-permissions? permissions-set)
diff --git a/src/metabase/models/user.clj b/src/metabase/models/user.clj
index 15ec1856fe0197f72d31b9e7514439450ccae8da..ed3a769376ce4c7c132e86596fb5f54ea9c3d6ec 100644
--- a/src/metabase/models/user.clj
+++ b/src/metabase/models/user.clj
@@ -19,7 +19,7 @@
 (models/defmodel User :core_user)
 
 (defn- pre-insert [{:keys [email password reset_token] :as user}]
-  (assert (u/is-email? email)
+  (assert (u/email? email)
     (format "Not a valid email: '%s'" email))
   (assert (and (string? password)
                (not (s/blank? password))))
@@ -69,7 +69,7 @@
                                          :group_id (:id (group/admin))               ; which leads to a stack overflow of calls between the two
                                          :user_id  id))))                            ; TODO - could we fix this issue by using `post-delete!`?
   (when email
-    (assert (u/is-email? email)))
+    (assert (u/email? email)))
   ;; If we're setting the reset_token then encrypt it before it goes into the DB
   (cond-> user
     reset_token (assoc :reset_token (creds/hash-bcrypt reset_token))))
@@ -121,7 +121,7 @@
 (defn invite-user!
   "Convenience function for inviting a new `User` and sending out the welcome email."
   [first-name last-name email-address password invitor]
-  {:pre [(string? first-name) (string? last-name) (u/is-email? email-address) (u/maybe? string? password) (map? invitor)]}
+  {:pre [(string? first-name) (string? last-name) (u/email? email-address) (u/maybe? string? password) (map? invitor)]}
   ;; create the new user
   (u/prog1 (db/insert! User
              :email       email-address
@@ -134,7 +134,7 @@
   "Convenience for creating a new user via Google Auth. This account is considered active immediately; thus all active
   admins will recieve an email right away."
   [first-name last-name email-address]
-  {:pre [(string? first-name) (string? last-name) (u/is-email? email-address)]}
+  {:pre [(string? first-name) (string? last-name) (u/email? email-address)]}
   (u/prog1 (db/insert! User
              :email       email-address
              :first_name  first-name
@@ -148,7 +148,7 @@
   "Convenience for creating a new user via LDAP. This account is considered active immediately; thus all active admins
   will recieve an email right away."
   [first-name last-name email-address password]
-  {:pre [(string? first-name) (string? last-name) (u/is-email? email-address)]}
+  {:pre [(string? first-name) (string? last-name) (u/email? email-address)]}
   (db/insert! User :email      email-address
                    :first_name first-name
                    :last_name  last-name
diff --git a/src/metabase/public_settings.clj b/src/metabase/public_settings.clj
index 89e9e435897d5138faaf1fae0f947f8eddebc9b3..a1b707ac377f1f633603aae626cec95ba4194249 100644
--- a/src/metabase/public_settings.clj
+++ b/src/metabase/public_settings.clj
@@ -52,7 +52,7 @@
 
 (defsetting site-locale
   (str  (tru "The default language for this Metabase instance.")
-        (tru "This only applies to emails, Pulses, etc. Users' browsers will specify the language used in the user interface."))
+        (tru "This only applies to emails, Pulses, etc. Users'' browsers will specify the language used in the user interface."))
   :type    :string
   :setter  (fn [new-value]
              (setting/set-string! :site-locale new-value)
@@ -111,7 +111,7 @@
   :default 60)
 
 (defsetting query-caching-ttl-ratio
-  (str (tru "To determine how long each saved question's cached result should stick around, we take the query's average execution time and multiply that by whatever you input here.")
+  (str (tru "To determine how long each saved question''s cached result should stick around, we take the query''s average execution time and multiply that by whatever you input here.")
        (tru "So if a query takes on average 2 minutes to run, and you input 10 for your multiplier, its cache entry will persist for 20 minutes."))
   :type    :integer
   :default 10)
diff --git a/src/metabase/pulse.clj b/src/metabase/pulse.clj
index c0d0d3ed0537a30c0d85ac28e3ba192b809d35d5..41ab09ac923d04a59d8ce76bf4bf7fc4c64e0088 100644
--- a/src/metabase/pulse.clj
+++ b/src/metabase/pulse.clj
@@ -34,9 +34,10 @@
     (let [{:keys [creator_id dataset_query]} card]
       (try
         {:card   card
-         :result (qp/process-query-and-save-execution! dataset_query
-                   (merge {:executed-by creator_id, :context :pulse, :card-id card-id}
-                          options))}
+         :result (qp/process-query-and-save-with-max! dataset_query (merge {:executed-by creator_id,
+                                                                            :context     :pulse,
+                                                                            :card-id     card-id}
+                                                                           options))}
         (catch Throwable t
           (log/warn (format "Error running card query (%n)" card-id) t))))))
 
@@ -131,7 +132,7 @@
     (goal-met? alert results)
 
     :else
-    (let [^String error-text (tru "Unrecognized alert with condition '{0}'" alert_condition)]
+    (let [^String error-text (tru "Unrecognized alert with condition ''{0}''" alert_condition)]
       (throw (IllegalArgumentException. error-text)))))
 
 (defmethod should-send-notification? :pulse
@@ -150,7 +151,7 @@
   [{:keys [id name] :as pulse} results {:keys [recipients] :as channel}]
   (log/debug (format "Sending Pulse (%d: %s) via Channel :email" id name))
   (let [email-subject    (str "Pulse: " name)
-        email-recipients (filterv u/is-email? (map :email recipients))
+        email-recipients (filterv u/email? (map :email recipients))
         timezone         (-> results first :card defaulted-timezone)]
     {:subject      email-subject
      :recipients   email-recipients
@@ -171,7 +172,7 @@
         email-subject    (format "Metabase alert: %s has %s"
                                  (first-question-name pulse)
                                  (get alert-notification-condition-text condition-kwd))
-        email-recipients (filterv u/is-email? (map :email recipients))
+        email-recipients (filterv u/email? (map :email recipients))
         first-result     (first results)
         timezone         (-> first-result :card defaulted-timezone)]
     {:subject      email-subject
diff --git a/src/metabase/pulse/render.clj b/src/metabase/pulse/render.clj
index 2c02ae9fb86bb0fe07af09f1388765566a48dd22..d9bbd926f1aac471821230a87a6f5a9d64a4e673 100644
--- a/src/metabase/pulse/render.clj
+++ b/src/metabase/pulse/render.clj
@@ -8,7 +8,9 @@
              [string :as str]]
             [clojure.java.io :as io]
             [clojure.tools.logging :as log]
-            [hiccup.core :refer [h html]]
+            [hiccup
+             [core :refer [h html]]
+             [util :as hutil]]
             [metabase.util :as u]
             [metabase.util
              [ui-logic :as ui-logic]
@@ -35,21 +37,24 @@
 ;;; # ------------------------------------------------------------ STYLES ------------------------------------------------------------
 
 (def ^:private ^:const card-width 400)
-(def ^:private ^:const rows-limit 10)
-(def ^:private ^:const cols-limit 3)
+(def ^:private ^:const rows-limit 20)
+(def ^:private ^:const cols-limit 10)
 (def ^:private ^:const sparkline-dot-radius 6)
 (def ^:private ^:const sparkline-thickness 3)
 (def ^:private ^:const sparkline-pad 8)
 
 ;;; ## STYLES
-(def ^:private ^:const color-brand  "rgb(45,134,212)")
-(def ^:private ^:const color-purple "rgb(135,93,175)")
-(def ^:private ^:const color-gold   "#F9D45C")
-(def ^:private ^:const color-error  "#EF8C8C")
-(def ^:private ^:const color-gray-1 "rgb(248,248,248)")
-(def ^:private ^:const color-gray-2 "rgb(189,193,191)")
-(def ^:private ^:const color-gray-3 "rgb(124,131,129)")
+(def ^:private ^:const color-brand      "rgb(45,134,212)")
+(def ^:private ^:const color-purple     "rgb(135,93,175)")
+(def ^:private ^:const color-gold       "#F9D45C")
+(def ^:private ^:const color-error      "#EF8C8C")
+(def ^:private ^:const color-gray-1     "rgb(248,248,248)")
+(def ^:private ^:const color-gray-2     "rgb(189,193,191)")
+(def ^:private ^:const color-gray-3     "rgb(124,131,129)")
 (def ^:const color-gray-4 "A ~25% Gray color." "rgb(57,67,64)")
+(def ^:private ^:const color-dark-gray  "#616D75")
+(def ^:private ^:const color-row-border "#EDF0F1")
+
 
 (def ^:private ^:const font-style    {:font-family "Lato, \"Helvetica Neue\", Helvetica, Arial, sans-serif"})
 (def ^:const section-style
@@ -68,19 +73,47 @@
                      :color       color-brand}))
 
 (def ^:private ^:const bar-th-style
-  (merge font-style {:font-size      :10px
-                     :font-weight    400
-                     :color          color-gray-4
-                     :border-bottom  (str "4px solid " color-gray-1)
-                     :padding-top    :0px
-                     :padding-bottom :10px}))
+  (merge font-style {:font-size       :14.22px
+                     :font-weight     700
+                     :color           color-gray-4
+                     :border-bottom   (str "1px solid " color-row-border)
+                     :padding-top     :20px
+                     :padding-bottom  :5px}))
+
+;; TO-DO for @senior: apply this style to headings of numeric columns
+(def ^:private ^:const bar-th-numeric-style
+  (merge font-style {:text-align      :right
+                     :font-size       :14.22px
+                     :font-weight     700
+                     :color           color-gray-4
+                     :border-bottom   (str "1px solid " color-row-border)
+                     :padding-top     :20px
+                     :padding-bottom  :5px}))
 
 (def ^:private ^:const bar-td-style
-  (merge font-style {:font-size     :16px
-                     :font-weight   400
-                     :text-align    :left
-                     :padding-right :1em
-                     :padding-top   :8px}))
+  (merge font-style {:font-size      :14.22px
+                     :font-weight    400
+                     :color          color-dark-gray
+                     :text-align     :left
+                     :padding-right  :1em
+                     :padding-top    :2px
+                     :padding-bottom :1px
+                     :max-width      :500px
+                     :overflow       :hidden
+                     :text-overflow  :ellipsis
+                     :border-bottom  (str "1px solid " color-row-border)}))
+
+;; TO-DO for @senior: apply this style to numeric cells
+(def ^:private ^:const bar-td-style-numeric
+  (merge font-style {:font-size      :14.22px
+                     :font-weight    400
+                     :color          color-dark-gray
+                     :text-align     :right
+                     :padding-right  :1em
+                     :padding-top    :2px
+                     :padding-bottom :1px
+                     :font-family    "Courier, Monospace"
+                     :border-bottom  (str "1px solid " color-row-border)}))
 
 (def ^:private RenderedPulseCard
   "Schema used for functions that operate on pulse card contents and their attachments"
@@ -98,6 +131,11 @@
                       :let  [v (if (keyword? v) (name v) v)]]
                   (str (name k) ": " v ";"))))
 
+(defn- graphing-columns [card {:keys [cols] :as data}]
+  [(or (ui-logic/x-axis-rowfn card data)
+       first)
+   (or (ui-logic/y-axis-rowfn card data)
+       second)])
 
 (defn- datetime-field?
   [field]
@@ -109,12 +147,53 @@
   (or (isa? (:base_type field)    :type/Number)
       (isa? (:special_type field) :type/Number)))
 
+(defn detect-pulse-card-type
+  "Determine the pulse (visualization) type of a CARD, e.g. `:scalar` or `:bar`."
+  [card data]
+  (let [col-count                 (-> data :cols count)
+        row-count                 (-> data :rows count)
+        [col-1-rowfn col-2-rowfn] (graphing-columns card data)
+        col-1                     (col-1-rowfn (:cols data))
+        col-2                     (col-2-rowfn (:cols data))
+        aggregation               (-> card :dataset_query :query :aggregation first)]
+    (cond
+      (or (zero? row-count)
+          ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters
+          (= [[nil]] (-> data :rows)))                             :empty
+      (contains? #{:pin_map :state :country} (:display card))      nil
+      (and (= col-count 1)
+           (= row-count 1))                                        :scalar
+      (and (= col-count 2)
+           (> row-count 1)
+           (datetime-field? col-1)
+           (number-field? col-2))                                  :sparkline
+      (and (= col-count 2)
+           (number-field? col-2))                                  :bar
+      :else                                                        :table)))
+
+(defn include-csv-attachment?
+  "Returns true if this card and resultset should include a CSV attachment"
+  [{:keys [include_csv] :as card} {:keys [cols rows] :as result-data}]
+  (or (:include_csv card)
+      (and (= :table (detect-pulse-card-type card result-data))
+           (or (< cols-limit (count cols))
+               (< rows-limit (count rows))))))
+
+(defn include-xls-attachment?
+  "Returns true if this card and resultset should include an XLS attachment"
+  [{:keys [include_csv] :as card} result-data]
+  (:include_xls card))
+
 
 ;;; # ------------------------------------------------------------ FORMATTING ------------------------------------------------------------
 
+(defrecord NumericWrapper [num-str]
+  hutil/ToString
+  (to-str [_] num-str))
+
 (defn- format-number
   [n]
-  (cl-format nil (if (integer? n) "~:d" "~,2f") n))
+  (NumericWrapper. (cl-format nil (if (integer? n) "~:d" "~,2f") n)))
 
 (defn- reformat-timestamp [timezone old-format-timestamp new-format-string]
   (f/unparse (f/with-zone (f/formatter new-format-string)
@@ -251,22 +330,35 @@
     (render-to-png html os width)
     (.toByteArray os)))
 
+
+(defn- heading-style-for-type
+  [cell]
+  (if (instance? NumericWrapper cell)
+    bar-th-numeric-style
+    bar-th-style))
+
+(defn- row-style-for-type
+  [cell]
+  (if (instance? NumericWrapper cell)
+    bar-td-style-numeric
+    bar-td-style))
+
 (defn- render-table
   [header+rows]
-  [:table {:style (style {:padding-bottom :8px, :border-bottom (str "4px solid " color-gray-1)})}
+  [:table {:style (style {:max-width (str "100%"), :white-space :nowrap, :padding-bottom :8px, :border-collapse :collapse})}
    (let [{header-row :row bar-width :bar-width} (first header+rows)]
      [:thead
       [:tr
        (for [header-cell header-row]
-         [:th {:style (style bar-td-style bar-th-style {:min-width :60px})}
+         [:th {:style (style (row-style-for-type header-cell) (heading-style-for-type header-cell) {:min-width :60px})}
           (h header-cell)])
        (when bar-width
          [:th {:style (style bar-td-style bar-th-style {:width (str bar-width "%")})}])]])
    [:tbody
     (map-indexed (fn [row-idx {:keys [row bar-width]}]
-                   [:tr {:style (style {:color (if (odd? row-idx) color-gray-2 color-gray-3)})}
+                   [:tr {:style (style {:color color-gray-3})}
                     (map-indexed (fn [col-idx cell]
-                                   [:td {:style (style bar-td-style (when (and bar-width (= col-idx 1)) {:font-weight 700}))}
+                                   [:td {:style (style (row-style-for-type cell) (when (and bar-width (= col-idx 1)) {:font-weight 700}))}
                                     (h cell)])
                                  row)
                     (when bar-width
@@ -289,18 +381,27 @@
               :when remapped_from]
           [remapped_from col-idx])))
 
+(defn- show-in-table? [{:keys [special_type visibility_type] :as column}]
+  (and (not (isa? special_type :type/Description))
+       (not (contains? #{:details-only :retired :sensitive} visibility_type))))
+
 (defn- query-results->header-row
   "Returns a row structure with header info from `COLS`. These values
   are strings that are ready to be rendered as HTML"
   [remapping-lookup cols include-bar?]
   {:row (for [maybe-remapped-col cols
-              :let [col (if (:remapped_to maybe-remapped-col)
-                          (nth cols (get remapping-lookup (:name maybe-remapped-col)))
-                          maybe-remapped-col)]
+              :when (show-in-table? maybe-remapped-col)
+              :let [{:keys [base_type special_type] :as col} (if (:remapped_to maybe-remapped-col)
+                                                               (nth cols (get remapping-lookup (:name maybe-remapped-col)))
+                                                               maybe-remapped-col)
+                    column-name (name (or (:display_name col) (:name col)))]
               ;; If this column is remapped from another, it's already
               ;; in the output and should be skipped
               :when (not (:remapped_from maybe-remapped-col))]
-          (str/upper-case (name (or (:display_name col) (:name col)))))
+          (if (or (isa? base_type :type/Number)
+                  (isa? special_type :type/Number))
+            (NumericWrapper. column-name)
+            column-name))
    :bar-width (when include-bar? 99)})
 
 (defn- query-results->row-seq
@@ -311,7 +412,8 @@
                   ;; cast to double to avoid "Non-terminating decimal expansion" errors
                   (float (* 100 (/ (double (bar-column row)) max-value))))
      :row (for [[maybe-remapped-col maybe-remapped-row-cell] (map vector cols row)
-                :when (not (:remapped_from maybe-remapped-col))
+                :when (and (not (:remapped_from maybe-remapped-col))
+                           (show-in-table? maybe-remapped-col))
                 :let [[col row-cell] (if (:remapped_to maybe-remapped-col)
                                        [(nth cols (get remapping-lookup (:name maybe-remapped-col)))
                                         (nth row (get remapping-lookup (:name maybe-remapped-col)))]
@@ -328,38 +430,58 @@
      (query-results->header-row remapping-lookup limited-cols bar-column)
      (query-results->row-seq timezone remapping-lookup limited-cols (take rows-limit rows) bar-column max-value))))
 
+(defn- strong-limit-text [number]
+  [:strong {:style (style {:color color-gray-3})} (h (format-number number))])
+
 (defn- render-truncation-warning
   [col-limit col-count row-limit row-count]
-  (if (or (> row-count row-limit)
-          (> col-count col-limit))
-    [:div {:style (style {:padding-top :16px})}
-     (cond
-       (> row-count row-limit)
-       [:div {:style (style {:color color-gray-2
-                             :padding-bottom :10px})}
-        "Showing " [:strong {:style (style {:color color-gray-3})} (format-number row-limit)]
-        " of "     [:strong {:style (style {:color color-gray-3})} (format-number row-count)]
-        " rows."]
-
-       (> col-count col-limit)
-       [:div {:style (style {:color          color-gray-2
-                             :padding-bottom :10px})}
-        "Showing " [:strong {:style (style {:color color-gray-3})} (format-number col-limit)]
-        " of "     [:strong {:style (style {:color color-gray-3})} (format-number col-count)]
-        " columns."])]))
+  (let [over-row-limit (> row-count row-limit)
+        over-col-limit (> col-count col-limit)]
+    (when (or over-row-limit over-col-limit)
+      [:div {:style (style {:padding-top :16px})}
+       (cond
+
+         (and over-row-limit over-col-limit)
+         [:div {:style (style {:color color-gray-2
+                               :padding-bottom :10px})}
+          "Showing " (strong-limit-text row-limit)
+          " of "     (strong-limit-text row-count)
+          " rows and " (strong-limit-text col-limit)
+          " of "     (strong-limit-text col-count)
+          " columns."]
+
+         over-row-limit
+         [:div {:style (style {:color color-gray-2
+                               :padding-bottom :10px})}
+          "Showing " (strong-limit-text row-limit)
+          " of "     (strong-limit-text row-count)
+          " rows."]
+
+         over-col-limit
+         [:div {:style (style {:color          color-gray-2
+                               :padding-bottom :10px})}
+          "Showing " (strong-limit-text col-limit)
+          " of "     (strong-limit-text col-count)
+          " columns."])])))
+
+(defn- attached-results-text
+  "Returns hiccup structures to indicate truncated results are available as an attachment"
+  [render-type cols cols-limit rows rows-limit]
+  (when (and (not= :inline render-type)
+             (or (< cols-limit (count cols))
+                 (< rows-limit (count rows))))
+    [:div {:style (style {:color color-gray-2})}
+     "More results have been included as a file attachment"]))
 
 (s/defn ^:private render:table :- RenderedPulseCard
-  [timezone card {:keys [cols rows] :as data}]
-  {:attachments nil
-   :content     [:div
-                 (render-table (prep-for-html-rendering timezone cols rows nil nil cols-limit))
-                 (render-truncation-warning cols-limit (count cols) rows-limit (count rows))]})
-
-(defn- graphing-columns [card {:keys [cols] :as data}]
-  [(or (ui-logic/x-axis-rowfn card data)
-       first)
-   (or (ui-logic/y-axis-rowfn card data)
-       second)])
+  [render-type timezone card {:keys [cols rows] :as data}]
+  (let [table-body [:div
+                    (render-table (prep-for-html-rendering timezone cols rows nil nil cols-limit))
+                    (render-truncation-warning cols-limit (count cols) rows-limit (count rows))]]
+    {:attachments nil
+     :content     (if-let [results-attached (attached-results-text render-type cols cols-limit rows rows-limit)]
+                    (list results-attached table-body)
+                    (list table-body))}))
 
 (s/defn ^:private render:bar :- RenderedPulseCard
   [timezone card {:keys [cols rows] :as data}]
@@ -599,31 +721,6 @@
                                       :padding     :16px})}
                  "An error occurred while displaying this card."]})
 
-(defn detect-pulse-card-type
-  "Determine the pulse (visualization) type of a CARD, e.g. `:scalar` or `:bar`."
-  [card data]
-  (let [col-count                 (-> data :cols count)
-        row-count                 (-> data :rows count)
-        [col-1-rowfn col-2-rowfn] (graphing-columns card data)
-        col-1                     (col-1-rowfn (:cols data))
-        col-2                     (col-2-rowfn (:cols data))
-        aggregation               (-> card :dataset_query :query :aggregation first)]
-    (cond
-      (or (zero? row-count)
-          ;; Many aggregations result in [[nil]] if there are no rows to aggregate after filters
-          (= [[nil]] (-> data :rows)))                             :empty
-      (or (> col-count 3)
-          (contains? #{:pin_map :state :country} (:display card))) nil
-      (and (= col-count 1)
-           (= row-count 1))                                        :scalar
-      (and (= col-count 2)
-           (> row-count 1)
-           (datetime-field? col-1)
-           (number-field? col-2))                                  :sparkline
-      (and (= col-count 2)
-           (number-field? col-2))                                  :bar
-      :else                                                        :table)))
-
 (s/defn ^:private make-title-if-needed :- (s/maybe RenderedPulseCard)
   [render-type card]
   (when *include-title*
@@ -659,7 +756,7 @@
       :scalar    (render:scalar    timezone card data)
       :sparkline (render:sparkline render-type timezone card data)
       :bar       (render:bar       timezone card data)
-      :table     (render:table     timezone card data)
+      :table     (render:table     render-type timezone card data)
       (if (is-attached? card)
         (render:attached render-type card data)
         (render:unknown card data)))
@@ -691,16 +788,20 @@
 
 (s/defn render-pulse-section :- RenderedPulseCard
   "Render a specific section of a Pulse, i.e. a single Card, to Hiccup HTML."
-  [timezone {:keys [card result]}]
+  [timezone {card :card {:keys [data] :as result} :result}]
   (let [{:keys [attachments content]} (binding [*include-title* true]
                                         (render-pulse-card :attachment timezone card result))]
     {:attachments attachments
-     :content     [:div {:style (style {:margin-top       :10px
-                                        :margin-bottom    :20px
-                                        :border           "1px solid #dddddd"
-                                        :border-radius    :2px
-                                        :background-color :white
-                                        :box-shadow       "0 1px 2px rgba(0, 0, 0, .08)"})}
+     :content     [:div {:style (style (merge {:margin-top       :10px
+                                               :margin-bottom    :20px}
+                                              ;; Don't include the border on cards rendered with a table as the table
+                                              ;; will be to larger and overrun the border
+                                              (when-not (= :table (detect-pulse-card-type card data))
+                                                {:border           "1px solid #dddddd"
+                                                 :border-radius    :2px
+                                                 :background-color :white
+                                                 :width            "500px !important"
+                                                 :box-shadow       "0 1px 2px rgba(0, 0, 0, .08)"})))}
                    content]}))
 
 (defn render-pulse-card-to-png
diff --git a/src/metabase/query_processor.clj b/src/metabase/query_processor.clj
index b87f5e8d1dedb2cea8dd30cc7d6e4ee4a00cf12e..bf203e52134c805cc1b50c1c44965bd27a2c6d4a 100644
--- a/src/metabase/query_processor.clj
+++ b/src/metabase/query_processor.clj
@@ -281,3 +281,22 @@
   (run-and-save-query! (assoc query :info (assoc options
                                             :query-hash (qputil/query-hash query)
                                             :query-type (if (qputil/mbql-query? query) "MBQL" "native")))))
+
+(def ^:private ^:const max-results-bare-rows
+  "Maximum number of rows to return specifically on :rows type queries via the API."
+  2000)
+
+(def ^:private ^:const max-results
+  "General maximum number of rows to return from an API query."
+  10000)
+
+(def default-query-constraints
+  "Default map of constraints that we apply on dataset queries executed by the api."
+  {:max-results           max-results
+   :max-results-bare-rows max-results-bare-rows})
+
+(s/defn process-query-and-save-with-max!
+  "Same as `process-query-and-save-execution!` but will include the default max rows returned as a constraint"
+  {:style/indent 1}
+  [query, options :- DatasetQueryOptions]
+  (process-query-and-save-execution! (assoc query :constraints default-query-constraints) options))
diff --git a/src/metabase/query_processor/interface.clj b/src/metabase/query_processor/interface.clj
index 83124eef448490984cb09b1feee51e4ba9a42c10..84096d96939ed40580d87fce838ea604f54f5f77 100644
--- a/src/metabase/query_processor/interface.clj
+++ b/src/metabase/query_processor/interface.clj
@@ -422,9 +422,11 @@
                             field        :- AnyField
                             max-val      :- OrderableValueOrPlaceholder])
 
-(s/defrecord StringFilter [filter-type :- (s/enum :starts-with :contains :ends-with)
-                           field       :- AnyField
-                           value       :- (s/cond-pre s/Str StringValueOrPlaceholder)]) ; TODO - not 100% sure why this is also allowed to accept a plain string
+(s/defrecord StringFilter [filter-type     :- (s/enum :starts-with :contains :ends-with)
+                           field           :- AnyField
+                           ;; TODO - not 100% sure why this is also allowed to accept a plain string
+                           value           :- (s/cond-pre s/Str StringValueOrPlaceholder)
+                           case-sensitive? :- s/Bool])
 
 (def SimpleFilterClause
   "Schema for a non-compound, non-`not` MBQL `filter` clause."
diff --git a/src/metabase/query_processor/middleware/expand.clj b/src/metabase/query_processor/middleware/expand.clj
index 43b45179b06d92dcd76cdd3ab8072006fccfb24e..a2561f8a48d0945bad50ea72fd241af1a448f7e2 100644
--- a/src/metabase/query_processor/middleware/expand.clj
+++ b/src/metabase/query_processor/middleware/expand.clj
@@ -4,6 +4,7 @@
   (:refer-clojure :exclude [< <= > >= = != and or not filter count distinct sum min max + - / *])
   (:require [clojure.core :as core]
             [clojure.tools.logging :as log]
+            [metabase.driver :as driver]
             [metabase.query-processor
              [interface :as i]
              [util :as qputil]]
@@ -108,7 +109,10 @@
     (instance? DateTimeValue v)         v
     (instance? RelativeDatetime v)      (i/map->RelativeDateTimeValue (assoc v :unit (datetime-unit f v), :field (datetime-field f (datetime-unit f v))))
     (instance? DateTimeField f)         (i/map->DateTimeValue {:value (u/->Timestamp v), :field f})
-    (instance? FieldLiteral f)          (i/map->Value {:value v, :field f})
+    (instance? FieldLiteral f)          (if (isa? (:base-type f) :type/DateTime)
+                                          (i/map->DateTimeValue {:value (u/->Timestamp v)
+                                                                 :field (i/map->DateTimeField {:field f :unit :default})})
+                                          (i/map->Value {:value v, :field f}))
     :else                               (i/map->ValuePlaceholder {:field-placeholder (field f), :value v})))
 
 (s/defn ^:private field-or-value
@@ -289,12 +293,40 @@
   (and (between lat-field lat-min lat-max)
        (between lon-field lon-min lon-max)))
 
-(s/defn ^:private string-filter :- StringFilter [filter-type f s]
-  (i/map->StringFilter {:filter-type filter-type, :field (field f), :value (value f s)}))
 
-(def ^:ql ^{:arglists '([f s])} starts-with "Filter subclause. Return results where F starts with the string S."    (partial string-filter :starts-with))
-(def ^:ql ^{:arglists '([f s])} contains    "Filter subclause. Return results where F contains the string S."       (partial string-filter :contains))
-(def ^:ql ^{:arglists '([f s])} ends-with   "Filter subclause. Return results where F ends with with the string S." (partial string-filter :ends-with))
+(s/defn ^:private string-filter :- StringFilter
+  "String search filter clauses: `contains`, `starts-with`, and `ends-with`. First shipped in `0.11.0` (before initial
+  public release) but only supported case-sensitive searches. In `0.29.0` support for case-insensitive searches was
+  added. For backwards-compatibility, and to avoid possible performance implications, case-sensitive is the default
+  option if no `options-maps` is specified for all drivers except GA. Whether we should default to case-sensitive can
+  be specified by the `IDriver` method `default-to-case-sensitive?`."
+  ([filter-type f s]
+   (string-filter filter-type f s {:case-sensitive (if i/*driver*
+                                                     (driver/default-to-case-sensitive? i/*driver*)
+                                                     ;; if *driver* isn't bound then just assume `true`
+                                                     true)}))
+  ([filter-type f s options-map]
+   (i/strict-map->StringFilter
+    {:filter-type     filter-type
+     :field           (field f)
+     :value           (value f s)
+     :case-sensitive? (qputil/get-normalized options-map :case-sensitive true)})))
+
+(def ^:ql ^{:arglists '([f s] [f s options-map])} starts-with
+  "Filter subclause. Return results where F starts with the string S. By default, is case-sensitive, but you may pass an
+  `options-map` with `{:case-sensitive false}` for case-insensitive searches."
+  (partial string-filter :starts-with))
+
+(def ^:ql ^{:arglists '([f s] [f s options-map])} contains
+  "Filter subclause. Return results where F contains the string S. By default, is case-sensitive, but you may pass an
+  `options-map` with `{:case-sensitive false}` for case-insensitive searches."
+  (partial string-filter :contains))
+
+(def ^:ql ^{:arglists '([f s] [f s options-map])} ends-with
+  "Filter subclause. Return results where F ends with with the string S. By default, is case-sensitive, but you may pass
+  an `options-map` with `{:case-sensitive false}` for case-insensitive searches."
+  (partial string-filter :ends-with))
+
 
 (s/defn ^:ql not :- i/Filter
   "Filter subclause. Return results that do *not* satisfy SUBCLAUSE.
diff --git a/src/metabase/query_processor/middleware/parameters/sql.clj b/src/metabase/query_processor/middleware/parameters/sql.clj
index 934a4624f77ea65859d59f7917dfe3732184907c..86f9a27faa8c3833326978c55ba8fcf64579fcf6 100644
--- a/src/metabase/query_processor/middleware/parameters/sql.clj
+++ b/src/metabase/query_processor/middleware/parameters/sql.clj
@@ -6,6 +6,8 @@
   (:require [clojure.string :as str]
             [clojure.tools.logging :as log]
             [honeysql.core :as hsql]
+            [instaparse.core :as insta]
+            [metabase.driver :as driver]
             [metabase.models.field :as field :refer [Field]]
             [metabase.query-processor.middleware.parameters.dates :as date-params]
             [metabase.query-processor.middleware.expand :as ql]
@@ -62,6 +64,8 @@
 ;; values.
 (defrecord ^:private NoValue [])
 
+(defn- no-value? [x]
+  (instance? NoValue x))
 
 ;; various schemas are used to check that various functions return things in expected formats
 
@@ -108,11 +112,7 @@
   {s/Keyword ParamValue})
 
 (def ^:private ParamSnippetInfo
-  {(s/optional-key :param-key)               s/Keyword
-   (s/optional-key :original-snippet)        su/NonBlankString
-   (s/optional-key :variable-snippet)        su/NonBlankString
-   (s/optional-key :optional-snippet)        (s/maybe su/NonBlankString)
-   (s/optional-key :replacement-snippet)     s/Str                       ; allowed to be blank if this is an optional param
+  {(s/optional-key :replacement-snippet)     s/Str                       ; allowed to be blank if this is an optional param
    (s/optional-key :prepared-statement-args) [s/Any]})
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
@@ -223,7 +223,7 @@
 (s/defn ^:private parse-value-for-type :- ParamValue
   [param-type value]
   (cond
-    (instance? NoValue value)                        value
+    (no-value? value)                                value
     (= param-type "number")                          (value->number value)
     (= param-type "date")                            (map->Date {:s value})
     (and (= param-type "dimension")
@@ -389,136 +389,124 @@
       :else
       (update (dimension->replacement-snippet-info param) :replacement-snippet (partial str (field->identifier field (:type param)) " ")))))
 
-
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                         QUERY PARSING / PARAM SNIPPETS                                         |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-;; These functions parse a query and look for param snippets, which look like:
-;;
-;; * {{...}} (required)
-;; * [[...{{...}}...]] (optional)
-;;
-;; and creates a list of these snippets, keeping the original order.
-;;
-;; The details maps returned have the format:
-;;
-;;    {:param-key        :timestamp                           ; name of the param being replaced
-;;     :original-snippet "[[AND timestamp < {{timestamp}}]]"  ; full text of the snippet to be replaced
-;;     :optional-snippet "AND timestamp < {{timestamp}}"      ; portion of the snippet inside [[optional]] brackets, or `nil` if the snippet isn't optional
-;;     :variable-snippet "{{timestamp}}"}                     ; portion of the snippet referencing the variable itself, e.g. {{x}}
-
-(s/defn ^:private param-snippet->param-name :- s/Keyword
-  "Return the keyword name of the param being referenced inside PARAM-SNIPPET.
-
-     (param-snippet->param-name \"{{x}}\") -> :x"
-  [param-snippet :- su/NonBlankString]
-  (keyword (second (re-find #"\{\{\s*(\w+)\s*\}\}" param-snippet))))
-
-(s/defn ^:private sql->params-snippets-info :- [ParamSnippetInfo]
-  "Return a sequence of maps containing information about the param snippets found by paring SQL."
-  [sql :- su/NonBlankString]
-  (for [[param-snippet optional-replacement-snippet] (re-seq #"(?:\[\[(.+?)\]\])|(?:\{\{\s*\w+\s*\}\})" sql)]
-    {:param-key        (param-snippet->param-name param-snippet)
-     :original-snippet  param-snippet
-     :variable-snippet (re-find #"\{\{\s*\w+\s*\}\}" param-snippet)
-     :optional-snippet optional-replacement-snippet}))
-
-
 ;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                              PARAMS DETAILS LIST                                               |
+;;; |                                            PARSING THE SQL TEMPLATE                                            |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
-;; These functions combine the info from the other 3 stages (Param Info Resolution, Param->SQL Substitution, and Query
-;; Parsing) and create a sequence of maps containing param details that has all the information needed to do SQL
-;; substituion. This sequence is returned in the same order as params encountered in the
-;;
-;; original query, making passing prepared statement args simple
-;;
-;; The details maps returned have the format:
-;;
-;;    {:original-snippet        "[[AND timestamp < {{timestamp}}]]"  ; full text of the snippet to be replaced
-;;     :replacement-snippet     "AND timestamp < ?"                  ; full text that the snippet should be replaced with
-;;     :prepared-statement-args [#inst "2016-01-01"]}                ; prepared statement args needed by the replacement snippet
-;;
-;; (Basically these functions take `:param-key`, `:optional-snippet`, and `:variable-snippet` from the Query Parsing stage and the info from the other stages
-;; to add the appropriate info for `:replacement-snippet` and `:prepared-statement-args`.)
-
-(s/defn ^:private snippet-value :- ParamValue
-  "Fetch the value from PARAM-KEY->VALUE for SNIPPET-INFO.
-   If no value is specified, return `NoValue` if the snippet is optional; otherwise throw an Exception."
-  [{:keys [param-key optional-snippet]} :- ParamSnippetInfo, param-key->value :- ParamValues]
-  (u/prog1 (get param-key->value param-key (NoValue.))
-    ;; if ::no-value was specified an the param is not [[optional]], throw an exception
-    (when (and (instance? NoValue <>)
-               (not optional-snippet))
-      (throw (ex-info (format "Unable to substitute '%s': param not specified.\nFound: %s" param-key (keys param-key->value))
-               {:status-code 400})))))
-
-(s/defn ^:private handle-optional-snippet :- ParamSnippetInfo
-  "Create the approprate `:replacement-snippet` for PARAM, combining the value of REPLACEMENT-SNIPPET from the Param->SQL Substitution phase
-   with the OPTIONAL-SNIPPET, if any."
-  [{:keys [variable-snippet optional-snippet replacement-snippet prepared-statement-args], :as snippet-info} :- ParamSnippetInfo]
-  (assoc snippet-info
-    :replacement-snippet     (cond
-                               (not optional-snippet)    replacement-snippet                                                 ; if there is no optional-snippet return replacement as-is
-                               (seq replacement-snippet) (str/replace optional-snippet variable-snippet replacement-snippet) ; if replacement-snippet is non blank splice into optional-snippet
-                               :else                     "")                                                                 ; otherwise return blank replacement (i.e. for NoValue)
-    ;; for every thime the `variable-snippet` occurs in the `optional-snippet` we need to supply an additional set of `prepared-statment-args`
-    ;; e.g. [[ AND ID = {{id}} OR USER_ID = {{id}} ]] should have *2* sets of the prepared statement args for {{id}} since it occurs twice
-    :prepared-statement-args (if-let [occurances (u/occurances-of-substring optional-snippet variable-snippet)]
-                               (apply concat (repeat occurances prepared-statement-args))
-                               prepared-statement-args)))
-
-(s/defn ^:private add-replacement-snippet-info :- [ParamSnippetInfo]
-  "Add `:replacement-snippet` and `:prepared-statement-args` info to the maps in PARAMS-SNIPPETS-INFO by looking at
-   PARAM-KEY->VALUE and using the Param->SQL substituion functions."
-  [params-snippets-info :- [ParamSnippetInfo], param-key->value :- ParamValues]
-  (for [snippet-info params-snippets-info]
-    (handle-optional-snippet
-     (merge snippet-info
-            (s/validate ParamSnippetInfo (->replacement-snippet-info (snippet-value snippet-info param-key->value)))))))
+(def ^:private sql-template-parser
+  (insta/parser
+   "SQL := (ANYTHING_NOT_RESERVED | SINGLE_BRACKET_PLUS_ANYTHING | SINGLE_BRACE_PLUS_ANYTHING | OPTIONAL | PARAM)*
 
+    (* Treat double brackets and braces as special, pretty much everything else is good to go *)
+    <SINGLE_BRACKET_PLUS_ANYTHING> := !'[[' '[' (ANYTHING_NOT_RESERVED | ']' | SINGLE_BRACKET_PLUS_ANYTHING | SINGLE_BRACE_PLUS_ANYTHING)*
+    <SINGLE_BRACE_PLUS_ANYTHING> := !'{{' '{' (ANYTHING_NOT_RESERVED | '}' | SINGLE_BRACE_PLUS_ANYTHING  | SINGLE_BRACKET_PLUS_ANYTHING)*
+    <ANYTHING_NOT_RESERVED> := #'[^\\[\\]\\{\\}]+'
 
-;;; +----------------------------------------------------------------------------------------------------------------+
-;;; |                                                 SUBSTITUITION                                                  |
-;;; +----------------------------------------------------------------------------------------------------------------+
-
-;; These functions take the information about parameters from the Params Details List functions and then convert the
-;; original SQL Query into a SQL query with appropriate subtitutions and a sequence of prepared statement args
+    (* Parameters can have whitespace, but must be word characters for the name of the parameter *)
+    PARAM = <'{{'> <WHITESPACE*> TOKEN <WHITESPACE*> <'}}'>
 
-(s/defn ^:private substitute-one
-  [sql :- su/NonBlankString, {:keys [original-snippet replacement-snippet]} :- ParamSnippetInfo]
-  (str/replace-first sql original-snippet replacement-snippet))
+    (* Parameters, braces and brackets are all good here, just no nesting of optional clauses *)
+    OPTIONAL := <'[['> (ANYTHING_NOT_RESERVED | SINGLE_BRACKET_PLUS_ANYTHING | SINGLE_BRACE_PLUS_ANYTHING | PARAM)* <']]'>
+    <TOKEN>    := #'(\\w)+'
+    WHITESPACE := #'\\s+'"))
 
+(defrecord ^:private Param [param-key sql-value prepared-statement-args])
 
-(s/defn ^:private substitute :- {:query su/NonBlankString, :params [s/Any]}
-  "Using the PARAM-SNIPPET-INFO built from the stages above, replace the snippets in SQL and return a vector of
-   `[sql & prepared-statement-params]`."
-  {:style/indent 1}
-  [sql :- su/NonBlankString, param-snippets-info :- [ParamSnippetInfo]]
-  (log/debug (format "PARAM INFO: %s\n%s" (u/emoji "🔥") (u/pprint-to-str 'yellow param-snippets-info)))
-  (loop [sql sql, prepared-statement-args [], [snippet-info & more] param-snippets-info]
-    (if-not snippet-info
-      {:query (str/trim sql), :params (for [arg prepared-statement-args]
-                                        ((resolve 'metabase.driver.generic-sql/prepare-sql-param) *driver* arg))}
-      (recur (substitute-one sql snippet-info)
-             (concat prepared-statement-args (:prepared-statement-args snippet-info))
-             more))))
+(defn- param? [maybe-param]
+  (instance? Param maybe-param))
 
+(defn- merge-query-map [query-map node]
+  (cond
+    (string? node)
+    (update query-map :query str node)
+
+    (param? node)
+    (-> query-map
+        (update :query str (:sql-value node))
+        (update :params concat (:prepared-statement-args node)))
+
+    :else
+    (-> query-map
+        (update :query str (:query node))
+        (update :params concat (:params node)))))
+
+(def ^:private empty-query-map {:query "" :params []})
+
+(defn- no-value-param? [maybe-param]
+  (and (param? maybe-param)
+       (no-value? (:sql-value maybe-param))))
+
+(defn- transform-sql
+  "Returns the combined query-map from all of the parameters, optional clauses etc. At this point there should not be
+  a NoValue leaf. If so, it's an error (i.e. missing a required parameter."
+  [param-key->value]
+  (fn [& nodes]
+    (doseq [maybe-param nodes
+            :when (no-value-param? maybe-param)]
+      (throw (ex-info (format "Unable to substitute '%s': param not specified.\nFound: %s"
+                              (:param-name maybe-param) (keys param-key->value))
+               {:status-code 400})))
+    (-> (reduce merge-query-map empty-query-map nodes)
+        (update :query str/trim))))
+
+(defn- transform-optional
+  "Converts the `OPTIONAL` clause to a query map. If one or more parameters are not populated for this optional
+  clause, it will return an empty-query-map, which will be omitted from the query."
+  [& nodes]
+  (if (some no-value-param? nodes)
+    empty-query-map
+    (reduce merge-query-map empty-query-map nodes)))
+
+(defn- transform-param
+  "Converts a `PARAM` parse leaf to a query map that includes the SQL snippet to replace the `{{param}}` value and the
+  param itself for the prepared statement"
+  [param-key->value]
+  (fn [token]
+    (let [val (get param-key->value (keyword token) (NoValue.))]
+      (if (no-value? val)
+        (map->Param {:param-key token, :sql-value val, :prepared-statement-args []})
+        (let [{:keys [replacement-snippet prepared-statement-args]} (->replacement-snippet-info val)]
+          (map->Param {:param-key               token
+                       :sql-value               replacement-snippet
+                       :prepared-statement-args prepared-statement-args}))))))
+
+(defn- parse-transform-map
+  "Instaparse returns things like [:SQL token token token...]. This map will be used when crawling the parse tree from
+  the bottom up. When encountering the a `:PARAM` node, it will invoke the included function, invoking the function
+  with each item in the list as arguments "
+  [param-key->value]
+  {:SQL      (transform-sql param-key->value)
+   :OPTIONAL transform-optional
+   :PARAM    (transform-param param-key->value)})
 
 ;;; +----------------------------------------------------------------------------------------------------------------+
 ;;; |                                            PUTTING IT ALL TOGETHER                                             |
 ;;; +----------------------------------------------------------------------------------------------------------------+
 
+(defn- prepare-sql-param-for-driver [param]
+  ((resolve 'metabase.driver.generic-sql/prepare-sql-param) *driver* param))
+
 (s/defn ^:private expand-query-params
   [{sql :query, :as native}, param-key->value :- ParamValues]
-  (merge native (when-let [param-snippets-info (seq (add-replacement-snippet-info (sql->params-snippets-info sql) param-key->value))]
-                  (substitute sql param-snippets-info))))
+  (merge native
+         (-> (parse-transform-map param-key->value)
+             (insta/transform (insta/parse sql-template-parser sql))
+             ;; `prepare-sql-param-for-driver` can't be lazy as it needs `*driver*` to be bound
+             (update :params #(mapv prepare-sql-param-for-driver %)))))
+
+(defn- ensure-driver
+  "Depending on where the query came from (the user, permissions check etc) there might not be an driver associated to
+  the query. If there is no driver, use the database to find the right driver or throw."
+  [{:keys [driver database] :as query}]
+  (or driver
+      (driver/database-id->driver database)
+      (throw (IllegalArgumentException. "Could not resolve driver"))))
 
 (defn expand
   "Expand parameters inside a *SQL* QUERY."
   [query]
-  (binding [*driver*   (:driver query)
+  (binding [*driver*   (ensure-driver query)
             *timezone* (get-in query [:settings :report-timezone])]
-    (update query :native expand-query-params (query->params-map query))))
+    (if (driver/driver-supports? *driver* :native-query-params)
+      (update query :native expand-query-params (query->params-map query))
+      query)))
diff --git a/src/metabase/query_processor/util.clj b/src/metabase/query_processor/util.clj
index 4000b9adf1631aa4bb8c141bfb9886962b78c25e..5441fa30c01096ff1fed8fcd3795fafcc819d0a0 100644
--- a/src/metabase/query_processor/util.clj
+++ b/src/metabase/query_processor/util.clj
@@ -66,14 +66,17 @@
   ([m k]
    {:pre [(or (u/maybe? map? m)
               (println "Not a map:" m))]}
-   (let [k (normalize-token k)]
-     (some (fn [[map-k v]]
-             (when (= k (normalize-token map-k))
-               v))
-           m)))
+   (when (seq m)
+     (let [k (normalize-token k)]
+       (loop [[[map-k v] & more] (seq m)]
+         (cond
+           (= k (normalize-token map-k)) v
+           (seq more)                    (recur more))))))
   ([m k not-found]
-   (or (get-normalized m k)
-       not-found)))
+   (let [v (get-normalized m k)]
+     (if (some? v)
+       v
+       not-found))))
 
 (defn get-in-normalized
   "Like `get-normalized`, but accepts a sequence of keys KS, like `get-in`.
@@ -86,8 +89,10 @@
        m
        (recur (get-normalized m k) more))))
   ([m ks not-found]
-   (or (get-in-normalized m ks)
-       not-found)))
+   (let [v (get-in-normalized m ks)]
+     (if (some? v)
+       v
+       not-found))))
 
 (defn dissoc-normalized
   "Remove all matching keys from map M regardless of case, string/keyword, or hypens/underscores.
@@ -122,3 +127,14 @@
   "Return a 256-bit SHA3 hash of QUERY as a key for the cache. (This is returned as a byte array.)"
   [query]
   (hash/sha3-256 (json/generate-string (select-keys-for-hashing query))))
+
+
+;;; --------------------------------------------- Query Source Card IDs ----------------------------------------------
+
+(defn query->source-card-id
+  "Return the ID of the Card used as the \"source\" query of this query, if applicable; otherwise return `nil`."
+  ^Integer [outer-query]
+  (let [source-table (get-in-normalized outer-query [:query :source-table])]
+    (when (string? source-table)
+      (when-let [[_ card-id-str] (re-matches #"^card__(\d+$)" source-table)]
+        (Integer/parseInt card-id-str)))))
diff --git a/src/metabase/sync/analyze/classifiers/text_fingerprint.clj b/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
index 3caaa64926fe35f20c7a6655b7ae4919520f2e0d..e803baab5c95d5a9944e9c8840a78e174338365b 100644
--- a/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
+++ b/src/metabase/sync/analyze/classifiers/text_fingerprint.clj
@@ -9,7 +9,7 @@
             [schema.core :as s]))
 
 (def ^:private ^:const ^Float percent-valid-threshold
-  "Fields that have at least this percent of values that are satisfy some predicate (such as `u/is-email?`)
+  "Fields that have at least this percent of values that are satisfy some predicate (such as `u/email?`)
    should be given the corresponding special type (such as `:type/Email`)."
   0.95)
 
diff --git a/src/metabase/sync/analyze/fingerprint/text.clj b/src/metabase/sync/analyze/fingerprint/text.clj
index 20c995217a1bc0c2a35cd4e102e58cf1ca976916..767c429b8b2a8ef3c3c766d3388c339906069fb7 100644
--- a/src/metabase/sync/analyze/fingerprint/text.clj
+++ b/src/metabase/sync/analyze/fingerprint/text.clj
@@ -34,6 +34,6 @@
   "Generate a fingerprint containing information about values that belong to a `:type/Text` Field."
   [values :- i/FieldSample]
   {:percent-json   (percent-satisfying-predicate valid-serialized-json? values)
-   :percent-url    (percent-satisfying-predicate u/is-url? values)
-   :percent-email  (percent-satisfying-predicate u/is-email? values)
+   :percent-url    (percent-satisfying-predicate u/url? values)
+   :percent-email  (percent-satisfying-predicate u/email? values)
    :average-length (average-length values)})
diff --git a/src/metabase/util.clj b/src/metabase/util.clj
index 39a19ab146dfb6ac5d74d1fed4a34871d04739ec..73278ead925f982096cf0660de583026a1ca4de6 100644
--- a/src/metabase/util.clj
+++ b/src/metabase/util.clj
@@ -376,16 +376,15 @@
       [default args]))
 
 
-;; TODO - rename to `email?`
-(defn is-email?
+(defn email?
   "Is STRING a valid email address?"
   ^Boolean [^String s]
   (boolean (when (string? s)
              (re-matches #"[a-z0-9!#$%&'*+/=?^_`{|}~-]+(?:\.[a-z0-9!#$%&'*+/=?^_`{|}~-]+)*@(?:[a-z0-9](?:[a-z0-9-]*[a-z0-9])?\.)+[a-z0-9](?:[a-z0-9-]*[a-z0-9])?"
                          (s/lower-case s)))))
 
-;; TODO - rename to `url?`
-(defn is-url?
+
+(defn url?
   "Is STRING a valid HTTP/HTTPS URL? (This only handles `localhost` and domains like `metabase.com`; URLs containing
   IP addresses will return `false`.)"
   ^Boolean [^String s]
@@ -923,7 +922,7 @@
   "Parse `TIME-STR` and return a `java.sql.Time` instance. Returns nil
   if `TIME-STR` can't be parsed."
   ([^String date-str]
-   (str->date-time date-str nil))
+   (str->time date-str nil))
   ([^String date-str ^TimeZone tz]
    (some-> (str->date-time-with-formatters ordered-time-parsers date-str tz)
            coerce/to-long
diff --git a/src/metabase/util/schema.clj b/src/metabase/util/schema.clj
index b32bf539073df310bdf07499e8d98434b57e2fc4..83b551e227e3a6da0cefad44c39ba2f4366b46a9 100644
--- a/src/metabase/util/schema.clj
+++ b/src/metabase/util/schema.clj
@@ -134,7 +134,7 @@
 
 (def Email
   "Schema for a valid email string."
-  (with-api-error-message (s/constrained s/Str u/is-email? "Valid email address")
+  (with-api-error-message (s/constrained s/Str u/email? "Valid email address")
     "value must be a valid email address."))
 
 (def ComplexPassword
diff --git a/test/metabase/api/common_test.clj b/test/metabase/api/common_test.clj
index be50c1fef387045a4a1e52b1e5683098b5672ae9..c738b4ed259f2a59259adca9e1b0425160a9006c 100644
--- a/test/metabase/api/common_test.clj
+++ b/test/metabase/api/common_test.clj
@@ -1,6 +1,7 @@
 (ns metabase.api.common-test
-  (:require [expectations :refer :all]
-            [metabase.api.common :refer :all]
+  (:require [clojure.core.async :as async]
+            [expectations :refer :all]
+            [metabase.api.common :refer :all :as api]
             [metabase.api.common.internal :refer :all]
             [metabase.test.data :refer :all]
             [metabase.util.schema :as su]))
@@ -94,3 +95,86 @@
     (defendpoint GET "/:id" [id]
       {id su/IntGreaterThanZero}
       (select-one Card :id id))))
+
+(def ^:private long-timeout
+  ;; 2 minutes
+  (* 2 60000))
+
+(defn- take-with-timeout [response-chan]
+  (let [[response c] (async/alts!! [response-chan
+                                    ;; We should never reach this unless something is REALLY wrong
+                                    (async/timeout long-timeout)])]
+    (when (and (nil? response)
+               (not= c response-chan))
+      (throw (Exception. "Taking from streaming endpoint timed out!")))
+
+    response))
+
+(defn- wait-for-future-cancellation
+  "Once a client disconnects, the next heartbeat sent will result in an exception that should cancel the future. In
+  theory 1 keepalive-interval should be enough, but building in some wiggle room here for poor concurrency timing in
+  tests."
+  [fut]
+  (let [keepalive-interval (var-get #'api/streaming-response-keep-alive-interval-ms)
+        max-iterations     (long (/ long-timeout keepalive-interval))]
+    (loop [i 0]
+      (if (or (future-cancelled? fut) (> i max-iterations))
+        fut
+        (do
+          (Thread/sleep keepalive-interval)
+          (recur (inc i)))))))
+
+;; This simulates 2 keepalive-intervals followed by the query response
+(expect
+  [\newline \newline {:success true} false]
+  (let [send-response (promise)
+        {:keys [output-channel error-channel response-future]} (#'api/invoke-thunk-with-keepalive (fn [] @send-response))]
+    [(take-with-timeout output-channel)
+     (take-with-timeout output-channel)
+     (do
+       (deliver send-response {:success true})
+       (take-with-timeout output-channel))
+     (future-cancelled? response-future)]))
+
+;; This simulates an immediate query response
+(expect
+  [{:success true} false]
+  (let [{:keys [output-channel error-channel response-future]} (#'api/invoke-thunk-with-keepalive (fn [] {:success true}))]
+    [(take-with-timeout output-channel)
+     (future-cancelled? response-future)]))
+
+;; This simulates a closed connection from the client, should cancel the future
+(expect
+  [\newline \newline true]
+  (let [send-response (promise)
+        {:keys [output-channel error-channel response-future]} (#'api/invoke-thunk-with-keepalive (fn [] (Thread/sleep long-timeout)))]
+    [(take-with-timeout output-channel)
+     (take-with-timeout output-channel)
+     (do
+       (async/close! output-channel)
+       (future-cancelled? (wait-for-future-cancellation response-future)))]))
+
+;; When an immediate exception happens, we should know that via the error channel
+(expect
+  ;; Each channel should have the failure and then get closed
+  ["It failed" "It failed" nil nil]
+  (let [{:keys [output-channel error-channel response-future]} (#'api/invoke-thunk-with-keepalive (fn [] (throw (Exception. "It failed"))))]
+    [(.getMessage (take-with-timeout error-channel))
+     (.getMessage (take-with-timeout output-channel))
+     (async/<!! error-channel)
+     (async/<!! output-channel)]))
+
+;; This simulates a slow failure, we'll still get an exception, but the error channel is closed, so at this point
+;; we've assumed it would be a success, but it wasn't
+(expect
+  [\newline nil \newline "It failed" false]
+  (let [now-throw-exception (promise)
+        {:keys [output-channel error-channel response-future]} (#'api/invoke-thunk-with-keepalive
+                                                                (fn [] @now-throw-exception (throw (Exception. "It failed"))))]
+    [(take-with-timeout output-channel)
+     (take-with-timeout error-channel)
+     (take-with-timeout output-channel)
+     (do
+       (deliver now-throw-exception true)
+       (.getMessage (take-with-timeout output-channel)))
+     (future-cancelled? response-future)]))
diff --git a/test/metabase/api/database_test.clj b/test/metabase/api/database_test.clj
index 7b1331b64311ac418a046240a81cab1bf0fbae08..829d15f48be7a848598412328f5cf44d894685c2 100644
--- a/test/metabase/api/database_test.clj
+++ b/test/metabase/api/database_test.clj
@@ -68,6 +68,7 @@
    :points_of_interest          nil
    :cache_field_values_schedule "0 50 0 * * ? *"
    :metadata_sync_schedule      "0 50 * * * ? *"
+   :options                     nil
    :timezone                    nil})
 
 (defn- db-details
@@ -272,7 +273,7 @@
 (defn- field-details [field]
   (merge
    default-field-details
-   (match-$ (hydrate/hydrate field :values)
+   (match-$ field
      {:updated_at          $
       :id                  $
       :raw_column_id       $
@@ -280,11 +281,9 @@
       :last_analyzed       $
       :fingerprint         $
       :fingerprint_version $
-      :fk_target_field_id  $
-      :values              $})))
+      :fk_target_field_id  $})))
 
-;; ## GET /api/meta/table/:id/query_metadata
-;; TODO - add in example with Field :values
+;; ## GET /api/database/:id/metadata
 (expect
   (merge default-db-details
          (match-$ (db)
@@ -301,21 +300,23 @@
                                    :name         "CATEGORIES"
                                    :display_name "Categories"
                                    :fields       [(assoc (field-details (Field (id :categories :id)))
-                                                    :table_id        (id :categories)
-                                                    :special_type    "type/PK"
-                                                    :name            "ID"
-                                                    :display_name    "ID"
-                                                    :database_type   "BIGINT"
-                                                    :base_type       "type/BigInteger"
-                                                    :visibility_type "normal")
+                                                    :table_id         (id :categories)
+                                                    :special_type     "type/PK"
+                                                    :name             "ID"
+                                                    :display_name     "ID"
+                                                    :database_type    "BIGINT"
+                                                    :base_type        "type/BigInteger"
+                                                    :visibility_type  "normal"
+                                                    :has_field_values "search")
                                                   (assoc (field-details (Field (id :categories :name)))
-                                                    :table_id           (id :categories)
-                                                    :special_type       "type/Name"
-                                                    :name               "NAME"
-                                                    :display_name       "Name"
-                                                    :database_type      "VARCHAR"
-                                                    :base_type          "type/Text"
-                                                    :visibility_type    "normal")]
+                                                    :table_id         (id :categories)
+                                                    :special_type     "type/Name"
+                                                    :name             "NAME"
+                                                    :display_name     "Name"
+                                                    :database_type    "VARCHAR"
+                                                    :base_type        "type/Text"
+                                                    :visibility_type  "normal"
+                                                    :has_field_values "list")]
                                    :segments     []
                                    :metrics      []
                                    :rows         75
diff --git a/test/metabase/api/dataset_test.clj b/test/metabase/api/dataset_test.clj
index 024a930ef0c589c3cad1a2539b375115437af501..bb42648738dc820e0b7d62af5b41d22ef78d6842 100644
--- a/test/metabase/api/dataset_test.clj
+++ b/test/metabase/api/dataset_test.clj
@@ -8,10 +8,10 @@
             [dk.ative.docjure.spreadsheet :as spreadsheet]
             [expectations :refer :all]
             [medley.core :as m]
-            [metabase.api.dataset :refer [default-query-constraints]]
             [metabase.models
              [database :refer [Database]]
              [query-execution :refer [QueryExecution]]]
+            [metabase.query-processor :as qp]
             [metabase.query-processor.middleware.expand :as ql]
             [metabase.sync :as sync]
             [metabase.test
@@ -76,7 +76,7 @@
                                     (ql/aggregation (ql/count))))
                                 (assoc :type "query")
                                 (assoc-in [:query :aggregation] [{:aggregation-type "count", :custom-name nil}])
-                                (assoc :constraints default-query-constraints))
+                                (assoc :constraints qp/default-query-constraints))
     :started_at             true
     :running_time           true
     :average_execution_time nil}
@@ -114,7 +114,7 @@
     :json_query   {:database    (id)
                    :type        "native"
                    :native      {:query "foobar"}
-                   :constraints default-query-constraints}
+                   :constraints qp/default-query-constraints}
     :started_at   true
     :running_time true}
    ;; QueryExecution entry in the DB
@@ -190,21 +190,11 @@
    ["3" "2014-09-15" "" "8" "56"]
    ["4" "2014-03-11" "" "5" "4"]
    ["5" "2013-05-05" "" "3" "49"]]
-  (with-db (get-or-create-database! defs/test-data)
-    (let [db (Database :name "test-data")]
-      (jdbc/with-db-connection [conn {:classname "org.h2.Driver", :subprotocol "h2", :subname "mem:test-data"}]
-        ;; test-data doesn't include any null date values, add a date column to ensure we can handle null dates on export
-        (jdbc/execute! conn "ALTER TABLE CHECKINS ADD COLUMN MYDATECOL DATE")
-        (sync/sync-database! db)
-        (try
-          (let [result ((user->client :rasta) :post 200 "dataset/csv" :query
-                        (json/generate-string (wrap-inner-query
-                                                (query checkins))))]
-            (take 5 (parse-and-sort-csv result)))
-          (finally
-            ;; ensure we remove the column when we're done otherwise subsequent tests will break
-            (jdbc/execute! conn "ALTER TABLE CHECKINS DROP COLUMN MYDATECOL")
-            (sync/sync-database! db)))))))
+  (with-db (get-or-create-database! defs/test-data-with-null-date-checkins)
+    (let [result ((user->client :rasta) :post 200 "dataset/csv" :query
+                  (json/generate-string (wrap-inner-query
+                                          (query checkins))))]
+      (take 5 (parse-and-sort-csv result)))))
 
 ;; SQLite doesn't return proper date objects but strings, they just pass through the qp untouched
 (expect-with-engine :sqlite
diff --git a/test/metabase/api/field_test.clj b/test/metabase/api/field_test.clj
index a8587328de8bac8aaf86eb95840bc9c146b5cd61..2658d5c229b49c2382e2d6cfe635af90ac133db3 100644
--- a/test/metabase/api/field_test.clj
+++ b/test/metabase/api/field_test.clj
@@ -36,6 +36,7 @@
      :features                    (mapv name (driver/features (driver/engine->driver :h2)))
      :cache_field_values_schedule "0 50 0 * * ? *"
      :metadata_sync_schedule      "0 50 * * * ? *"
+     :options                     nil
      :timezone                    $}))
 
 ;; ## GET /api/field/:id
@@ -80,6 +81,7 @@
      :created_at          $
      :database_type       "VARCHAR"
      :base_type           "type/Text"
+     :has_field_values    "list"
      :fk_target_field_id  nil
      :parent_id           nil})
   ((user->client :rasta) :get 200 (format "field/%d" (id :users :name))))
diff --git a/test/metabase/api/session_test.clj b/test/metabase/api/session_test.clj
index 3412c041c24f61d11048b0072ab23c5b90f4bbc9..4a8c49e7fb8fd7f52e675037959a852f8bb8a5de 100644
--- a/test/metabase/api/session_test.clj
+++ b/test/metabase/api/session_test.clj
@@ -2,6 +2,7 @@
   "Tests for /api/session"
   (:require [expectations :refer :all]
             [metabase
+             [email-test :as et]
              [http-client :refer :all]
              [public-settings :as public-settings]
              [util :as u]]
@@ -68,16 +69,17 @@
 ;; ## POST /api/session/forgot_password
 ;; Test that we can initiate password reset
 (expect
-  (let [reset-fields-set? (fn []
-                            (let [{:keys [reset_token reset_triggered]} (db/select-one [User :reset_token :reset_triggered], :id (user->id :rasta))]
-                              (boolean (and reset_token reset_triggered))))]
-    ;; make sure user is starting with no values
-    (db/update! User (user->id :rasta), :reset_token nil, :reset_triggered nil)
-    (assert (not (reset-fields-set?)))
-    ;; issue reset request (token & timestamp should be saved)
-    ((user->client :rasta) :post 200 "session/forgot_password" {:email (:username (user->credentials :rasta))})
-    ;; TODO - how can we test email sent here?
-    (reset-fields-set?)))
+  (et/with-fake-inbox
+    (let [reset-fields-set? (fn []
+                              (let [{:keys [reset_token reset_triggered]} (db/select-one [User :reset_token :reset_triggered], :id (user->id :rasta))]
+                                (boolean (and reset_token reset_triggered))))]
+      ;; make sure user is starting with no values
+      (db/update! User (user->id :rasta), :reset_token nil, :reset_triggered nil)
+      (assert (not (reset-fields-set?)))
+      ;; issue reset request (token & timestamp should be saved)
+      ((user->client :rasta) :post 200 "session/forgot_password" {:email (:username (user->credentials :rasta))})
+      ;; TODO - how can we test email sent here?
+      (reset-fields-set?))))
 
 ;; Test that email is required
 (expect {:errors {:email "value must be a valid email address."}}
@@ -94,39 +96,41 @@
 (expect
   {:reset_token     nil
    :reset_triggered nil}
-  (let [password {:old "password"
-                  :new "whateverUP12!!"}]
-    (tt/with-temp User [{:keys [email id]} {:password (:old password), :reset_triggered (System/currentTimeMillis)}]
-      (let [token (u/prog1 (str id "_" (java.util.UUID/randomUUID))
-                    (db/update! User id, :reset_token <>))
-            creds {:old {:password (:old password)
-                         :username email}
-                   :new {:password (:new password)
-                         :username email}}]
-        ;; Check that creds work
-        (client :post 200 "session" (:old creds))
-
-        ;; Call reset password endpoint to change the PW
-        (client :post 200 "session/reset_password" {:token    token
-                                                    :password (:new password)})
-        ;; Old creds should no longer work
-        (assert (= (client :post 400 "session" (:old creds))
-                   {:errors {:password "did not match stored password"}}))
-        ;; New creds *should* work
-        (client :post 200 "session" (:new creds))
-        ;; Double check that reset token was cleared
-        (db/select-one [User :reset_token :reset_triggered], :id id)))))
+  (et/with-fake-inbox
+    (let [password {:old "password"
+                    :new "whateverUP12!!"}]
+      (tt/with-temp User [{:keys [email id]} {:password (:old password), :reset_triggered (System/currentTimeMillis)}]
+        (let [token (u/prog1 (str id "_" (java.util.UUID/randomUUID))
+                      (db/update! User id, :reset_token <>))
+              creds {:old {:password (:old password)
+                           :username email}
+                     :new {:password (:new password)
+                           :username email}}]
+          ;; Check that creds work
+          (client :post 200 "session" (:old creds))
+
+          ;; Call reset password endpoint to change the PW
+          (client :post 200 "session/reset_password" {:token    token
+                                                      :password (:new password)})
+          ;; Old creds should no longer work
+          (assert (= (client :post 400 "session" (:old creds))
+                     {:errors {:password "did not match stored password"}}))
+          ;; New creds *should* work
+          (client :post 200 "session" (:new creds))
+          ;; Double check that reset token was cleared
+          (db/select-one [User :reset_token :reset_triggered], :id id))))))
 
 ;; Check that password reset returns a valid session token
 (expect
   {:success    true
    :session_id true}
-  (tt/with-temp User [{:keys [id]} {:reset_triggered (System/currentTimeMillis)}]
-    (let [token (u/prog1 (str id "_" (java.util.UUID/randomUUID))
-                  (db/update! User id, :reset_token <>))]
-      (-> (client :post 200 "session/reset_password" {:token    token
-                                                      :password "whateverUP12!!"})
-          (update :session_id tu/is-uuid-string?)))))
+  (et/with-fake-inbox
+    (tt/with-temp User [{:keys [id]} {:reset_triggered (System/currentTimeMillis)}]
+      (let [token (u/prog1 (str id "_" (java.util.UUID/randomUUID))
+                    (db/update! User id, :reset_token <>))]
+        (-> (client :post 200 "session/reset_password" {:token    token
+                                                        :password "whateverUP12!!"})
+            (update :session_id tu/is-uuid-string?))))))
 
 ;; Test that token and password are required
 (expect {:errors {:token "value must be a non-blank string."}}
@@ -217,11 +221,12 @@
 ;; should totally work if the email domains match up
 (expect
   {:first_name "Rasta", :last_name "Toucan", :email "rasta@sf-toucannery.com"}
-  (tu/with-temporary-setting-values [google-auth-auto-create-accounts-domain "sf-toucannery.com"
-                                     admin-email                             "rasta@toucans.com"]
-    (select-keys (u/prog1 (#'session-api/google-auth-create-new-user! "Rasta" "Toucan" "rasta@sf-toucannery.com")
-                   (db/delete! User :id (:id <>))) ; make sure we clean up after ourselves !
-                 [:first_name :last_name :email])))
+  (et/with-fake-inbox
+    (tu/with-temporary-setting-values [google-auth-auto-create-accounts-domain "sf-toucannery.com"
+                                       admin-email                             "rasta@toucans.com"]
+      (select-keys (u/prog1 (#'session-api/google-auth-create-new-user! "Rasta" "Toucan" "rasta@sf-toucannery.com")
+                     (db/delete! User :id (:id <>))) ; make sure we clean up after ourselves !
+                   [:first_name :last_name :email]))))
 
 
 ;;; tests for google-auth-fetch-or-create-user!
@@ -248,10 +253,11 @@
 ;; test that a user that doesn't exist with the *same* domain as the auto-create accounts domain means a new user gets
 ;; created
 (expect
-  (tu/with-temporary-setting-values [google-auth-auto-create-accounts-domain "sf-toucannery.com"
-                                     admin-email                             "rasta@toucans.com"]
-    (u/prog1 (is-session? (#'session-api/google-auth-fetch-or-create-user! "Rasta" "Toucan" "rasta@sf-toucannery.com"))
-      (db/delete! User :email "rasta@sf-toucannery.com")))) ; clean up after ourselves
+  (et/with-fake-inbox
+    (tu/with-temporary-setting-values [google-auth-auto-create-accounts-domain "sf-toucannery.com"
+                                       admin-email                             "rasta@toucans.com"]
+      (u/prog1 (is-session? (#'session-api/google-auth-fetch-or-create-user! "Rasta" "Toucan" "rasta@sf-toucannery.com"))
+        (db/delete! User :email "rasta@sf-toucannery.com"))))) ; clean up after ourselves
 
 
 ;;; ------------------------------------------- TESTS FOR LDAP AUTH STUFF --------------------------------------------
diff --git a/test/metabase/api/table_test.clj b/test/metabase/api/table_test.clj
index 881e39d6c9ccabf242b65b136d7654bd8c00f49e..0443fca5722baf1e0b53e14d48e38e509a2a86a8 100644
--- a/test/metabase/api/table_test.clj
+++ b/test/metabase/api/table_test.clj
@@ -58,6 +58,7 @@
      :features                    (mapv name (driver/features (driver/engine->driver :h2)))
      :cache_field_values_schedule "0 50 0 * * ? *"
      :metadata_sync_schedule      "0 50 * * * ? *"
+     :options                     nil
      :timezone                    $}))
 
 (defn- table-defaults []
@@ -86,8 +87,8 @@
    :special_type             nil
    :parent_id                nil
    :dimensions               []
-   :values                   []
    :dimension_options        []
+   :has_field_values         nil
    :default_dimension_option nil})
 
 (defn- field-details [field]
@@ -176,12 +177,13 @@
             :name         "CATEGORIES"
             :display_name "Categories"
             :fields       [(assoc (field-details (Field (data/id :categories :id)))
-                             :table_id     (data/id :categories)
-                             :special_type  "type/PK"
-                             :name          "ID"
-                             :display_name  "ID"
-                             :database_type "BIGINT"
-                             :base_type     "type/BigInteger")
+                             :table_id         (data/id :categories)
+                             :special_type     "type/PK"
+                             :name             "ID"
+                             :display_name     "ID"
+                             :database_type    "BIGINT"
+                             :base_type        "type/BigInteger"
+                             :has_field_values "search")
                            (assoc (field-details (Field (data/id :categories :name)))
                              :table_id                 (data/id :categories)
                              :special_type             "type/Name"
@@ -189,9 +191,9 @@
                              :display_name             "Name"
                              :database_type            "VARCHAR"
                              :base_type                "type/Text"
-                             :values                   data/venue-categories
                              :dimension_options        []
-                             :default_dimension_option nil)]
+                             :default_dimension_option nil
+                             :has_field_values         "list")]
             :rows         75
             :updated_at   $
             :id           (data/id :categories)
@@ -227,13 +229,14 @@
             :name         "USERS"
             :display_name "Users"
             :fields       [(assoc (field-details (Field (data/id :users :id)))
-                             :special_type    "type/PK"
-                             :table_id        (data/id :users)
-                             :name            "ID"
-                             :display_name    "ID"
-                             :database_type   "BIGINT"
-                             :base_type       "type/BigInteger"
-                             :visibility_type "normal")
+                             :special_type     "type/PK"
+                             :table_id         (data/id :users)
+                             :name             "ID"
+                             :display_name     "ID"
+                             :database_type    "BIGINT"
+                             :base_type        "type/BigInteger"
+                             :visibility_type  "normal"
+                             :has_field_values "search")
                            (assoc (field-details (Field (data/id :users :last_login)))
                              :table_id                 (data/id :users)
                              :name                     "LAST_LOGIN"
@@ -242,7 +245,8 @@
                              :base_type                "type/DateTime"
                              :visibility_type          "normal"
                              :dimension_options        (var-get #'table-api/datetime-dimension-indexes)
-                             :default_dimension_option (var-get #'table-api/date-default-index))
+                             :default_dimension_option (var-get #'table-api/date-default-index)
+                             :has_field_values         "search")
                            (assoc (field-details (Field (data/id :users :name)))
                              :special_type             "type/Name"
                              :table_id                 (data/id :users)
@@ -251,17 +255,18 @@
                              :database_type            "VARCHAR"
                              :base_type                "type/Text"
                              :visibility_type          "normal"
-                             :values                   (map vector (sort user-full-names))
                              :dimension_options        []
-                             :default_dimension_option nil)
+                             :default_dimension_option nil
+                             :has_field_values         "list")
                            (assoc (field-details (Field :table_id (data/id :users), :name "PASSWORD"))
-                             :special_type    "type/Category"
-                             :table_id        (data/id :users)
-                             :name            "PASSWORD"
-                             :display_name    "Password"
-                             :database_type   "VARCHAR"
-                             :base_type       "type/Text"
-                             :visibility_type "sensitive")]
+                             :special_type     "type/Category"
+                             :table_id         (data/id :users)
+                             :name             "PASSWORD"
+                             :display_name     "Password"
+                             :database_type    "VARCHAR"
+                             :base_type        "type/Text"
+                             :visibility_type  "sensitive"
+                             :has_field_values "list")]
             :rows         15
             :updated_at   $
             :id           (data/id :users)
@@ -278,12 +283,13 @@
             :name         "USERS"
             :display_name "Users"
             :fields       [(assoc (field-details (Field (data/id :users :id)))
-                             :table_id      (data/id :users)
-                             :special_type  "type/PK"
-                             :name          "ID"
-                             :display_name  "ID"
-                             :database_type "BIGINT"
-                             :base_type     "type/BigInteger")
+                             :table_id         (data/id :users)
+                             :special_type     "type/PK"
+                             :name             "ID"
+                             :display_name     "ID"
+                             :database_type    "BIGINT"
+                             :base_type        "type/BigInteger"
+                             :has_field_values "search")
                            (assoc (field-details (Field (data/id :users :last_login)))
                              :table_id                 (data/id :users)
                              :name                     "LAST_LOGIN"
@@ -291,29 +297,16 @@
                              :database_type            "TIMESTAMP"
                              :base_type                "type/DateTime"
                              :dimension_options        (var-get #'table-api/datetime-dimension-indexes)
-                             :default_dimension_option (var-get #'table-api/date-default-index))
+                             :default_dimension_option (var-get #'table-api/date-default-index)
+                             :has_field_values         "search")
                            (assoc (field-details (Field (data/id :users :name)))
-                             :table_id      (data/id :users)
-                             :special_type  "type/Name"
-                             :name          "NAME"
-                             :display_name  "Name"
-                             :database_type "VARCHAR"
-                             :base_type     "type/Text"
-                             :values        [["Broen Olujimi"]
-                                             ["Conchúr Tihomir"]
-                                             ["Dwight Gresham"]
-                                             ["Felipinho Asklepios"]
-                                             ["Frans Hevel"]
-                                             ["Kaneonuskatew Eiran"]
-                                             ["Kfir Caj"]
-                                             ["Nils Gotam"]
-                                             ["Plato Yeshua"]
-                                             ["Quentin Sören"]
-                                             ["Rüstem Hebel"]
-                                             ["Shad Ferdynand"]
-                                             ["Simcha Yan"]
-                                             ["Spiros Teofil"]
-                                             ["Szymon Theutrich"]])]
+                             :table_id         (data/id :users)
+                             :special_type     "type/Name"
+                             :name             "NAME"
+                             :display_name     "Name"
+                             :database_type    "VARCHAR"
+                             :base_type        "type/Text"
+                             :has_field_values "list")]
             :rows         15
             :updated_at   $
             :id           (data/id :users)
@@ -527,12 +520,10 @@
   [{:table_id   (data/id :venues)
     :id         (data/id :venues :category_id)
     :name       "CATEGORY_ID"
-    :values     (map-indexed (fn [idx [category]] [idx category]) data/venue-categories)
     :dimensions {:name "Foo", :field_id (data/id :venues :category_id), :human_readable_field_id nil, :type "internal"}}
    {:id         (data/id :venues :price)
     :table_id   (data/id :venues)
     :name       "PRICE"
-    :values     [[1] [2] [3] [4]]
     :dimensions []}]
   (data/with-data
     (data/create-venue-category-remapping "Foo")
@@ -548,12 +539,10 @@
   [{:table_id   (data/id :venues)
     :id         (data/id :venues :category_id)
     :name       "CATEGORY_ID"
-    :values     (map-indexed (fn [idx [category]] [idx category]) data/venue-categories)
     :dimensions {:name "Foo", :field_id (data/id :venues :category_id), :human_readable_field_id nil, :type "internal"}}
    {:id         (data/id :venues :price)
     :table_id   (data/id :venues)
     :name       "PRICE"
-    :values     [[1] [2] [3] [4]]
     :dimensions []}]
   (data/with-data
     (data/create-venue-category-remapping "Foo")
@@ -569,12 +558,10 @@
   [{:table_id   (data/id :venues)
     :id         (data/id :venues :category_id)
     :name       "CATEGORY_ID"
-    :values     []
     :dimensions {:name "Foo", :field_id (data/id :venues :category_id), :human_readable_field_id (data/id :categories :name), :type "external"}}
    {:id         (data/id :venues :price)
     :table_id   (data/id :venues)
     :name       "PRICE"
-    :values     [[1] [2] [3] [4]]
     :dimensions []}]
   (data/with-data
     (data/create-venue-category-fk-remapping "Foo")
diff --git a/test/metabase/api/user_test.clj b/test/metabase/api/user_test.clj
index 8e82e371e6890feb60e58ffeb2798ab0fe10a31c..04f58fa3e8c73443d339ee49a2da72fe0b961eb9 100644
--- a/test/metabase/api/user_test.clj
+++ b/test/metabase/api/user_test.clj
@@ -2,6 +2,7 @@
   "Tests for /api/user endpoints."
   (:require [expectations :refer :all]
             [metabase
+             [email-test :as et]
              [http-client :as http]
              [middleware :as middleware]
              [util :as u]]
@@ -73,13 +74,14 @@
      :common_name  (str user-name " " user-name)
      :is_superuser false
      :is_qbnewb    true}
-    (do ((user->client :crowberto) :post 200 "user" {:first_name user-name
-                                                     :last_name  user-name
-                                                     :email      email})
-        (u/prog1 (db/select-one [User :email :first_name :last_name :is_superuser :is_qbnewb]
-                   :email email)
-          ;; clean up after ourselves
-          (db/delete! User :email email)))))
+    (et/with-fake-inbox
+      ((user->client :crowberto) :post 200 "user" {:first_name user-name
+                                                   :last_name  user-name
+                                                   :email      email})
+      (u/prog1 (db/select-one [User :email :first_name :last_name :is_superuser :is_qbnewb]
+                              :email email)
+               ;; clean up after ourselves
+               (db/delete! User :email email)))))
 
 
 ;; Test that reactivating a disabled account works
diff --git a/test/metabase/driver/bigquery_test.clj b/test/metabase/driver/bigquery_test.clj
index 742a279c3531edb3599423ef65f485e4577507de..887e7f5da746f724fcd82bb23649b5431c999cd0 100644
--- a/test/metabase/driver/bigquery_test.clj
+++ b/test/metabase/driver/bigquery_test.clj
@@ -13,7 +13,7 @@
             [metabase.test
              [data :as data]
              [util :as tu]]
-            [metabase.test.data.datasets :refer [expect-with-engine]]))
+            [metabase.test.data.datasets :refer [expect-with-engine do-with-engine]]))
 
 (def ^:private col-defaults
   {:remapped_to nil, :remapped_from nil})
@@ -76,24 +76,38 @@
                                                                       ["field-id" (data/id :checkins :venue_id)]]]
                                                   "User ID Plus Venue ID"]]}})))
 
+(defn- aggregation-names [query-map]
+  (->> query-map
+       :aggregation
+       (map :custom-name)))
+
+(defn- pre-alias-aggregations' [query-map]
+  (binding [qpi/*driver* (driver/engine->driver :bigquery)]
+    (aggregation-names (#'bigquery/pre-alias-aggregations query-map))))
+
+(defn- agg-query-map [aggregations]
+  (-> {}
+      (ql/source-table 1)
+      (ql/aggregation aggregations)))
+
 ;; make sure BigQuery can handle two aggregations with the same name (#4089)
 (expect
   ["sum" "count" "sum_2" "avg" "sum_3" "min"]
-  (#'bigquery/deduplicate-aliases ["sum" "count" "sum" "avg" "sum" "min"]))
+  (pre-alias-aggregations' (agg-query-map [(ql/sum (ql/field-id 2))
+                                           (ql/count (ql/field-id 2))
+                                           (ql/sum (ql/field-id 2))
+                                           (ql/avg (ql/field-id 2))
+                                           (ql/sum (ql/field-id 2))
+                                           (ql/min (ql/field-id 2))])))
 
 (expect
   ["sum" "count" "sum_2" "avg" "sum_2_2" "min"]
-  (#'bigquery/deduplicate-aliases ["sum" "count" "sum" "avg" "sum_2" "min"]))
-
-(expect
-  ["sum" "count" nil "sum_2"]
-  (#'bigquery/deduplicate-aliases ["sum" "count" nil "sum"]))
-
-(expect
-  [[:user_id "user_id_2"] :venue_id]
-  (#'bigquery/update-select-subclause-aliases [[:user_id "user_id"] :venue_id]
-                                              ["user_id_2" nil]))
-
+  (pre-alias-aggregations' (agg-query-map [(ql/sum (ql/field-id 2))
+                                           (ql/count (ql/field-id 2))
+                                           (ql/sum (ql/field-id 2))
+                                           (ql/avg (ql/field-id 2))
+                                           (assoc (ql/sum (ql/field-id 2)) :custom-name "sum_2")
+                                           (ql/min (ql/field-id 2))])))
 
 (expect-with-engine :bigquery
   {:rows [[7929 7929]], :columns ["sum" "sum_2"]}
diff --git a/test/metabase/driver/druid_test.clj b/test/metabase/driver/druid_test.clj
index a0aebf770af0a0c8b742bd33cb0c93230801d944..9be935a17c4f1f4a6b5b2f54ce7561eaffff86f2 100644
--- a/test/metabase/driver/druid_test.clj
+++ b/test/metabase/driver/druid_test.clj
@@ -9,16 +9,19 @@
              [query-processor-test :refer [rows rows+column-names]]
              [timeseries-query-processor-test :as timeseries-qp-test]
              [util :as u]]
-            metabase.driver.druid
+            [metabase.driver.druid :as druid]
             [metabase.models
              [field :refer [Field]]
              [metric :refer [Metric]]
              [table :refer [Table]]]
             [metabase.query-processor.middleware.expand :as ql]
+            [metabase.query-processor-test.query-cancellation-test :as cancel-test]
             [metabase.test
              [data :as data]
              [util :as tu]]
-            [metabase.test.data.datasets :as datasets :refer [expect-with-engine]]
+            [metabase.test.data
+             [dataset-definitions :as defs]
+             [datasets :as datasets :refer [expect-with-engine]]]
             [toucan.util.test :as tt])
   (:import metabase.driver.druid.DruidDriver))
 
@@ -332,3 +335,22 @@
       (driver/can-connect-with-details? engine details :rethrow-exceptions))
        (catch Exception e
          (.getMessage e))))
+
+;; Query cancellation test, needs careful coordination between the query thread, cancellation thread to ensure
+;; everything works correctly together
+(datasets/expect-with-engine :druid
+  [false ;; Ensure the query promise hasn't fired yet
+   false ;; Ensure the cancellation promise hasn't fired yet
+   true  ;; Was query called?
+   false ;; Cancel should not have been called yet
+   true  ;; Cancel should have been called now
+   true  ;; The paused query can proceed now
+   ]
+  (tu/call-with-paused-query
+   (fn [query-thunk called-query? called-cancel? pause-query]
+     (future
+       ;; stub out the query and delete functions so that we know when one is called vs. the other
+       (with-redefs [druid/do-query (fn [details query] (deliver called-query? true) @pause-query)
+                     druid/DELETE   (fn [url] (deliver called-cancel? true))]
+         (data/run-query checkins
+           (ql/aggregation (ql/count))))))))
diff --git a/test/metabase/driver/generic_sql/native_test.clj b/test/metabase/driver/generic_sql/native_test.clj
index f91553a304fc1e651397778140147d27d6ae393c..63af244d78357736767c54b0e6d8042e29449e53 100644
--- a/test/metabase/driver/generic_sql/native_test.clj
+++ b/test/metabase/driver/generic_sql/native_test.clj
@@ -17,7 +17,7 @@
                              [99]]
                :columns     ["ID"]
                :cols        [(merge col-defaults {:name "ID", :display_name "ID", :base_type :type/Integer})]
-               :native_form {:query "SELECT ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}}}
+               :native_form {:query "SELECT ID FROM VENUES ORDER BY ID DESC LIMIT 2;", :params []}}}
   (-> (qp/process-query {:native   {:query "SELECT ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}
                          :type     :native
                          :database (id)})
@@ -34,7 +34,7 @@
                                   [{:name "ID",          :display_name "ID",          :base_type :type/Integer}
                                    {:name "NAME",        :display_name "Name",        :base_type :type/Text}
                                    {:name "CATEGORY_ID", :display_name "Category ID", :base_type :type/Integer}])
-               :native_form {:query "SELECT ID, NAME, CATEGORY_ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}}}
+               :native_form {:query "SELECT ID, NAME, CATEGORY_ID FROM VENUES ORDER BY ID DESC LIMIT 2;", :params []}}}
   (-> (qp/process-query {:native   {:query "SELECT ID, NAME, CATEGORY_ID FROM VENUES ORDER BY ID DESC LIMIT 2;"}
                          :type     :native
                          :database (id)})
diff --git a/test/metabase/driver/presto_test.clj b/test/metabase/driver/presto_test.clj
index a262f342471ca131c3c7022cc2d3ca8c7e03e13c..b8812612469a6d288436828decbae2181832b3e8 100644
--- a/test/metabase/driver/presto_test.clj
+++ b/test/metabase/driver/presto_test.clj
@@ -1,14 +1,18 @@
 (ns metabase.driver.presto-test
-  (:require [expectations :refer [expect]]
+  (:require [clj-http.client :as http]
+            [expectations :refer [expect]]
             [metabase.driver :as driver]
             [metabase.driver.presto :as presto]
             [metabase.models
              [field :refer [Field]]
              [table :as table :refer [Table]]]
+            [metabase.query-processor.middleware.expand :as ql]
             [metabase.test
              [data :as data]
              [util :as tu]]
-            [metabase.test.data.datasets :as datasets]
+            [metabase.test.data
+             [dataset-definitions :as defs]
+             [datasets :as datasets :refer [expect-with-engine]]]
             [toucan.db :as db])
   (:import metabase.driver.presto.PrestoDriver))
 
@@ -152,3 +156,20 @@
 (datasets/expect-with-engine :presto
   "UTC"
   (tu/db-timezone-id))
+
+;; Query cancellation test, needs careful coordination between the query thread, cancellation thread to ensure
+;; everything works correctly together
+(datasets/expect-with-engine :presto
+  [false ;; Ensure the query promise hasn't fired yet
+   false ;; Ensure the cancellation promise hasn't fired yet
+   true  ;; Was query called?
+   false ;; Cancel should not have been called yet
+   true  ;; Cancel should have been called now
+   true  ;; The paused query can proceed now
+   ]
+  (tu/call-with-paused-query
+   (fn [query-thunk called-query? called-cancel? pause-query]
+     (future
+       (with-redefs [presto/fetch-presto-results! (fn [_ _ _] (deliver called-query? true) @pause-query)
+                     http/delete                  (fn [_ _] (deliver called-cancel? true))]
+         (query-thunk))))))
diff --git a/test/metabase/middleware_test.clj b/test/metabase/middleware_test.clj
index 1fa312cdea7e35c7862566930d73e1ab30986e29..1e3b37e33a2ab79d754510babdb3c4ab40b68e61 100644
--- a/test/metabase/middleware_test.clj
+++ b/test/metabase/middleware_test.clj
@@ -1,13 +1,12 @@
 (ns metabase.middleware-test
   (:require [cheshire.core :as json]
-            [clojure.core.async :as async]
             [clojure.java.io :as io]
             [clojure.tools.logging :as log]
             [compojure.core :refer [GET]]
             [expectations :refer :all]
             [metabase
              [config :as config]
-             [middleware :as middleware :refer :all]
+             [middleware :as middleware :refer :all :as mid]
              [routes :as routes]
              [util :as u]]
             [metabase.api.common :refer [*current-user* *current-user-id*]]
@@ -185,93 +184,3 @@
 (expect "{\"my-bytes\":\"0xC42360D7\"}"
         (json/generate-string {:my-bytes (byte-array [196 35  96 215  8 106 108 248 183 215 244 143  17 160 53 186
                                                       213 30 116  25 87  31 123 172 207 108  47 107 191 215 76  92])}))
-
-;; Handlers that will generate response. Some of them take a `BLOCK-ON` argument that will hold up the response until
-;; the promise has been delivered. This allows coordination between the scaffolding and the expected response and
-;; avoids the sleeps
-
-(defn- streaming-fast-success [_]
-  (resp/response {:success true}))
-
-(defn- streaming-fast-failure [_]
-  (throw (Exception. "immediate failure")))
-
-(defn- streaming-slow-success [block-on]
-  (fn [_]
-    @block-on
-    (resp/response {:success true})))
-
-(defn- streaming-slow-failure [block-on]
-  (fn [_]
-    @block-on
-    (throw (Exception. "delayed failure"))))
-
-(def ^:private long-timeout
-  ;; 2 minutes
-  (* 2 60000))
-
-(defn- take-with-timeout [response-chan]
-  (let [[response c] (async/alts!! [response-chan
-                                    ;; We should never reach this unless something is REALLY wrong
-                                    (async/timeout long-timeout)])]
-    (when-not response
-      (throw (Exception. "Taking from streaming endpoint timed out!")))
-
-    response))
-
-(defn- test-streaming-endpoint [handler handle-response-fn]
-  (let [path (str handler)]
-    (with-redefs [metabase.routes/routes (compojure.core/routes
-                                          (GET (str "/" path) [] (middleware/streaming-json-response
-                                                                  handler)))]
-      (let  [connection (async/chan 1000)
-             reader (io/input-stream (str "http://localhost:" (config/config-int :mb-jetty-port) "/" path))]
-        (async/go-loop [next-char (.read reader)]
-          (if (pos? next-char)
-            (do
-              (async/>! connection (char next-char))
-              (recur (.read reader)))
-            (async/close! connection)))
-        (handle-response-fn connection)))))
-
-;;slow success
-(expect
-  [\newline \newline "{\"success\":true}"]
-  (let [send-response (promise)]
-    (test-streaming-endpoint (streaming-slow-success send-response)
-                             (fn [response-chan]
-                               [(take-with-timeout response-chan)
-                                (take-with-timeout response-chan)
-                                (do
-                                  (deliver send-response true)
-                                  (string/trim (apply str (async/<!! (async/into [] response-chan)))))]))))
-
-;; immediate success should have no padding
-(expect
-  "{\"success\":true}"
-  (test-streaming-endpoint streaming-fast-success
-                           (fn [response-chan]
-                             (string/trim (apply str (async/<!! (async/into [] response-chan)))))))
-
-;; we know delayed failures (exception thrown) will just drop the connection
-(expect
-  [\newline \newline ""]
-  (let [send-response (promise)]
-    (test-streaming-endpoint (streaming-slow-failure send-response)
-                             (fn [response-chan]
-                               [(take-with-timeout response-chan)
-                                (take-with-timeout response-chan)
-                                (do
-                                  (deliver send-response true)
-                                  (string/trim (apply str (async/<!! (async/into [] response-chan)))))]))))
-
-;; immediate failures (where an exception is thown will return a 500
-(expect
-  #"Server returned HTTP response code: 500 for URL:.*"
-  (try
-    (test-streaming-endpoint streaming-fast-failure
-                             (fn [response-chan]
-                               ;; Should never reach here
-                               (throw (Exception. "Should not process a message"))))
-    (catch java.io.IOException e
-      (.getMessage e))))
diff --git a/test/metabase/models/card_test.clj b/test/metabase/models/card_test.clj
index 37769bdceaf889bbc075a02fefcab77748e5e1bd..26fd2374dfb40afcba5e36f1ad1584234bd7e15f 100644
--- a/test/metabase/models/card_test.clj
+++ b/test/metabase/models/card_test.clj
@@ -38,26 +38,29 @@
 (expect
   {:Segment #{2 3}
    :Metric  nil}
-  (card-dependencies Card 12 {:dataset_query {:type :query
-                                              :query {:aggregation ["rows"]
-                                                      :filter      ["AND" [">" 4 "2014-10-19"] ["=" 5 "yes"] ["SEGMENT" 2] ["SEGMENT" 3]]}}}))
+  (card-dependencies
+   {:dataset_query {:type :query
+                    :query {:aggregation ["rows"]
+                            :filter      ["AND" [">" 4 "2014-10-19"] ["=" 5 "yes"] ["SEGMENT" 2] ["SEGMENT" 3]]}}}))
 
 (expect
   {:Segment #{1}
    :Metric #{7}}
-  (card-dependencies Card 12 {:dataset_query {:type :query
-                                              :query {:aggregation ["METRIC" 7]
-                                                      :filter      ["AND" [">" 4 "2014-10-19"] ["=" 5 "yes"] ["OR" ["SEGMENT" 1] ["!=" 5 "5"]]]}}}))
+  (card-dependencies
+   {:dataset_query {:type :query
+                    :query {:aggregation ["METRIC" 7]
+                            :filter      ["AND" [">" 4 "2014-10-19"] ["=" 5 "yes"] ["OR" ["SEGMENT" 1] ["!=" 5 "5"]]]}}}))
 
 (expect
   {:Segment nil
    :Metric  nil}
-  (card-dependencies Card 12 {:dataset_query {:type :query
-                                              :query {:aggregation nil
-                                                      :filter      nil}}}))
+  (card-dependencies
+   {:dataset_query {:type :query
+                    :query {:aggregation nil
+                            :filter      nil}}}))
 
 
-;;; ------------------------------------------------------------ Permissions Checking ------------------------------------------------------------
+;;; ---------------------------------------------- Permissions Checking ----------------------------------------------
 
 (expect
   false
@@ -150,13 +153,24 @@
                                             :native   {:query "SELECT * FROM CHECKINS"}}}]
     (query-perms-set (query-with-source-card card) :read)))
 
+;; You should still only need native READ permissions if you want to save a Card based on another Card you can already
+;; READ.
 (expect
-  #{(perms/native-readwrite-path (data/id))}
+  #{(perms/native-read-path (data/id))}
   (tt/with-temp Card [card {:dataset_query {:database (data/id)
                                             :type     :native
                                             :native   {:query "SELECT * FROM CHECKINS"}}}]
     (query-perms-set (query-with-source-card card) :write)))
 
+;; However if you just pass in the same query directly as a `:source-query` you will still require READWRITE
+;; permissions to save the query since we can't verify that it belongs to a Card that you can view.
+(expect
+  #{(perms/native-readwrite-path (data/id))}
+  (query-perms-set {:database (data/id)
+                    :type     :query
+                    :query    {:source-query {:native "SELECT * FROM CHECKINS"}}}
+                   :write))
+
 ;; invalid/legacy card should return perms for something that doesn't exist so no one gets to see it
 (expect
   #{"/db/0/"}
diff --git a/test/metabase/pulse/render_test.clj b/test/metabase/pulse/render_test.clj
index 43e01a9cf5ac0c26855ec811bf6f0b051c8ed3dc..2f5e95915630407467e54ee74ef4a7f0e2df6e82 100644
--- a/test/metabase/pulse/render_test.clj
+++ b/test/metabase/pulse/render_test.clj
@@ -1,5 +1,6 @@
 (ns metabase.pulse.render-test
-  (:require [expectations :refer :all]
+  (:require [clojure.walk :as walk]
+            [expectations :refer :all]
             [hiccup.core :refer [html]]
             [metabase.pulse.render :as render :refer :all])
   (:import java.util.TimeZone))
@@ -7,58 +8,120 @@
 (def ^:private pacific-tz (TimeZone/getTimeZone "America/Los_Angeles"))
 
 (def ^:private test-columns
-  [{:name         "ID",
-    :display_name "ID",
-    :base_type    :type/BigInteger
-    :special_type nil}
-   {:name         "latitude"
-    :display_name "Latitude"
-    :base-type    :type/Float
-    :special-type :type/Latitude}
-   {:name         "last_login"
-    :display_name "Last Login"
-    :base_type    :type/DateTime
-    :special_type nil}
-   {:name         "name"
-    :display_name "Name"
-    :base-type    :type/Text
-    :special_type nil}])
+  [{:name            "ID",
+    :display_name    "ID",
+    :base_type       :type/BigInteger
+    :special_type    nil
+    :visibility_type :normal}
+   {:name            "latitude"
+    :display_name    "Latitude"
+    :base_type       :type/Float
+    :special_type    :type/Latitude
+    :visibility_type :normal}
+   {:name            "last_login"
+    :display_name    "Last Login"
+    :base_type       :type/DateTime
+    :special_type    nil
+    :visibility_type :normal}
+   {:name            "name"
+    :display_name    "Name"
+    :base_type       :type/Text
+    :special_type    nil
+    :visibility_type :normal}])
 
 (def ^:private test-data
   [[1 34.0996 "2014-04-01T08:30:00.0000" "Stout Burgers & Beers"]
    [2 34.0406 "2014-12-05T15:15:00.0000" "The Apple Pan"]
    [3 34.0474 "2014-08-01T12:45:00.0000" "The Gorbals"]])
 
+(defn- col-counts [results]
+  (set (map (comp count :row) results)))
+
+(defn- number [x]
+  (#'render/map->NumericWrapper {:num-str x}))
+
+(def ^:private default-header-result
+  [{:row       [(number "ID") (number "Latitude") "Last Login" "Name"]
+    :bar-width nil}
+   #{4}])
+
+(defn- prep-for-html-rendering'
+  [cols rows bar-column max-value]
+  (let [results (#'render/prep-for-html-rendering pacific-tz cols rows bar-column max-value (count cols))]
+    [(first results)
+     (col-counts results)]))
+
+
+
 ;; Testing the format of headers
 (expect
-  {:row ["ID" "LATITUDE" "LAST LOGIN" "NAME"]
-   :bar-width nil}
-  (first (#'render/prep-for-html-rendering pacific-tz test-columns test-data nil nil (count test-columns))))
+  default-header-result
+  (prep-for-html-rendering' test-columns test-data nil nil))
+
+(expect
+  default-header-result
+  (let [cols-with-desc (conj test-columns {:name         "desc_col"
+                                                   :display_name "Description Column"
+                                                   :base_type    :type/Text
+                                                   :special_type :type/Description
+                                                   :visibility_type :normal})
+        data-with-desc (mapv #(conj % "Desc") test-data)]
+    (prep-for-html-rendering' cols-with-desc data-with-desc nil nil)))
+
+(expect
+  default-header-result
+  (let [cols-with-details (conj test-columns {:name            "detail_col"
+                                              :display_name    "Details Column"
+                                              :base_type       :type/Text
+                                              :special_type    nil
+                                              :visibility_type :details-only})
+        data-with-details (mapv #(conj % "Details") test-data)]
+    (prep-for-html-rendering' cols-with-details data-with-details nil nil)))
+
+(expect
+  default-header-result
+  (let [cols-with-sensitive (conj test-columns {:name            "sensitive_col"
+                                                :display_name    "Sensitive Column"
+                                                :base_type       :type/Text
+                                                :special_type    nil
+                                                :visibility_type :sensitive})
+        data-with-sensitive (mapv #(conj % "Sensitive") test-data)]
+    (prep-for-html-rendering' cols-with-sensitive data-with-sensitive nil nil)))
+
+(expect
+  default-header-result
+  (let [columns-with-retired (conj test-columns {:name            "retired_col"
+                                                 :display_name    "Retired Column"
+                                                 :base_type       :type/Text
+                                                 :special_type    nil
+                                                 :visibility_type :retired})
+        data-with-retired    (mapv #(conj % "Retired") test-data)]
+    (prep-for-html-rendering' columns-with-retired data-with-retired nil nil)))
 
 ;; When including a bar column, bar-width is 99%
 (expect
-  {:row ["ID" "LATITUDE" "LAST LOGIN" "NAME"]
-   :bar-width 99}
-  (first (#'render/prep-for-html-rendering pacific-tz test-columns test-data second 40.0 (count test-columns))))
+  (assoc-in default-header-result [0 :bar-width] 99)
+  (prep-for-html-rendering' test-columns test-data second 40.0))
 
 ;; When there are too many columns, #'render/prep-for-html-rendering show narrow it
 (expect
-  {:row ["ID" "LATITUDE"]
-   :bar-width 99}
-  (first (#'render/prep-for-html-rendering pacific-tz test-columns test-data second 40.0 2)))
+  [{:row [(number "ID") (number "Latitude")]
+    :bar-width 99}
+   #{2}]
+  (prep-for-html-rendering' (subvec test-columns 0 2) test-data second 40.0 ))
 
 ;; Basic test that result rows are formatted correctly (dates, floating point numbers etc)
 (expect
-  [{:bar-width nil, :row ["1" "34.10" "Apr 1, 2014" "Stout Burgers & Beers"]}
-   {:bar-width nil, :row ["2" "34.04" "Dec 5, 2014" "The Apple Pan"]}
-   {:bar-width nil, :row ["3" "34.05" "Aug 1, 2014" "The Gorbals"]}]
+  [{:bar-width nil, :row [(number "1") (number "34.10") "Apr 1, 2014" "Stout Burgers & Beers"]}
+   {:bar-width nil, :row [(number "2") (number "34.04") "Dec 5, 2014" "The Apple Pan"]}
+   {:bar-width nil, :row [(number "3") (number "34.05") "Aug 1, 2014" "The Gorbals"]}]
   (rest (#'render/prep-for-html-rendering pacific-tz test-columns test-data nil nil (count test-columns))))
 
 ;; Testing the bar-column, which is the % of this row relative to the max of that column
 (expect
-  [{:bar-width (float 85.249),  :row ["1" "34.10" "Apr 1, 2014" "Stout Burgers & Beers"]}
-   {:bar-width (float 85.1015), :row ["2" "34.04" "Dec 5, 2014" "The Apple Pan"]}
-   {:bar-width (float 85.1185), :row ["3" "34.05" "Aug 1, 2014" "The Gorbals"]}]
+  [{:bar-width (float 85.249),  :row [(number "1") (number "34.10") "Apr 1, 2014" "Stout Burgers & Beers"]}
+   {:bar-width (float 85.1015), :row [(number "2") (number "34.04") "Dec 5, 2014" "The Apple Pan"]}
+   {:bar-width (float 85.1185), :row [(number "3") (number "34.05") "Aug 1, 2014" "The Gorbals"]}]
   (rest (#'render/prep-for-html-rendering pacific-tz test-columns test-data second 40 (count test-columns))))
 
 (defn- add-rating
@@ -91,15 +154,16 @@
 
 ;; With a remapped column, the header should contain the name of the remapped column (not the original)
 (expect
-  {:row ["ID" "LATITUDE" "RATING DESC" "LAST LOGIN" "NAME"]
-   :bar-width nil}
-  (first (#'render/prep-for-html-rendering pacific-tz test-columns-with-remapping test-data-with-remapping nil nil (count test-columns-with-remapping))))
+  [{:row [(number "ID") (number "Latitude") "Rating Desc" "Last Login" "Name"]
+    :bar-width nil}
+   #{5}]
+  (prep-for-html-rendering' test-columns-with-remapping test-data-with-remapping nil nil))
 
 ;; Result rows should include only the remapped column value, not the original
 (expect
-  [["1" "34.10" "Bad" "Apr 1, 2014" "Stout Burgers & Beers"]
-   ["2" "34.04" "Ok" "Dec 5, 2014" "The Apple Pan"]
-   ["3" "34.05" "Good" "Aug 1, 2014" "The Gorbals"]]
+  [[(number "1") (number "34.10") "Bad" "Apr 1, 2014" "Stout Burgers & Beers"]
+   [(number "2") (number "34.04") "Ok" "Dec 5, 2014" "The Apple Pan"]
+   [(number "3") (number "34.05") "Good" "Aug 1, 2014" "The Gorbals"]]
   (map :row (rest (#'render/prep-for-html-rendering pacific-tz test-columns-with-remapping test-data-with-remapping nil nil (count test-columns-with-remapping)))))
 
 ;; There should be no truncation warning if the number of rows/cols is fewer than the row/column limit
@@ -126,9 +190,9 @@
                                 :special_type :type/DateTime}))
 
 (expect
-  [{:bar-width nil, :row ["1" "34.10" "Apr 1, 2014" "Stout Burgers & Beers"]}
-   {:bar-width nil, :row ["2" "34.04" "Dec 5, 2014" "The Apple Pan"]}
-   {:bar-width nil, :row ["3" "34.05" "Aug 1, 2014" "The Gorbals"]}]
+  [{:bar-width nil, :row [(number "1") (number "34.10") "Apr 1, 2014" "Stout Burgers & Beers"]}
+   {:bar-width nil, :row [(number "2") (number "34.04") "Dec 5, 2014" "The Apple Pan"]}
+   {:bar-width nil, :row [(number "3") (number "34.05") "Aug 1, 2014" "The Gorbals"]}]
   (rest (#'render/prep-for-html-rendering pacific-tz test-columns-with-date-special-type test-data nil nil (count test-columns))))
 
 (defn- render-scalar-value [results]
@@ -166,3 +230,47 @@
                                 :base_type    :type/DateTime
                                 :special_type nil}]
                         :rows [["2014-04-01T08:30:00.0000"]]}))
+
+(defn- replace-style-maps [hiccup-map]
+  (walk/postwalk (fn [maybe-map]
+                   (if (and (map? maybe-map)
+                            (contains? maybe-map :style))
+                     :style-map
+                     maybe-map)) hiccup-map))
+
+(def ^:private render-truncation-warning'
+  (comp replace-style-maps #'render/render-truncation-warning))
+
+(expect
+  nil
+  (render-truncation-warning' 10 5 20 10))
+
+(expect
+  [:div :style-map
+   [:div :style-map
+    "Showing " [:strong :style-map "10"] " of "
+    [:strong :style-map "11"] " columns."]]
+  (render-truncation-warning' 10 11 20 10))
+
+(expect
+  [:div
+   :style-map
+   [:div :style-map "Showing "
+    [:strong :style-map "20"] " of " [:strong :style-map "21"] " rows."]]
+  (render-truncation-warning' 10 5 20 21))
+
+(expect
+  [:div
+   :style-map
+   [:div
+    :style-map
+    "Showing "
+    [:strong :style-map "20"]
+    " of "
+    [:strong :style-map "21"]
+    " rows and "
+    [:strong :style-map "10"]
+    " of "
+    [:strong :style-map "11"]
+    " columns."]]
+  (render-truncation-warning' 10 11 20 21))
diff --git a/test/metabase/pulse_test.clj b/test/metabase/pulse_test.clj
index 7c4b06ecfaeddabe54957b1af6a5cd3758befd7e..2bce6e85ef8a39936523b961fbfe7061d3dbf9a3 100644
--- a/test/metabase/pulse_test.clj
+++ b/test/metabase/pulse_test.clj
@@ -1,17 +1,20 @@
 (ns metabase.pulse-test
-  (:require [clojure.walk :as walk]
+  (:require [clojure.string :as str]
+            [clojure.walk :as walk]
             [expectations :refer :all]
             [medley.core :as m]
             [metabase.integrations.slack :as slack]
             [metabase
              [email-test :as et]
-             [pulse :refer :all]]
+             [pulse :refer :all]
+             [query-processor :as qp]]
             [metabase.models
              [card :refer [Card]]
              [pulse :refer [Pulse retrieve-pulse retrieve-pulse-or-alert]]
              [pulse-card :refer [PulseCard]]
              [pulse-channel :refer [PulseChannel]]
              [pulse-channel-recipient :refer [PulseChannelRecipient]]]
+            [metabase.pulse.render :as render]
             [metabase.test
              [data :as data]
              [util :as tu]]
@@ -73,6 +76,15 @@
                                       png-attachment]}
                              email)))
 
+(def ^:private csv-attachment
+  {:type :attachment, :content-type "text/csv", :file-name "Test card.csv",
+   :content java.net.URL, :description "More results for 'Test card'", :content-id false})
+
+(def ^:private xls-attachment
+  {:type :attachment, :file-name "Test card.xlsx",
+   :content-type "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
+   :content java.net.URL, :description "More results for 'Test card'", :content-id false})
+
 ;; Basic test, 1 card, 1 recipient
 (expect
   (rasta-pulse-email)
@@ -89,6 +101,72 @@
      (send-pulse! (retrieve-pulse pulse-id))
      (et/summarize-multipart-email #"Pulse Name"))))
 
+;; Basic test, 1 card, 1 recipient, 21 results results in a CSV being attached and a table being sent
+(expect
+  (rasta-pulse-email {:body [{"Pulse Name"                      true
+                              "More results have been included" true
+                              "ID</th>"                         true},
+                             csv-attachment]})
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:aggregation nil
+                                                                        :limit       21})]
+                  Pulse                [{pulse-id :id} {:name          "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"  #"More results have been included" #"ID</th>"))))
+
+;; Validate pulse queries are limited by qp/default-query-constraints
+(expect
+  31 ;; Should return 30 results (the redef'd limit) plus the header row
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:aggregation nil})]
+                  Pulse                [{pulse-id :id} {:name          "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (with-redefs [qp/default-query-constraints {:max-results           10000
+                                                 :max-results-bare-rows 30}]
+       (send-pulse! (retrieve-pulse pulse-id))
+       ;; Slurp in the generated CSV and count the lines found in the file
+       (-> @et/inbox
+           vals
+           ffirst
+           :body
+           last
+           :content
+           slurp
+           str/split-lines
+           count)))))
+
+;; Basic test, 1 card, 1 recipient, 19 results, so no attachment
+(expect
+  (rasta-pulse-email {:body [{"Pulse Name"                      true
+                              "More results have been included" false
+                              "ID</th>"                         true}]})
+  (tt/with-temp* [Card                 [{card-id :id}  (checkins-query {:aggregation nil
+                                                                        :limit       19})]
+                  Pulse                [{pulse-id :id} {:name          "Pulse Name"
+                                                        :skip_if_empty false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id          (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse pulse-id))
+     (et/summarize-multipart-email #"Pulse Name"  #"More results have been included" #"ID</th>"))))
+
 ;; Pulse should be sent to two recipients
 (expect
   (into {} (map (fn [user-kwd]
@@ -193,7 +271,8 @@
 ;; Rows alert with data
 (expect
   (rasta-alert-email "Metabase alert: Test card has results"
-                     [{"Test card.*has results for you to see" true}, png-attachment])
+                     [{"Test card.*has results for you to see" true
+                       "More results have been included"       false}, png-attachment])
   (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
                   Pulse                 [{pulse-id :id} {:alert_condition  "rows"
                                                          :alert_first_only false}]
@@ -205,7 +284,28 @@
                                                         :pulse_channel_id pc-id}]]
     (email-test-setup
      (send-pulse! (retrieve-pulse-or-alert pulse-id))
-     (et/summarize-multipart-email #"Test card.*has results for you to see"))))
+     (et/summarize-multipart-email #"Test card.*has results for you to see" #"More results have been included"))))
+
+;; Rows alert with too much data will attach as CSV and include a table
+(expect
+  (rasta-alert-email "Metabase alert: Test card has results"
+                     [{"Test card.*has results for you to see" true
+                       "More results have been included"       true
+                       "ID</th>"                               true},
+                      csv-attachment])
+  (tt/with-temp* [Card                  [{card-id :id}  (checkins-query {:limit 21
+                                                                         :aggregation nil})]
+                  Pulse                 [{pulse-id :id} {:alert_condition  "rows"
+                                                         :alert_first_only false}]
+                  PulseCard             [_             {:pulse_id pulse-id
+                                                        :card_id  card-id
+                                                        :position 0}]
+                  PulseChannel          [{pc-id :id}   {:pulse_id pulse-id}]
+                  PulseChannelRecipient [_             {:user_id (rasta-id)
+                                                        :pulse_channel_id pc-id}]]
+    (email-test-setup
+     (send-pulse! (retrieve-pulse-or-alert pulse-id))
+     (et/summarize-multipart-email #"Test card.*has results for you to see" #"More results have been included" #"ID</th>"))))
 
 ;; Above goal alert with data
 (expect
@@ -322,6 +422,42 @@
   (assoc result :attachments (for [attachment-info attachments]
                                (update attachment-info :attachment-bytes-thunk fn?))))
 
+(defprotocol WrappedFunction
+  (input [_])
+  (output [_]))
+
+(defn- invoke-with-wrapping
+  "Apply `args` to `func`, capturing the arguments of the invocation and the result of the invocation. Store the arguments in
+  `input-atom` and the result in `output-atom`."
+  [input-atom output-atom func args]
+  (swap! input-atom conj args)
+  (let [result (apply func args)]
+    (swap! output-atom conj result)
+    result))
+
+(defn- wrap-function
+  "Return a function that wraps `func`, not interfering with it but recording it's input and output, which is
+  available via the `input` function and `output`function that can be used directly on this object"
+  [func]
+  (let [input (atom nil)
+        output (atom nil)]
+    (reify WrappedFunction
+      (input [_] @input)
+      (output [_] @output)
+      clojure.lang.IFn
+      (invoke [_ x1]
+        (invoke-with-wrapping input output func [x1]))
+      (invoke [_ x1 x2]
+        (invoke-with-wrapping input output func [x1 x2]))
+      (invoke [_ x1 x2 x3]
+        (invoke-with-wrapping input output func [x1 x2 x3]))
+      (invoke [_ x1 x2 x3 x4]
+        (invoke-with-wrapping input output func [x1 x2 x3 x4]))
+      (invoke [_ x1 x2 x3 x4 x5]
+        (invoke-with-wrapping input output func [x1 x2 x3 x4 x5]))
+      (invoke [_ x1 x2 x3 x4 x5 x6]
+        (invoke-with-wrapping input output func [x1 x2 x3 x4 x5 x6])))))
+
 ;; Basic slack test, 1 card, 1 recipient channel
 (tt/expect-with-temp [Card         [{card-id :id}  (checkins-query {:breakout [["datetime-field" (data/id :checkins :date) "hour"]]})]
                       Pulse        [{pulse-id :id} {:name "Pulse Name"
@@ -346,6 +482,47 @@
        first
        thunk->boolean)))
 
+(defn- force-bytes-thunk
+  "Grabs the thunk that produces the image byte array and invokes it"
+  [results]
+  ((-> results
+       :attachments
+       first
+       :attachment-bytes-thunk)))
+
+;; Basic slack test, 1 card, 1 recipient channel, verifies that "more results in attachment" text is not present for
+;; slack pulses
+(tt/expect-with-temp [Card         [{card-id :id}  (checkins-query {:aggregation nil
+                                                                    :limit       25})]
+                      Pulse        [{pulse-id :id} {:name          "Pulse Name"
+                                                    :skip_if_empty false}]
+                      PulseCard    [_              {:pulse_id pulse-id
+                                                    :card_id  card-id
+                                                    :position 0}]
+                      PulseChannel [{pc-id :id}    {:pulse_id     pulse-id
+                                                    :channel_type "slack"
+                                                    :details      {:channel "#general"}}]]
+  [{:channel-id "#general",
+     :message    "Pulse: Pulse Name",
+     :attachments
+     [{:title                  "Test card",
+       :attachment-bytes-thunk true
+       :title_link             (str "https://metabase.com/testmb/question/" card-id),
+       :attachment-name        "image.png",
+       :channel-id             "FOO",
+       :fallback               "Test card"}]}
+   1     ;; -> attached-results-text should be invoked exactly once
+   [nil] ;; -> attached-results-text should return nil since it's a slack message
+   ]
+  (slack-test-setup
+   (with-redefs [render/attached-results-text (wrap-function (var-get #'render/attached-results-text))]
+     (let [[pulse-results] (send-pulse! (retrieve-pulse pulse-id))]
+       ;; If we don't force the thunk, the rendering code will never execute and attached-results-text won't be called
+       (force-bytes-thunk pulse-results)
+       [(thunk->boolean pulse-results)
+        (count (input (var-get #'render/attached-results-text)))
+        (output (var-get #'render/attached-results-text))]))))
+
 (defn- produces-bytes? [{:keys [attachment-bytes-thunk]}]
   (< 0 (alength (attachment-bytes-thunk))))
 
@@ -542,15 +719,6 @@
      [@et/inbox
       (db/exists? Pulse :id pulse-id)])))
 
-(def ^:private csv-attachment
-  {:type :attachment, :content-type "text/csv", :file-name "Test card.csv",
-   :content java.net.URL, :description "Full results for 'Test card'", :content-id false})
-
-(def ^:private xls-attachment
-  {:type :attachment, :file-name "Test card.xlsx",
-   :content-type "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
-   :content java.net.URL, :description "Full results for 'Test card'", :content-id false})
-
 (defn- add-rasta-attachment
   "Append `ATTACHMENT` to the first email found for Rasta"
   [email attachment]
diff --git a/test/metabase/query_processor/middleware/expand_macros_test.clj b/test/metabase/query_processor/middleware/expand_macros_test.clj
index 9772093d54e4ba0e3a2eb9adb71067092c506f0a..f3496b5e3d18510e017aa067aae7ef47bb0f9bc5 100644
--- a/test/metabase/query_processor/middleware/expand_macros_test.clj
+++ b/test/metabase/query_processor/middleware/expand_macros_test.clj
@@ -165,7 +165,7 @@
                                                              :order_by    [[1 "ASC"]]}})))
 
 ;; Check that a metric w/ multiple aggregation syntax (nested vector) still works correctly
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[2 118]
    [3  39]
    [4  24]]
diff --git a/test/metabase/query_processor/middleware/fetch_source_query_test.clj b/test/metabase/query_processor/middleware/fetch_source_query_test.clj
index c68fdbfa649d1b8a9c77b6f722df792af93a4f9a..9d254e0d34b524583965be937091b53ec73471ab 100644
--- a/test/metabase/query_processor/middleware/fetch_source_query_test.clj
+++ b/test/metabase/query_processor/middleware/fetch_source_query_test.clj
@@ -1,5 +1,6 @@
 (ns metabase.query-processor.middleware.fetch-source-query-test
-  (:require [expectations :refer [expect]]
+  (:require [clj-time.coerce :as tcoerce]
+            [expectations :refer [expect]]
             [medley.core :as m]
             [metabase
              [query-processor :as qp]
@@ -46,23 +47,47 @@
                                     :aggregation  [:count]
                                     :breakout     [[:field-literal :price :type/Integer]]}})))
 
+(defn- expand-and-scrub [query-map]
+  (-> query-map
+      qp/expand
+      (m/dissoc-in [:database :features])
+      (m/dissoc-in [:database :details])
+      (m/dissoc-in [:database :timezone])))
 
-;; test that the `metabase.query-processor/expand` function properly handles nested queries (this function should call `fetch-source-query`)
-(expect
+(defn default-expanded-results [query]
   {:database     {:name "test-data", :id (data/id), :engine :h2}
    :type         :query
-   :query        {:source-query {:source-table {:schema "PUBLIC", :name "VENUES", :id (data/id :venues)}
-                                 :join-tables  nil}}
-   :fk-field-ids #{}}
+   :fk-field-ids #{}
+   :query        query})
+
+;; test that the `metabase.query-processor/expand` function properly handles nested queries (this function should call `fetch-source-query`)
+(expect
+  (default-expanded-results
+   {:source-query {:source-table {:schema "PUBLIC", :name "VENUES", :id (data/id :venues)}
+                   :join-tables  nil}})
   (tt/with-temp Card [card {:dataset_query {:database (data/id)
                                             :type     :query
                                             :query    {:source-table (data/id :venues)}}}]
-    (-> (qp/expand {:database database/virtual-id
-                    :type     :query
-                    :query    {:source-table (str "card__" (u/get-id card))}})
-        (m/dissoc-in [:database :features])
-        (m/dissoc-in [:database :details])
-        (m/dissoc-in [:database :timezone]))))
+    (expand-and-scrub {:database database/virtual-id
+                       :type     :query
+                       :query    {:source-table (str "card__" (u/get-id card))}})))
+
+(expect
+  (default-expanded-results
+   {:source-query {:source-table {:schema "PUBLIC" :name "CHECKINS" :id (data/id :checkins)}, :join-tables nil}
+    :filter {:filter-type :between,
+             :field {:field-name "date", :base-type :type/Date},
+             :min-val {:value (tcoerce/to-timestamp (u/str->date-time "2015-01-01"))
+                       :field {:field {:field-name "date", :base-type :type/Date}, :unit :default}},
+             :max-val {:value (tcoerce/to-timestamp (u/str->date-time "2015-02-01"))
+                       :field {:field {:field-name "date", :base-type :type/Date}, :unit :default}}}})
+  (tt/with-temp Card [card {:dataset_query {:database (data/id)
+                                            :type     :query
+                                            :query    {:source-table (data/id :checkins)}}}]
+    (expand-and-scrub {:database database/virtual-id
+                       :type     :query
+                       :query    {:source-table (str "card__" (u/get-id card))
+                                  :filter ["BETWEEN" ["field-id" ["field-literal" "date" "type/Date"]] "2015-01-01" "2015-02-01"]}})))
 
 ;; make sure that nested nested queries work as expected
 (expect
@@ -83,23 +108,18 @@
                                                        :query    {:source-table (str "card__" (u/get-id card-2)), :limit 25}})))
 
 (expect
-  {:database     {:name "test-data", :id (data/id), :engine :h2}
-   :type         :query
-   :query        {:limit        25
-                  :source-query {:limit 50
-                                 :source-query {:source-table {:schema "PUBLIC", :name "VENUES", :id (data/id :venues)}
-                                                :limit        100
-                                                :join-tables  nil}}}
-   :fk-field-ids #{}}
+  (default-expanded-results
+   {:limit        25
+    :source-query {:limit 50
+                   :source-query {:source-table {:schema "PUBLIC", :name "VENUES", :id (data/id :venues)}
+                                  :limit        100
+                                  :join-tables  nil}}})
   (tt/with-temp* [Card [card-1 {:dataset_query {:database (data/id)
                                                 :type     :query
                                                 :query    {:source-table (data/id :venues), :limit 100}}}]
                   Card [card-2 {:dataset_query {:database database/virtual-id
                                                 :type     :query
                                                 :query    {:source-table (str "card__" (u/get-id card-1)), :limit 50}}}]]
-    (-> (qp/expand {:database database/virtual-id
-                    :type     :query
-                    :query    {:source-table (str "card__" (u/get-id card-2)), :limit 25}})
-        (m/dissoc-in [:database :features])
-        (m/dissoc-in [:database :details])
-        (m/dissoc-in [:database :timezone]))))
+    (expand-and-scrub {:database database/virtual-id
+                       :type     :query
+                       :query    {:source-table (str "card__" (u/get-id card-2)), :limit 25}})))
diff --git a/test/metabase/query_processor/middleware/parameters/sql_test.clj b/test/metabase/query_processor/middleware/parameters/sql_test.clj
index 19caca4dde770d2fb273adbbc45a497e45892a10..91efbb7834ffcf9560770b0a91a50b3478f58dc5 100644
--- a/test/metabase/query_processor/middleware/parameters/sql_test.clj
+++ b/test/metabase/query_processor/middleware/parameters/sql_test.clj
@@ -5,7 +5,7 @@
             [metabase
              [driver :as driver]
              [query-processor :as qp]
-             [query-processor-test :refer [engines-that-support first-row format-rows-by]]]
+             [query-processor-test :refer [non-timeseries-engines-with-feature first-row format-rows-by]]]
             [metabase.query-processor.middleware.parameters.sql :as sql :refer :all]
             [metabase.test.data :as data]
             [metabase.test.data
@@ -13,6 +13,44 @@
              [generic-sql :as generic-sql]]
             [toucan.db :as db]))
 
+;;; ------------------------------------------ basic parser tests ------------------------------------------
+
+(expect
+  [:SQL "select * from foo where bar=1"]
+  (#'sql/sql-template-parser "select * from foo where bar=1"))
+
+(expect
+  [:SQL "select * from foo where bar=" [:PARAM "baz"]]
+  (#'sql/sql-template-parser "select * from foo where bar={{baz}}"))
+
+(expect
+  [:SQL "select * from foo " [:OPTIONAL "where bar = " [:PARAM "baz"] " "]]
+  (#'sql/sql-template-parser "select * from foo [[where bar = {{baz}} ]]"))
+
+(expect
+  [:SQL "select * from foobars "
+   [:OPTIONAL " where foobars.id in (string_to_array(" [:PARAM "foobar_id"] ", ',')::integer" "[" "]" ") "]]
+  (#'sql/sql-template-parser "select * from foobars [[ where foobars.id in (string_to_array({{foobar_id}}, ',')::integer[]) ]]"))
+
+(expect
+  [:SQL
+   "SELECT " "[" "test_data.checkins.venue_id" "]" " AS " "[" "venue_id" "]"
+   ",        " "[" "test_data.checkins.user_id" "]" " AS " "[" "user_id" "]"
+   ",        " "[" "test_data.checkins.id" "]" " AS " "[" "checkins_id" "]"
+   " FROM " "[" "test_data.checkins" "]" " LIMIT 2"]
+  (-> (str "SELECT [test_data.checkins.venue_id] AS [venue_id], "
+             "       [test_data.checkins.user_id] AS [user_id], "
+             "       [test_data.checkins.id] AS [checkins_id] "
+             "FROM [test_data.checkins] "
+             "LIMIT 2")
+      (#'sql/sql-template-parser)
+      (update 1 #(apply str %))))
+
+;; Valid syntax in PG
+(expect
+  [:SQL "SELECT array_dims(1 || '" "[" "0:1" "]" "=" "{" "2,3" "}" "'::int" "[" "]" ")"]
+  (#'sql/sql-template-parser "SELECT array_dims(1 || '[0:1]={2,3}'::int[])"))
+
 ;;; ------------------------------------------ simple substitution -- {{x}} ------------------------------------------
 
 (defn- substitute {:style/indent 1} [sql params]
@@ -43,7 +81,6 @@
   (substitute "SELECT * FROM bird_facts WHERE toucans_are_cool = {{toucans_are_cool}} AND bird_type = {{bird_type}}"
     {:toucans_are_cool true}))
 
-
 ;;; ---------------------------------- optional substitution -- [[ ... {{x}} ... ]] ----------------------------------
 
 (expect
@@ -82,6 +119,13 @@
   (substitute "SELECT * FROM bird_facts [[WHERE toucans_are_cool = {{toucans_are_cool}} AND bird_type = 'toucan']]"
     {:toucans_are_cool true}))
 
+;; Two parameters in an optional
+(expect
+  {:query  "SELECT * FROM bird_facts WHERE toucans_are_cool = TRUE AND bird_type = ?"
+   :params ["toucan"]}
+  (substitute "SELECT * FROM bird_facts [[WHERE toucans_are_cool = {{toucans_are_cool}} AND bird_type = {{bird_type}}]]"
+    {:toucans_are_cool true, :bird_type "toucan"}))
+
 (expect
   {:query  "SELECT * FROM bird_facts"
    :params []}
@@ -437,7 +481,7 @@
 
 ;; as with the MBQL parameters tests Redshift and Crate fail for unknown reasons; disable their tests for now
 (def ^:private ^:const sql-parameters-engines
-  (disj (engines-that-support :native-parameters) :redshift :crate))
+  (disj (non-timeseries-engines-with-feature :native-parameters) :redshift :crate))
 
 (defn- process-native {:style/indent 0} [& kvs]
   (qp/process-query
diff --git a/test/metabase/query_processor/util_test.clj b/test/metabase/query_processor/util_test.clj
index eea0cdd83e2330b1a3097b7e79db11fd24f8ab31..1bb0c29a9fc28564ba54cdb65c574fd007af6a84 100644
--- a/test/metabase/query_processor/util_test.clj
+++ b/test/metabase/query_processor/util_test.clj
@@ -116,6 +116,18 @@
 (expect 2 (qputil/get-normalized {:NUM_TOUCANS 2}  :num-toucans))
 (expect 2 (qputil/get-normalized {:num-toucans 2}  :num-toucans))
 
+(expect
+  false
+  (qputil/get-normalized {:case-sensitive false} :case-sensitive))
+
+(expect
+  false
+  (qputil/get-normalized {:case-sensitive false} :case-sensitive true))
+
+(expect
+  true
+  (qputil/get-normalized {:explodes-database false} :case-sensitive true))
+
 (expect
   nil
   (qputil/get-normalized nil :num-toucans))
diff --git a/test/metabase/query_processor_test.clj b/test/metabase/query_processor_test.clj
index 47c7914faf6507e78425baa53086ca8481b33b1f..cf5ed299b62bea9c546e2c62d3978696ee3e6039 100644
--- a/test/metabase/query_processor_test.clj
+++ b/test/metabase/query_processor_test.clj
@@ -30,15 +30,17 @@
   "Set of engines for non-timeseries DBs (i.e., every driver except `:druid`)."
   (set/difference datasets/all-valid-engines timeseries-engines))
 
-(defn engines-that-support
+(defn non-timeseries-engines-with-feature
   "Set of engines that support a given FEATURE."
   [feature]
   (set (for [engine non-timeseries-engines
              :when  (contains? (driver/features (driver/engine->driver engine)) feature)]
          engine)))
 
-(defn engines-that-dont-support [feature]
-  (set/difference non-timeseries-engines (engines-that-support feature)))
+(defn non-timeseries-engines-without-feature
+  "Return a set of all non-timeseries engines (e.g., everything except Druid) that DO NOT support `feature`."
+  [feature]
+  (set/difference non-timeseries-engines (non-timeseries-engines-with-feature feature)))
 
 (defmacro expect-with-non-timeseries-dbs
   {:style/indent 0}
@@ -290,9 +292,8 @@
 
 ;; TODO - maybe this needs a new name now that it also removes the results_metadata
 (defn booleanize-native-form
-  "Convert `:native_form` attribute to a boolean to make test results comparisons easier. Remove
-  `data.results_metadata` as well since it just takes a lot of space and the checksum can vary based on whether
-  encryption is enabled."
+  "Convert `:native_form` attribute to a boolean to make test results comparisons easier. Remove `data.results_metadata`
+  as well since it just takes a lot of space and the checksum can vary based on whether encryption is enabled."
   [m]
   (-> m
       (update-in [:data :native_form] boolean)
diff --git a/test/metabase/query_processor_test/aggregation_test.clj b/test/metabase/query_processor_test/aggregation_test.clj
index a32bef8a47249b04bd742acd897f8111a87ee5dd..7c024623d7dcf9c3eb96f49958dbc0e4b00b6d71 100644
--- a/test/metabase/query_processor_test/aggregation_test.clj
+++ b/test/metabase/query_processor_test/aggregation_test.clj
@@ -8,7 +8,7 @@
             [metabase.test.data.datasets :as datasets]
             [metabase.test.util :as tu]))
 
-;;; ------------------------------------------------------------ "COUNT" AGGREGATION ------------------------------------------------------------
+;;; ---------------------------------------------- "COUNT" AGGREGATION -----------------------------------------------
 
 (qp-expect-with-all-engines
     {:rows        [[100]]
@@ -21,7 +21,7 @@
          (format-rows-by [int])))
 
 
-;;; ------------------------------------------------------------ "SUM" AGGREGATION ------------------------------------------------------------
+;;; ----------------------------------------------- "SUM" AGGREGATION ------------------------------------------------
 (qp-expect-with-all-engines
     {:rows        [[203]]
      :columns     ["sum"]
@@ -33,7 +33,7 @@
          (format-rows-by [int])))
 
 
-;;; ------------------------------------------------------------ "AVG" AGGREGATION ------------------------------------------------------------
+;;; ----------------------------------------------- "AVG" AGGREGATION ------------------------------------------------
 (qp-expect-with-all-engines
     {:rows        [[35.5059]]
      :columns     ["avg"]
@@ -45,7 +45,7 @@
          (format-rows-by [(partial u/round-to-decimals 4)])))
 
 
-;;; ------------------------------------------------------------ "DISTINCT COUNT" AGGREGATION ------------------------------------------------------------
+;;; ------------------------------------------ "DISTINCT COUNT" AGGREGATION ------------------------------------------
 (qp-expect-with-all-engines
     {:rows        [[15]]
      :columns     ["count"]
@@ -57,8 +57,8 @@
          (format-rows-by [int])))
 
 
-;;; ------------------------------------------------------------ "ROWS" AGGREGATION ------------------------------------------------------------
-;; Test that a rows aggregation just returns rows as-is.
+;;; ------------------------------------------------- NO AGGREGATION -------------------------------------------------
+;; Test that no aggregation (formerly known as a 'rows' aggregation in MBQL '95) just returns rows as-is.
 (qp-expect-with-all-engines
     {:rows        [[ 1 "Red Medicine"                  4 10.0646 -165.374 3]
                    [ 2 "Stout Burgers & Beers"        11 34.0996 -118.329 2]
@@ -81,9 +81,9 @@
         tu/round-fingerprint-cols))
 
 
-;;; ------------------------------------------------------------ STDDEV AGGREGATION ------------------------------------------------------------
+;;; ----------------------------------------------- STDDEV AGGREGATION -----------------------------------------------
 
-(qp-expect-with-engines (engines-that-support :standard-deviation-aggregations)
+(qp-expect-with-engines (non-timeseries-engines-with-feature :standard-deviation-aggregations)
   {:columns     ["stddev"]
    :cols        [(aggregate-col :stddev (venues-col :latitude))]
    :rows        [[3.4]]
@@ -95,7 +95,7 @@
                                  [[(u/round-to-decimals 1 v)]]))))
 
 ;; Make sure standard deviation fails for the Mongo driver since its not supported
-(datasets/expect-with-engines (engines-that-dont-support :standard-deviation-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :standard-deviation-aggregations)
   {:status :failed
    :error  "standard-deviation-aggregations is not supported by this driver."}
   (select-keys (data/run-query venues
@@ -103,9 +103,9 @@
                [:status :error]))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------------+
-;;; |                                                      MIN & MAX                                                       |
-;;; +----------------------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                                   MIN & MAX                                                    |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 (expect-with-non-timeseries-dbs [1] (first-row
                                       (format-rows-by [int]
@@ -132,9 +132,9 @@
             (ql/breakout $price)))))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------------+
-;;; |                                                 MULTIPLE AGGREGATIONS                                                |
-;;; +----------------------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                             MULTIPLE AGGREGATIONS                                              |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 ;; can we run a simple query with *two* aggregations?
 (expect-with-non-timeseries-dbs
@@ -151,7 +151,9 @@
             (ql/aggregation (ql/avg $price) (ql/count) (ql/sum $price))))))
 
 ;; make sure that multiple aggregations of the same type have the correct metadata (#4003)
-;; (TODO - this isn't tested against Mongo or BigQuery because those drivers don't currently work correctly with multiple columns with the same name)
+;;
+;; (TODO - this isn't tested against Mongo or BigQuery because those drivers don't currently work correctly with
+;; multiple columns with the same name)
 (datasets/expect-with-engines (disj non-timeseries-engines :mongo :bigquery)
   [(aggregate-col :count)
    (assoc (aggregate-col :count)
@@ -163,7 +165,7 @@
       :data :cols))
 
 
-;;; ------------------------------------------------------------ CUMULATIVE SUM ------------------------------------------------------------
+;;; ------------------------------------------------- CUMULATIVE SUM -------------------------------------------------
 
 ;;; cum_sum w/o breakout should be treated the same as sum
 (qp-expect-with-all-engines
@@ -254,7 +256,7 @@
        (format-rows-by [int int])))
 
 
-;;; ------------------------------------------------------------ CUMULATIVE COUNT ------------------------------------------------------------
+;;; ------------------------------------------------ CUMULATIVE COUNT ------------------------------------------------
 
 (defn- cumulative-count-col [col-fn col-name]
   (assoc (aggregate-col :count (col-fn col-name))
diff --git a/test/metabase/query_processor_test/breakout_test.clj b/test/metabase/query_processor_test/breakout_test.clj
index 5060fcc3833846f41cc0bfb6682057f0f72fad08..cdca9fa82415ed980cb3b9e5995c0838ca0075d5 100644
--- a/test/metabase/query_processor_test/breakout_test.clj
+++ b/test/metabase/query_processor_test/breakout_test.clj
@@ -115,7 +115,7 @@
          booleanize-native-form
          (format-rows-by [int int str]))))
 
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   [["Wine Bar" "Thai" "Thai" "Thai" "Thai" "Steakhouse" "Steakhouse" "Steakhouse" "Steakhouse" "Southern"]
    ["American" "American" "American" "American" "American" "American" "American" "American" "Artisan" "Artisan"]]
   (data/with-data
@@ -135,21 +135,21 @@
            rows
            (map last))]))
 
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   [[10.0 1] [32.0 4] [34.0 57] [36.0 29] [40.0 9]]
   (format-rows-by [(partial u/round-to-decimals 1) int]
     (rows (data/run-query venues
             (ql/aggregation (ql/count))
             (ql/breakout (ql/binning-strategy $latitude :num-bins 20))))))
 
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
  [[0.0 1] [20.0 90] [40.0 9]]
   (format-rows-by [(partial u/round-to-decimals 1) int]
     (rows (data/run-query venues
             (ql/aggregation (ql/count))
             (ql/breakout (ql/binning-strategy $latitude :num-bins 3))))))
 
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
    [[10.0 -170.0 1] [32.0 -120.0 4] [34.0 -120.0 57] [36.0 -125.0 29] [40.0 -75.0 9]]
   (format-rows-by [(partial u/round-to-decimals 1) (partial u/round-to-decimals 1) int]
     (rows (data/run-query venues
@@ -159,14 +159,14 @@
 
 ;; Currently defaults to 8 bins when the number of bins isn't
 ;; specified
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   [[10.0 1] [30.0 90] [40.0 9]]
   (format-rows-by [(partial u/round-to-decimals 1) int]
     (rows (data/run-query venues
             (ql/aggregation (ql/count))
             (ql/breakout (ql/binning-strategy $latitude :default))))))
 
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   [[10.0 1] [30.0 61] [35.0 29] [40.0 9]]
   (tu/with-temporary-setting-values [breakout-bin-width 5.0]
     (format-rows-by [(partial u/round-to-decimals 1) int]
@@ -175,7 +175,7 @@
               (ql/breakout (ql/binning-strategy $latitude :default)))))))
 
 ;; Testing bin-width
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   [[10.0 1] [33.0 4] [34.0 57] [37.0 29] [40.0 9]]
   (format-rows-by [(partial u/round-to-decimals 1) int]
     (rows (data/run-query venues
@@ -183,14 +183,14 @@
             (ql/breakout (ql/binning-strategy $latitude :bin-width 1))))))
 
 ;; Testing bin-width using a float
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   [[10.0 1] [32.5 61] [37.5 29] [40.0 9]]
   (format-rows-by [(partial u/round-to-decimals 1) int]
     (rows (data/run-query venues
             (ql/aggregation (ql/count))
             (ql/breakout (ql/binning-strategy $latitude :bin-width 2.5))))))
 
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   [[33.0 4] [34.0 57]]
   (tu/with-temporary-setting-values [breakout-bin-width 1.0]
     (format-rows-by [(partial u/round-to-decimals 1) int]
@@ -209,7 +209,7 @@
         (update-in [:binning_info :max_value] round-to-decimal))))
 
 ;;Validate binning info is returned with the binning-strategy
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   (assoc (breakout-col (venues-col :latitude))
          :binning_info {:binning_strategy :bin-width, :bin_width 10.0,
                         :num_bins         4,          :min_value 10.0
@@ -221,7 +221,7 @@
       (get-in [:data :cols])
       first))
 
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   (assoc (breakout-col (venues-col :latitude))
          :binning_info {:binning_strategy :num-bins, :bin_width 7.5,
                         :num_bins         5,         :min_value 7.5,
@@ -234,7 +234,7 @@
       first))
 
 ;;Validate binning info is returned with the binning-strategy
-(datasets/expect-with-engines (engines-that-support :binning)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :binning)
   {:status :failed
    :class Exception
    :error (format "Unable to bin field '%s' with id '%s' without a min/max value"
diff --git a/test/metabase/query_processor_test/expression_aggregations_test.clj b/test/metabase/query_processor_test/expression_aggregations_test.clj
index 24babcaf3f3efffd4cd68b10a1ed1557684fc5f0..3380695d77a17609bdf81e8ed9c7f4121a90ff47 100644
--- a/test/metabase/query_processor_test/expression_aggregations_test.clj
+++ b/test/metabase/query_processor_test/expression_aggregations_test.clj
@@ -12,7 +12,7 @@
             [toucan.util.test :as tt]))
 
 ;; sum, *
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1 1211]
    [2 5710]
    [3 1845]
@@ -23,7 +23,7 @@
             (ql/breakout $price)))))
 
 ;; min, +
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1 10]
    [2  4]
    [3  4]
@@ -34,7 +34,7 @@
             (ql/breakout $price)))))
 
 ;; max, /
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1 94]
    [2 50]
    [3 26]
@@ -45,7 +45,7 @@
             (ql/breakout $price)))))
 
 ;; avg, -
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   (if (= *engine* :h2)
     [[1  55]
      [2  97]
@@ -61,7 +61,7 @@
             (ql/breakout $price)))))
 
 ;; post-aggregation math w/ 2 args: count + sum
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1  44]
    [2 177]
    [3  52]
@@ -73,7 +73,7 @@
             (ql/breakout $price)))))
 
 ;; post-aggregation math w/ 3 args: count + sum + count
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1  66]
    [2 236]
    [3  65]
@@ -86,7 +86,7 @@
             (ql/breakout $price)))))
 
 ;; post-aggregation math w/ a constant: count * 10
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1 220]
    [2 590]
    [3 130]
@@ -98,7 +98,7 @@
             (ql/breakout $price)))))
 
 ;; nested post-aggregation math: count + (count * sum)
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1  506]
    [2 7021]
    [3  520]
@@ -111,7 +111,7 @@
             (ql/breakout $price)))))
 
 ;; post-aggregation math w/ avg: count + avg
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   (if (= *engine* :h2)
     [[1  77]
      [2 107]
@@ -128,7 +128,7 @@
             (ql/breakout $price)))))
 
 ;; post aggregation math + math inside aggregations: max(venue_price) + min(venue_price - id)
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1 -92]
    [2 -96]
    [3 -74]
@@ -140,7 +140,7 @@
             (ql/breakout $price)))))
 
 ;; aggregation w/o field
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1 23]
    [2 60]
    [3 14]
@@ -150,8 +150,17 @@
             (ql/aggregation (ql/+ 1 (ql/count)))
             (ql/breakout $price)))))
 
+;; Sorting by an un-named aggregate expression
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
+  [[1 2] [2 2] [12 2] [4 4] [7 4] [10 4] [11 4] [8 8]]
+  (format-rows-by [int int]
+    (rows (data/run-query users
+            (ql/aggregation (ql/* (ql/count) 2))
+            (ql/breakout (ql/datetime-field $last_login :month-of-year))
+            (ql/order-by (ql/asc (ql/aggregation 0)))))))
+
 ;; aggregation with math inside the aggregation :scream_cat:
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1  44]
    [2 177]
    [3  52]
@@ -162,7 +171,7 @@
             (ql/breakout $price)))))
 
 ;; check that we can name an expression aggregation w/ aggregation at top-level
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   {:rows    [[1  44]
              [2 177]
              [3  52]
@@ -175,7 +184,7 @@
                          (ql/breakout $price)))))
 
 ;; check that we can name an expression aggregation w/ expression at top-level
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   {:rows    [[1 -19]
              [2  77]
              [3  -2]
@@ -188,7 +197,7 @@
                          (ql/breakout $price)))))
 
 ;; check that we can handle METRICS (ick) inside expression aggregation clauses
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[2 119]
    [3  40]
    [4  25]]
@@ -204,7 +213,7 @@
                           :breakout     [(ql/breakout (ql/field-id (data/id :venues :price)))]}})))))
 
 ;; check that we can handle METRICS (ick) inside a NAMED clause
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   {:rows    [[2 118]
              [3  39]
              [4  24]]
@@ -222,7 +231,7 @@
                                        :breakout     [(ql/breakout (ql/field-id (data/id :venues :price)))]}})))))
 
 ;; check that METRICS (ick) with a nested aggregation still work inside a NAMED clause
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   {:rows    [[2 118]
              [3  39]
              [4  24]]
@@ -240,7 +249,7 @@
                                        :breakout     [(ql/breakout (ql/field-id (data/id :venues :price)))]}})))))
 
 ;; check that named aggregations come back with the correct column metadata (#4002)
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   (let [col-name (driver/format-custom-field-name *driver* "Count of Things")]
     (assoc (aggregate-col :count)
       :name         col-name
@@ -253,7 +262,7 @@
       :data :cols first))
 
 ;; check that we can use cumlative count in expression aggregations
-(datasets/expect-with-engines (engines-that-support :expression-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expression-aggregations)
   [[1000]]
   (format-rows-by [int]
     (rows (qp/process-query
diff --git a/test/metabase/query_processor_test/expressions_test.clj b/test/metabase/query_processor_test/expressions_test.clj
index 2b19acf4f2707465489f7a2d84ceeb50a1e6bfb6..5e3f7ed40e695e422cbe76ca45dcb2133751134a 100644
--- a/test/metabase/query_processor_test/expressions_test.clj
+++ b/test/metabase/query_processor_test/expressions_test.clj
@@ -22,7 +22,7 @@
 
 
 ;; Do a basic query including an expression
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[1 "Red Medicine"                 4  10.0646 -165.374 3 5.0]
    [2 "Stout Burgers & Beers"        11 34.0996 -118.329 2 4.0]
    [3 "The Apple Pan"                11 34.0406 -118.428 2 4.0]
@@ -35,7 +35,7 @@
             (ql/order-by (ql/asc $id))))))
 
 ;; Make sure FLOATING POINT division is done
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[1 "Red Medicine"           4 10.0646 -165.374 3 1.5]     ; 3 / 2 SHOULD BE 1.5, NOT 1 (!)
    [2 "Stout Burgers & Beers" 11 34.0996 -118.329 2 1.0]
    [3 "The Apple Pan"         11 34.0406 -118.428 2 1.0]]
@@ -46,7 +46,7 @@
             (ql/order-by (ql/asc $id))))))
 
 ;; Can we do NESTED EXPRESSIONS ?
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[1 "Red Medicine"           4 10.0646 -165.374 3 3.0]
    [2 "Stout Burgers & Beers" 11 34.0996 -118.329 2 2.0]
    [3 "The Apple Pan"         11 34.0406 -118.428 2 2.0]]
@@ -57,7 +57,7 @@
             (ql/order-by (ql/asc $id))))))
 
 ;; Can we have MULTIPLE EXPRESSIONS?
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[1 "Red Medicine"           4 10.0646 -165.374 3 2.0 4.0]
    [2 "Stout Burgers & Beers" 11 34.0996 -118.329 2 1.0 3.0]
    [3 "The Apple Pan"         11 34.0406 -118.428 2 1.0 3.0]]
@@ -69,7 +69,7 @@
             (ql/order-by (ql/asc $id))))))
 
 ;; Can we refer to expressions inside a FIELDS clause?
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[4] [4] [5]]
   (format-rows-by [int]
     (rows (data/run-query venues
@@ -79,7 +79,7 @@
             (ql/order-by (ql/asc $id))))))
 
 ;; Can we refer to expressions inside an ORDER BY clause?
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[100 "Mohawk Bend"         46 34.0777 -118.265 2 102.0]
    [99  "Golden Road Brewing" 10 34.1505 -118.274 2 101.0]
    [98  "Lucky Baldwin's Pub"  7 34.1454 -118.149 2 100.0]]
@@ -90,7 +90,7 @@
             (ql/order-by (ql/desc (ql/expression :x)))))))
 
 ;; Can we AGGREGATE + BREAKOUT by an EXPRESSION?
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   [[2 22] [4 59] [6 13] [8 6]]
   (format-rows-by [int int]
     (rows (data/run-query venues
@@ -99,7 +99,7 @@
             (ql/breakout (ql/expression :x))))))
 
 ;; Custom aggregation expressions should include their type
-(datasets/expect-with-engines (engines-that-support :expressions)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :expressions)
   (conj #{{:name "x" :base_type :type/Float}}
         (if (= datasets/*engine* :oracle)
           {:name (data/format-name "category_id") :base_type :type/Decimal}
diff --git a/test/metabase/query_processor_test/filter_test.clj b/test/metabase/query_processor_test/filter_test.clj
index 95e5bdb58c4722c7c6d2c27dc981a86e78054407..536d1bef4ff443236e872f6caabafbaa357148cc 100644
--- a/test/metabase/query_processor_test/filter_test.clj
+++ b/test/metabase/query_processor_test/filter_test.clj
@@ -2,9 +2,10 @@
   "Tests for the `:filter` clause."
   (:require [metabase.query-processor-test :refer :all]
             [metabase.query-processor.middleware.expand :as ql]
-            [metabase.test.data :as data]))
+            [metabase.test.data :as data]
+            [metabase.test.data.datasets :as datasets]))
 
-;;; ------------------------------------------------------------ "FILTER" CLAUSE ------------------------------------------------------------
+;;; ------------------------------------------------ "FILTER" CLAUSE -------------------------------------------------
 
 ;;; FILTER -- "AND", ">", ">="
 (expect-with-non-timeseries-dbs
@@ -140,11 +141,11 @@
         (nil? result))))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------------+
-;;; |                                  NEW FILTER TYPES - CONTAINS, STARTS_WITH, ENDS_WITH                                 |
-;;; +----------------------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                            STRING SEARCH FILTERS - CONTAINS, STARTS-WITH, ENDS-WITH                            |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
-;;; ------------------------------------------------------------ STARTS_WITH ------------------------------------------------------------
+;;; -------------------------------------------------- starts-with ---------------------------------------------------
 (expect-with-non-timeseries-dbs
   [[41 "Cheese Steak Shop" 18 37.7855 -122.44  1]
    [74 "Chez Jay"           2 34.0104 -118.493 2]]
@@ -153,8 +154,23 @@
         (ql/order-by (ql/asc $id)))
       rows formatted-venues-rows))
 
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :no-case-sensitivity-string-filter-options)
+  []
+  (-> (data/run-query venues
+        (ql/filter (ql/starts-with $name "CHE"))
+        (ql/order-by (ql/asc $id)))
+      rows formatted-venues-rows))
+
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :no-case-sensitivity-string-filter-options)
+  [[41 "Cheese Steak Shop" 18 37.7855 -122.44  1]
+   [74 "Chez Jay"           2 34.0104 -118.493 2]]
+  (-> (data/run-query venues
+        (ql/filter (ql/starts-with $name "CHE" {:case-sensitive false}))
+        (ql/order-by (ql/asc $id)))
+      rows formatted-venues-rows))
 
-;;; ------------------------------------------------------------ ENDS_WITH ------------------------------------------------------------
+
+;;; --------------------------------------------------- ends-with ----------------------------------------------------
 (expect-with-non-timeseries-dbs
   [[ 5 "Brite Spot Family Restaurant" 20 34.0778 -118.261 2]
    [ 7 "Don Day Korean Restaurant"    44 34.0689 -118.305 2]
@@ -166,7 +182,25 @@
         (ql/order-by (ql/asc $id)))
       rows formatted-venues-rows))
 
-;;; ------------------------------------------------------------ CONTAINS ------------------------------------------------------------
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :no-case-sensitivity-string-filter-options)
+  []
+  (-> (data/run-query venues
+        (ql/filter (ql/ends-with $name "RESTAURANT"))
+        (ql/order-by (ql/asc $id)))
+      rows formatted-venues-rows))
+
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :no-case-sensitivity-string-filter-options)
+  [[ 5 "Brite Spot Family Restaurant" 20 34.0778 -118.261 2]
+   [ 7 "Don Day Korean Restaurant"    44 34.0689 -118.305 2]
+   [17 "Ruen Pair Thai Restaurant"    71 34.1021 -118.306 2]
+   [45 "Tu Lan Restaurant"             4 37.7821 -122.41  1]
+   [55 "Dal Rae Restaurant"           67 33.983  -118.096 4]]
+  (-> (data/run-query venues
+        (ql/filter (ql/ends-with $name "RESTAURANT" {:case-sensitive false}))
+        (ql/order-by (ql/asc $id)))
+      rows formatted-venues-rows))
+
+;;; ---------------------------------------------------- contains ----------------------------------------------------
 (expect-with-non-timeseries-dbs
   [[31 "Bludso's BBQ"             5 33.8894 -118.207 2]
    [34 "Beachwood BBQ & Brewing" 10 33.7701 -118.191 2]
@@ -176,7 +210,28 @@
         (ql/order-by (ql/asc $id)))
       rows formatted-venues-rows))
 
-;;; ------------------------------------------------------------ Nested AND / OR ------------------------------------------------------------
+;; case-insensitive
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :no-case-sensitivity-string-filter-options)
+  []
+  (-> (data/run-query venues
+        (ql/filter (ql/contains $name "bbq"))
+        (ql/order-by (ql/asc $id)))
+      rows formatted-venues-rows))
+
+;; case-insensitive
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :no-case-sensitivity-string-filter-options)
+  [[31 "Bludso's BBQ"             5 33.8894 -118.207 2]
+   [34 "Beachwood BBQ & Brewing" 10 33.7701 -118.191 2]
+   [39 "Baby Blues BBQ"           5 34.0003 -118.465 2]]
+  (-> (data/run-query venues
+        (ql/filter (ql/contains $name "bbq" {:case-sensitive false}))
+        (ql/order-by (ql/asc $id)))
+      rows formatted-venues-rows))
+
+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                             NESTED AND/OR CLAUSES                                              |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 (expect-with-non-timeseries-dbs
   [[81]]
@@ -188,7 +243,9 @@
        rows (format-rows-by [int])))
 
 
-;;; ------------------------------------------------------------ = / != with multiple values ------------------------------------------------------------
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                         = AND != WITH MULTIPLE VALUES                                          |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 (expect-with-non-timeseries-dbs
   [[81]]
@@ -205,14 +262,18 @@
        rows (format-rows-by [int])))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------------+
-;;; |                                                      NOT FILTER                                                      |
-;;; +----------------------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                                   NOT FILTER                                                   |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 ;; `not` filter -- Test that we can negate the various other filter clauses
-;; The majority of these tests aren't necessary since `not` automatically translates them to simpler, logically equivalent expressions
-;; but I already wrote them so in this case it doesn't hurt to have a little more test coverage than we need
-;; TODO - maybe it makes sense to have a separate namespace to test the Query eXpander so we don't need to run all these extra queries?
+;;
+;; The majority of these tests aren't necessary since `not` automatically translates them to simpler, logically
+;; equivalent expressions but I already wrote them so in this case it doesn't hurt to have a little more test coverage
+;; than we need
+;;
+;; TODO - maybe it makes sense to have a separate namespace to test the Query eXpander so we don't need to run all
+;; these extra queries?
 
 ;;; =
 (expect-with-non-timeseries-dbs
@@ -313,7 +374,9 @@
         (ql/filter (ql/not (ql/contains $name "BBQ")))))))
 
 ;;; does-not-contain
-;; This should literally be the exact same query as the one above by the time it leaves the Query eXpander, so this is more of a QX test than anything else
+;;
+;; This should literally be the exact same query as the one above by the time it leaves the Query eXpander, so this is
+;; more of a QX test than anything else
 (expect-with-non-timeseries-dbs
   [97]
   (first-row
diff --git a/test/metabase/query_processor_test/joins_test.clj b/test/metabase/query_processor_test/joins_test.clj
index 1ce40cf4c5c874cf84fb9ed833de3d614378da2a..8bf39505309b540fb41ee9b1a0e40c2eb86a1fb3 100644
--- a/test/metabase/query_processor_test/joins_test.clj
+++ b/test/metabase/query_processor_test/joins_test.clj
@@ -7,7 +7,7 @@
 
 ;; The top 10 cities by number of Tupac sightings
 ;; Test that we can breakout on an FK field (Note how the FK Field is returned in the results)
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   [["Arlington"    16]
    ["Albany"       15]
    ["Portland"     14]
@@ -30,7 +30,7 @@
 ;; Number of Tupac sightings in the Expa office
 ;; (he was spotted here 60 times)
 ;; Test that we can filter on an FK field
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   [[60]]
   (->> (data/dataset tupac-sightings
          (data/run-query sightings
@@ -42,7 +42,7 @@
 ;; THE 10 MOST RECENT TUPAC SIGHTINGS (!)
 ;; (What he was doing when we saw him, sighting ID)
 ;; Check that we can include an FK field in the :fields clause
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   [[772 "In the Park"]
    [894 "Working at a Pet Store"]
    [684 "At the Airport"]
@@ -65,7 +65,7 @@
 ;;    (this query targets sightings and orders by cities.name and categories.name)
 ;; 2. Check that we can join MULTIPLE tables in a single query
 ;;    (this query joins both cities and categories)
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   ;; CITY_ID, CATEGORY_ID, ID
   ;; Cities are already alphabetized in the source data which is why CITY_ID is sorted
   [[1 12   6]
@@ -84,11 +84,12 @@
                         (ql/desc $category_id->categories.name)
                         (ql/asc $id))
            (ql/limit 10)))
-       rows (map butlast) (map reverse) (format-rows-by [int int int]))) ; drop timestamps. reverse ordering to make the results columns order match order_by
+       ;; drop timestamps. reverse ordering to make the results columns order match order_by
+       rows (map butlast) (map reverse) (format-rows-by [int int int])))
 
 
 ;; Check that trying to use a Foreign Key fails for Mongo
-(datasets/expect-with-engines (engines-that-dont-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-without-feature :foreign-keys)
   {:status :failed
    :error "foreign-keys is not supported by this driver."}
   (select-keys (data/dataset tupac-sightings
@@ -100,9 +101,9 @@
                [:status :error]))
 
 
-;;; +----------------------------------------------------------------------------------------------------------------------+
-;;; |                                                    MULTIPLE JOINS                                                    |
-;;; +----------------------------------------------------------------------------------------------------------------------+
+;;; +----------------------------------------------------------------------------------------------------------------+
+;;; |                                                 MULTIPLE JOINS                                                 |
+;;; +----------------------------------------------------------------------------------------------------------------+
 
 ;;; CAN WE JOIN AGAINST THE SAME TABLE TWICE (MULTIPLE FKS TO A SINGLE TABLE!?)
 ;; Query should look something like:
@@ -115,7 +116,7 @@
 ;; WHERE USERS__via__RECIEVER_ID.NAME = 'Rasta Toucan'
 ;; GROUP BY USERS__via__SENDER_ID.NAME
 ;; ORDER BY USERS__via__SENDER_ID.NAME ASC
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   [["Bob the Sea Gull" 2]
    ["Brenda Blackbird" 2]
    ["Lucky Pigeon"     2]
diff --git a/test/metabase/query_processor_test/nested_field_test.clj b/test/metabase/query_processor_test/nested_field_test.clj
index a77550db9401619b302f2187805b89d179cf28ad..41945c8aabcccf273bee044eac12278d9565521e 100644
--- a/test/metabase/query_processor_test/nested_field_test.clj
+++ b/test/metabase/query_processor_test/nested_field_test.clj
@@ -7,7 +7,7 @@
 
 ;;; Nested Field in FILTER
 ;; Get the first 10 tips where tip.venue.name == "Kyle's Low-Carb Grill"
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   [[8   "Kyle's Low-Carb Grill"]
    [67  "Kyle's Low-Carb Grill"]
    [80  "Kyle's Low-Carb Grill"]
@@ -26,7 +26,7 @@
 
 ;;; Nested Field in ORDER
 ;; Let's get all the tips Kyle posted on Twitter sorted by tip.venue.name
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   [[446
     {:mentions ["@cams_mexican_gastro_pub"], :tags ["#mexican" "#gastro" "#pub"], :service "twitter", :username "kyle"}
     "Cam's Mexican Gastro Pub is a historical and underappreciated place to conduct a business meeting with friends."
@@ -63,14 +63,14 @@
 
 ;; Nested Field in AGGREGATION
 ;; Let's see how many *distinct* venue names are mentioned
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   [99]
   (first-row (data/dataset geographical-tips
                (data/run-query tips
                  (ql/aggregation (ql/distinct $tips.venue.name))))))
 
 ;; Now let's just get the regular count
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   [500]
   (first-row (data/dataset geographical-tips
                (data/run-query tips
@@ -78,7 +78,7 @@
 
 ;;; Nested Field in BREAKOUT
 ;; Let's see how many tips we have by source.service
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   {:rows        [["facebook"   107]
                  ["flare"      105]
                  ["foursquare" 100]
@@ -95,7 +95,7 @@
 
 ;;; Nested Field in FIELDS
 ;; Return the first 10 tips with just tip.venue.name
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   {:columns ["venue.name"]
    :rows    [["Lucky's Gluten-Free Café"]
              ["Joe's Homestyle Eatery"]
@@ -116,7 +116,7 @@
 
 
 ;;; Nested Field w/ ordering by aggregation
-(datasets/expect-with-engines (engines-that-support :nested-fields)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-fields)
   [["jane"           4]
    ["kyle"           5]
    ["tupac"          5]
diff --git a/test/metabase/query_processor_test/nested_queries_test.clj b/test/metabase/query_processor_test/nested_queries_test.clj
index 34575bb63d5902dad7fb1b6a4678cd19d15e8bba..fcd817f818b4bc5d112c02b814fcd63d19606f42 100644
--- a/test/metabase/query_processor_test/nested_queries_test.clj
+++ b/test/metabase/query_processor_test/nested_queries_test.clj
@@ -9,13 +9,19 @@
              [util :as u]]
             [metabase.driver.generic-sql :as generic-sql]
             [metabase.models
-             [card :refer [Card]]
-             [database :as database]
+             [card :as card :refer [Card]]
+             [database :as database :refer [Database]]
              [field :refer [Field]]
+             [permissions :as perms]
+             [permissions-group :as perms-group]
              [segment :refer [Segment]]
              [table :refer [Table]]]
-            [metabase.test.data :as data]
-            [metabase.test.data.datasets :as datasets]
+            [metabase.test
+             [data :as data]
+             [util :as tu]]
+            [metabase.test.data
+             [datasets :as datasets]
+             [users :refer [user->client]]]
             [toucan.db :as db]
             [toucan.util.test :as tt]))
 
@@ -31,7 +37,7 @@
 
 
 ;; make sure we can do a basic query with MBQL source-query
-(datasets/expect-with-engines (engines-that-support :nested-queries)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-queries)
   {:rows [[1 "Red Medicine"                  4 10.0646 -165.374 3]
           [2 "Stout Burgers & Beers"        11 34.0996 -118.329 2]
           [3 "The Apple Pan"                11 34.0406 -118.428 2]
@@ -74,7 +80,7 @@
   (comp quote-identifier identifier))
 
 ;; make sure we can do a basic query with a SQL source-query
-(datasets/expect-with-engines (engines-that-support :nested-queries)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-queries)
   {:rows [[1 -165.374  4 3 "Red Medicine"                 10.0646]
           [2 -118.329 11 2 "Stout Burgers & Beers"        34.0996]
           [3 -118.428 11 2 "The Apple Pan"                34.0406]
@@ -112,7 +118,7 @@
           {:name "count", :base_type :type/Integer}]})
 
 ;; make sure we can do a query with breakout and aggregation using an MBQL source query
-(datasets/expect-with-engines (engines-that-support :nested-queries)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-queries)
   breakout-results
   (rows+cols
     (format-rows-by [int int]
@@ -124,7 +130,7 @@
                     :breakout     [[:field-literal (keyword (data/format-name :price)) :type/Integer]]}}))))
 
 ;; make sure we can do a query with breakout and aggregation using a SQL source query
-(datasets/expect-with-engines (engines-that-support :nested-queries)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-queries)
   breakout-results
   (rows+cols
     (format-rows-by [int int]
@@ -410,7 +416,7 @@
       results-metadata))
 
 ;; make sure using a time interval filter works
-(datasets/expect-with-engines (engines-that-support :nested-queries)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-queries)
   :completed
   (tt/with-temp Card [card (mbql-card-def
                              :source-table (data/id :checkins))]
@@ -420,7 +426,7 @@
         :status)))
 
 ;; make sure that wrapping a field literal in a datetime-field clause works correctly in filters & breakouts
-(datasets/expect-with-engines (engines-that-support :nested-queries)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :nested-queries)
   :completed
   (tt/with-temp Card [card (mbql-card-def
                              :source-table (data/id :checkins))]
@@ -458,3 +464,62 @@
           :aggregation [:count])
         qp/process-query
         rows)))
+
+
+;; Make suer you're allowed to save a query that uses a SQL-based source query even if you don't have SQL *write*
+;; permissions (#6845)
+
+;; Check that write perms for a Card with a source query require that you are able to *read* (i.e., view) the source
+;; query rather than be able to write (i.e., save) it. For example you should be able to save a query that uses a
+;; native query as its source query if you have permissions to view that query, even if you aren't allowed to create
+;; new ad-hoc SQL queries yourself.
+(expect
+ #{(perms/native-read-path (data/id))}
+ (tt/with-temp Card [card {:dataset_query {:database (data/id)
+                                           :type     :native
+                                           :native   {:query "SELECT * FROM VENUES"}}}]
+   (card/query-perms-set (query-with-source-card card :aggregation [:count]) :write)))
+
+;; try this in an end-to-end fashion using the API and make sure we can save a Card if we have appropriate read
+;; permissions for the source query
+(defn- do-with-temp-copy-of-test-db
+  "Run `f` with a temporary Database that copies the details from the standard test database. `f` is invoked as `(f
+  db)`."
+  [f]
+  (tt/with-temp Database [db {:details (:details (data/db)), :engine "h2"}]
+    (f db)))
+
+(defn- save-card-via-API-with-native-source-query!
+  "Attempt to save a Card that uses a native source query for Database with `db-id` via the API using Rasta. Use this to
+  test how the API endpoint behaves based on certain permissions grants for the `All Users` group."
+  [expected-status-code db-id]
+  (tt/with-temp Card [card {:dataset_query {:database db-id
+                                            :type     :native
+                                            :native   {:query "SELECT * FROM VENUES"}}}]
+    ((user->client :rasta) :post "card"
+     {:name                   (tu/random-name)
+      :display                "scalar"
+      :visualization_settings {}
+      :dataset_query          (query-with-source-card card
+                                :aggregation [:count])})))
+
+;; ok... grant native *read* permissions which means we should be able to view our source query generated with the
+;; function above. API should allow use to save here because write permissions for a query require read permissions
+;; for any source queries
+(expect
+  :ok
+  (do-with-temp-copy-of-test-db
+   (fn [db]
+     (perms/revoke-permissions! (perms-group/all-users) (u/get-id db))
+     (perms/grant-permissions! (perms-group/all-users) (perms/native-read-path (u/get-id db)))
+     (save-card-via-API-with-native-source-query! 200 (u/get-id db))
+     :ok)))
+
+;; however, if we do *not* have read permissions for the source query, we shouldn't be allowed to save the query. This
+;; API call should fail
+(expect
+  "You don't have permissions to do that."
+  (do-with-temp-copy-of-test-db
+   (fn [db]
+     (perms/revoke-permissions! (perms-group/all-users) (u/get-id db))
+     (save-card-via-API-with-native-source-query! 403 (u/get-id db)))))
diff --git a/test/metabase/query_processor_test/order_by_test.clj b/test/metabase/query_processor_test/order_by_test.clj
index 1271d122a88536b9c0334993b4608d1bd110a0bd..be0c7ff93f4b66823684dc9adf4359cb52c9ee7a 100644
--- a/test/metabase/query_processor_test/order_by_test.clj
+++ b/test/metabase/query_processor_test/order_by_test.clj
@@ -106,7 +106,7 @@
 ;;; ### order_by aggregate ["stddev" field-id]
 ;; SQRT calculations are always NOT EXACT (normal behavior) so round everything to the nearest int.
 ;; Databases might use different versions of SQRT implementations
-(datasets/expect-with-engines (engines-that-support :standard-deviation-aggregations)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :standard-deviation-aggregations)
   {:columns     [(data/format-name "price")
                  "stddev"]
    :rows        [[3 (if (contains? #{:mysql :crate} *engine*) 25 26)]
diff --git a/test/metabase/query_processor_test/query_cancellation_test.clj b/test/metabase/query_processor_test/query_cancellation_test.clj
new file mode 100644
index 0000000000000000000000000000000000000000..4396394c118221fc315b1dffeb87298041d3cc7a
--- /dev/null
+++ b/test/metabase/query_processor_test/query_cancellation_test.clj
@@ -0,0 +1,54 @@
+(ns metabase.query-processor-test.query-cancellation-test
+  (:require [clojure.java.jdbc :as jdbc]
+            [expectations :refer :all]
+            [metabase.test.util :as tu]))
+
+(deftype FakePreparedStatement [called-cancel?]
+  java.sql.PreparedStatement
+  (cancel [_] (deliver called-cancel? true))
+  (close [_] true))
+
+(defn- make-fake-prep-stmt
+  "Returns `fake-value` whenenver the `sql` parameter returns a truthy value when passed to `use-fake-value?`"
+  [orig-make-prep-stmt use-fake-value? faked-value]
+  (fn [connection sql options]
+    (if (use-fake-value? sql)
+      faked-value
+      (orig-make-prep-stmt connection sql options))))
+
+(defn- fake-query
+  "Function to replace the `clojure.java.jdbc/query` function. Will invoke `call-on-query`, then `call-to-pause` whe
+  passed an instance of `FakePreparedStatement`"
+  [orig-jdbc-query call-on-query call-to-pause]
+  (fn
+    ([conn stmt+params]
+     (orig-jdbc-query conn stmt+params))
+    ([conn stmt+params opts]
+     (if (instance? FakePreparedStatement (first stmt+params))
+       (do
+         (call-on-query)
+         (call-to-pause))
+       (orig-jdbc-query conn stmt+params opts)))))
+
+(expect
+  [false ;; Ensure the query promise hasn't fired yet
+   false ;; Ensure the cancellation promise hasn't fired yet
+   true  ;; Was query called?
+   false ;; Cancel should not have been called yet
+   true  ;; Cancel should have been called now
+   true  ;; The paused query can proceed now
+   ]
+  (tu/call-with-paused-query
+   (fn [query-thunk called-query? called-cancel? pause-query]
+     (let [ ;; This fake prepared statement is cancellable like a prepared statement, but will allow us to tell the
+           ;; difference between our Prepared statement and the real thing
+           fake-prep-stmt             (->FakePreparedStatement called-cancel?)
+           ;; Much of the underlying plumbing of MB requires a working jdbc/query and jdbc/prepared-statement (such
+           ;; as queryies for the application database). Let binding the original versions of the functions allows
+           ;; us to delegate to them when it's not the query we're trying to test
+           orig-jdbc-query            jdbc/query
+           orig-prep-stmt             jdbc/prepare-statement]
+       (future
+         (with-redefs [jdbc/prepare-statement (make-fake-prep-stmt orig-prep-stmt (fn [table-name] (re-find #"CHECKINS" table-name)) fake-prep-stmt)
+                       jdbc/query             (fake-query orig-jdbc-query #(deliver called-query? true) #(deref pause-query))]
+           (query-thunk)))))))
diff --git a/test/metabase/query_processor_test/remapping_test.clj b/test/metabase/query_processor_test/remapping_test.clj
index bd5867da0617ddef28be32274b3fd0a9ae8939c1..36cf45f351a1bee8c049481920038dba3cc75bbe 100644
--- a/test/metabase/query_processor_test/remapping_test.clj
+++ b/test/metabase/query_processor_test/remapping_test.clj
@@ -52,7 +52,7 @@
                    (fn [rows]
                      (map #(mapv % col-indexes) rows))))))
 
-(datasets/expect-with-engines (engines-that-support :foreign-keys)
+(datasets/expect-with-engines (non-timeseries-engines-with-feature :foreign-keys)
   {:rows   [["20th Century Cafe" 2 "Café"]
             ["25°" 2 "Burger"]
             ["33 Taps" 2 "Bar"]
diff --git a/test/metabase/sync_database_test.clj b/test/metabase/sync_database_test.clj
index d1ab8bda903649bafebb7bb1c26dcd860063e2a6..a683b8795a2ada6b5e74f58695a242dee4a597b2 100644
--- a/test/metabase/sync_database_test.clj
+++ b/test/metabase/sync_database_test.clj
@@ -123,6 +123,7 @@
    :created_at          true
    :updated_at          true
    :last_analyzed       true
+   :has_field_values    nil
    :fingerprint         true
    :fingerprint_version true})
 
diff --git a/test/metabase/test/data/dataset_definitions.clj b/test/metabase/test/data/dataset_definitions.clj
index d8b7b80e925e7f0202419a7336bae03f236cf6d1..e23901b268a42002bff987c521130556c10ed18f 100644
--- a/test/metabase/test/data/dataset_definitions.clj
+++ b/test/metabase/test/data/dataset_definitions.clj
@@ -66,6 +66,15 @@
                          (mapv (fn [[username last-login password-text]]
                                  [username (date-only last-login) (time-only last-login) password-text])
                                rows))
+                       (for [[table-name :as orig-def] (di/slurp-edn-table-def "test-data")
+                             :when (= table-name "users")]
+                         orig-def)))
+
+(di/def-database-definition test-data-with-null-date-checkins
+  (di/update-table-def "checkins"
+                       #(vec (concat % [{:field-name "null_only_date" :base-type :type/Date}]))
+                       (fn [rows]
+                         (mapv #(conj % nil) rows))
                        (di/slurp-edn-table-def "test-data")))
 
 (def test-data-map
diff --git a/test/metabase/test/data/interface.clj b/test/metabase/test/data/interface.clj
index 36d3584dea3f7c1c39786a523d49bf660ba456b3..9b70db6e0bae5f8437ba689fb1229c6b3c9ea2fc 100644
--- a/test/metabase/test/data/interface.clj
+++ b/test/metabase/test/data/interface.clj
@@ -181,11 +181,12 @@
   invocation)."
   [table-name-to-update update-table-def-fn update-rows-fn table-def]
   (vec
-   (for [[table-name table-def rows] table-def
-         :when (= table-name table-name-to-update)]
-     [table-name
-      (update-table-def-fn table-def)
-      (update-rows-fn rows)])))
+   (for [[table-name table-def rows :as orig-table-def] table-def]
+     (if (= table-name table-name-to-update)
+       [table-name
+        (update-table-def-fn table-def)
+        (update-rows-fn rows)]
+       orig-table-def))))
 
 (defmacro def-database-definition
   "Convenience for creating a new `DatabaseDefinition` named by the symbol DATASET-NAME."
diff --git a/test/metabase/test/mock/toucanery.clj b/test/metabase/test/mock/toucanery.clj
index 9af412daa3b2e1ba10e837cc73a2dc562cb949e5..a03ea13a85696a792afcbfc7f45ba32a4af2537d 100644
--- a/test/metabase/test/mock/toucanery.clj
+++ b/test/metabase/test/mock/toucanery.clj
@@ -1,7 +1,6 @@
 (ns metabase.test.mock.toucanery
-  "A document style database mocked for testing.
-   This is a `:dynamic-schema` db with `:nested-fields`.
-   Most notably meant to serve as a representation of a Mongo database."
+  "A document style database mocked for testing. This is a dynamic schema db with `:nested-fields`. Most notably meant
+  to serve as a representation of a Mongo database."
   (:require [metabase.driver :as driver]
             [metabase.test.mock.util :as mock-util]))
 
@@ -76,7 +75,7 @@
   (merge driver/IDriverDefaultsMixin
          {:describe-database        describe-database
           :describe-table           describe-table
-          :features                 (constantly #{:dynamic-schema :nested-fields})
+          :features                 (constantly #{:nested-fields})
           :details-fields           (constantly [])
           :table-rows-seq           table-rows-seq
           :process-query-in-context mock-util/process-query-in-context}))
diff --git a/test/metabase/test/util.clj b/test/metabase/test/util.clj
index b4cb669e60289fbd6a3f434416c4a68e0f6324b5..2b359601c68650a0aae3a8ae0a95eff740827fd9 100644
--- a/test/metabase/test/util.clj
+++ b/test/metabase/test/util.clj
@@ -27,8 +27,11 @@
              [setting :as setting]
              [table :refer [Table]]
              [user :refer [User]]]
+            [metabase.query-processor.middleware.expand :as ql]
             [metabase.test.data :as data]
-            [metabase.test.data.datasets :refer [*driver*]]
+            [metabase.test.data
+             [datasets :refer [*driver*]]
+             [dataset-definitions :as defs]]
             [toucan.db :as db]
             [toucan.util.test :as test])
   (:import java.util.TimeZone
@@ -478,3 +481,46 @@
      (finally
        (doseq [model# ~model-seq]
          (db/delete! model#)))))
+
+(defn call-with-paused-query
+  "This is a function to make testing query cancellation eaiser as it can be complex handling the multiple threads
+  needed to orchestrate a query cancellation.
+
+  This function takes `f` which is a function of 4 arguments:
+     - query-thunk - no-arg function that will invoke a query
+     - query promise - promise used to validate the query function was called
+     - cancel promise - promise used to validate a cancellation function was called
+     - pause query promise - promise used to hang the query function, allowing cancellation
+
+  This function returns a vector of booleans indicating the various statuses of the promises, useful for comparison
+  in an `expect`"
+  [f]
+  (data/with-db (data/get-or-create-database! defs/test-data)
+    (let [called-cancel?             (promise)
+          called-query?              (promise)
+          pause-query                (promise)
+          before-query-called-cancel (realized? called-cancel?)
+          before-query-called-query  (realized? called-query?)
+          query-thunk                (fn [] (data/run-query checkins
+                                              (ql/aggregation (ql/count))))
+          ;; When the query is ran via the datasets endpoint, it will run in a future. That future can be cancelled,
+          ;; which should cause an interrupt
+          query-future               (f query-thunk called-query? called-cancel? pause-query)]
+
+      ;; Make sure that we start out with our promises not having a value
+      [before-query-called-cancel
+       before-query-called-query
+       ;; The cancelled-query? and called-cancel? timeouts are very high and are really just intended to
+       ;; prevent the test from hanging indefinitely. It shouldn't be hit unless something is really wrong
+       (deref called-query? 120000 ::query-never-called)
+       ;; At this point in time, the query is blocked, waiting for `pause-query` do be delivered
+       (realized? called-cancel?)
+       (do
+         ;; If we cancel the future, it should throw an InterruptedException, which should call the cancel
+         ;; method on the prepared statement
+         (future-cancel query-future)
+         (deref called-cancel? 120000 ::cancel-never-called))
+       (do
+         ;; This releases the fake query function so it finishes
+         (deliver pause-query true)
+         true)])))
diff --git a/test/metabase/timeseries_query_processor_test.clj b/test/metabase/timeseries_query_processor_test.clj
index 599c0fde15e26a38dbcf1a2bac4ef142f997e204..7fff141eda069f3f65eef05b2428d8743732b4f7 100644
--- a/test/metabase/timeseries_query_processor_test.clj
+++ b/test/metabase/timeseries_query_processor_test.clj
@@ -15,8 +15,8 @@
   #{:druid})
 
 (def ^:private flattened-db-def
-  "The normal test-data DB definition as a flattened, single-table DB definition.
-  (this is a function rather than a straight delay because clojure complains when they delay gets embedding in expanded macros)"
+  "The normal test-data DB definition as a flattened, single-table DB definition. (This is a function rather than a
+  straight delay because clojure complains when they delay gets embedding in expanded macros)"
   (delay (i/flatten-dbdef defs/test-data "checkins")))
 
 ;; force loading of the flattened db definitions for the DBs that need it
@@ -397,6 +397,20 @@
           (ql/breakout $venue_category_name)
           (ql/filter (ql/starts-with $venue_category_name "Me")))))
 
+(expect-with-timeseries-dbs
+  {:columns ["venue_category_name"]
+   :rows    []}
+  (data (data/run-query checkins
+          (ql/breakout $venue_category_name)
+          (ql/filter (ql/starts-with $venue_category_name "ME")))))
+
+(expect-with-timeseries-dbs
+  {:columns ["venue_category_name"]
+   :rows    [["Mediterannian"] ["Mexican"]]}
+  (data (data/run-query checkins
+          (ql/breakout $venue_category_name)
+          (ql/filter (ql/starts-with $venue_category_name "ME" {:case-sensitive false})))))
+
 ;;; filter ENDS_WITH
 (expect-with-timeseries-dbs
   {:columns ["venue_category_name"]
@@ -412,6 +426,27 @@
           (ql/breakout $venue_category_name)
           (ql/filter (ql/ends-with $venue_category_name "an")))))
 
+(expect-with-timeseries-dbs
+  {:columns ["venue_category_name"]
+   :rows    []}
+  (data (data/run-query checkins
+          (ql/breakout $venue_category_name)
+          (ql/filter (ql/ends-with $venue_category_name "AN")))))
+
+(expect-with-timeseries-dbs
+  {:columns ["venue_category_name"]
+   :rows    [["American"]
+             ["Artisan"]
+             ["Asian"]
+             ["Caribbean"]
+             ["German"]
+             ["Korean"]
+             ["Mediterannian"]
+             ["Mexican"]]}
+  (data (data/run-query checkins
+          (ql/breakout $venue_category_name)
+          (ql/filter (ql/ends-with $venue_category_name "AN" {:case-sensitive false})))))
+
 ;;; filter CONTAINS
 (expect-with-timeseries-dbs
   {:columns ["venue_category_name"]
@@ -427,6 +462,27 @@
           (ql/breakout $venue_category_name)
           (ql/filter (ql/contains $venue_category_name "er")))))
 
+(expect-with-timeseries-dbs
+  {:columns ["venue_category_name"]
+   :rows    []}
+  (data (data/run-query checkins
+          (ql/breakout $venue_category_name)
+          (ql/filter (ql/contains $venue_category_name "eR")))))
+
+(expect-with-timeseries-dbs
+  {:columns ["venue_category_name"]
+   :rows    [["American"]
+             ["Bakery"]
+             ["Brewery"]
+             ["Burger"]
+             ["Diner"]
+             ["German"]
+             ["Mediterannian"]
+             ["Southern"]]}
+  (data (data/run-query checkins
+          (ql/breakout $venue_category_name)
+          (ql/filter (ql/contains $venue_category_name "eR" {:case-sensitive false})))))
+
 ;;; order by aggregate field (?)
 (expect-with-timeseries-dbs
   {:columns ["user_name" "venue_category_name" "count"]
@@ -754,9 +810,13 @@
                                                                 (ql/contains $venue_name "BBQ"))))))
 
 ;;; time-interval
-(expect-with-timeseries-dbs [1000] (first-row (data/run-query checkins
-                                                (ql/aggregation (ql/count)) ; test data is all in the past so nothing happened today <3
-                                                (ql/filter (ql/not (ql/time-interval $timestamp :current :day))))))
+(expect-with-timeseries-dbs
+  [1000]
+  (first-row
+    (data/run-query checkins
+      (ql/aggregation (ql/count))
+      ;; test data is all in the past so nothing happened today <3
+      (ql/filter (ql/not (ql/time-interval $timestamp :current :day))))))
 
 
 
@@ -777,7 +837,8 @@
                                                (ql/aggregation (ql/min $count)))))
 
 (expect-with-timeseries-dbs
-  [["1" 34.0071] ["2" 33.7701] ["3" 10.0646] ["4" 33.983]] ; some sort of weird quirk w/ druid where all columns in breakout get converted to strings
+ ;; some sort of weird quirk w/ druid where all columns in breakout get converted to strings
+  [["1" 34.0071] ["2" 33.7701] ["3" 10.0646] ["4" 33.983]]
   (rows (data/run-query checkins
           (ql/aggregation (ql/min $venue_latitude))
           (ql/breakout $venue_price))))
diff --git a/test/metabase/util_test.clj b/test/metabase/util_test.clj
index 15dfb955f41f1a68fa2e8d1a96ef2b6841048960..40022e26ea13755dcf070e868331e948e8e42ae9 100644
--- a/test/metabase/util_test.clj
+++ b/test/metabase/util_test.clj
@@ -100,24 +100,24 @@
   (host-port-up? "nosuchhost" 8005))
 
 
-;;; ## tests for IS-URL?
-
-(expect true (is-url? "http://google.com"))
-(expect true (is-url? "https://google.com"))
-(expect true (is-url? "https://amazon.co.uk"))
-(expect true (is-url? "http://google.com?q=my-query&etc"))
-(expect true (is-url? "http://www.cool.com"))
-(expect true (is-url? "http://localhost/"))
-(expect true (is-url? "http://localhost:3000"))
-(expect true (is-url? "https://www.mapbox.com/help/data/stations.geojson"))
-(expect true (is-url? "http://www.cool.com:3000"))
-(expect true (is-url? "http://localhost:3000/auth/reset_password/144_f98987de-53ca-4335-81da-31bb0de8ea2b#new"))
-(expect false (is-url? "google.com"))                      ; missing protocol
-(expect false (is-url? "ftp://metabase.com"))              ; protocol isn't HTTP/HTTPS
-(expect false (is-url? "http://metabasecom"))              ; no period / TLD
-(expect false (is-url? "http://.com"))                     ; no domain
-(expect false (is-url? "http://google."))                  ; no TLD
-(expect false (is-url? "http:/"))                          ; nil .getAuthority needs to be handled or NullPointerException
+;;; ## tests for URL?
+
+(expect true (url? "http://google.com"))
+(expect true (url? "https://google.com"))
+(expect true (url? "https://amazon.co.uk"))
+(expect true (url? "http://google.com?q=my-query&etc"))
+(expect true (url? "http://www.cool.com"))
+(expect true (url? "http://localhost/"))
+(expect true (url? "http://localhost:3000"))
+(expect true (url? "https://www.mapbox.com/help/data/stations.geojson"))
+(expect true (url? "http://www.cool.com:3000"))
+(expect true (url? "http://localhost:3000/auth/reset_password/144_f98987de-53ca-4335-81da-31bb0de8ea2b#new"))
+(expect false (url? "google.com"))                      ; missing protocol
+(expect false (url? "ftp://metabase.com"))              ; protocol isn't HTTP/HTTPS
+(expect false (url? "http://metabasecom"))              ; no period / TLD
+(expect false (url? "http://.com"))                     ; no domain
+(expect false (url? "http://google."))                  ; no TLD
+(expect false (url? "http:/"))                          ; nil .getAuthority needs to be handled or NullPointerException
 
 ;;; ## tests for RPARTIAL
 
diff --git a/webpack.config.js b/webpack.config.js
index 73ec4b5613b14a0b768cfd9c706f09e4acc49c00..b6175627fc922491fe2b3270a29714b6a5de5a96 100644
--- a/webpack.config.js
+++ b/webpack.config.js
@@ -117,6 +117,7 @@ var config = module.exports = {
                     "**/types/*.js",
                     "**/*.spec.*",
                     "**/__support__/*.js",
+                    "**/__mocks__/*.js*",
                     "public/lib/types.js",
                     "internal/lib/components-node.js"
                 ]
diff --git a/yarn.lock b/yarn.lock
index d5100b81a481c4927d6c2a5bbc92567e78f4ba39..746764665592b93af5e78526c1946839766d33cc 100644
--- a/yarn.lock
+++ b/yarn.lock
@@ -362,14 +362,6 @@ assert@^1.1.1:
   dependencies:
     util "0.10.3"
 
-ast-types@0.8.18:
-  version "0.8.18"
-  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.8.18.tgz#c8b98574898e8914e9d8de74b947564a9fe929af"
-
-ast-types@0.9.4:
-  version "0.9.4"
-  resolved "https://registry.yarnpkg.com/ast-types/-/ast-types-0.9.4.tgz#410d1f81890aeb8e0a38621558ba5869ae53c91b"
-
 async-each@^1.0.0:
   version "1.0.1"
   resolved "https://registry.yarnpkg.com/async-each/-/async-each-1.0.1.tgz#19d386a1d9edc6e7c1c85d388aedbcc56d33602d"
@@ -444,14 +436,6 @@ babel-cli@^6.11.4:
   optionalDependencies:
     chokidar "^1.6.1"
 
-babel-code-frame@6.22.0:
-  version "6.22.0"
-  resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.22.0.tgz#027620bee567a88c32561574e7fd0801d33118e4"
-  dependencies:
-    chalk "^1.1.0"
-    esutils "^2.0.2"
-    js-tokens "^3.0.0"
-
 babel-code-frame@^6.11.0, babel-code-frame@^6.16.0, babel-code-frame@^6.22.0, babel-code-frame@^6.26.0:
   version "6.26.0"
   resolved "https://registry.yarnpkg.com/babel-code-frame/-/babel-code-frame-6.26.0.tgz#63fd43f7dc1e3bb7ce35947db8fe369a3f58c74b"
@@ -1258,10 +1242,6 @@ babelify@^7.3.0:
     babel-core "^6.0.14"
     object-assign "^4.0.0"
 
-babylon@6.15.0:
-  version "6.15.0"
-  resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.15.0.tgz#ba65cfa1a80e1759b0e89fb562e27dccae70348e"
-
 babylon@^6.11.0, babylon@^6.17.0, babylon@^6.17.2, babylon@^6.18.0:
   version "6.18.0"
   resolved "https://registry.yarnpkg.com/babylon/-/babylon-6.18.0.tgz#af2f3b88fa6f5c1e4c634d1a0f8eac4f55b395e3"
@@ -1731,7 +1711,7 @@ chalk@0.5.1:
     strip-ansi "^0.3.0"
     supports-color "^0.2.0"
 
-chalk@1.1.3, chalk@^1.0.0, chalk@^1.1.0, chalk@^1.1.1, chalk@^1.1.3:
+chalk@^1.0.0, chalk@^1.1.1, chalk@^1.1.3:
   version "1.1.3"
   resolved "https://registry.yarnpkg.com/chalk/-/chalk-1.1.3.tgz#a8115c55e4a702fe4d150abd3872822a7e09fc98"
   dependencies:
@@ -2014,7 +1994,7 @@ colors@1.0.x:
   version "1.0.3"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.0.3.tgz#0433f44d809680fdeb60ed260f1b0c262e82a40b"
 
-colors@1.1.2, colors@>=0.6.2, colors@^1.1.0, colors@~1.1.2:
+colors@1.1.2, colors@^1.1.0, colors@~1.1.2:
   version "1.1.2"
   resolved "https://registry.yarnpkg.com/colors/-/colors-1.1.2.tgz#168a4701756b6a7f51a12ce0c97bfa28c084ed63"
 
@@ -3304,7 +3284,7 @@ estraverse@^4.0.0, estraverse@^4.1.0, estraverse@^4.1.1, estraverse@^4.2.0:
   version "4.2.0"
   resolved "https://registry.yarnpkg.com/estraverse/-/estraverse-4.2.0.tgz#0dee3fed31fcd469618ce7342099fc1afa0bdb13"
 
-esutils@2.0.2, esutils@^2.0.2:
+esutils@^2.0.2:
   version "2.0.2"
   resolved "https://registry.yarnpkg.com/esutils/-/esutils-2.0.2.tgz#0abf4f1caa5bcb1f7a9d8acc6dea4faaa04bac9b"
 
@@ -3669,14 +3649,6 @@ flow-bin@^0.37.4:
   version "0.37.4"
   resolved "https://registry.yarnpkg.com/flow-bin/-/flow-bin-0.37.4.tgz#3d8da2ef746e80e730d166e09040f4198969b76b"
 
-flow-parser@0.40.0:
-  version "0.40.0"
-  resolved "https://registry.yarnpkg.com/flow-parser/-/flow-parser-0.40.0.tgz#b3444742189093323c4319c4fe9d35391f46bcbc"
-  dependencies:
-    ast-types "0.8.18"
-    colors ">=0.6.2"
-    minimist ">=0.2.0"
-
 flush-write-stream@^1.0.0:
   version "1.0.2"
   resolved "https://registry.yarnpkg.com/flush-write-stream/-/flush-write-stream-1.0.2.tgz#c81b90d8746766f1a609a46809946c45dd8ae417"
@@ -3855,10 +3827,6 @@ get-port@^3.1.0:
   version "3.2.0"
   resolved "https://registry.yarnpkg.com/get-port/-/get-port-3.2.0.tgz#dd7ce7de187c06c8bf353796ac71e099f0980ebc"
 
-get-stdin@5.0.1:
-  version "5.0.1"
-  resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-5.0.1.tgz#122e161591e21ff4c52530305693f20e6393a398"
-
 get-stdin@^4.0.1:
   version "4.0.1"
   resolved "https://registry.yarnpkg.com/get-stdin/-/get-stdin-4.0.1.tgz#b968c6b0a04384324902e8bf1a5df32579a450fe"
@@ -3941,17 +3909,6 @@ glob-stream@^5.3.2:
     to-absolute-glob "^0.1.1"
     unique-stream "^2.0.2"
 
-glob@7.1.1, glob@^7.1.1:
-  version "7.1.1"
-  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
-  dependencies:
-    fs.realpath "^1.0.0"
-    inflight "^1.0.4"
-    inherits "2"
-    minimatch "^3.0.2"
-    once "^1.3.0"
-    path-is-absolute "^1.0.0"
-
 glob@^5.0.3:
   version "5.0.15"
   resolved "https://registry.yarnpkg.com/glob/-/glob-5.0.15.tgz#1bc936b9e02f4a603fcc222ecf7633d30b8b93b1"
@@ -3973,6 +3930,17 @@ glob@^7.0.0, glob@^7.0.3, glob@^7.0.5, glob@^7.0.6, glob@^7.1.2:
     once "^1.3.0"
     path-is-absolute "^1.0.0"
 
+glob@^7.1.1:
+  version "7.1.1"
+  resolved "https://registry.yarnpkg.com/glob/-/glob-7.1.1.tgz#805211df04faaf1c63a3600306cdf5ade50b2ec8"
+  dependencies:
+    fs.realpath "^1.0.0"
+    inflight "^1.0.4"
+    inherits "2"
+    minimatch "^3.0.2"
+    once "^1.3.0"
+    path-is-absolute "^1.0.0"
+
 globals-docs@^2.3.0:
   version "2.3.0"
   resolved "https://registry.yarnpkg.com/globals-docs/-/globals-docs-2.3.0.tgz#dca4088af196f7800f4eba783eaeff335cb6759c"
@@ -5176,15 +5144,6 @@ jest-util@^19.0.2:
     leven "^2.0.0"
     mkdirp "^0.5.1"
 
-jest-validate@19.0.0:
-  version "19.0.0"
-  resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-19.0.0.tgz#8c6318a20ecfeaba0ba5378bfbb8277abded4173"
-  dependencies:
-    chalk "^1.1.1"
-    jest-matcher-utils "^19.0.0"
-    leven "^2.0.0"
-    pretty-format "^19.0.0"
-
 jest-validate@^19.0.2:
   version "19.0.2"
   resolved "https://registry.yarnpkg.com/jest-validate/-/jest-validate-19.0.2.tgz#dc534df5f1278d5b63df32b14241d4dbf7244c0c"
@@ -6092,7 +6051,7 @@ minimist@0.0.8, minimist@~0.0.1:
   version "0.0.8"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-0.0.8.tgz#857fcabfc3397d2625b8228262e86aa7a011b05d"
 
-minimist@1.2.0, minimist@>=0.2.0, minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
+minimist@^1.1.0, minimist@^1.1.1, minimist@^1.1.3, minimist@^1.2.0:
   version "1.2.0"
   resolved "https://registry.yarnpkg.com/minimist/-/minimist-1.2.0.tgz#a35008b20f41383eec1fb914f4cd5df79a264284"
 
@@ -7472,20 +7431,9 @@ preserve@^0.2.0:
   version "0.2.0"
   resolved "https://registry.yarnpkg.com/preserve/-/preserve-0.2.0.tgz#815ed1f6ebc65926f865b310c0713bcb3315ce4b"
 
-prettier@^0.22.0:
-  version "0.22.0"
-  resolved "https://registry.yarnpkg.com/prettier/-/prettier-0.22.0.tgz#7b37c4480d0858180407e5a8e13f0f47da7385d2"
-  dependencies:
-    ast-types "0.9.4"
-    babel-code-frame "6.22.0"
-    babylon "6.15.0"
-    chalk "1.1.3"
-    esutils "2.0.2"
-    flow-parser "0.40.0"
-    get-stdin "5.0.1"
-    glob "7.1.1"
-    jest-validate "19.0.0"
-    minimist "1.2.0"
+prettier@^1.10.2:
+  version "1.10.2"
+  resolved "https://registry.yarnpkg.com/prettier/-/prettier-1.10.2.tgz#1af8356d1842276a99a5b5529c82dd9e9ad3cc93"
 
 pretty-error@^2.0.2:
   version "2.1.1"